package golang

import (
	"context"
	"go/ast"
	"go/parser"
	"go/printer"
	"go/token"
	"sort"
	"strings"

	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/extractor"
	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/scanner"
)

type Extractor struct{}

func New() Extractor { return Extractor{} }

func (Extractor) Extract(ctx context.Context, file scanner.FileInfo) (extractor.StructuralAnalysis, error) {
	if err := ctx.Err(); err != nil {
		return extractor.StructuralAnalysis{}, err
	}
	fset := token.NewFileSet()
	parsed, err := parser.ParseFile(fset, file.AbsPath, nil, parser.ParseComments)
	if err != nil {
		return extractor.StructuralAnalysis{}, err
	}
	analysis := extractor.StructuralAnalysis{
		FilePath:              file.Path,
		Language:              file.Language,
		ContentHash:           file.ContentHash,
		TotalLines:            file.Lines,
		HasStructuralAnalysis: true,
		PackageName:           parsed.Name.Name,
	}
	analysis.Imports = collectImports(parsed)
	typesByName := map[string]int{}
	for _, decl := range parsed.Decls {
		switch decl := decl.(type) {
		case *ast.GenDecl:
			if decl.Tok != token.TYPE {
				continue
			}
			for _, spec := range decl.Specs {
				typeSpec, ok := spec.(*ast.TypeSpec)
				if !ok {
					continue
				}
				kind := typeKind(typeSpec.Type)
				typ := extractor.Type{
					Name:       typeSpec.Name.Name,
					Kind:       kind,
					Exported:   typeSpec.Name.IsExported(),
					LineRange:  lineRange(fset, typeSpec.Pos(), typeSpec.End()),
					DocComment: typeDocComment(decl, typeSpec),
				}
				typesByName[typ.Name] = len(analysis.Types)
				analysis.Types = append(analysis.Types, typ)
				if typ.Exported {
					analysis.Exports = append(analysis.Exports, typ.Name)
				}
			}
		case *ast.FuncDecl:
			fn := extractor.Function{
				Name:       decl.Name.Name,
				Receiver:   receiverName(decl.Recv),
				Params:     fieldTypes(decl.Type.Params),
				ReturnType: resultTypes(decl.Type.Results),
				Exported:   decl.Name.IsExported(),
				LineRange:  lineRange(fset, decl.Pos(), decl.End()),
				DocComment: docText(decl.Doc),
			}
			analysis.Functions = append(analysis.Functions, fn)
			if fn.Exported {
				analysis.Exports = append(analysis.Exports, fn.Name)
			}
			if fn.Receiver != "" {
				if idx, ok := typesByName[fn.Receiver]; ok {
					analysis.Types[idx].Methods = append(analysis.Types[idx].Methods, fn.Name)
				}
			}
			analysis.Calls = append(analysis.Calls, collectCalls(fset, fn, decl.Body)...)
		}
	}
	sort.Strings(analysis.Exports)
	for i := range analysis.Types {
		sort.Strings(analysis.Types[i].Methods)
	}
	return analysis, nil
}

func typeDocComment(decl *ast.GenDecl, spec *ast.TypeSpec) string {
	if spec.Doc != nil {
		return docText(spec.Doc)
	}
	return docText(decl.Doc)
}

func docText(group *ast.CommentGroup) string {
	if group == nil {
		return ""
	}
	return strings.TrimSpace(group.Text())
}

func collectImports(file *ast.File) []extractor.Import {
	imports := make([]extractor.Import, 0, len(file.Imports))
	for _, spec := range file.Imports {
		path := strings.Trim(spec.Path.Value, "\"")
		alias := ""
		if spec.Name != nil {
			alias = spec.Name.Name
		}
		imports = append(imports, extractor.Import{Path: path, Alias: alias})
	}
	sort.Slice(imports, func(i, j int) bool { return imports[i].Path < imports[j].Path })
	return imports
}

func collectCalls(fset *token.FileSet, fn extractor.Function, body *ast.BlockStmt) []extractor.Call {
	if body == nil {
		return nil
	}
	calls := []extractor.Call{}
	ast.Inspect(body, func(node ast.Node) bool {
		call, ok := node.(*ast.CallExpr)
		if !ok {
			return true
		}
		name := callName(call.Fun)
		if name == "" {
			return true
		}
		calls = append(calls, extractor.Call{Caller: fn.Name, Callee: name, Line: fset.Position(call.Pos()).Line})
		return true
	})
	return calls
}

func callName(expr ast.Expr) string {
	switch expr := expr.(type) {
	case *ast.Ident:
		return expr.Name
	case *ast.SelectorExpr:
		return expr.Sel.Name
	}
	return ""
}

func typeKind(expr ast.Expr) string {
	switch expr.(type) {
	case *ast.StructType:
		return "struct"
	case *ast.InterfaceType:
		return "interface"
	default:
		return "type"
	}
}

func receiverName(fields *ast.FieldList) string {
	if fields == nil || len(fields.List) == 0 {
		return ""
	}
	return receiverTypeName(fields.List[0].Type)
}

func receiverTypeName(expr ast.Expr) string {
	switch expr := expr.(type) {
	case *ast.Ident:
		return expr.Name
	case *ast.StarExpr:
		return receiverTypeName(expr.X)
	}
	return ""
}

func fieldTypes(fields *ast.FieldList) []string {
	if fields == nil {
		return nil
	}
	values := []string{}
	for _, field := range fields.List {
		value := exprString(field.Type)
		if len(field.Names) == 0 {
			values = append(values, value)
			continue
		}
		for range field.Names {
			values = append(values, value)
		}
	}
	return values
}

func resultTypes(fields *ast.FieldList) string {
	values := fieldTypes(fields)
	return strings.Join(values, ",")
}

func exprString(expr ast.Expr) string {
	if expr == nil {
		return ""
	}
	var b strings.Builder
	if err := printer.Fprint(&b, token.NewFileSet(), expr); err != nil {
		return ""
	}
	return b.String()
}

func lineRange(fset *token.FileSet, start token.Pos, end token.Pos) [2]int {
	return [2]int{fset.Position(start).Line, fset.Position(end).Line}
}
