"""
Dependency Graph Engine — Phase 1 of Kodara pipeline.

Builds a directed graph representing architectural relationships between
modules. Nodes are files/modules; edges are import dependencies.

Design decision: NetworkX DiGraph gives us first-class graph algorithms
(BFS, DFS, centrality, connected components) for free, making the context
engine's traversal logic clean and extensible.
"""

from __future__ import annotations

from pathlib import Path
from dataclasses import dataclass, field

try:
    import networkx as nx
except ImportError:
    raise ImportError("networkx is required: pip install networkx")

from ..indexer.scanner import ScanResult, FileInfo


@dataclass
class GraphNode:
    path: str
    module_name: str
    language: str
    classes: list[str]
    functions: list[str]
    exports: list[str]
    summary: str = ""


class DependencyGraph:
    """
    Directed dependency graph where:
      - Nodes represent source files / modules
      - Edges represent import relationships (A → B means A imports B)

    Provides methods for:
      - Building from ScanResult
      - Resolving cross-module dependencies
      - Querying related modules via graph traversal
      - Exporting to JSON or Graphviz DOT
    """

    def __init__(self):
        self.graph: nx.DiGraph = nx.DiGraph()

    # ── Build ────────────────────────────────────────────────────────────────

    def build(self, scan: ScanResult) -> None:
        """Build the dependency graph from a scan result."""
        self.graph.clear()

        # 1. Add all files as nodes
        for path, info in scan.files.items():
            self.graph.add_node(
                path,
                module_name=self._path_to_module(path),
                language=info.language,
                classes=list(info.classes),
                functions=list(info.functions),
                exports=list(info.exports),
                summary="",
            )

        # 2. Resolve imports → edges
        for path, info in scan.files.items():
            for imp in info.imports:
                target = self._resolve_import(imp, path, scan)
                if target and target != path and target in self.graph:
                    self.graph.add_edge(path, target, kind="import")

    def _path_to_module(self, path: str) -> str:
        """
        Convert a file path to a human-readable module name.
        Examples:
          src/auth/login.py  → auth.login
          components/Button.tsx → components.Button
        """
        p = Path(path)
        parts = list(p.with_suffix("").parts)

        # Strip common prefixes that add noise
        noise = {"src", "lib", "pkg", "app", "source", "sources"}
        while parts and parts[0].lower() in noise:
            parts = parts[1:]

        return ".".join(parts) if parts else p.stem

    def _resolve_import(
        self, imp: str, from_file: str, scan: ScanResult
    ) -> str | None:
        """
        Try to match an import string to a file in the scan.
        Handles:
          - Relative imports (./, ../)
          - Dotted absolute imports (django.http → django/http/__init__.py)
          - Internal package imports (auth → auth/index.ts or auth.py)
          - Same-name stem matching
        """
        if not imp:
            return None

        from_dir = Path(from_file).parent

        # ── Dotted absolute imports: "django.http" → django/http/__init__.py ──
        if "." in imp and not imp.startswith("."):
            parts = [p.lower().replace("-", "_") for p in imp.split(".")]
            for path in scan.files:
                p = Path(path)
                # Match as package __init__.py: parts = ["django","http"]
                # → django/http/__init__.py
                if p.stem == "__init__":
                    dir_parts = [pt.lower().replace("-", "_") for pt in p.parent.parts]
                    if len(dir_parts) >= len(parts) and dir_parts[-len(parts):] == parts:
                        return path
                # Match as direct module file: "auth.middleware" → auth/middleware.py
                file_parts = [pt.lower().replace("-", "_")
                              for pt in p.with_suffix("").parts]
                if len(file_parts) >= len(parts) and file_parts[-len(parts):] == parts:
                    return path

        imp_lower = imp.lower().lstrip("./").replace("/", ".").replace("-", "_")
        if not imp_lower:
            return None

        for path in scan.files:
            p = Path(path)
            stem = p.stem.lower().replace("-", "_")
            norm_path = path.lower().replace("/", ".").replace("\\", ".").replace("-", "_")

            # Exact stem match
            if stem == imp_lower or stem == imp_lower.split(".")[-1]:
                return path

            # Path contains the import as a segment
            if f".{imp_lower}." in f".{norm_path}." or norm_path.endswith(f".{imp_lower}"):
                return path

            # Relative import: resolve relative to from_file's directory
            if imp.startswith("."):
                try:
                    resolved = (from_dir / imp.lstrip("./")).as_posix()
                    if path.startswith(resolved) or path == resolved + ".py":
                        return path
                except Exception:
                    pass

        return None

    # ── Queries ──────────────────────────────────────────────────────────────

    def get_dependencies(self, path: str) -> list[str]:
        """Files that `path` imports (outgoing edges)."""
        return list(self.graph.successors(path)) if path in self.graph else []

    def get_dependents(self, path: str) -> list[str]:
        """Files that import `path` (incoming edges)."""
        return list(self.graph.predecessors(path)) if path in self.graph else []

    def get_related_modules(self, path: str, depth: int = 2) -> set[str]:
        """
        BFS from `path` collecting all modules within `depth` hops
        in both directions (imports + dependents).
        """
        related: set[str] = set()
        queue: list[tuple[str, int]] = [(path, 0)]
        visited: set[str] = {path}

        while queue:
            node, d = queue.pop(0)
            if d >= depth:
                continue
            for neighbor in list(self.graph.predecessors(node)) + list(self.graph.successors(node)):
                if neighbor not in visited:
                    visited.add(neighbor)
                    related.add(neighbor)
                    queue.append((neighbor, d + 1))

        return related

    def find_modules_by_keyword(self, query: str) -> list[tuple[float, str]]:
        """
        Search nodes for `query` in path, module_name, classes, functions.
        Returns list of (score, path) sorted by score descending.
        """
        q = query.lower()
        results: list[tuple[float, str]] = []

        for node, data in self.graph.nodes(data=True):
            score = 0.0
            if q in node.lower():
                score += 4
            if q in data.get("module_name", "").lower():
                score += 10
            for cls in data.get("classes", []):
                if q in cls.lower():
                    score += 8
            for fn in data.get("functions", []):
                if q in fn.lower():
                    score += 5
            if q in data.get("summary", "").lower():
                score += 3

            if score > 0:
                results.append((score, node))

        results.sort(key=lambda x: -x[0])
        return results

    def set_summary(self, path: str, summary: str) -> None:
        """Attach a semantic summary to a graph node."""
        if path in self.graph:
            self.graph.nodes[path]["summary"] = summary

    def get_hub_modules(self, top_n: int = 10) -> list[tuple[str, int]]:
        """
        Return the most-imported modules (highest in-degree).
        These are architectural hubs — critical for understanding the project.
        """
        in_degrees = [(n, self.graph.in_degree(n)) for n in self.graph.nodes]
        in_degrees.sort(key=lambda x: -x[1])
        return in_degrees[:top_n]

    # ── Export ───────────────────────────────────────────────────────────────

    def to_dict(self) -> dict:
        """Serialize graph to JSON-compatible dict."""
        return {
            "nodes": [
                {
                    "id": n,
                    "module_name": data.get("module_name", ""),
                    "language": data.get("language", ""),
                    "classes": data.get("classes", []),
                    "functions": data.get("functions", []),
                    "exports": data.get("exports", []),
                    "summary": data.get("summary", ""),
                }
                for n, data in self.graph.nodes(data=True)
            ],
            "edges": [
                {"from": u, "to": v, "kind": self.graph.edges[u, v].get("kind", "import")}
                for u, v in self.graph.edges
            ],
        }

    def node_data(self, path: str) -> dict:
        """Get node attributes dict."""
        return dict(self.graph.nodes.get(path, {}))
