package languages

import (
	"strings"

	"github.com/zzet/gortex/internal/graph"
	"github.com/zzet/gortex/internal/parser"
	sitter "github.com/zzet/gortex/internal/parser/tsitter"
	"github.com/zzet/gortex/internal/parser/tsitter/elixir"
)

// elixirKeywords are call targets that represent language constructs, not user calls.
var elixirKeywords = map[string]bool{
	"defmodule": true, "def": true, "defp": true,
	"import": true, "alias": true, "use": true, "require": true,
	"defmacro": true, "defmacrop": true, "defguard": true,
	"defstruct": true, "defprotocol": true, "defimpl": true,
	"defdelegate": true, "defexception": true, "defoverridable": true,
	"test": true, "describe": true, "setup": true,
}

// ElixirExtractor extracts Elixir source files. Elixir's grammar
// represents nearly everything as `call` nodes, so extraction is a
// single manual walkNode cursor: structural constructs (modules,
// defs, imports, attributes) are dispatched as the walk descends, and
// call sites inside each def body are collected in that same pass.
type ElixirExtractor struct {
	lang *sitter.Language
}

func NewElixirExtractor() *ElixirExtractor {
	return &ElixirExtractor{lang: elixir.GetLanguage()}
}

func (e *ElixirExtractor) Language() string     { return "elixir" }
func (e *ElixirExtractor) Extensions() []string { return []string{".ex", ".exs"} }

func (e *ElixirExtractor) Extract(filePath string, src []byte) (*parser.ExtractionResult, error) {
	tree, err := parser.ParseFile(src, e.lang)
	if err != nil {
		return nil, err
	}
	defer tree.Close()

	root := tree.RootNode()
	result := &parser.ExtractionResult{}

	fileNode := &graph.Node{
		ID: filePath, Kind: graph.KindFile, Name: filePath,
		FilePath: filePath, StartLine: 1, EndLine: int(root.EndPoint().Row) + 1,
		Language: "elixir",
	}
	result.Nodes = append(result.Nodes, fileNode)

	seen := make(map[string]bool)

	// Walk the AST manually to handle Elixir's call-based structure.
	// A single cursor: structural constructs are extracted as the walk
	// descends and call sites are collected from each def body.
	e.walkNode(root, src, filePath, fileNode.ID, "", result, seen)

	return result, nil
}

// walkNode recursively walks the AST to extract modules, functions, imports, and attributes.
func (e *ElixirExtractor) walkNode(node *sitter.Node, src []byte, filePath, fileID, currentModule string, result *parser.ExtractionResult, seen map[string]bool) {
	if node == nil {
		return
	}

	if node.Type() == "call" {
		target := e.getCallTarget(node, src)
		switch target {
		case "defmodule":
			e.handleDefmodule(node, src, filePath, fileID, result, seen)
			return // handleDefmodule recurses into the body
		case "def", "defp":
			e.handleDef(node, src, filePath, fileID, currentModule, target == "defp", result, seen)
			return
		case "import", "alias", "use", "require":
			e.handleImport(node, src, filePath, fileID, target, result)
		}
	}

	// Handle module attributes: @attr value
	if node.Type() == "unary_operator" {
		e.handleAttribute(node, src, filePath, fileID, currentModule, result, seen)
	}

	// Recurse into children.
	for i := 0; i < int(node.ChildCount()); i++ {
		child := node.Child(i)
		e.walkNode(child, src, filePath, fileID, currentModule, result, seen)
	}
}

// getCallTarget returns the identifier name of a call's target, or "".
func (e *ElixirExtractor) getCallTarget(callNode *sitter.Node, src []byte) string {
	for i := 0; i < int(callNode.ChildCount()); i++ {
		child := callNode.Child(i)
		if callNode.FieldNameForChild(i) == "target" && child.Type() == "identifier" {
			return child.Content(src)
		}
	}
	return ""
}

// handleDefmodule extracts a module node and recurses into its body.
func (e *ElixirExtractor) handleDefmodule(callNode *sitter.Node, src []byte, filePath, fileID string, result *parser.ExtractionResult, seen map[string]bool) {
	modName := e.extractModuleName(callNode, src)
	if modName == "" {
		return
	}

	id := filePath + "::" + modName
	if seen[id] {
		return
	}
	seen[id] = true

	result.Nodes = append(result.Nodes, &graph.Node{
		ID: id, Kind: graph.KindType, Name: modName,
		FilePath: filePath, StartLine: int(callNode.StartPoint().Row) + 1,
		EndLine: int(callNode.EndPoint().Row) + 1, Language: "elixir",
	})
	result.Edges = append(result.Edges, &graph.Edge{
		From: fileID, To: id, Kind: graph.EdgeDefines,
		FilePath: filePath, Line: int(callNode.StartPoint().Row) + 1,
	})

	// Walk children with module context so functions become methods.
	body := e.findDoBlock(callNode)
	if body != nil {
		for i := 0; i < int(body.ChildCount()); i++ {
			e.walkNode(body.Child(i), src, filePath, fileID, modName, result, seen)
		}
		// Phoenix plug dispatch: `plug :name` (optionally with
		// `when action in [...]`) declares a middleware that fires
		// before each action. Emit one EdgeCalls per (action, plug)
		// pair after the body walk so defs are already registered.
		e.emitPhoenixPlugBindings(body, src, filePath, modName, result)
		// Ecto model attribution: `schema "name" do ... end` macro
		// → EdgeModelsTable to a synthetic KindTable node.
		detectEcto(body, src, id, modName, filePath, result)
		// HEEx component attribution: `~H"""..."""` sigils render
		// uppercase-first-letter or dot-prefixed components.
		emitElixirHEExEdges(id, body, src, filePath, result)
	}
}

// handleDef extracts a function or method node, then collects the
// call sites inside its body in the same walk pass.
func (e *ElixirExtractor) handleDef(callNode *sitter.Node, src []byte, filePath, fileID, currentModule string, isPrivate bool, result *parser.ExtractionResult, seen map[string]bool) {
	funcName := e.extractFuncName(callNode, src)
	if funcName == "" {
		return
	}

	startLine := int(callNode.StartPoint().Row) + 1
	endLine := int(callNode.EndPoint().Row) + 1

	var id string
	if currentModule != "" {
		// Function inside a module -> method with MemberOf edge.
		id = filePath + "::" + currentModule + "." + funcName
		if seen[id] {
			return
		}
		seen[id] = true

		meta := map[string]any{
			"receiver":  currentModule,
			"signature": "def " + funcName + "(...)",
		}
		if isPrivate {
			meta["visibility"] = "private"
			meta["signature"] = "defp " + funcName + "(...)"
		}

		result.Nodes = append(result.Nodes, &graph.Node{
			ID: id, Kind: graph.KindMethod, Name: funcName,
			FilePath: filePath, StartLine: startLine, EndLine: endLine,
			Language: "elixir", Meta: meta,
		})
		result.Edges = append(result.Edges, &graph.Edge{
			From: fileID, To: id, Kind: graph.EdgeDefines,
			FilePath: filePath, Line: startLine,
		})
		typeID := filePath + "::" + currentModule
		result.Edges = append(result.Edges, &graph.Edge{
			From: id, To: typeID, Kind: graph.EdgeMemberOf,
			FilePath: filePath, Line: startLine,
		})
	} else {
		// Top-level function.
		id = filePath + "::" + funcName
		if seen[id] {
			return
		}
		seen[id] = true

		meta := map[string]any{"signature": "def " + funcName + "(...)"}
		if isPrivate {
			meta["visibility"] = "private"
			meta["signature"] = "defp " + funcName + "(...)"
		}

		result.Nodes = append(result.Nodes, &graph.Node{
			ID: id, Kind: graph.KindFunction, Name: funcName,
			FilePath: filePath, StartLine: startLine, EndLine: endLine,
			Language: "elixir", Meta: meta,
		})
		result.Edges = append(result.Edges, &graph.Edge{
			From: fileID, To: id, Kind: graph.EdgeDefines,
			FilePath: filePath, Line: startLine,
		})
	}

	// Call sites inside the body are attributed to this def directly —
	// exact attribution without a line-range lookup. walkNode does not
	// descend into def bodies, so the body region is scanned here and
	// every other region by walkNode: one cursor over the whole tree.
	if body := e.findDoBlock(callNode); body != nil {
		e.collectCalls(body, src, filePath, id, result)
	}
}

// handleImport extracts import/alias/use/require edges.
func (e *ElixirExtractor) handleImport(callNode *sitter.Node, src []byte, filePath, fileID, keyword string, result *parser.ExtractionResult) {
	modName := e.extractFirstArgText(callNode, src)
	if modName == "" {
		return
	}
	result.Edges = append(result.Edges, &graph.Edge{
		From: fileID, To: "unresolved::import::" + modName,
		Kind: graph.EdgeImports, FilePath: filePath,
		Line: int(callNode.StartPoint().Row) + 1,
	})
}

// handleAttribute extracts module attributes (@attr value) as variables.
func (e *ElixirExtractor) handleAttribute(node *sitter.Node, src []byte, filePath, fileID, currentModule string, result *parser.ExtractionResult, seen map[string]bool) {
	if node.Type() != "unary_operator" {
		return
	}
	// Check if operator is "@".
	opText := ""
	for i := 0; i < int(node.ChildCount()); i++ {
		child := node.Child(i)
		if child.Type() == "@" || (node.FieldNameForChild(i) == "operator" && child.Content(src) == "@") {
			opText = "@"
			break
		}
	}
	if opText != "@" {
		return
	}

	// The operand is typically a call node with the attribute name as target.
	attrName := ""
	for i := 0; i < int(node.ChildCount()); i++ {
		child := node.Child(i)
		fieldName := node.FieldNameForChild(i)
		if fieldName == "operand" {
			if child.Type() == "call" {
				attrName = e.getCallTarget(child, src)
			} else if child.Type() == "identifier" {
				attrName = child.Content(src)
			}
			break
		}
	}
	if attrName == "" || attrName == "doc" || attrName == "moduledoc" || attrName == "spec" || attrName == "type" || attrName == "typep" || attrName == "callback" || attrName == "behaviour" || attrName == "behavior" {
		return
	}

	prefix := filePath + "::"
	if currentModule != "" {
		prefix = filePath + "::" + currentModule + "."
	}
	id := prefix + "@" + attrName
	if seen[id] {
		return
	}
	seen[id] = true

	result.Nodes = append(result.Nodes, &graph.Node{
		ID: id, Kind: graph.KindVariable, Name: "@" + attrName,
		FilePath: filePath, StartLine: int(node.StartPoint().Row) + 1,
		EndLine: int(node.EndPoint().Row) + 1, Language: "elixir",
	})
	result.Edges = append(result.Edges, &graph.Edge{
		From: fileID, To: id, Kind: graph.EdgeDefines,
		FilePath: filePath, Line: int(node.StartPoint().Row) + 1,
	})
}

// collectCalls recursively scans a def body for call sites, emitting
// one EdgeCalls per non-keyword call. The caller is the enclosing def,
// passed in as callerID. Nested call nodes (`foo(bar())`) each emit
// their own edge, matching the previous whole-tree query behaviour.
func (e *ElixirExtractor) collectCalls(node *sitter.Node, src []byte, filePath, callerID string, result *parser.ExtractionResult) {
	if node == nil {
		return
	}
	if node.Type() == "call" {
		e.emitCallEdge(node, src, filePath, callerID, result)
	}
	for i := 0; i < int(node.ChildCount()); i++ {
		e.collectCalls(node.Child(i), src, filePath, callerID, result)
	}
}

// emitCallEdge inspects a call node's target and emits the appropriate
// EdgeCalls. A `dot` target is a qualified call (Module.fun ->
// unresolved::*.fun); a plain identifier target is a local call
// (unresolved::name), filtered against elixirKeywords / do / end.
func (e *ElixirExtractor) emitCallEdge(callNode *sitter.Node, src []byte, filePath, callerID string, result *parser.ExtractionResult) {
	line := int(callNode.StartPoint().Row) + 1
	for i := 0; i < int(callNode.ChildCount()); i++ {
		child := callNode.Child(i)
		if child == nil || callNode.FieldNameForChild(i) != "target" {
			continue
		}
		switch child.Type() {
		case "dot":
			method := e.dotCallMethod(child, src)
			if method == "" {
				return
			}
			result.Edges = append(result.Edges, &graph.Edge{
				From: callerID, To: "unresolved::*." + method,
				Kind: graph.EdgeCalls, FilePath: filePath, Line: line,
			})
		case "identifier":
			name := child.Content(src)
			if elixirKeywords[name] || name == "do" || name == "end" {
				return
			}
			result.Edges = append(result.Edges, &graph.Edge{
				From: callerID, To: "unresolved::" + name,
				Kind: graph.EdgeCalls, FilePath: filePath, Line: line,
			})
		}
		return
	}
}

// dotCallMethod returns the right-hand identifier of a `dot` node
// (the method name in `Module.method`), or "" when absent.
func (e *ElixirExtractor) dotCallMethod(dotNode *sitter.Node, src []byte) string {
	for i := 0; i < int(dotNode.ChildCount()); i++ {
		child := dotNode.Child(i)
		if child != nil && dotNode.FieldNameForChild(i) == "right" && child.Type() == "identifier" {
			return child.Content(src)
		}
	}
	return ""
}

// --- AST helpers ---

// extractModuleName gets the module name from a defmodule call node.
func (e *ElixirExtractor) extractModuleName(callNode *sitter.Node, src []byte) string {
	// Look for (arguments (alias) @name) or just the first argument text.
	args := e.findArguments(callNode)
	if args == nil {
		return ""
	}
	for i := 0; i < int(args.NamedChildCount()); i++ {
		child := args.NamedChild(i)
		t := child.Type()
		if t == "alias" || t == "dot" {
			return child.Content(src)
		}
	}
	// Fallback: first named child.
	if args.NamedChildCount() > 0 {
		text := args.NamedChild(0).Content(src)
		text = strings.TrimSpace(text)
		if text != "" && text != "do" {
			return text
		}
	}
	return ""
}

// extractFuncName gets the function name from a def/defp call node.
// The first argument of def is itself a call node whose target is the function name.
func (e *ElixirExtractor) extractFuncName(callNode *sitter.Node, src []byte) string {
	args := e.findArguments(callNode)
	if args == nil {
		return ""
	}
	for i := 0; i < int(args.NamedChildCount()); i++ {
		child := args.NamedChild(i)
		if child.Type() == "call" {
			// def func_name(args) -> call target is func_name
			return e.getCallTarget(child, src)
		}
		if child.Type() == "identifier" {
			// def func_name (no args)
			return child.Content(src)
		}
		if child.Type() == "binary_operator" {
			// Pattern: def func_name(args) when guard -> binary_operator with "when"
			// The left side should be the call with the function name.
			for j := 0; j < int(child.NamedChildCount()); j++ {
				sub := child.NamedChild(j)
				if sub.Type() == "call" {
					name := e.getCallTarget(sub, src)
					if name != "" {
						return name
					}
				}
			}
		}
	}
	return ""
}

// extractFirstArgText gets the text of the first argument (for import/alias/use/require).
func (e *ElixirExtractor) extractFirstArgText(callNode *sitter.Node, src []byte) string {
	args := e.findArguments(callNode)
	if args == nil {
		return ""
	}
	if args.NamedChildCount() > 0 {
		child := args.NamedChild(0)
		text := child.Content(src)
		text = strings.TrimSpace(text)
		return text
	}
	return ""
}

// findArguments locates the arguments node within a call node.
// In Elixir's tree-sitter grammar, the arguments node has no field name,
// so we find it by its node type.
func (e *ElixirExtractor) findArguments(callNode *sitter.Node) *sitter.Node {
	for i := 0; i < int(callNode.ChildCount()); i++ {
		child := callNode.Child(i)
		if child.Type() == "arguments" {
			return child
		}
	}
	return nil
}

// findDoBlock locates the do-block body within a call node.
func (e *ElixirExtractor) findDoBlock(callNode *sitter.Node) *sitter.Node {
	for i := 0; i < int(callNode.ChildCount()); i++ {
		child := callNode.Child(i)
		if child.Type() == "do_block" {
			return child
		}
	}
	// Also check inside arguments for inline do blocks.
	args := e.findArguments(callNode)
	if args != nil {
		for i := 0; i < int(args.ChildCount()); i++ {
			child := args.Child(i)
			if child.Type() == "do_block" {
				return child
			}
		}
	}
	return nil
}

// emitPhoenixPlugBindings walks the body of a defmodule for
// `plug :name` / `plug :name when action in [...]` macro calls and
// emits synthetic EdgeCalls from each matching action function to
// the named plug function. Phoenix dispatches plugs via module
// metadata so there's no explicit call site in source — the edges
// make `callers:plug_name` return the guarded actions.
func (e *ElixirExtractor) emitPhoenixPlugBindings(body *sitter.Node, src []byte, filePath, modName string, result *parser.ExtractionResult) {
	type plugEntry struct {
		name   string
		line   int
		filter map[string]struct{} // empty = applies to all actions
	}
	var plugs []plugEntry
	actions := make(map[string]int) // name → start line
	allPlugs := make(map[string]struct{})

	for i := 0; i < int(body.ChildCount()); i++ {
		c := body.Child(i)
		if c == nil || c.Type() != "call" {
			continue
		}
		target := e.getCallTarget(c, src)
		switch target {
		case "plug":
			entry := parsePhoenixPlugCall(c, src)
			if entry.name == "" {
				continue
			}
			plugs = append(plugs, plugEntry{
				name:   entry.name,
				line:   int(c.StartPoint().Row) + 1,
				filter: entry.filter,
			})
			allPlugs[entry.name] = struct{}{}
		case "def":
			if name := e.extractFuncName(c, src); name != "" {
				actions[name] = int(c.StartPoint().Row) + 1
			}
		}
	}
	if len(plugs) == 0 {
		return
	}
	for _, p := range plugs {
		plugID := filePath + "::" + modName + "." + p.name
		for action := range actions {
			// Plug functions themselves aren't actions — don't guard
			// them with other plugs.
			if _, isPlug := allPlugs[action]; isPlug {
				continue
			}
			if len(p.filter) > 0 {
				if _, ok := p.filter[action]; !ok {
					continue
				}
			}
			actionID := filePath + "::" + modName + "." + action
			result.Edges = append(result.Edges, &graph.Edge{
				From:     actionID,
				To:       plugID,
				Kind:     graph.EdgeCalls,
				FilePath: filePath,
				Line:     p.line,
				Meta: map[string]any{
					"dispatch_macro": "plug",
					"phoenix_plug":   p.name,
				},
			})
		}
	}
}

// parsePhoenixPlugCall extracts the plug function name and an optional
// set of action names (from `when action in [:a, :b, :c]`) from a plug
// call node. Returns zero values when the call doesn't parse.
type phoenixPlugParsed struct {
	name   string
	filter map[string]struct{}
}

func parsePhoenixPlugCall(callNode *sitter.Node, src []byte) phoenixPlugParsed {
	var out phoenixPlugParsed
	var args *sitter.Node
	for i := 0; i < int(callNode.NamedChildCount()); i++ {
		c := callNode.NamedChild(i)
		if c != nil && c.Type() == "arguments" {
			args = c
			break
		}
	}
	if args == nil || args.NamedChildCount() == 0 {
		return out
	}
	arg := args.NamedChild(0)
	switch arg.Type() {
	case "atom":
		out.name = strings.TrimPrefix(arg.Content(src), ":")
	case "binary_operator":
		// `:name when action in [...]` — the outer op is `when`,
		// left is the plug atom, right is an `in` expression whose
		// right side is a list of atoms.
		left := arg.NamedChild(0)
		right := arg.NamedChild(1)
		if left == nil || left.Type() != "atom" || right == nil {
			return out
		}
		out.name = strings.TrimPrefix(left.Content(src), ":")
		if right.Type() == "binary_operator" {
			list := right.NamedChild(1)
			if list != nil && list.Type() == "list" {
				out.filter = make(map[string]struct{})
				for i := 0; i < int(list.NamedChildCount()); i++ {
					item := list.NamedChild(i)
					if item != nil && item.Type() == "atom" {
						out.filter[strings.TrimPrefix(item.Content(src), ":")] = struct{}{}
					}
				}
			}
		}
	}
	return out
}
