"""
Impact Analyzer — answers "what breaks if I change this file?"

Uses the pre-built dependency graph (dependents field per module).
No extra indexing needed — works from existing .kodara/index.json.

Algorithm:
  1. Find the target file in the index
  2. BFS through the reverse dependency graph (who imports this?)
  3. Categorize results: direct (1 hop), indirect (2+ hops)
  4. Identify critical paths (chains through hub modules)
  5. Calculate risk level based on affected module count and hub presence
"""

from __future__ import annotations

from collections import deque
from dataclasses import dataclass, field
from pathlib import Path

from ..memory.store import KodaraIndex


@dataclass
class AffectedModule:
    path: str
    module_name: str
    distance: int          # hops from target (1 = direct, 2+ = transitive)
    summary: str = ""
    is_hub: bool = False   # imported by many others — extra risky


@dataclass
class ImpactResult:
    target: str                              # the file being changed
    target_module: str
    target_summary: str
    direct: list[AffectedModule]             # distance == 1
    indirect: list[AffectedModule]           # distance >= 2
    risk_level: str                          # LOW / MEDIUM / HIGH / CRITICAL
    risk_reason: str
    critical_paths: list[list[str]]          # chains to hub modules
    total_affected: int


def analyze(index: KodaraIndex, target_path: str) -> ImpactResult:
    """
    Analyze the blast radius of changing target_path.

    Args:
        index:       Loaded KodaraIndex
        target_path: File path to analyze (absolute or relative — will fuzzy match)

    Returns:
        ImpactResult with full impact breakdown.

    Raises:
        ValueError: if target_path not found in index.
    """
    # Resolve target: exact match first, then fuzzy suffix match
    resolved = _resolve_path(index, target_path)
    if not resolved:
        raise ValueError(
            f"File not found in index: {target_path}\n"
            f"Run 'kodara status' to see indexed files."
        )

    target_mod = index.modules[resolved]

    # Build dependents map: path → list of paths that import it
    # (already stored per module as "dependents")
    dependents_map: dict[str, list[str]] = {}
    for path, mod in index.modules.items():
        dependents_map[path] = mod.get("dependents", [])

    # Identify hub modules (imported by many — high in-degree)
    import_counts: dict[str, int] = {}
    for mod in index.modules.values():
        for dep in mod.get("dependencies", []):
            import_counts[dep] = import_counts.get(dep, 0) + 1

    hub_threshold = max(2, len(index.modules) // 10)
    hubs = {p for p, c in import_counts.items() if c >= hub_threshold}

    # BFS through reverse graph (who imports this file?)
    visited: dict[str, int] = {}  # path → distance
    queue: deque[tuple[str, int]] = deque([(resolved, 0)])
    visited[resolved] = 0

    while queue:
        node, dist = queue.popleft()
        for dependent in dependents_map.get(node, []):
            if dependent not in visited:
                visited[dependent] = dist + 1
                queue.append((dependent, dist + 1))

    # Build result (excluding the target itself)
    direct: list[AffectedModule] = []
    indirect: list[AffectedModule] = []

    for path, dist in visited.items():
        if path == resolved:
            continue
        mod = index.modules.get(path, {})
        affected = AffectedModule(
            path=path,
            module_name=mod.get("module_name", Path(path).stem),
            distance=dist,
            summary=mod.get("summary", ""),
            is_hub=path in hubs,
        )
        if dist == 1:
            direct.append(affected)
        else:
            indirect.append(affected)

    # Sort by distance then hub status
    direct.sort(key=lambda x: (not x.is_hub, x.module_name))
    indirect.sort(key=lambda x: (x.distance, not x.is_hub, x.module_name))

    # Find critical paths: chains that pass through hub modules
    critical_paths = _find_critical_paths(resolved, dependents_map, hubs, max_paths=3)

    # Risk assessment
    total = len(direct) + len(indirect)
    hub_hits = sum(1 for a in direct + indirect if a.is_hub)
    risk_level, risk_reason = _assess_risk(total, hub_hits, len(direct), index)

    return ImpactResult(
        target=resolved,
        target_module=target_mod.get("module_name", Path(resolved).stem),
        target_summary=target_mod.get("summary", ""),
        direct=direct,
        indirect=indirect,
        risk_level=risk_level,
        risk_reason=risk_reason,
        critical_paths=critical_paths,
        total_affected=total,
    )


# ── Risk assessment ───────────────────────────────────────────────────────────


def _assess_risk(
    total: int,
    hub_hits: int,
    direct_count: int,
    index: KodaraIndex,
) -> tuple[str, str]:
    total_modules = len(index.modules)
    pct = (total / total_modules * 100) if total_modules else 0

    if hub_hits >= 2 or pct >= 40:
        return "CRITICAL", f"Affects {total} modules ({pct:.0f}% of codebase), including {hub_hits} architectural hubs"
    if hub_hits == 1 or pct >= 20 or direct_count >= 5:
        return "HIGH", f"Affects {total} modules including a hub — changes likely cascade"
    if total >= 3 or pct >= 10:
        return "MEDIUM", f"Affects {total} modules — review dependents before changing"
    if total > 0:
        return "LOW", f"Affects {total} module(s) — contained change"
    return "NONE", "No other modules import this file — safe to change"


# ── Critical path finder ──────────────────────────────────────────────────────


def _find_critical_paths(
    start: str,
    dependents_map: dict[str, list[str]],
    hubs: set[str],
    max_paths: int = 3,
) -> list[list[str]]:
    """Find shortest paths from start to hub modules (the risky chains)."""
    paths: list[list[str]] = []
    visited_starts: set[str] = set()

    for hub in hubs:
        if hub == start:
            continue
        path = _bfs_path(start, hub, dependents_map)
        if path and len(path) > 1:
            key = path[-1]
            if key not in visited_starts:
                visited_starts.add(key)
                paths.append(path)
        if len(paths) >= max_paths:
            break

    return paths


def _bfs_path(
    start: str, end: str, dependents_map: dict[str, list[str]]
) -> list[str] | None:
    """BFS to find shortest path from start to end in dependents graph."""
    queue: deque[list[str]] = deque([[start]])
    visited: set[str] = {start}

    while queue:
        path = queue.popleft()
        node = path[-1]
        if node == end:
            return path
        for dep in dependents_map.get(node, []):
            if dep not in visited:
                visited.add(dep)
                queue.append(path + [dep])
    return None


# ── Path resolution ───────────────────────────────────────────────────────────


def _resolve_path(index: KodaraIndex, target: str) -> str | None:
    """Match target to an indexed path. Tries exact, then suffix, then stem."""
    if target in index.modules:
        return target

    # Normalise separators
    target_norm = target.replace("\\", "/")

    for path in index.modules:
        path_norm = path.replace("\\", "/")
        if path_norm.endswith(target_norm) or path_norm.endswith("/" + target_norm):
            return path

    # Stem match (filename without extension)
    target_stem = Path(target).stem.lower()
    for path in index.modules:
        if Path(path).stem.lower() == target_stem:
            return path

    return None
