package analyzers

import (
	"context"
	"fmt"
	"go/parser"
	"go/token"
	"os"
	"strings"

	"github.com/yourorg/devour/internal/quality"
	"golang.org/x/tools/go/packages"
)

type DeadCodeDetector struct {
	*quality.BaseDetector
}

func NewDeadCodeDetector(finder quality.FileFinder) *DeadCodeDetector {
	return &DeadCodeDetector{
		BaseDetector: quality.NewBaseDetector("dead_code", quality.SeverityT2, finder),
	}
}

func (d *DeadCodeDetector) Name() string {
	return "dead_code"
}

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

func (d *DeadCodeDetector) shouldSkipExport(name, objType string) bool {
	// Skip common API surface exports that might be used externally
	skipPatterns := []string{
		"Version", "License", "APIKey", "Config", "Options",
		"Client", "Server", "Handler", "Service", "Manager",
		"Store", "Cache", "Index", "Search", "Query",
	}

	// Skip type definitions and constants
	if strings.Contains(objType, "type") ||
		strings.Contains(objType, "const") ||
		strings.Contains(objType, "var") {
		return true
	}

	// Skip common naming patterns
	for _, pattern := range skipPatterns {
		if name == pattern {
			return true
		}
	}

	return false
}

func (d *DeadCodeDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) {
	cfg := &packages.Config{
		Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedFiles,
		Dir:  path,
	}

	pkgs, err := packages.Load(cfg, "./...")
	if err != nil {
		return nil, fmt.Errorf("failed to load packages: %w", err)
	}

	var findings []quality.Finding
	used := make(map[string]bool)

	for _, pkg := range pkgs {
		for _, obj := range pkg.TypesInfo.Uses {
			if obj != nil && obj.Pkg() != nil {
				used[obj.Pkg().Path()+"."+obj.Name()] = true
			}
		}
	}

	for _, pkg := range pkgs {
		for _, obj := range pkg.TypesInfo.Defs {
			if obj == nil || obj.Pkg() == nil {
				continue
			}

			// Skip unexported objects - they're internal
			if !obj.Exported() {
				continue
			}

			key := obj.Pkg().Path() + "." + obj.Name()
			if !used[key] {
				// Skip certain types of exports that are commonly legitimate
				if d.shouldSkipExport(obj.Name(), obj.Type().String()) {
					continue
				}

				pos := pkg.Fset.Position(obj.Pos())
				finding := quality.Finding{
					ID:          fmt.Sprintf("dead_code::%s::%s", pos.Filename, obj.Name()),
					Type:        "dead_code",
					Title:       fmt.Sprintf("Unused exported identifier: %s", obj.Name()),
					Description: fmt.Sprintf("The exported %s '%s' is never used in codebase. Consider removing it or documenting its intended use.", obj.Type(), obj.Name()),
					File:        pos.Filename,
					Line:        pos.Line,
					Severity:    quality.SeverityT2,
					Score:       5,
					Status:      quality.StatusOpen,
					Metadata: map[string]string{
						"name":     obj.Name(),
						"type":     obj.Type().String(),
						"package":  obj.Pkg().Path(),
						"exported": "true",
					},
				}
				findings = append(findings, finding)
			}
		}
	}

	return findings, nil
}

type UnusedImportDetector struct {
	*quality.BaseDetector
}

func NewUnusedImportDetector(finder quality.FileFinder) *UnusedImportDetector {
	return &UnusedImportDetector{
		BaseDetector: quality.NewBaseDetector("unused_import", quality.SeverityT1, finder),
	}
}

func (d *UnusedImportDetector) Name() string {
	return "unused_import"
}

func (d *UnusedImportDetector) Severity() quality.Severity {
	return quality.SeverityT1
}

func (d *UnusedImportDetector) 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 *UnusedImportDetector) analyzeFile(path string) ([]quality.Finding, error) {
	fset := token.NewFileSet()
	node, err := parser.ParseFile(fset, path, nil, parser.ImportsOnly|parser.ParseComments)
	if err != nil {
		return nil, err
	}

	imports := make(map[string]string)
	for _, imp := range node.Imports {
		pkgPath := strings.Trim(imp.Path.Value, `"`)
		name := ""
		if imp.Name != nil {
			name = imp.Name.Name
		} else {
			name = inferImportName(pkgPath)
		}
		imports[pkgPath] = name
	}

	content, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	contentStr := string(content)

	var findings []quality.Finding
	for _, imp := range node.Imports {
		pkgPath := strings.Trim(imp.Path.Value, `"`)
		name := ""
		if imp.Name != nil {
			name = imp.Name.Name
		} else {
			name = inferImportName(pkgPath)
		}

		if name == "_" || name == "." {
			continue
		}

		pattern := name + "."
		if !strings.Contains(contentStr, pattern) {
			pos := fset.Position(imp.Pos())
			finding := quality.Finding{
				ID:          fmt.Sprintf("unused_import::%s::%s", path, pkgPath),
				Type:        "unused_import",
				Title:       fmt.Sprintf("Unused import: %s", pkgPath),
				Description: fmt.Sprintf("The import '%s' is not used in this file. Remove it to clean up the code.", pkgPath),
				File:        path,
				Line:        pos.Line,
				Severity:    quality.SeverityT1,
				Score:       2,
				Status:      quality.StatusOpen,
				Metadata: map[string]string{
					"import_path": pkgPath,
					"alias":       name,
				},
			}
			findings = append(findings, finding)
		}
	}

	return findings, nil
}

func inferImportName(pkgPath string) string {
	parts := strings.Split(pkgPath, "/")
	if len(parts) == 0 {
		return pkgPath
	}

	last := parts[len(parts)-1]
	if isSemverSegment(last) && len(parts) >= 2 {
		last = parts[len(parts)-2]
	}
	if idx := strings.Index(last, ".v"); idx > 0 && isDigits(last[idx+2:]) {
		last = last[:idx]
	}

	return last
}

func isSemverSegment(segment string) bool {
	if len(segment) < 2 || segment[0] != 'v' {
		return false
	}
	return isDigits(segment[1:])
}

func isDigits(value string) bool {
	if value == "" {
		return false
	}
	for _, r := range value {
		if r < '0' || r > '9' {
			return false
		}
	}
	return true
}

type CycleDetector struct {
	*quality.BaseDetector
}

func NewCycleDetector(finder quality.FileFinder) *CycleDetector {
	return &CycleDetector{
		BaseDetector: quality.NewBaseDetector("import_cycle", quality.SeverityT4, finder),
	}
}

func (d *CycleDetector) Name() string {
	return "import_cycle"
}

func (d *CycleDetector) Severity() quality.Severity {
	return quality.SeverityT4
}

func (d *CycleDetector) Detect(ctx context.Context, path string, config *quality.Config) ([]quality.Finding, error) {
	cfg := &packages.Config{
		Mode: packages.NeedName | packages.NeedImports,
		Dir:  path,
	}

	pkgs, err := packages.Load(cfg, "./...")
	if err != nil {
		return nil, fmt.Errorf("failed to load packages: %w", err)
	}

	localPkgs := make(map[string]bool)
	for _, pkg := range pkgs {
		localPkgs[pkg.PkgPath] = true
	}

	graph := make(map[string][]string)
	for _, pkg := range pkgs {
		for _, imp := range pkg.Imports {
			if localPkgs[imp.PkgPath] {
				graph[pkg.PkgPath] = append(graph[pkg.PkgPath], imp.PkgPath)
			}
		}
	}

	cycles := d.findCycles(graph)

	var findings []quality.Finding
	for i, cycle := range cycles {
		finding := quality.Finding{
			ID:          fmt.Sprintf("import_cycle::%d", i),
			Type:        "import_cycle",
			Title:       "Import cycle detected",
			Description: fmt.Sprintf("Circular import dependency: %s", strings.Join(cycle, " → ")),
			File:        cycle[0],
			Line:        1,
			Severity:    quality.SeverityT4,
			Score:       20,
			Status:      quality.StatusOpen,
			Metadata: map[string]string{
				"cycle": strings.Join(cycle, ","),
			},
		}
		findings = append(findings, finding)
	}

	return findings, nil
}

func (d *CycleDetector) findCycles(graph map[string][]string) [][]string {
	var cycles [][]string
	visited := make(map[string]bool)
	recStack := make(map[string]bool)

	var dfs func(node string, path []string)
	dfs = func(node string, path []string) {
		visited[node] = true
		recStack[node] = true
		path = append(path, node)

		for _, neighbor := range graph[node] {
			if !visited[neighbor] {
				dfs(neighbor, path)
			} else if recStack[neighbor] {
				cycleStart := -1
				for i, n := range path {
					if n == neighbor {
						cycleStart = i
						break
					}
				}
				if cycleStart >= 0 {
					cycle := make([]string, len(path)-cycleStart)
					copy(cycle, path[cycleStart:])
					cycles = append(cycles, cycle)
				}
			}
		}

		recStack[node] = false
	}

	for node := range graph {
		if !visited[node] {
			dfs(node, []string{})
		}
	}

	return cycles
}
