package goplugin

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

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

type GoPlugin struct{}

func New() *GoPlugin {
	return &GoPlugin{}
}

func (p *GoPlugin) Name() string {
	return "go"
}

func (p *GoPlugin) Extensions() []string {
	return []string{".go"}
}

func (p *GoPlugin) MarkerFiles() []string {
	return []string{"go.mod", "go.sum"}
}

func (p *GoPlugin) DefaultSrcDir() string {
	return "."
}

func (p *GoPlugin) CreateDetectors(finder quality.FileFinder) []quality.Detector {
	return []quality.Detector{
		analyzers.NewEnhancedDeadCodeDetector(finder),
		analyzers.NewUnusedImportDetector(finder),
		analyzers.NewCycleDetector(finder),
		analyzers.NewSecurityDetector(finder),
		analyzers.NewComplexityASTDetector(finder),
		analyzers.NewLargeFileDetector(finder),
		analyzers.NewGodStructDetector(finder),
		analyzers.NewGodFunctionDetector(finder),
		analyzers.NewDebugLogDetector(finder),
		analyzers.NewSingleUseDetector(finder),
		analyzers.NewCouplingDetector(finder),
		analyzers.NewTestCoverageDetector(finder),
		analyzers.NewUntestedFuncDetector(finder),
		analyzers.NewOrphanedFileDetector(finder),
		analyzers.NewDeprecatedUsageDetector(finder),
	}
}

func (p *GoPlugin) CreateFixers() []plugins.Fixer {
	return []plugins.Fixer{
		fixers.NewUnusedImportFixer(),
		fixers.NewFormattingFixer(),
		fixers.NewDeadCodeFixer(),
		fixers.NewComplexityHintFixer(),
		fixers.NewIoutilFixer(),
		fixers.NewDocCommentFixer(),
	}
}

func (p *GoPlugin) AnalyzeFile(ctx context.Context, path string, config *quality.Config) (*plugins.FileAnalysis, error) {
	fset := token.NewFileSet()

	node, err := parser.ParseFile(fset, path, nil, parser.ParseComments|parser.AllErrors)
	if err != nil {
		return nil, fmt.Errorf("parse error: %w", err)
	}

	analysis := &plugins.FileAnalysis{
		Path:    path,
		Package: node.Name.Name,
	}
	loc, err := countLOC(path)
	if err != nil {
		return nil, fmt.Errorf("count loc for %s: %w", path, err)
	}
	analysis.LOC = loc

	analysis.Imports = p.extractImports(node, fset)
	analysis.Functions = p.extractFunctions(node, path, fset)
	analysis.Types = p.extractTypes(node, path, fset)
	analysis.Variables = p.extractVariables(node, path, fset)
	analysis.Comments = p.extractComments(node, path, fset)

	return analysis, nil
}

func (p *GoPlugin) BuildDependencyGraph(ctx context.Context, rootPath string) (*plugins.DependencyGraph, error) {
	cfg := &packages.Config{
		Mode: packages.NeedName | packages.NeedImports | packages.NeedFiles,
		Dir:  rootPath,
	}

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

	graph := &plugins.DependencyGraph{
		Packages: make(map[string]*plugins.PackageNode),
		Edges:    []plugins.DependencyEdge{},
	}

	for _, pkg := range pkgs {
		node := &plugins.PackageNode{
			Name:    pkg.Name,
			Path:    pkg.PkgPath,
			Files:   pkg.GoFiles,
			IsLocal: true,
		}

		for _, imp := range pkg.Imports {
			node.Imports = append(node.Imports, imp.PkgPath)
			graph.Edges = append(graph.Edges, plugins.DependencyEdge{
				From: pkg.PkgPath,
				To:   imp.PkgPath,
				Type: plugins.EdgeTypeImport,
			})
		}

		graph.Packages[pkg.PkgPath] = node
	}

	graph.Cycles = p.detectCycles(graph)

	return graph, nil
}

func (p *GoPlugin) extractImports(node *ast.File, fset *token.FileSet) []plugins.ImportInfo {
	var imports []plugins.ImportInfo
	for _, imp := range node.Imports {
		info := plugins.ImportInfo{
			Path: strings.Trim(imp.Path.Value, `"`),
			Line: fset.Position(imp.Pos()).Line,
		}
		if imp.Name != nil {
			info.Alias = imp.Name.Name
		}
		imports = append(imports, info)
	}
	return imports
}

func (p *GoPlugin) extractFunctions(node *ast.File, path string, fset *token.FileSet) []quality.FunctionInfo {
	var functions []quality.FunctionInfo

	for _, decl := range node.Decls {
		fn, ok := decl.(*ast.FuncDecl)
		if !ok {
			continue
		}

		info := quality.FunctionInfo{
			Name:    fn.Name.Name,
			File:    path,
			Line:    fset.Position(fn.Pos()).Line,
			EndLine: fset.Position(fn.End()).Line,
		}

		info.LOC = info.EndLine - info.Line + 1

		var params []string
		if fn.Type.Params != nil {
			for _, field := range fn.Type.Params.List {
				for _, name := range field.Names {
					params = append(params, name.Name)
				}
			}
		}
		info.Params = params

		if fn.Type.Results != nil {
			info.ReturnAnnotation = fmt.Sprintf("%v", fn.Type.Results)
		}

		functions = append(functions, info)
	}

	return functions
}

func (p *GoPlugin) extractTypes(node *ast.File, path string, fset *token.FileSet) []plugins.TypeInfo {
	var typeInfos []plugins.TypeInfo

	for _, decl := range node.Decls {
		gen, ok := decl.(*ast.GenDecl)
		if !ok || gen.Tok != token.TYPE {
			continue
		}

		for _, spec := range gen.Specs {
			typeSpec, ok := spec.(*ast.TypeSpec)
			if !ok {
				continue
			}

			info := plugins.TypeInfo{
				Name:       typeSpec.Name.Name,
				File:       path,
				Line:       fset.Position(typeSpec.Pos()).Line,
				IsExported: ast.IsExported(typeSpec.Name.Name),
			}

			switch t := typeSpec.Type.(type) {
			case *ast.StructType:
				info.Underlying = "struct"
			case *ast.InterfaceType:
				info.Underlying = "interface"
			default:
				info.Underlying = fmt.Sprintf("%T", t)
			}

			typeInfos = append(typeInfos, info)
		}
	}

	return typeInfos
}

func (p *GoPlugin) extractVariables(node *ast.File, path string, fset *token.FileSet) []plugins.VariableInfo {
	var variables []plugins.VariableInfo

	for _, decl := range node.Decls {
		gen, ok := decl.(*ast.GenDecl)
		if !ok || (gen.Tok != token.VAR && gen.Tok != token.CONST) {
			continue
		}

		for _, spec := range gen.Specs {
			valueSpec, ok := spec.(*ast.ValueSpec)
			if !ok {
				continue
			}

			for _, name := range valueSpec.Names {
				info := plugins.VariableInfo{
					Name:       name.Name,
					File:       path,
					Line:       fset.Position(name.Pos()).Line,
					IsExported: ast.IsExported(name.Name),
				}

				if valueSpec.Type != nil {
					info.Type = fmt.Sprintf("%v", valueSpec.Type)
				}

				variables = append(variables, info)
			}
		}
	}

	return variables
}

func (p *GoPlugin) extractComments(node *ast.File, path string, fset *token.FileSet) []plugins.CommentInfo {
	var comments []plugins.CommentInfo

	for _, group := range node.Comments {
		for _, comment := range group.List {
			info := plugins.CommentInfo{
				Text:  comment.Text,
				File:  path,
				Line:  fset.Position(comment.Pos()).Line,
				IsDoc: strings.HasPrefix(comment.Text, "//"),
			}
			comments = append(comments, info)
		}
	}

	return comments
}

func (p *GoPlugin) detectCycles(graph *plugins.DependencyGraph) [][]string {
	var cycles [][]string
	visited := make(map[string]bool)
	recStack := make(map[string]bool)
	path := []string{}

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

		node, exists := graph.Packages[pkg]
		if !exists {
			return false
		}

		for _, imp := range node.Imports {
			if !visited[imp] {
				if dfs(imp) {
					return true
				}
			} else if recStack[imp] {
				cycleStart := -1
				for i, p := range path {
					if p == imp {
						cycleStart = i
						break
					}
				}
				if cycleStart >= 0 {
					cycle := make([]string, len(path)-cycleStart)
					copy(cycle, path[cycleStart:])
					cycles = append(cycles, cycle)
				}
			}
		}

		path = path[:len(path)-1]
		recStack[pkg] = false
		return false
	}

	for pkg := range graph.Packages {
		if !visited[pkg] {
			dfs(pkg)
		}
	}

	return cycles
}

func (p *GoPlugin) LoadTypesInfo(ctx context.Context, path string) (*types.Info, *token.FileSet, error) {
	cfg := &packages.Config{
		Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo,
		Dir:  filepath.Dir(path),
	}

	pkgs, err := packages.Load(cfg, filepath.Base(path))
	if err != nil {
		return nil, nil, err
	}

	if len(pkgs) == 0 {
		return nil, nil, fmt.Errorf("no packages found")
	}

	return pkgs[0].TypesInfo, pkgs[0].Fset, nil
}

func countLOC(path string) (int, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return 0, fmt.Errorf("read file for loc %q: %w", path, err)
	}
	return strings.Count(string(data), "\n") + 1, nil
}

var pluginRegistrationErr error

// RegistrationError returns a plugin registration error captured during init, if any.
func RegistrationError() error {
	return pluginRegistrationErr
}

func init() {
	if err := plugins.Register(New()); err != nil {
		pluginRegistrationErr = fmt.Errorf("register go quality plugin: %w", err)
		_, _ = fmt.Fprintf(os.Stderr, "warning: %v\n", pluginRegistrationErr)
	}
}
