package builtin

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"

	"reasonix/internal/tool"
)

func init() { tool.RegisterBuiltin(codeIndex{}) }

type codeIndex struct {
	workDir     string
	forbidRoots []string
}

func (codeIndex) Name() string { return "code_index" }

func (codeIndex) Description() string {
	return "Lightweight built-in code symbol index. Prefer lsp_* for language semantics and installed code graph MCP tools for call graph, impact, and architecture relationships; use this as the local fallback for file outlines and symbol definition candidates, then verify with read_file or grep."
}

func (codeIndex) Schema() json.RawMessage {
	return json.RawMessage(`{
"type":"object",
"properties":{
  "action":{"type":"string","enum":["outline","search"],"description":"outline lists symbols under path; search finds symbol definition candidates by name."},
  "path":{"type":"string","description":"File or directory path to inspect (default \".\")."},
  "query":{"type":"string","description":"Symbol name or substring for action=search."},
  "kind":{"type":"string","description":"Optional symbol kind filter, such as func, method, class, type, interface, const, var, struct, enum, trait."},
  "limit":{"type":"integer","description":"Maximum symbols to return (default 100, max 200).","minimum":1}
},
"required":["action"]
}`)
}

func (codeIndex) ReadOnly() bool { return true }

const (
	codeIndexDefaultLimit = 100
	codeIndexMaxLimit     = 200
	codeIndexMaxFileSize  = 1 << 20
	codeIndexMaxFiles     = 2000
)

type codeIndexArgs struct {
	Action string `json:"action"`
	Path   string `json:"path"`
	Query  string `json:"query"`
	Kind   string `json:"kind"`
	Limit  int    `json:"limit"`
}

type codeSymbol struct {
	Name      string
	Kind      string
	File      string
	Line      int
	Parent    string
	Signature string
}

func (c codeIndex) Execute(ctx context.Context, args json.RawMessage) (string, error) {
	p := codeIndexArgs{Path: ".", Limit: codeIndexDefaultLimit}
	if err := json.Unmarshal(args, &p); err != nil {
		return "", fmt.Errorf("invalid args: %w", err)
	}
	p.Action = strings.ToLower(strings.TrimSpace(p.Action))
	if p.Action != "outline" && p.Action != "search" {
		return "", fmt.Errorf("action must be outline or search")
	}
	if p.Path == "" {
		p.Path = "."
	}
	if p.Limit <= 0 {
		p.Limit = codeIndexDefaultLimit
	}
	if p.Limit > codeIndexMaxLimit {
		p.Limit = codeIndexMaxLimit
	}
	if p.Action == "search" && strings.TrimSpace(p.Query) == "" {
		return "", fmt.Errorf("query is required for action=search")
	}

	root := resolveIn(c.workDir, p.Path)
	collectionLimit := p.Limit
	if p.Action == "outline" && hasCodeSymbolFilter(p) {
		collectionLimit = 0
	}
	symbols, truncated, err := c.collect(ctx, root, collectionLimit, p.Action == "outline")
	if err != nil {
		return "", err
	}
	symbols = filterCodeSymbols(symbols, p)
	if len(symbols) > p.Limit {
		symbols = symbols[:p.Limit]
		truncated = true
	}
	return formatCodeSymbols(symbols, truncated), nil
}

func (c codeIndex) collect(ctx context.Context, root string, limit int, outline bool) ([]codeSymbol, bool, error) {
	if confineRead(c.forbidRoots, root) {
		return nil, false, nil
	}
	info, err := os.Stat(root)
	if err != nil {
		return nil, false, fmt.Errorf("code_index %s: %w", root, err)
	}
	var files []string
	if !info.IsDir() {
		if supportedCodeIndexFile(root) {
			files = append(files, root)
		}
	} else {
		err = filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
			if ctx.Err() != nil {
				return ctx.Err()
			}
			if walkErr != nil {
				return nil
			}
			if d.IsDir() {
				if skipForbidDir(path, c.forbidRoots) {
					return filepath.SkipDir
				}
				if path != root && skipCodeIndexDir(d.Name()) {
					return filepath.SkipDir
				}
				return nil
			}
			if supportedCodeIndexFile(path) && !confineRead(c.forbidRoots, path) {
				files = append(files, path)
				if len(files) >= codeIndexMaxFiles {
					return filepath.SkipAll
				}
			}
			return nil
		})
		if err != nil {
			return nil, false, fmt.Errorf("code_index walk %s: %w", root, err)
		}
	}
	sort.Strings(files)

	var symbols []codeSymbol
	truncated := len(files) >= codeIndexMaxFiles
	for _, file := range files {
		if ctx.Err() != nil {
			return nil, truncated, ctx.Err()
		}
		found, err := c.parseFile(file)
		if err != nil {
			continue
		}
		symbols = append(symbols, found...)
		if outline && limit > 0 && len(symbols) >= limit {
			truncated = true
			break
		}
	}
	sort.Slice(symbols, func(i, j int) bool {
		if symbols[i].File != symbols[j].File {
			return symbols[i].File < symbols[j].File
		}
		if symbols[i].Line != symbols[j].Line {
			return symbols[i].Line < symbols[j].Line
		}
		return symbols[i].Name < symbols[j].Name
	})
	return symbols, truncated, nil
}

func (c codeIndex) parseFile(path string) ([]codeSymbol, error) {
	if info, err := os.Stat(path); err != nil || info.Size() > codeIndexMaxFileSize {
		if err != nil {
			return nil, err
		}
		return nil, fmt.Errorf("file too large")
	}
	if filepath.Ext(path) == ".go" {
		return c.parseGo(path)
	}
	if treeSymbols, ok, err := c.parseTreeSitter(path); ok && err == nil {
		textSymbols, _ := c.parseText(path)
		return mergeCodeSymbols(treeSymbols, textSymbols), nil
	}
	return c.parseText(path)
}

func (c codeIndex) parseGo(path string) ([]codeSymbol, error) {
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, path, nil, parser.SkipObjectResolution)
	if err != nil {
		return nil, err
	}
	var out []codeSymbol
	add := func(name, kind string, pos token.Pos, parent, sig string) {
		if name == "" {
			return
		}
		out = append(out, codeSymbol{
			Name:      name,
			Kind:      kind,
			File:      c.displayPath(path),
			Line:      fset.Position(pos).Line,
			Parent:    parent,
			Signature: sig,
		})
	}
	for _, decl := range f.Decls {
		switch d := decl.(type) {
		case *ast.FuncDecl:
			parent := ""
			kind := "func"
			if d.Recv != nil && len(d.Recv.List) > 0 {
				parent = exprName(d.Recv.List[0].Type)
				kind = "method"
			}
			add(d.Name.Name, kind, d.Name.Pos(), parent, goFuncSignature(d, parent))
		case *ast.GenDecl:
			for _, spec := range d.Specs {
				switch s := spec.(type) {
				case *ast.TypeSpec:
					kind := "type"
					switch s.Type.(type) {
					case *ast.StructType:
						kind = "struct"
					case *ast.InterfaceType:
						kind = "interface"
					}
					add(s.Name.Name, kind, s.Name.Pos(), "", kind+" "+s.Name.Name)
				case *ast.ValueSpec:
					kind := strings.ToLower(d.Tok.String())
					for _, name := range s.Names {
						add(name.Name, kind, name.Pos(), "", kind+" "+name.Name)
					}
				}
			}
		}
	}
	return out, nil
}

func (c codeIndex) parseText(path string) ([]codeSymbol, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var out []codeSymbol
	sc := bufio.NewScanner(f)
	line := 0
	for sc.Scan() {
		line++
		text := sc.Text()
		for _, m := range codeIndexMatchers(filepath.Ext(path)) {
			if sym, ok := m.match(text); ok {
				sym.File = c.displayPath(path)
				sym.Line = line
				out = append(out, sym)
				break
			}
		}
	}
	if err := sc.Err(); err != nil {
		return nil, err
	}
	return out, nil
}

func (c codeIndex) displayPath(path string) string {
	if c.workDir != "" {
		if rel, err := filepath.Rel(c.workDir, path); err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
			return filepath.ToSlash(rel)
		}
	}
	return filepath.ToSlash(path)
}

func hasCodeSymbolFilter(p codeIndexArgs) bool {
	return strings.TrimSpace(p.Kind) != "" || strings.TrimSpace(p.Query) != ""
}

func filterCodeSymbols(in []codeSymbol, p codeIndexArgs) []codeSymbol {
	query := strings.ToLower(strings.TrimSpace(p.Query))
	kind := strings.ToLower(strings.TrimSpace(p.Kind))
	out := make([]codeSymbol, 0, len(in))
	for _, s := range in {
		if kind != "" && strings.ToLower(s.Kind) != kind {
			continue
		}
		if query != "" {
			haystack := strings.ToLower(s.Name + " " + s.Parent + " " + s.Signature)
			if !strings.Contains(haystack, query) {
				continue
			}
		}
		out = append(out, s)
	}
	return out
}

func formatCodeSymbols(symbols []codeSymbol, truncated bool) string {
	if len(symbols) == 0 {
		return "(no symbols)"
	}
	var b strings.Builder
	for _, s := range symbols {
		name := s.Name
		if s.Parent != "" {
			name = s.Parent + "." + name
		}
		if s.Signature != "" {
			fmt.Fprintf(&b, "%s:%d: %s %s — %s\n", s.File, s.Line, s.Kind, name, s.Signature)
		} else {
			fmt.Fprintf(&b, "%s:%d: %s %s\n", s.File, s.Line, s.Kind, name)
		}
	}
	if truncated {
		b.WriteString("... (truncated; narrow path/query/kind or raise limit)\n")
	}
	return strings.TrimRight(b.String(), "\n")
}

func mergeCodeSymbols(primary, fallback []codeSymbol) []codeSymbol {
	if len(primary) == 0 {
		return fallback
	}
	if len(fallback) == 0 {
		return primary
	}
	out := append([]codeSymbol(nil), primary...)
	seen := make(map[string]struct{}, len(primary)+len(fallback))
	for _, s := range primary {
		seen[codeSymbolIdentity(s)] = struct{}{}
	}
	for _, s := range fallback {
		key := codeSymbolIdentity(s)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, s)
	}
	return out
}

func codeSymbolIdentity(s codeSymbol) string {
	return fmt.Sprintf("%s:%d:%s:%s:%s", s.File, s.Line, s.Kind, s.Parent, s.Name)
}

func supportedCodeIndexFile(path string) bool {
	switch filepath.Ext(path) {
	case ".go", ".js", ".jsx", ".ts", ".tsx", ".py", ".java", ".kt", ".kts", ".cs", ".rs", ".c", ".cc", ".cpp", ".h", ".hpp":
		return true
	default:
		return false
	}
}

func skipCodeIndexDir(name string) bool {
	switch name {
	case ".git", "node_modules", "vendor", "__pycache__", ".idea", ".vscode", ".next", "dist", "build", "target", "coverage":
		return true
	default:
		return false
	}
}

func exprName(expr ast.Expr) string {
	switch t := expr.(type) {
	case *ast.Ident:
		return t.Name
	case *ast.StarExpr:
		return exprName(t.X)
	case *ast.SelectorExpr:
		return exprName(t.X) + "." + t.Sel.Name
	default:
		return ""
	}
}

func goFuncSignature(fn *ast.FuncDecl, parent string) string {
	if parent == "" {
		return "func " + fn.Name.Name
	}
	return "func (" + parent + ") " + fn.Name.Name
}

type codeIndexMatcher struct {
	kind      string
	re        *regexp.Regexp
	nameGroup int
	kindGroup int
}

func (m codeIndexMatcher) match(line string) (codeSymbol, bool) {
	match := m.re.FindStringSubmatch(line)
	if match == nil || m.nameGroup >= len(match) {
		return codeSymbol{}, false
	}
	name := strings.TrimSpace(match[m.nameGroup])
	if name == "" {
		return codeSymbol{}, false
	}
	kind := m.kind
	if m.kindGroup > 0 && m.kindGroup < len(match) && strings.TrimSpace(match[m.kindGroup]) != "" {
		kind = normalizeCodeIndexKind(match[m.kindGroup])
	}
	return codeSymbol{Name: name, Kind: kind, Signature: strings.TrimSpace(line)}, true
}

func normalizeCodeIndexKind(kind string) string {
	kind = strings.ToLower(strings.TrimSpace(kind))
	switch kind {
	case "function":
		return "func"
	default:
		return kind
	}
}

var (
	rePyClass     = regexp.MustCompile(`^\s*class\s+([A-Za-z_][A-Za-z0-9_]*)\b`)
	rePyFunc      = regexp.MustCompile(`^\s*(?:async\s+)?def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(`)
	reJSClass     = regexp.MustCompile(`^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)\b`)
	reJSFunc      = regexp.MustCompile(`^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(`)
	reJSInterface = regexp.MustCompile(`^\s*(?:export\s+)?interface\s+([A-Za-z_$][A-Za-z0-9_$]*)\b`)
	reJSType      = regexp.MustCompile(`^\s*(?:export\s+)?type\s+([A-Za-z_$][A-Za-z0-9_$]*)\b`)
	reJSEnum      = regexp.MustCompile(`^\s*(?:export\s+)?enum\s+([A-Za-z_$][A-Za-z0-9_$]*)\b`)
	reJSArrow     = regexp.MustCompile(`^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>`)
	reJavaType    = regexp.MustCompile(`^\s*(?:public|protected|private|abstract|final|static|sealed|data|\s)*\s*(class|interface|enum|record|object)\s+([A-Za-z_][A-Za-z0-9_]*)\b`)
	reJavaMeth    = regexp.MustCompile(`^\s*(?:public|protected|private|static|final|abstract|synchronized|native|\s)+[\w<>\[\], ?]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*\([^;]*\)\s*(?:\{|$)`)
	reRustItem    = regexp.MustCompile(`^\s*(?:pub(?:\([^)]*\))?\s+)?(fn|struct|enum|trait)\s+([A-Za-z_][A-Za-z0-9_]*)\b`)
	reCFunc       = regexp.MustCompile(`^\s*(?:[\w:*&<>\[\],]+\s+)+([A-Za-z_][A-Za-z0-9_]*)\s*\([^;]*\)\s*(?:\{|$)`)
)

func codeIndexMatchers(ext string) []codeIndexMatcher {
	switch ext {
	case ".py":
		return []codeIndexMatcher{{kind: "class", re: rePyClass, nameGroup: 1}, {kind: "func", re: rePyFunc, nameGroup: 1}}
	case ".js", ".jsx", ".ts", ".tsx":
		return []codeIndexMatcher{
			{kind: "class", re: reJSClass, nameGroup: 1},
			{kind: "func", re: reJSFunc, nameGroup: 1},
			{kind: "interface", re: reJSInterface, nameGroup: 1},
			{kind: "type", re: reJSType, nameGroup: 1},
			{kind: "enum", re: reJSEnum, nameGroup: 1},
			{kind: "func", re: reJSArrow, nameGroup: 1},
		}
	case ".java", ".kt", ".kts", ".cs":
		return []codeIndexMatcher{{re: reJavaType, nameGroup: 2, kindGroup: 1}, {kind: "method", re: reJavaMeth, nameGroup: 1}}
	case ".rs":
		return []codeIndexMatcher{{re: reRustItem, nameGroup: 2, kindGroup: 1}}
	case ".c", ".cc", ".cpp", ".h", ".hpp":
		return []codeIndexMatcher{{kind: "func", re: reCFunc, nameGroup: 1}}
	default:
		return nil
	}
}
