package analyzers

import (
	"context"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"go/types"
	"path/filepath"
	"strings"

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

type SingleUseDetector struct {
	*quality.BaseDetector
	minLOC int
}

func NewSingleUseDetector(finder quality.FileFinder) *SingleUseDetector {
	return &SingleUseDetector{
		BaseDetector: quality.NewBaseDetector("single_use", quality.SeverityT3, finder),
		minLOC:       10,
	}
}

func (d *SingleUseDetector) Name() string {
	return "single_use"
}

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

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

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

	callCounts := make(map[string]int)
	funcDefs := make(map[string]FuncDef)
	typeUsages := make(map[string]int)
	typeDefs := make(map[string]TypeDef)

	for _, pkg := range pkgs {
		for _, obj := range pkg.TypesInfo.Uses {
			if obj == nil {
				continue
			}

			switch obj := obj.(type) {
			case *types.Func:
				key, ok := functionKey(obj)
				if !ok {
					continue
				}
				callCounts[key]++
			case *types.TypeName:
				if obj.Pkg() != nil {
					key := obj.Pkg().Path() + "." + obj.Name()
					typeUsages[key]++
				}
			}
		}

		for _, obj := range pkg.TypesInfo.Defs {
			if obj == nil {
				continue
			}

			switch obj := obj.(type) {
			case *types.Func:
				key, ok := functionKey(obj)
				if !ok {
					continue
				}
				pos := pkg.Fset.Position(obj.Pos())
				funcDefs[key] = FuncDef{
					Name:      obj.Name(),
					File:      pos.Filename,
					Line:      pos.Line,
					Package:   obj.Pkg().Path(),
					Exported:  obj.Exported(),
					Signature: obj.Type().String(),
				}
			case *types.TypeName:
				if obj.Pkg() != nil {
					key := obj.Pkg().Path() + "." + obj.Name()
					pos := pkg.Fset.Position(obj.Pos())
					typeDefs[key] = TypeDef{
						Name:       obj.Name(),
						File:       pos.Filename,
						Line:       pos.Line,
						Package:    obj.Pkg().Path(),
						Exported:   obj.Exported(),
						Underlying: obj.Type().Underlying().String(),
					}
				}
			}
		}
	}

	entryPoints := d.findEntryPoints(pkgs)

	var findings []quality.Finding

	for key, def := range funcDefs {
		if def.Exported || isLikelyEntrypointFile(def.File) {
			continue
		}
		if strings.HasSuffix(def.Name, "Test") || strings.HasPrefix(def.Name, "Test") {
			continue
		}
		if strings.HasSuffix(def.Name, "Handler") || strings.HasSuffix(def.Name, "Middleware") {
			continue
		}

		count := callCounts[key]
		if count == 1 && !d.isEntryPoint(def.Name, entryPoints) {
			loc, _ := d.getFuncLOC(def.File, def.Line)
			if loc >= d.minLOC {
				finding := quality.Finding{
					ID:          fmt.Sprintf("single_use_func::%s::%s", def.File, def.Name),
					Type:        "single_use",
					Title:       fmt.Sprintf("Single-use function: %s", def.Name),
					Description: fmt.Sprintf("Function '%s' is only used once. Consider inlining it or documenting its purpose.", def.Name),
					File:        def.File,
					Line:        def.Line,
					Severity:    quality.SeverityT3,
					Score:       3,
					Status:      quality.StatusOpen,
					Metadata: map[string]string{
						"name":        def.Name,
						"usage_count": fmt.Sprintf("%d", count),
						"loc":         fmt.Sprintf("%d", loc),
						"exported":    fmt.Sprintf("%v", def.Exported),
					},
				}
				findings = append(findings, finding)
			}
		}
	}

	for key, def := range typeDefs {
		if def.Exported || isLikelyEntrypointFile(def.File) {
			continue
		}
		if strings.HasSuffix(def.Name, "Error") || strings.HasSuffix(def.Name, "Options") {
			continue
		}
		if strings.HasSuffix(def.Name, "Config") || strings.HasSuffix(def.Name, "Params") {
			continue
		}
		if !strings.Contains(def.Underlying, "struct") && !strings.Contains(def.Underlying, "interface") {
			continue
		}

		count := typeUsages[key]
		if count == 1 {
			finding := quality.Finding{
				ID:          fmt.Sprintf("single_use_type::%s::%s", def.File, def.Name),
				Type:        "single_use",
				Title:       fmt.Sprintf("Single-use type: %s", def.Name),
				Description: fmt.Sprintf("Type '%s' is only used once. Consider if this abstraction is necessary.", def.Name),
				File:        def.File,
				Line:        def.Line,
				Severity:    quality.SeverityT3,
				Score:       4,
				Status:      quality.StatusOpen,
				Metadata: map[string]string{
					"name":        def.Name,
					"usage_count": fmt.Sprintf("%d", count),
					"exported":    fmt.Sprintf("%v", def.Exported),
					"underlying":  def.Underlying,
				},
			}
			findings = append(findings, finding)
		}
	}

	return findings, nil
}

func (d *SingleUseDetector) findEntryPoints(pkgs []*packages.Package) map[string]bool {
	entryPoints := make(map[string]bool)

	for _, pkg := range pkgs {
		for _, file := range pkg.Syntax {
			ast.Inspect(file, func(n ast.Node) bool {
				switch node := n.(type) {
				case *ast.FuncDecl:
					if node.Name.Name == "main" {
						entryPoints[pkg.PkgPath+".main"] = true
					}
					if node.Name.Name == "init" {
						entryPoints[pkg.PkgPath+".init"] = true
					}
					if node.Recv == nil {
						for _, decl := range node.Type.Params.List {
							if d.isHTTPHandlerType(decl.Type) {
								entryPoints[pkg.PkgPath+"."+node.Name.Name] = true
							}
						}
					}
				}
				return true
			})
		}
	}

	return entryPoints
}

func (d *SingleUseDetector) isHTTPHandlerType(expr ast.Expr) bool {
	if sel, ok := expr.(*ast.SelectorExpr); ok {
		if ident, ok := sel.X.(*ast.Ident); ok {
			return (ident.Name == "http" && (sel.Sel.Name == "Handler" || sel.Sel.Name == "HandlerFunc" || sel.Sel.Name == "ResponseWriter"))
		}
	}
	if star, ok := expr.(*ast.StarExpr); ok {
		return d.isHTTPHandlerType(star.X)
	}
	return false
}

func (d *SingleUseDetector) isEntryPoint(name string, entryPoints map[string]bool) bool {
	return entryPoints[name] || name == "main" || name == "init"
}

func (d *SingleUseDetector) getFuncLOC(file string, startLine int) (int, error) {
	fset := token.NewFileSet()
	node, err := parser.ParseFile(fset, file, nil, 0)
	if err != nil {
		return 0, fmt.Errorf("parse %s for function loc lookup: %w", file, err)
	}

	loc := 0
	ast.Inspect(node, func(n ast.Node) bool {
		if fn, ok := n.(*ast.FuncDecl); ok {
			pos := fset.Position(fn.Pos())
			if pos.Line == startLine {
				end := fset.Position(fn.End())
				loc = end.Line - pos.Line + 1
				return false
			}
		}
		return true
	})

	return loc, nil
}

func functionKey(fn *types.Func) (string, bool) {
	if fn == nil || fn.Pkg() == nil {
		return "", false
	}
	sig, ok := fn.Type().(*types.Signature)
	if ok && sig.Recv() != nil {
		return "", false
	}
	return fn.Pkg().Path() + "." + fn.Name(), true
}

func isLikelyEntrypointFile(path string) bool {
	p := filepath.ToSlash(path)
	return strings.HasPrefix(p, "cmd/") || strings.Contains(p, "/cmd/") || strings.HasSuffix(p, "/main.go") || strings.HasSuffix(p, "_test.go")
}

type FuncDef struct {
	Name      string
	File      string
	Line      int
	Package   string
	Exported  bool
	Signature string
}

type TypeDef struct {
	Name       string
	File       string
	Line       int
	Package    string
	Exported   bool
	Underlying string
}

type CouplingDetector struct {
	*quality.BaseDetector
	maxFanOut int
}

func NewCouplingDetector(finder quality.FileFinder) *CouplingDetector {
	return &CouplingDetector{
		BaseDetector: quality.NewBaseDetector("coupling", quality.SeverityT3, finder),
		maxFanOut:    20, // Increased from 10 to 20 for more realistic threshold
	}
}

func (d *CouplingDetector) Name() string {
	return "coupling"
}

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

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

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

	pkgImports := make(map[string][]string)
	pkgImportedBy := make(map[string][]string)
	pkgFiles := make(map[string]int)

	for _, pkg := range pkgs {
		pkgFiles[pkg.PkgPath] = len(pkg.GoFiles)
		for _, imp := range pkg.Imports {
			pkgImports[pkg.PkgPath] = append(pkgImports[pkg.PkgPath], imp.PkgPath)
			pkgImportedBy[imp.PkgPath] = append(pkgImportedBy[imp.PkgPath], pkg.PkgPath)
		}
	}

	var findings []quality.Finding

	for pkg, imports := range pkgImports {
		fanOut := len(imports)
		if fanOut > d.maxFanOut {
			finding := quality.Finding{
				ID:          fmt.Sprintf("coupling_fanout::%s", pkg),
				Type:        "coupling",
				Title:       fmt.Sprintf("High fan-out coupling: %s", filepath.Base(pkg)),
				Description: fmt.Sprintf("Package '%s' imports %d packages (max: %d). Consider reducing dependencies.", pkg, fanOut, d.maxFanOut),
				File:        pkg,
				Line:        1,
				Severity:    quality.SeverityT3,
				Score:       fanOut - d.maxFanOut,
				Status:      quality.StatusOpen,
				Metadata: map[string]string{
					"package": pkg,
					"fan_out": fmt.Sprintf("%d", fanOut),
					"imports": strings.Join(imports, ","),
				},
			}
			findings = append(findings, finding)
		}
	}

	for pkg, importedBy := range pkgImportedBy {
		fanIn := len(importedBy)
		// Skip standard library packages from fan-in analysis
		if d.isStandardLibraryPackage(pkg) {
			continue
		}
		if fanIn > d.maxFanOut*3 { // Increased threshold for fan-in
			finding := quality.Finding{
				ID:          fmt.Sprintf("coupling_fanin::%s", pkg),
				Type:        "coupling",
				Title:       fmt.Sprintf("High fan-in coupling: %s", filepath.Base(pkg)),
				Description: fmt.Sprintf("Package '%s' is imported by %d packages. Ensure it's stable and well-documented.", pkg, fanIn),
				File:        pkg,
				Line:        1,
				Severity:    quality.SeverityT2,
				Score:       fanIn/10 - d.maxFanOut/10, // Reduced scoring
				Status:      quality.StatusOpen,
				Metadata: map[string]string{
					"package":     pkg,
					"fan_in":      fmt.Sprintf("%d", fanIn),
					"imported_by": strings.Join(importedBy, ","),
				},
			}
			findings = append(findings, finding)
		}
	}

	findings = append(findings, d.detectHubPackages(pkgImports, pkgImportedBy)...)

	return findings, nil
}

func (d *CouplingDetector) isStandardLibraryPackage(pkgPath string) bool {
	// Standard library packages that commonly have high fan-in
	standardLibs := []string{
		"fmt", "time", "strings", "context", "os", "io", "net/http",
		"encoding/json", "path/filepath", "sync", "math", "regexp",
	}

	for _, lib := range standardLibs {
		if strings.Contains(pkgPath, lib) && !strings.Contains(pkgPath, "github.com") {
			return true
		}
	}
	return false
}

func (d *CouplingDetector) detectHubPackages(pkgImports, pkgImportedBy map[string][]string) []quality.Finding {
	var findings []quality.Finding

	for pkg, imports := range pkgImports {
		importedBy := pkgImportedBy[pkg]
		centrality := len(imports) + len(importedBy)

		if centrality > d.maxFanOut*3 {
			finding := quality.Finding{
				ID:          fmt.Sprintf("coupling_hub::%s", pkg),
				Type:        "coupling",
				Title:       fmt.Sprintf("Hub package detected: %s", filepath.Base(pkg)),
				Description: fmt.Sprintf("Package '%s' is a coupling hub with %d connections (%d imports, %d imported by). Consider splitting.", pkg, centrality, len(imports), len(importedBy)),
				File:        pkg,
				Line:        1,
				Severity:    quality.SeverityT4,
				Score:       centrality / 5,
				Status:      quality.StatusOpen,
				Metadata: map[string]string{
					"package":    pkg,
					"centrality": fmt.Sprintf("%d", centrality),
					"fan_out":    fmt.Sprintf("%d", len(imports)),
					"fan_in":     fmt.Sprintf("%d", len(importedBy)),
				},
			}
			findings = append(findings, finding)
		}
	}

	return findings
}

type EnhancedDeadCodeDetector struct {
	*quality.BaseDetector
}

func NewEnhancedDeadCodeDetector(finder quality.FileFinder) *EnhancedDeadCodeDetector {
	return &EnhancedDeadCodeDetector{
		BaseDetector: quality.NewBaseDetector("dead_code_enhanced", quality.SeverityT2, finder),
	}
}

func (d *EnhancedDeadCodeDetector) Name() string {
	return "dead_code_enhanced"
}

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

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

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

	used := make(map[string]bool)
	defs := make(map[string]ObjInfo)
	entryPoints := make(map[string]bool)

	for _, pkg := range pkgs {
		if pkg.Name == "main" {
			for _, file := range pkg.Syntax {
				ast.Inspect(file, func(n ast.Node) bool {
					if fn, ok := n.(*ast.FuncDecl); ok {
						if fn.Name.Name == "main" || fn.Name.Name == "init" {
							entryPoints[pkg.PkgPath+"."+fn.Name.Name] = true
						}
					}
					return true
				})
			}
		}

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

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

			key := obj.Pkg().Path() + "." + obj.Name()
			pos := pkg.Fset.Position(obj.Pos())

			switch o := obj.(type) {
			case *types.Func:
				defs[key] = ObjInfo{
					Name:        obj.Name(),
					Type:        "function",
					File:        pos.Filename,
					Line:        pos.Line,
					Package:     obj.Pkg().Path(),
					PackageName: pkg.Name,
					Exported:    obj.Exported(),
					Signature:   o.Type().String(),
				}
			case *types.TypeName:
				defs[key] = ObjInfo{
					Name:        obj.Name(),
					Type:        "type",
					File:        pos.Filename,
					Line:        pos.Line,
					Package:     obj.Pkg().Path(),
					PackageName: pkg.Name,
					Exported:    obj.Exported(),
					Underlying:  o.Type().Underlying().String(),
				}
			case *types.Var:
				if obj.Exported() && !o.IsField() {
					defs[key] = ObjInfo{
						Name:        obj.Name(),
						Type:        "variable",
						File:        pos.Filename,
						Line:        pos.Line,
						Package:     obj.Pkg().Path(),
						PackageName: pkg.Name,
						Exported:    obj.Exported(),
					}
				}
			}
		}
	}

	testPkgs := make(map[string]bool)
	for _, pkg := range pkgs {
		if strings.HasSuffix(pkg.PkgPath, "_test") || strings.Contains(pkg.Name, "test") {
			testPkgs[pkg.PkgPath] = true
		}
		for _, file := range pkg.GoFiles {
			if strings.HasSuffix(file, "_test.go") {
				testPkgs[pkg.PkgPath] = true
			}
		}
	}

	var findings []quality.Finding
	for key, def := range defs {
		if entryPoints[key] {
			continue
		}
		if !strings.Contains(def.Package, "/internal/") || def.PackageName == "main" {
			continue
		}
		if isLikelyEntrypointFile(def.File) {
			continue
		}

		if strings.HasPrefix(def.Name, "Test") || strings.HasPrefix(def.Name, "Benchmark") || strings.HasPrefix(def.Name, "Fuzz") {
			continue
		}
		if def.Type == "function" && strings.HasPrefix(def.Name, "New") {
			continue
		}
		if def.Type == "type" && (strings.HasSuffix(def.Name, "Config") || strings.HasSuffix(def.Name, "Options")) {
			continue
		}

		if strings.HasSuffix(def.Name, "Error") && def.Type == "type" {
			continue
		}

		if strings.Contains(def.File, "_test.go") {
			continue
		}

		if !used[key] && def.Exported {
			severity := quality.SeverityT2
			score := 5

			if strings.HasSuffix(def.File, "/cmd/") || strings.Contains(def.File, "/cmd/") {
				severity = quality.SeverityT3
				score = 3
			}

			if def.Type == "type" {
				severity = quality.SeverityT3
				score = 4
			}

			finding := quality.Finding{
				ID:          fmt.Sprintf("dead_code::%s::%s", def.File, def.Name),
				Type:        "dead_code",
				Title:       fmt.Sprintf("Unused exported %s: %s", def.Type, def.Name),
				Description: fmt.Sprintf("The exported %s '%s' is never used. Consider removing it or if it's part of a public API, document it.", def.Type, def.Name),
				File:        def.File,
				Line:        def.Line,
				Severity:    severity,
				Score:       score,
				Status:      quality.StatusOpen,
				Metadata: map[string]string{
					"name":     def.Name,
					"obj_type": def.Type,
					"package":  def.Package,
					"exported": "true",
				},
			}
			findings = append(findings, finding)
		}
	}

	return findings, nil
}

type ObjInfo struct {
	Name        string
	Type        string
	File        string
	Line        int
	Package     string
	PackageName string
	Exported    bool
	Signature   string
	Underlying  string
}
