package resolver

import (
	"strings"

	"github.com/zzet/gortex/internal/graph"
)

// Scope-resolution metadata keys. Extractors populate these on call
// edges and node payloads so the resolver can disambiguate same-named
// symbols using language-specific scope rules (C file-static, C++
// namespace + ADL, Java enclosing class / static-import, PHP
// namespace + parent::/self::) before falling back to the generic
// directory-locality cascade.
//
// Keep the key names short — every call edge in a million-symbol
// graph carries them, so a few bytes per key compound fast.
const (
	// MetaScopeNamespace — fully-qualified namespace the symbol lives
	// in (`std::detail`, `App\Service`, `com.example.foo`). Populated
	// on node payloads (KindFunction / KindMethod / KindType / …) and
	// on call edges (the namespace of the *caller*).
	MetaScopeNamespace = "scope_ns"

	// MetaScopeClass — name of the enclosing class for class-bound
	// callers / definitions (`User`, `App\Repository\UserRepo`).
	MetaScopeClass = "scope_class"

	// MetaScopeParentClass — name of the direct parent class. Set on
	// KindType nodes for C++/Java/PHP classes that extend a base.
	// Used by PHP's `parent::method` resolution and Java's super-call
	// disambiguation.
	MetaScopeParentClass = "scope_parent"

	// MetaScopeStatic — true on KindFunction nodes that have file-
	// local linkage (C `static void foo()`, PHP namespaced functions
	// that aren't reachable via `use function`). The resolver prefers
	// a static candidate defined in the caller's file over global
	// candidates with the same name.
	MetaScopeStatic = "scope_static"

	// MetaScopeKind — one of "unqualified", "qualified", "self",
	// "parent", "static", "method". Tells the resolver which scope-
	// resolution strategy to apply on a call edge.
	MetaScopeKind = "scope_kind"

	// MetaScopeArgTypes — comma-separated list of argument-type
	// names hinted by the C++ extractor for ADL ("Argument-Dependent
	// Lookup"). Each entry is a possibly-namespaced type
	// (`std::string`, `MyNs::Widget`); the resolver walks each
	// entry's namespace looking for a free function whose name
	// matches the call.
	MetaScopeArgTypes = "scope_arg_types"

	// MetaScopeUseAliases — semicolon-separated `alias=>target` pairs
	// from PHP `use function NS\foo as bar` declarations in the
	// caller's file. The resolver translates an unresolved call to
	// `bar` into a search for `NS\foo` before falling back to the
	// generic cascade.
	MetaScopeUseAliases = "scope_use_aliases"
)

// Per-language scope-kind constants. Stamped on the call edge by the
// extractor when the call site is something other than a plain
// unqualified identifier — `parent::foo()`, `self::foo()`,
// `Class::staticFoo()`, etc.
const (
	ScopeKindUnqualified = "unqualified"
	ScopeKindQualified   = "qualified"
	ScopeKindSelf        = "self"
	ScopeKindParent      = "parent"
	ScopeKindStatic      = "static"
)

// scopeMetaString returns the string value at key in m, or "" if
// missing / wrong type. Tiny accessor that keeps the hot path tidy.
func scopeMetaString(m map[string]any, key string) string {
	if m == nil {
		return ""
	}
	v, _ := m[key].(string)
	return v
}

// scopeMetaBool returns the bool value at key in m, or false if
// missing / wrong type.
func scopeMetaBool(m map[string]any, key string) bool {
	if m == nil {
		return false
	}
	v, _ := m[key].(bool)
	return v
}

// scopeArgTypeHints decodes the MetaScopeArgTypes payload into a
// slice of type-name strings (already split, no whitespace). Empty
// when the key is missing or the call has no positional arguments.
func scopeArgTypeHints(m map[string]any) []string {
	raw := scopeMetaString(m, MetaScopeArgTypes)
	if raw == "" {
		return nil
	}
	parts := strings.Split(raw, ",")
	out := parts[:0]
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		out = append(out, p)
	}
	return out
}

// scopeUseAliases decodes the MetaScopeUseAliases payload into a
// map of (local-alias → fully-qualified target). Empty when the file
// has no `use function` declarations.
func scopeUseAliases(m map[string]any) map[string]string {
	raw := scopeMetaString(m, MetaScopeUseAliases)
	if raw == "" {
		return nil
	}
	out := make(map[string]string, 4)
	for _, pair := range strings.Split(raw, ";") {
		pair = strings.TrimSpace(pair)
		if pair == "" {
			continue
		}
		eq := strings.IndexByte(pair, '=')
		if eq <= 0 || eq+2 > len(pair) || pair[eq+1] != '>' {
			continue
		}
		alias := strings.TrimSpace(pair[:eq])
		target := strings.TrimSpace(pair[eq+2:])
		if alias == "" || target == "" {
			continue
		}
		out[alias] = target
	}
	return out
}

// preferScopeCandidate returns the best per-language candidate for
// an unresolved call edge, or nil to fall through to the generic
// resolver cascade. Each language's branch is conservative — it
// returns nil unless it has high-confidence scope evidence — so the
// generic cascade still handles the long tail.
//
// The dispatch keys off the caller node's Language (not the edge,
// because legacy edges may not carry language). Returning nil keeps
// the resolver behavior identical for unsupported languages.
func (r *Resolver) preferScopeCandidate(e *graph.Edge, name string, candidates []*graph.Node) *graph.Node {
	caller := r.graph.GetNode(e.From)
	if caller == nil {
		return nil
	}
	switch caller.Language {
	case "c":
		return r.preferCStaticCandidate(e, caller, candidates)
	case "cpp":
		return r.preferCppScopeCandidate(e, caller, name, candidates)
	case "java":
		return r.preferJavaScopeCandidate(e, caller, name, candidates)
	case "php":
		return r.preferPhpScopeCandidate(e, caller, name, candidates)
	}
	return nil
}

// preferCStaticCandidate — C scope rule: a `static` function has
// file-local linkage, so a same-file static candidate is the only
// legal target for an unresolved call when one exists. Prevents the
// generic cascade from binding the call to an extern function of
// the same name in a different translation unit.
func (r *Resolver) preferCStaticCandidate(e *graph.Edge, caller *graph.Node, candidates []*graph.Node) *graph.Node {
	for _, c := range candidates {
		if c.Kind != graph.KindFunction {
			continue
		}
		if c.FilePath != caller.FilePath {
			continue
		}
		if scopeMetaBool(c.Meta, MetaScopeStatic) {
			return c
		}
	}
	// File-local-static rule cuts the other way too: if a candidate
	// is `static` in a *different* file, it cannot be a legal target.
	// Filter it out by returning the first non-static candidate when
	// every same-file alternative is non-static. Caller falls through
	// otherwise.
	return nil
}

// preferCppScopeCandidate — C++ scope rule: namespace match wins
// over directory/locality match. ADL (Argument-Dependent Lookup):
// for an unqualified call `foo(a, b)`, if any of a's, b's argument
// types name a class in namespace `N`, then `N::foo` is a candidate.
// Implementation order:
//   1. Same-namespace function/method match (lexical scope).
//   2. ADL: walk each scope_arg_types entry's namespace.
//   3. Fall through to the generic cascade.
func (r *Resolver) preferCppScopeCandidate(e *graph.Edge, caller *graph.Node, name string, candidates []*graph.Node) *graph.Node {
	callerNs := scopeMetaString(caller.Meta, MetaScopeNamespace)
	if callerNs != "" {
		for _, c := range candidates {
			if c.Kind != graph.KindFunction && c.Kind != graph.KindMethod {
				continue
			}
			if scopeMetaString(c.Meta, MetaScopeNamespace) == callerNs {
				return c
			}
		}
	}
	hints := scopeArgTypeHints(e.Meta)
	if len(hints) == 0 {
		return nil
	}
	adlNamespaces := make(map[string]struct{}, len(hints))
	for _, typeName := range hints {
		ns := splitNamespaceFromQualifiedName(typeName)
		if ns == "" {
			continue
		}
		adlNamespaces[ns] = struct{}{}
	}
	if len(adlNamespaces) == 0 {
		return nil
	}
	for _, c := range candidates {
		if c.Kind != graph.KindFunction && c.Kind != graph.KindMethod {
			continue
		}
		if _, ok := adlNamespaces[scopeMetaString(c.Meta, MetaScopeNamespace)]; ok {
			return c
		}
	}
	return nil
}

// preferJavaScopeCandidate — Java scope rule: an unqualified call
// inside class C must bind to a method declared on C (or inherited
// via extends/implements) before any other class. Static-imported
// methods + outer-class methods are tried in order before the
// generic cascade.
func (r *Resolver) preferJavaScopeCandidate(e *graph.Edge, caller *graph.Node, name string, candidates []*graph.Node) *graph.Node {
	enclosing := callerEnclosingClass(caller)
	if enclosing == "" {
		return nil
	}
	// Pass 1: exact same enclosing class.
	for _, c := range candidates {
		if c.Kind != graph.KindMethod {
			continue
		}
		if scopeMetaString(c.Meta, MetaScopeClass) == enclosing {
			return c
		}
		if receiverEquals(c, enclosing) {
			return c
		}
	}
	// Pass 2: super-class chain. We follow EdgeExtends from the
	// enclosing class to walk up the inheritance tree until either a
	// matching method is found or the chain runs out.
	visited := map[string]struct{}{enclosing: {}}
	current := enclosing
	for hops := 0; hops < 8; hops++ {
		parent := r.javaParentClass(caller, current)
		if parent == "" {
			break
		}
		if _, seen := visited[parent]; seen {
			break
		}
		visited[parent] = struct{}{}
		for _, c := range candidates {
			if c.Kind != graph.KindMethod {
				continue
			}
			if scopeMetaString(c.Meta, MetaScopeClass) == parent {
				return c
			}
			if receiverEquals(c, parent) {
				return c
			}
		}
		current = parent
	}
	return nil
}

// preferPhpScopeCandidate — PHP scope rule: `parent::foo` walks the
// extends chain; `self::foo` is the enclosing class; `use function`
// aliases translate before search; unqualified calls in a namespace
// resolve in the same namespace before the global one. The edge's
// MetaScopeKind tells us which subroutine to run.
func (r *Resolver) preferPhpScopeCandidate(e *graph.Edge, caller *graph.Node, name string, candidates []*graph.Node) *graph.Node {
	switch scopeMetaString(e.Meta, MetaScopeKind) {
	case ScopeKindParent:
		enclosing := callerEnclosingClass(caller)
		if enclosing == "" {
			return nil
		}
		parent := r.phpParentClass(caller, enclosing)
		if parent == "" {
			return nil
		}
		for _, c := range candidates {
			if c.Kind != graph.KindMethod {
				continue
			}
			if scopeMetaString(c.Meta, MetaScopeClass) == parent ||
				receiverEquals(c, parent) {
				return c
			}
		}
		return nil
	case ScopeKindSelf:
		enclosing := callerEnclosingClass(caller)
		if enclosing == "" {
			return nil
		}
		for _, c := range candidates {
			if c.Kind != graph.KindMethod {
				continue
			}
			if scopeMetaString(c.Meta, MetaScopeClass) == enclosing ||
				receiverEquals(c, enclosing) {
				return c
			}
		}
		return nil
	}
	// Default: respect `use function` aliases + same-namespace
	// preference for unqualified calls.
	if alias := scopeUseAliases(e.Meta)[name]; alias != "" {
		ns, baseName := splitQualifiedFunctionName(alias)
		for _, c := range candidates {
			if c.Kind != graph.KindFunction {
				continue
			}
			if c.Name != baseName {
				continue
			}
			if ns == "" || scopeMetaString(c.Meta, MetaScopeNamespace) == ns {
				return c
			}
		}
	}
	callerNs := scopeMetaString(caller.Meta, MetaScopeNamespace)
	if callerNs == "" {
		return nil
	}
	for _, c := range candidates {
		if c.Kind != graph.KindFunction {
			continue
		}
		if scopeMetaString(c.Meta, MetaScopeNamespace) == callerNs {
			return c
		}
	}
	return nil
}

// callerEnclosingClass returns the enclosing class name for a caller
// node. Prefers the explicit MetaScopeClass stamp; falls back to the
// receiver field for method nodes so older indexes still work.
func callerEnclosingClass(caller *graph.Node) string {
	if cls := scopeMetaString(caller.Meta, MetaScopeClass); cls != "" {
		return cls
	}
	if caller.Kind == graph.KindMethod {
		return nodeReceiverType(caller)
	}
	return ""
}

// receiverEquals returns true when candidate is a method whose
// receiver type matches name.
func receiverEquals(candidate *graph.Node, name string) bool {
	if candidate.Kind != graph.KindMethod {
		return false
	}
	return nodeReceiverType(candidate) == name
}

// javaParentClass returns the Java parent class name for `child`, by
// looking at the child class's MetaScopeParentClass stamp on its
// graph node. Caller is just used to constrain the search to the
// caller's file/package when stamp data is incomplete.
func (r *Resolver) javaParentClass(caller *graph.Node, child string) string {
	for _, n := range r.graph.FindNodesByName(child) {
		if n.Kind != graph.KindType && n.Kind != graph.KindInterface {
			continue
		}
		if n.Language != "java" {
			continue
		}
		if parent := scopeMetaString(n.Meta, MetaScopeParentClass); parent != "" {
			return parent
		}
	}
	return ""
}

// phpParentClass is the PHP analogue of javaParentClass. Same
// strategy, scoped to PHP-language nodes.
func (r *Resolver) phpParentClass(caller *graph.Node, child string) string {
	for _, n := range r.graph.FindNodesByName(child) {
		if n.Kind != graph.KindType && n.Kind != graph.KindInterface {
			continue
		}
		if n.Language != "php" {
			continue
		}
		if parent := scopeMetaString(n.Meta, MetaScopeParentClass); parent != "" {
			return parent
		}
	}
	return ""
}

// splitNamespaceFromQualifiedName splits a possibly-namespaced
// identifier into its (namespace, base) parts. "std::string" →
// "std", "App\Service\Foo" → "App\Service". Returns ""
// namespace for bare identifiers.
func splitNamespaceFromQualifiedName(name string) string {
	if i := strings.LastIndex(name, "::"); i >= 0 {
		return name[:i]
	}
	if i := strings.LastIndex(name, `\`); i >= 0 {
		return name[:i]
	}
	if i := strings.LastIndex(name, "."); i >= 0 {
		return name[:i]
	}
	return ""
}

// splitQualifiedFunctionName splits a fully-qualified function name
// into (namespace, base). Mirrors splitNamespaceFromQualifiedName
// but also returns the base name.
func splitQualifiedFunctionName(name string) (ns, base string) {
	if i := strings.LastIndex(name, "::"); i >= 0 {
		return name[:i], name[i+2:]
	}
	if i := strings.LastIndex(name, `\`); i >= 0 {
		return name[:i], name[i+1:]
	}
	if i := strings.LastIndex(name, "."); i >= 0 {
		return name[:i], name[i+1:]
	}
	return "", name
}

