"""Dependency analysis for import/include relationships."""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Iterator

from ieum.analysis.treesitter.parser import TreeSitterParser, ParsedFile


@dataclass
class Dependency:
    """Represents a dependency relationship."""

    source_file: str  # File that imports
    target: str  # Module/file being imported
    import_type: str  # "import", "from_import", "include", "require"
    line: int
    alias: str | None = None


@dataclass
class DependencyNode:
    """Node in the dependency graph."""

    file_path: str
    module_name: str
    imports: list[str] = field(default_factory=list)  # What this file imports
    imported_by: list[str] = field(default_factory=list)  # Files that import this


@dataclass
class DependencyGraph:
    """Complete dependency graph for a codebase."""

    nodes: dict[str, DependencyNode]  # file_path -> node
    edges: list[Dependency]
    external_deps: list[str]  # Dependencies not in the codebase
    circular_deps: list[tuple[str, str]]  # Circular dependency pairs

    def get_imports(self, file_path: str) -> list[str]:
        """Get all imports for a file."""
        if file_path in self.nodes:
            return self.nodes[file_path].imports
        return []

    def get_importers(self, file_path: str) -> list[str]:
        """Get all files that import the given file."""
        if file_path in self.nodes:
            return self.nodes[file_path].imported_by
        return []

    def get_dependency_tree(self, file_path: str, max_depth: int = 10) -> dict:
        """Get dependency tree starting from a file."""
        visited = set()

        def _get_tree(path: str, depth: int) -> dict | None:
            if depth > max_depth or path in visited:
                return None
            visited.add(path)

            if path not in self.nodes:
                return {"file": path, "imports": [], "external": True}

            node = self.nodes[path]
            return {
                "file": path,
                "module": node.module_name,
                "imports": [
                    _get_tree(imp, depth + 1)
                    for imp in node.imports
                    if imp not in visited
                ],
            }

        return _get_tree(file_path, 0) or {"file": file_path, "imports": []}

    def get_reverse_tree(self, file_path: str, max_depth: int = 10) -> dict:
        """Get reverse dependency tree (who imports this)."""
        visited = set()

        def _get_tree(path: str, depth: int) -> dict | None:
            if depth > max_depth or path in visited:
                return None
            visited.add(path)

            if path not in self.nodes:
                return {"file": path, "imported_by": []}

            node = self.nodes[path]
            return {
                "file": path,
                "module": node.module_name,
                "imported_by": [
                    _get_tree(imp, depth + 1)
                    for imp in node.imported_by
                    if imp not in visited
                ],
            }

        return _get_tree(file_path, 0) or {"file": file_path, "imported_by": []}

    def to_mermaid(self, focus_file: str | None = None, max_nodes: int = 50) -> str:
        """Export dependency graph to Mermaid diagram format."""
        lines = ["graph LR"]

        if focus_file and focus_file in self.nodes:
            # Show only related nodes
            related = {focus_file}
            node = self.nodes[focus_file]
            related.update(node.imports)
            related.update(node.imported_by)

            nodes_to_show = list(related)[:max_nodes]
        else:
            nodes_to_show = list(self.nodes.keys())[:max_nodes]

        for path in nodes_to_show:
            safe_name = Path(path).stem.replace(".", "_").replace("-", "_")
            display_name = Path(path).stem
            lines.append(f"    {safe_name}[{display_name}]")

        for edge in self.edges:
            if edge.source_file in nodes_to_show:
                # Find target in nodes
                target_node = None
                for path in nodes_to_show:
                    if edge.target in path or Path(path).stem == edge.target:
                        target_node = path
                        break

                if target_node:
                    source = Path(edge.source_file).stem.replace(".", "_").replace("-", "_")
                    target = Path(target_node).stem.replace(".", "_").replace("-", "_")
                    lines.append(f"    {source} --> {target}")

        return "\n".join(lines)

    def to_dot(self, focus_file: str | None = None) -> str:
        """Export dependency graph to DOT format."""
        lines = ["digraph Dependencies {", "    rankdir=LR;"]

        nodes_to_show = set(self.nodes.keys())
        if focus_file and focus_file in self.nodes:
            nodes_to_show = {focus_file}
            node = self.nodes[focus_file]
            nodes_to_show.update(node.imports)
            nodes_to_show.update(node.imported_by)

        for path in nodes_to_show:
            safe_name = Path(path).stem.replace(".", "_").replace("-", "_")
            display = Path(path).stem

            if path in [c[0] for c in self.circular_deps] or path in [c[1] for c in self.circular_deps]:
                lines.append(f'    {safe_name} [label="{display}" style=filled fillcolor=lightcoral];')
            else:
                lines.append(f'    {safe_name} [label="{display}"];')

        for edge in self.edges:
            if edge.source_file in nodes_to_show:
                target_node = None
                for path in nodes_to_show:
                    if edge.target in path or Path(path).stem == edge.target:
                        target_node = path
                        break

                if target_node:
                    source = Path(edge.source_file).stem.replace(".", "_").replace("-", "_")
                    target = Path(target_node).stem.replace(".", "_").replace("-", "_")
                    lines.append(f"    {source} -> {target};")

        lines.append("}")
        return "\n".join(lines)

    def find_circular_dependencies(self) -> list[list[str]]:
        """Find all circular dependency chains."""
        cycles: list[list[str]] = []
        visited = set()
        rec_stack: list[str] = []

        def _dfs(node: str) -> None:
            visited.add(node)
            rec_stack.append(node)

            if node in self.nodes:
                for neighbor in self.nodes[node].imports:
                    if neighbor not in visited:
                        _dfs(neighbor)
                    elif neighbor in rec_stack:
                        # Found cycle
                        cycle_start = rec_stack.index(neighbor)
                        cycle = rec_stack[cycle_start:] + [neighbor]
                        cycles.append(cycle)

            rec_stack.pop()

        for node in self.nodes:
            if node not in visited:
                _dfs(node)

        return cycles


class DependencyAnalyzer:
    """Analyzes import/include dependencies in a codebase."""

    def __init__(self) -> None:
        """Initialize the dependency analyzer."""
        self.parser = TreeSitterParser()

    def analyze_file(self, file_path: str | Path) -> DependencyGraph:
        """Analyze dependencies in a single file."""
        parsed = self.parser.parse_file(file_path)
        return self._build_graph([parsed])

    def analyze_directory(
        self,
        directory: str | Path,
        extensions: list[str] | None = None,
    ) -> DependencyGraph:
        """Analyze dependencies in all files in a directory."""
        parsed_files = list(self.parser.parse_directory(directory, extensions))
        return self._build_graph(parsed_files)

    def analyze_files(self, file_paths: list[str | Path]) -> DependencyGraph:
        """Analyze dependencies in a list of files."""
        parsed_files = [self.parser.parse_file(f) for f in file_paths]
        return self._build_graph(parsed_files)

    def _build_graph(self, parsed_files: list[ParsedFile]) -> DependencyGraph:
        """Build dependency graph from parsed files."""
        nodes: dict[str, DependencyNode] = {}
        edges: list[Dependency] = []
        external_deps: set[str] = set()

        # Build file path to module name mapping
        file_to_module: dict[str, str] = {}
        module_to_file: dict[str, str] = {}

        for parsed in parsed_files:
            module_name = Path(parsed.file_path).stem
            file_to_module[parsed.file_path] = module_name
            module_to_file[module_name] = parsed.file_path

            nodes[parsed.file_path] = DependencyNode(
                file_path=parsed.file_path,
                module_name=module_name,
            )

        # Extract dependencies
        for parsed in parsed_files:
            if parsed.language == "python":
                file_edges = self._extract_python_deps(parsed, module_to_file)
            elif parsed.language in ("javascript", "typescript", "tsx"):
                file_edges = self._extract_js_deps(parsed, module_to_file)
            elif parsed.language in ("c", "cpp"):
                file_edges = self._extract_cpp_deps(parsed, module_to_file)
            else:
                file_edges = []

            edges.extend(file_edges)

        # Build adjacency lists
        for edge in edges:
            source = edge.source_file

            # Try to find target file
            target_file = None
            target_module = edge.target.split(".")[0]  # Get top-level module

            if target_module in module_to_file:
                target_file = module_to_file[target_module]

            if target_file and target_file in nodes:
                if target_file not in nodes[source].imports:
                    nodes[source].imports.append(target_file)
                if source not in nodes[target_file].imported_by:
                    nodes[target_file].imported_by.append(source)
            else:
                external_deps.add(edge.target)

        # Find circular dependencies
        circular_deps: list[tuple[str, str]] = []
        for source in nodes:
            for target in nodes[source].imports:
                if source in nodes.get(target, DependencyNode("", "")).imports:
                    pair = tuple(sorted([source, target]))
                    if pair not in circular_deps:
                        circular_deps.append(pair)

        return DependencyGraph(
            nodes=nodes,
            edges=edges,
            external_deps=list(external_deps),
            circular_deps=circular_deps,
        )

    def _extract_python_deps(
        self, parsed: ParsedFile, module_to_file: dict[str, str]
    ) -> list[Dependency]:
        """Extract Python import dependencies."""
        edges: list[Dependency] = []

        try:
            source = Path(parsed.file_path).read_text(encoding="utf-8", errors="replace")
        except Exception:
            return edges

        lines = source.split("\n")

        # Import patterns
        import_pattern = re.compile(r"^import\s+([\w.]+)(?:\s+as\s+(\w+))?")
        from_pattern = re.compile(r"^from\s+([\w.]+)\s+import\s+(.+)")

        for i, line in enumerate(lines, 1):
            line = line.strip()

            match = import_pattern.match(line)
            if match:
                module = match.group(1)
                alias = match.group(2)
                edges.append(Dependency(
                    source_file=parsed.file_path,
                    target=module,
                    import_type="import",
                    line=i,
                    alias=alias,
                ))
                continue

            match = from_pattern.match(line)
            if match:
                module = match.group(1)
                edges.append(Dependency(
                    source_file=parsed.file_path,
                    target=module,
                    import_type="from_import",
                    line=i,
                ))

        return edges

    def _extract_js_deps(
        self, parsed: ParsedFile, module_to_file: dict[str, str]
    ) -> list[Dependency]:
        """Extract JavaScript/TypeScript import dependencies."""
        edges: list[Dependency] = []

        try:
            source = Path(parsed.file_path).read_text(encoding="utf-8", errors="replace")
        except Exception:
            return edges

        lines = source.split("\n")

        # Import patterns
        patterns = [
            (re.compile(r"import\s+.*?\s+from\s+['\"]([^'\"]+)['\"]"), "import"),
            (re.compile(r"import\s+['\"]([^'\"]+)['\"]"), "import"),
            (re.compile(r"require\s*\(\s*['\"]([^'\"]+)['\"]\s*\)"), "require"),
            (re.compile(r"export\s+.*?\s+from\s+['\"]([^'\"]+)['\"]"), "export_from"),
        ]

        for i, line in enumerate(lines, 1):
            for pattern, import_type in patterns:
                match = pattern.search(line)
                if match:
                    target = match.group(1)
                    # Normalize relative imports
                    if target.startswith("./") or target.startswith("../"):
                        target = self._resolve_relative_import(
                            parsed.file_path, target
                        )
                    edges.append(Dependency(
                        source_file=parsed.file_path,
                        target=target,
                        import_type=import_type,
                        line=i,
                    ))

        return edges

    def _extract_cpp_deps(
        self, parsed: ParsedFile, module_to_file: dict[str, str]
    ) -> list[Dependency]:
        """Extract C/C++ include dependencies."""
        edges: list[Dependency] = []

        try:
            source = Path(parsed.file_path).read_text(encoding="utf-8", errors="replace")
        except Exception:
            return edges

        lines = source.split("\n")

        # Include patterns
        include_pattern = re.compile(r'#include\s*([<"])([^>"]+)[>"]')

        for i, line in enumerate(lines, 1):
            match = include_pattern.search(line)
            if match:
                bracket_type = match.group(1)
                header = match.group(2)
                import_type = "include_system" if bracket_type == "<" else "include_local"
                edges.append(Dependency(
                    source_file=parsed.file_path,
                    target=header,
                    import_type=import_type,
                    line=i,
                ))

        return edges

    def _resolve_relative_import(self, source_file: str, target: str) -> str:
        """Resolve relative import path to module name."""
        source_dir = Path(source_file).parent
        target_path = (source_dir / target).resolve()

        # Remove extension
        if target_path.suffix in (".js", ".ts", ".jsx", ".tsx"):
            return target_path.stem

        return str(target_path)
