package languages

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

// emitTSThrowsEdges walks a TypeScript function body for throw_statement
// nodes and emits one EdgeThrows per distinct exception type. The
// extractor recognises the three idiomatic shapes:
//
//	throw new MyError("msg")     -> "MyError"
//	throw MyError                -> "MyError"
//	throw errs.MyError           -> "MyError" (trailing identifier of the
//	                                 member-expression chain)
//
// String / numeric / template-literal throws (`throw "oops"`) are
// skipped — they don't carry a referenceable type and would just create
// `unresolved::string` noise in the graph. Re-throws (`throw caught` in
// a catch clause that captured a typed name) fall through to the
// identifier branch and resolve like any other.
//
// Origin is OriginASTInferred because TS doesn't enforce a checked-
// exception contract — the body scan is a best-effort summary of every
// type that can propagate, not a proof. The resolver upgrades to
// OriginLSPResolved when the language server confirms the target.
//
// Nested function / arrow / method bodies are skipped: their throws
// belong to those inner scopes, not the enclosing function. This
// matches the Python and Rust emitters' walk policy.
func emitTSThrowsEdges(funcNode *sitter.Node, src []byte, fromID, filePath string, result *parser.ExtractionResult) {
	if funcNode == nil || fromID == "" {
		return
	}
	body := tsFunctionBody(funcNode)
	if body == nil {
		return
	}
	seen := map[string]bool{}
	tsWalkThrows(body, src, fromID, filePath, seen, result)
}

// tsWalkThrows is the recursive descent helper for emitTSThrowsEdges.
// Pulled out to keep the entry-point readable and to make the
// "don't descend into nested function bodies" rule visible at the
// single place that enforces it.
func tsWalkThrows(node *sitter.Node, src []byte, fromID, filePath string, seen map[string]bool, result *parser.ExtractionResult) {
	if node == nil {
		return
	}
	if node.Type() == "throw_statement" {
		name := tsThrowTypeName(node, src)
		if name != "" && !seen[name] {
			seen[name] = true
			result.Edges = append(result.Edges, &graph.Edge{
				From:     fromID,
				To:       "unresolved::" + name,
				Kind:     graph.EdgeThrows,
				FilePath: filePath,
				Line:     int(node.StartPoint().Row) + 1,
				Origin:   graph.OriginASTInferred,
			})
		}
		return
	}
	for i := 0; i < int(node.NamedChildCount()); i++ {
		c := node.NamedChild(i)
		if c == nil {
			continue
		}
		// Skip nested scopes — their throws belong to those scopes.
		switch c.Type() {
		case "function_declaration", "function_expression", "arrow_function",
			"method_definition", "generator_function_declaration",
			"class_declaration", "class_expression":
			continue
		}
		tsWalkThrows(c, src, fromID, filePath, seen, result)
	}
}

// tsThrowTypeName returns the typed name being thrown, or "" when the
// throw expression isn't a referenceable type. Handles:
//
//	throw new Foo(...) / throw new ns.Foo(...) — new_expression
//	throw Foo / throw Foo.SubFoo                — identifier / member_expression
//
// Anything else (string literal, template string, number, undefined,
// computed property access) returns "".
func tsThrowTypeName(throwNode *sitter.Node, src []byte) string {
	if throwNode == nil {
		return ""
	}
	for i := 0; i < int(throwNode.NamedChildCount()); i++ {
		c := throwNode.NamedChild(i)
		if c == nil {
			continue
		}
		switch c.Type() {
		case "new_expression":
			ctor := c.ChildByFieldName("constructor")
			if ctor == nil {
				continue
			}
			return tsTrailingIdentifier(ctor, src)
		case "identifier":
			return c.Content(src)
		case "member_expression":
			// `errs.MyError.Variant` → return the trailing identifier.
			// Captures the exception's leaf name regardless of
			// namespace depth.
			return tsTrailingIdentifier(c, src)
		case "call_expression":
			// `throw makeError(...)` — record the constructor-style
			// identifier so the resolver can still upgrade origin once
			// the call's return type is known. Skip when the callee
			// isn't a bare identifier (avoids `throw a.b.c()` producing
			// `c` as a throw type, which is misleading).
			fn := c.ChildByFieldName("function")
			if fn == nil || fn.Type() != "identifier" {
				continue
			}
			return fn.Content(src)
		}
	}
	return ""
}

// tsTrailingIdentifier walks a member_expression / nested_identifier
// chain returning the last (rightmost) identifier component. For
// `errs.kinds.MyError` returns "MyError"; for bare `Foo` returns
// "Foo". Returns "" for any node that isn't an identifier or a chain.
func tsTrailingIdentifier(node *sitter.Node, src []byte) string {
	if node == nil {
		return ""
	}
	switch node.Type() {
	case "identifier", "type_identifier":
		return node.Content(src)
	case "member_expression":
		// Walk the property side first (the rightmost segment of the chain).
		if prop := node.ChildByFieldName("property"); prop != nil {
			if name := tsTrailingIdentifier(prop, src); name != "" {
				return name
			}
		}
	case "nested_identifier":
		// `ns.Inner.Leaf` — last NamedChild is the leaf.
		count := int(node.NamedChildCount())
		if count == 0 {
			return ""
		}
		return tsTrailingIdentifier(node.NamedChild(count-1), src)
	}
	// Fall back to the verbatim content so unusual shapes still
	// surface SOMETHING the resolver can dedupe against.
	return node.Content(src)
}
