package analyzers

import (
	"context"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"regexp"
	"strings"

	"github.com/yourorg/devour/internal/quality"
)

type SecurityDetector struct {
	*quality.BaseDetector
	patterns []SecurityPattern
}

type SecurityPattern struct {
	Name        string
	Description string
	Pattern     *regexp.Regexp
	Severity    quality.Severity
	Score       int
}

func NewSecurityDetector(finder quality.FileFinder) *SecurityDetector {
	d := &SecurityDetector{
		BaseDetector: quality.NewBaseDetector("security", quality.SeverityT3, finder),
		patterns: []SecurityPattern{
			{
				Name:        "hardcoded_password",
				Description: "Hardcoded password or secret detected",
				Pattern:     regexp.MustCompile(`(?i)(password|passwd|pwd|secret|api_key|apikey|token)\s*[:=]\s*["'][^"']+["']`),
				Severity:    quality.SeverityT4,
				Score:       30,
			},
			{
				Name:        "sql_injection_risk",
				Description: "Potential SQL injection - string concatenation in query",
				Pattern:     regexp.MustCompile(`fmt\.Sprintf.*SELECT|fmt\.Sprintf.*INSERT|fmt\.Sprintf.*UPDATE|fmt\.Sprintf.*DELETE`),
				Severity:    quality.SeverityT4,
				Score:       25,
			},
			{
				Name:        "unsafe_sql_exec",
				Description: "Direct string interpolation in SQL execution",
				Pattern:     regexp.MustCompile(`db\.(Exec|Query).*\+|db\.(Exec|Query).*fmt\.Sprintf`),
				Severity:    quality.SeverityT4,
				Score:       25,
			},
			{
				Name:        "weak_random",
				Description: "Using math/rand for security-sensitive operations",
				Pattern:     regexp.MustCompile(`math/rand.*token|math/rand.*password|math/rand.*secret|math/rand.*key`),
				Severity:    quality.SeverityT3,
				Score:       15,
			},
			{
				Name:        "todo_security",
				Description: "TODO/FIXME related to security",
				Pattern:     regexp.MustCompile(`(?i)(TODO|FIXME|XXX).*security|(?i)(TODO|FIXME|XXX).*auth|(?i)(TODO|FIXME|XXX).*password`),
				Severity:    quality.SeverityT2,
				Score:       5,
			},
			{
				Name:        "os_exec_shell",
				Description: "Command execution with potential shell injection",
				Pattern:     regexp.MustCompile(`exec\.Command.*sh.*-c|exec\.Command.*bash.*-c`),
				Severity:    quality.SeverityT4,
				Score:       30,
			},
		},
	}
	return d
}

func (d *SecurityDetector) Name() string {
	return "security"
}

func (d *SecurityDetector) Severity() quality.Severity {
	return quality.SeverityT3
}

func (d *SecurityDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) {
	files, err := d.FindFiles(path, "go")
	if err != nil {
		return nil, err
	}

	var findings []quality.Finding

	for _, file := range files {
		fileFindings, err := d.analyzeFile(file)
		if err != nil {
			continue
		}
		findings = append(findings, fileFindings...)
	}

	return findings, nil
}

func (d *SecurityDetector) analyzeFile(filePath string) ([]quality.Finding, error) {
	fset := token.NewFileSet()
	node, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
	if err != nil {
		return nil, err
	}

	var findings []quality.Finding

	ast.Inspect(node, func(n ast.Node) bool {
		switch x := n.(type) {
		case *ast.CallExpr:
			d.checkCallExpr(x, fset, filePath, &findings)
		case *ast.AssignStmt:
			d.checkAssignStmt(x, fset, filePath, &findings)
		case *ast.ValueSpec:
			d.checkValueSpec(x, fset, filePath, &findings)
		}
		return true
	})

	d.checkComments(node, fset, filePath, &findings)

	return findings, nil
}

func (d *SecurityDetector) checkCallExpr(expr *ast.CallExpr, fset *token.FileSet, file string, findings *[]quality.Finding) {
	exprStr := d.nodeToString(expr)
	pos := fset.Position(expr.Pos())

	for _, pattern := range d.patterns {
		if pattern.Pattern.MatchString(exprStr) {
			finding := quality.Finding{
				ID:          fmt.Sprintf("security::%s::%d", file, pos.Line),
				Type:        "security",
				Title:       pattern.Name,
				Description: pattern.Description,
				File:        file,
				Line:        pos.Line,
				Severity:    pattern.Severity,
				Score:       pattern.Score,
				Status:      quality.StatusOpen,
				Metadata: map[string]string{
					"pattern":  pattern.Name,
					"match":    exprStr,
					"severity": fmt.Sprintf("%d", pattern.Severity),
				},
			}
			*findings = append(*findings, finding)
			break
		}
	}
}

func (d *SecurityDetector) checkAssignStmt(stmt *ast.AssignStmt, fset *token.FileSet, file string, findings *[]quality.Finding) {
	for _, expr := range stmt.Lhs {
		if ident, ok := expr.(*ast.Ident); ok {
			if strings.Contains(strings.ToLower(ident.Name), "password") ||
				strings.Contains(strings.ToLower(ident.Name), "secret") ||
				strings.Contains(strings.ToLower(ident.Name), "token") {
				for _, val := range stmt.Rhs {
					if basicLit, ok := val.(*ast.BasicLit); ok && basicLit.Kind == token.STRING {
						pos := fset.Position(stmt.Pos())
						finding := quality.Finding{
							ID:          fmt.Sprintf("security::%s::%d", file, pos.Line),
							Type:        "security",
							Title:       "hardcoded_credential",
							Description: fmt.Sprintf("Hardcoded credential in variable '%s'", ident.Name),
							File:        file,
							Line:        pos.Line,
							Severity:    quality.SeverityT4,
							Score:       30,
							Status:      quality.StatusOpen,
							Metadata: map[string]string{
								"variable": ident.Name,
							},
						}
						*findings = append(*findings, finding)
					}
				}
			}
		}
	}
}

func (d *SecurityDetector) checkValueSpec(spec *ast.ValueSpec, fset *token.FileSet, file string, findings *[]quality.Finding) {
	for i, name := range spec.Names {
		lowerName := strings.ToLower(name.Name)
		if strings.Contains(lowerName, "password") ||
			strings.Contains(lowerName, "secret") ||
			strings.Contains(lowerName, "apikey") ||
			strings.Contains(lowerName, "token") {
			if len(spec.Values) > i {
				if basicLit, ok := spec.Values[i].(*ast.BasicLit); ok && basicLit.Kind == token.STRING {
					pos := fset.Position(spec.Pos())
					finding := quality.Finding{
						ID:          fmt.Sprintf("security::%s::%d", file, pos.Line),
						Type:        "security",
						Title:       "hardcoded_credential",
						Description: fmt.Sprintf("Hardcoded credential in variable '%s'", name.Name),
						File:        file,
						Line:        pos.Line,
						Severity:    quality.SeverityT4,
						Score:       30,
						Status:      quality.StatusOpen,
						Metadata: map[string]string{
							"variable": name.Name,
						},
					}
					*findings = append(*findings, finding)
				}
			}
		}
	}
}

func (d *SecurityDetector) checkComments(node *ast.File, fset *token.FileSet, file string, findings *[]quality.Finding) {
	for _, group := range node.Comments {
		for _, comment := range group.List {
			text := comment.Text
			for _, pattern := range d.patterns {
				if pattern.Pattern.MatchString(text) {
					pos := fset.Position(comment.Pos())
					finding := quality.Finding{
						ID:          fmt.Sprintf("security::%s::%d", file, pos.Line),
						Type:        "security",
						Title:       pattern.Name,
						Description: pattern.Description,
						File:        file,
						Line:        pos.Line,
						Severity:    pattern.Severity,
						Score:       pattern.Score,
						Status:      quality.StatusOpen,
						Metadata: map[string]string{
							"pattern":    pattern.Name,
							"in_comment": "true",
						},
					}
					*findings = append(*findings, finding)
					break
				}
			}
		}
	}
}

func (d *SecurityDetector) nodeToString(node ast.Node) string {
	var b strings.Builder
	fmt.Fprint(&b, node)
	return b.String()
}

type ComplexityASTDetector struct {
	*quality.BaseDetector
	maxComplexity int
	maxNesting    int
}

func NewComplexityASTDetector(finder quality.FileFinder) *ComplexityASTDetector {
	return &ComplexityASTDetector{
		BaseDetector:  quality.NewBaseDetector("complexity_ast", quality.SeverityT2, finder),
		maxComplexity: 15,
		maxNesting:    4,
	}
}

func (d *ComplexityASTDetector) Name() string {
	return "complexity_ast"
}

func (d *ComplexityASTDetector) Severity() quality.Severity {
	return quality.SeverityT2
}

func (d *ComplexityASTDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) {
	files, err := d.FindFiles(path, "go")
	if err != nil {
		return nil, err
	}

	var findings []quality.Finding

	for _, file := range files {
		fileFindings, err := d.analyzeFile(file)
		if err != nil {
			continue
		}
		findings = append(findings, fileFindings...)
	}

	return findings, nil
}

func (d *ComplexityASTDetector) analyzeFile(filePath string) ([]quality.Finding, error) {
	fset := token.NewFileSet()
	node, err := parser.ParseFile(fset, filePath, nil, 0)
	if err != nil {
		return nil, err
	}

	var findings []quality.Finding

	for _, decl := range node.Decls {
		if fn, ok := decl.(*ast.FuncDecl); ok {
			complexity := d.calculateCyclomaticComplexity(fn)
			nesting := d.calculateNestingDepth(fn)

			if complexity > d.maxComplexity {
				pos := fset.Position(fn.Pos())
				finding := quality.Finding{
					ID:          fmt.Sprintf("complexity::%s::%s", filePath, fn.Name.Name),
					Type:        "complexity",
					Title:       fmt.Sprintf("High cyclomatic complexity in %s", fn.Name.Name),
					Description: fmt.Sprintf("Function '%s' has cyclomatic complexity of %d (max: %d). Consider breaking it into smaller functions.", fn.Name.Name, complexity, d.maxComplexity),
					File:        filePath,
					Line:        pos.Line,
					Severity:    quality.SeverityT2,
					Score:       complexity - d.maxComplexity,
					Status:      quality.StatusOpen,
					Metadata: map[string]string{
						"function":       fn.Name.Name,
						"complexity":     fmt.Sprintf("%d", complexity),
						"max_complexity": fmt.Sprintf("%d", d.maxComplexity),
					},
				}
				findings = append(findings, finding)
			}

			if nesting > d.maxNesting {
				pos := fset.Position(fn.Pos())
				finding := quality.Finding{
					ID:          fmt.Sprintf("nesting::%s::%s", filePath, fn.Name.Name),
					Type:        "complexity",
					Title:       fmt.Sprintf("Deep nesting in %s", fn.Name.Name),
					Description: fmt.Sprintf("Function '%s' has nesting depth of %d (max: %d). Consider extracting logic into helper functions.", fn.Name.Name, nesting, d.maxNesting),
					File:        filePath,
					Line:        pos.Line,
					Severity:    quality.SeverityT3,
					Score:       (nesting - d.maxNesting) * 3,
					Status:      quality.StatusOpen,
					Metadata: map[string]string{
						"function":    fn.Name.Name,
						"nesting":     fmt.Sprintf("%d", nesting),
						"max_nesting": fmt.Sprintf("%d", d.maxNesting),
					},
				}
				findings = append(findings, finding)
			}
		}
	}

	return findings, nil
}

func (d *ComplexityASTDetector) calculateCyclomaticComplexity(fn *ast.FuncDecl) int {
	complexity := 1

	ast.Inspect(fn, func(n ast.Node) bool {
		switch n.(type) {
		case *ast.IfStmt, *ast.ForStmt, *ast.RangeStmt:
			complexity++
		case *ast.CaseClause:
			complexity++
		case *ast.BinaryExpr:
			complexity++
		}
		return true
	})

	return complexity
}

func (d *ComplexityASTDetector) calculateNestingDepth(fn *ast.FuncDecl) int {
	maxDepth := 0

	var visit func(n ast.Node, depth int)
	visit = func(n ast.Node, depth int) {
		if depth > maxDepth {
			maxDepth = depth
		}

		switch stmt := n.(type) {
		case *ast.IfStmt:
			visit(stmt.Body, depth+1)
			if stmt.Else != nil {
				visit(stmt.Else, depth+1)
			}
		case *ast.ForStmt:
			visit(stmt.Body, depth+1)
		case *ast.RangeStmt:
			visit(stmt.Body, depth+1)
		case *ast.SwitchStmt:
			visit(stmt.Body, depth+1)
		case *ast.SelectStmt:
			visit(stmt.Body, depth+1)
		case *ast.BlockStmt:
			for _, s := range stmt.List {
				visit(s, depth)
			}
		}
	}

	visit(fn.Body, 0)
	return maxDepth
}
