from __future__ import annotations

from typing import Any

from ..schema import ArchitectureMetrics, FanInOut, Hotspot, ModuleEdge, SCC
from .graph_builder import build_module_adjacency
from .project_map_loader import _norm_path, get_file_facts


def tarjan_scc(nodes: list[str], adj: dict[str, dict[str, int]]) -> list[list[str]]:
    """
    Tarjan SCC algorithm.
    Returns list of components (each is list of node ids).
    """
    index = 0
    stack: list[str] = []
    on_stack: set[str] = set()
    idx: dict[str, int] = {}
    low: dict[str, int] = {}
    out: list[list[str]] = []

    def strongconnect(v: str) -> None:
        nonlocal index
        idx[v] = index
        low[v] = index
        index += 1
        stack.append(v)
        on_stack.add(v)

        for w in adj.get(v, {}).keys():
            if w not in idx:
                strongconnect(w)
                low[v] = min(low[v], low[w])
            elif w in on_stack:
                low[v] = min(low[v], idx[w])

        if low[v] == idx[v]:
            comp: list[str] = []
            while True:
                w = stack.pop()
                on_stack.remove(w)
                comp.append(w)
                if w == v:
                    break
            out.append(comp)

    for v in nodes:
        if v not in idx:
            strongconnect(v)
    return out


def compute_fanin_out(
    module_ids: list[str], module_edges: list[ModuleEdge]
) -> tuple[list[FanInOut], dict[str, int], dict[str, int], dict[str, int], dict[str, int]]:
    out_adj, in_adj = build_module_adjacency(module_edges)

    fan_in: dict[str, int] = {m: 0 for m in module_ids}
    fan_out: dict[str, int] = {m: 0 for m in module_ids}
    fan_in_w: dict[str, int] = {m: 0 for m in module_ids}
    fan_out_w: dict[str, int] = {m: 0 for m in module_ids}

    for m in module_ids:
        outs = out_adj.get(m, {})
        ins = in_adj.get(m, {})
        fan_out[m] = len(outs)
        fan_in[m] = len(ins)
        fan_out_w[m] = sum(int(w) for w in outs.values())
        fan_in_w[m] = sum(int(w) for w in ins.values())

    rows: list[FanInOut] = []
    for m in module_ids:
        rows.append(
            {
                "module_id": m,
                "fan_in": int(fan_in[m]),
                "fan_out": int(fan_out[m]),
                "fan_in_weight": int(fan_in_w[m]),
                "fan_out_weight": int(fan_out_w[m]),
            }
        )
    rows.sort(
        key=lambda r: (
            -int(r.get("fan_in_weight") or 0) - int(r.get("fan_out_weight") or 0),
            str(r.get("module_id") or ""),
        )
    )
    return rows, fan_in, fan_out, fan_in_w, fan_out_w


def compute_hotspots(
    project: dict[str, Any],
    *,
    file_to_module: dict[str, str],
    fan_in_w: dict[str, int],
    fan_out_w: dict[str, int],
    top_n: int = 12,
) -> list[Hotspot]:
    n = max(1, int(top_n))

    sizes: list[tuple[int, str]] = []
    for path in file_to_module.keys():
        facts = get_file_facts(project, path)
        try:
            sz = int(facts.get("size_bytes") or 0)
        except Exception:
            sz = 0
        sizes.append((sz, _norm_path(path)))
    sizes.sort(key=lambda x: (-x[0], x[1]))

    out: list[Hotspot] = []
    for sz, p in sizes[:n]:
        out.append(
            {
                "kind": "large_file",
                "path": p,
                "module_id": file_to_module.get(p, ""),
                "score": float(sz),
                "details": {"size_bytes": int(sz)},
            }
        )

    modules = sorted(set(file_to_module.values()))

    def add_top(kind: str, scores: dict[str, int], detail_key: str) -> None:
        ranked = sorted(((int(scores.get(m, 0)), m) for m in modules), key=lambda t: (-t[0], t[1]))
        for sc, mid in ranked[:n]:
            out.append(
                {
                    "kind": kind,  # type: ignore[typeddict-item]
                    "path": mid,
                    "module_id": mid,
                    "score": float(sc),
                    "details": {detail_key: int(sc)},
                }
            )

    add_top("high_fan_in", fan_in_w, "fan_in_weight")
    add_top("high_fan_out", fan_out_w, "fan_out_weight")
    degree_w = {m: int(fan_in_w.get(m, 0)) + int(fan_out_w.get(m, 0)) for m in modules}
    add_top("high_degree", degree_w, "degree_weight")
    return out


def compute_metrics(
    *,
    modules: list[dict[str, Any]],
    module_edges: list[ModuleEdge],
    project: dict[str, Any],
    file_to_module: dict[str, str],
) -> tuple[ArchitectureMetrics, dict[str, int], dict[str, int], dict[str, int], dict[str, int]]:
    module_ids = [str(m.get("module_id") or "") for m in modules if str(m.get("module_id") or "")]
    out_adj, _ = build_module_adjacency(module_edges)

    scc_comps = tarjan_scc(module_ids, out_adj)
    sccs: list[SCC] = []
    scc_id = 0
    for comp in sorted(scc_comps, key=lambda c: (-len(c), sorted(c))):
        comp_sorted = sorted(comp)
        sccs.append({"scc_id": scc_id, "modules": comp_sorted, "size": len(comp_sorted)})
        scc_id += 1

    fan_rows, fan_in, fan_out, fan_in_w, fan_out_w = compute_fanin_out(module_ids, module_edges)
    hotspots = compute_hotspots(project, file_to_module=file_to_module, fan_in_w=fan_in_w, fan_out_w=fan_out_w)

    metrics: ArchitectureMetrics = {
        "sccs": sccs,
        "fan_in_out": fan_rows,
        "hotspots": hotspots,
    }
    return metrics, fan_in, fan_out, fan_in_w, fan_out_w

