from __future__ import annotations

from typing import Any

from fileatlas.schema import SymbolDef

from ..schema import PublicInterface
from .project_map_loader import ProjectIndex, _norm_path, get_file_analysis


def _norm_iface(s: str) -> str:
    s = (s or "").strip()
    # strip trailing () if user wrote function call style
    if s.endswith("()"):
        s = s[:-2]
    return s


def _pick_best_symbol(cands: list[SymbolDef], *, allowed_paths: set[str], prefer_qualname: str | None = None) -> SymbolDef | None:
    if not cands:
        return None
    filtered = [c for c in cands if _norm_path(str(c.get("path") or "")) in allowed_paths]
    if not filtered:
        return None
    if prefer_qualname:
        for c in filtered:
            if str(c.get("qualname") or "") == prefer_qualname:
                return c
    # otherwise pick the one with a signature, then earliest span
    def key(c: SymbolDef) -> tuple[int, int, str]:
        sig = 0 if (c.get("signature") or "").strip() else 1
        sp = c.get("span") or {}
        sl = int(sp.get("start_line") or 10**9)
        return (sig, sl, str(c.get("symbol_id") or ""))

    filtered.sort(key=key)
    return filtered[0]


def extract_public_interfaces(
    *,
    modules: list[dict[str, Any]],
    idx: ProjectIndex,
    max_per_module: int = 30,
) -> None:
    """
    Mutates modules[*]["public_interfaces"] by extracting from file.analysis.key_interfaces
    and best-effort matching back to project_map.symbols for signature/span.
    """
    cap = max(0, int(max_per_module))

    for m in modules:
        files = m.get("files", []) if isinstance(m.get("files", []), list) else []
        allowed_paths = {_norm_path(str(p)) for p in files}

        raw_ifaces: list[str] = []
        for p in files:
            analysis = get_file_analysis(idx.project, str(p))
            kis = analysis.get("key_interfaces", []) if isinstance(analysis.get("key_interfaces", []), list) else []
            for ki in kis:
                if isinstance(ki, str):
                    raw_ifaces.append(ki)

        seen: set[str] = set()
        out: list[PublicInterface] = []

        for ki in raw_ifaces:
            if cap and len(out) >= cap:
                break
            iface = _norm_iface(ki)
            if not iface or iface in seen:
                continue
            seen.add(iface)

            # Exact qualname match first.
            sym = _pick_best_symbol(idx.symbols_by_qualname.get(iface, []), allowed_paths=allowed_paths, prefer_qualname=iface)
            if sym is None:
                # Name-based match.
                name = iface.split(".")[-1]
                sym = _pick_best_symbol(idx.symbols_by_name.get(name, []), allowed_paths=allowed_paths, prefer_qualname=iface)

            if sym is None:
                out.append({"qualname": iface, "name": iface.split(".")[-1], "evidence": "unresolved"})
                continue

            pi: PublicInterface = {
                "symbol_id": str(sym.get("symbol_id") or ""),
                "qualname": str(sym.get("qualname") or ""),
                "name": str(sym.get("name") or ""),
                "signature": str(sym.get("signature") or ""),
                "path": _norm_path(str(sym.get("path") or "")),
                "span": sym.get("span") or {},
                "evidence": "matched",
            }
            out.append(pi)

        m["public_interfaces"] = out

