"""
Code Graph Builder - Build and maintain code dependency graph

Extracts relationships between code elements:
- imports: Module/file import relationships
- inherits: Class inheritance relationships
- calls: Function/method call relationships
- contains: Hierarchical containment (package->module->class->function)

Uses AST analysis to extract relationships and stores them in SQLite
for efficient graph traversal during retrieval.
"""

import ast
from collections import deque
from dataclasses import dataclass
from typing import Any, Optional

from groknroll.storage.database import Database


@dataclass
class GraphNode:
    """Node in the code graph"""

    summary_id: int
    scope_type: str
    scope_path: str
    scope_name: Optional[str]
    depth: int = 0


class CodeGraphBuilder:
    """
    Build and maintain code dependency graph.

    Extracts relationships from AST and stores them in the database
    for efficient traversal during context expansion.
    """

    RELATION_TYPES = ["imports", "inherits", "calls", "contains"]

    def __init__(
        self,
        database: Database,
        project_id: int,
        verbose: bool = False,
    ):
        """
        Initialize graph builder.

        Args:
            database: Database instance
            project_id: Project ID
            verbose: Whether to print progress
        """
        self.database = database
        self.project_id = project_id
        self.verbose = verbose

        # Cache for summary lookups
        self._summary_cache: dict[str, int] = {}  # "type:path:name" -> summary_id

    def build_graph(self, files: dict[str, Any]) -> int:
        """
        Build dependency graph from indexed files.

        Args:
            files: Dict mapping relative_path to FileInfo from indexer

        Returns:
            Number of relations created
        """
        if self.verbose:
            print(f"🔗 Building code graph from {len(files)} files...")

        # Clear existing relations for fresh build
        self.database.clear_relations(self.project_id)

        # Build summary cache for efficient lookups
        self._build_summary_cache()

        relation_count = 0

        for rel_path, file_info in files.items():
            if file_info.language != "python" or not file_info.content:
                continue

            try:
                relations = self._extract_relations(rel_path, file_info.content)
                for source_key, target_key, rel_type in relations:
                    source_id = self._get_summary_id(source_key)
                    target_id = self._get_summary_id(target_key)

                    if source_id and target_id:
                        self.database.add_relation(source_id, target_id, rel_type)
                        relation_count += 1

            except Exception as e:
                if self.verbose:
                    print(f"   ⚠️  Error processing {rel_path}: {e}")

        # Add containment relations from summary hierarchy
        relation_count += self._add_containment_relations()

        if self.verbose:
            print(f"✅ Created {relation_count} relations")

        return relation_count

    def get_related(
        self,
        summary_id: int,
        max_depth: int = 2,
        relation_types: Optional[list[str]] = None,
    ) -> list[GraphNode]:
        """
        Get related summaries via graph traversal.

        Uses BFS to find all summaries within max_depth hops.

        Args:
            summary_id: Starting summary ID
            max_depth: Maximum traversal depth
            relation_types: Filter by relation types (optional)

        Returns:
            List of GraphNode objects with related summaries
        """
        if relation_types is None:
            relation_types = self.RELATION_TYPES

        visited: set[int] = {summary_id}
        results: list[GraphNode] = []
        queue: deque[tuple[int, int]] = deque([(summary_id, 0)])

        while queue:
            current_id, depth = queue.popleft()

            if depth >= max_depth:
                continue

            # Get related summaries
            related = self.database.get_related_summaries(
                current_id, relation_types, direction="both"
            )

            for summary in related:
                if summary.id not in visited:
                    visited.add(summary.id)
                    results.append(GraphNode(
                        summary_id=summary.id,
                        scope_type=summary.scope_type,
                        scope_path=summary.scope_path,
                        scope_name=summary.scope_name,
                        depth=depth + 1,
                    ))
                    queue.append((summary.id, depth + 1))

        return results

    def get_importers(self, module_path: str) -> list[GraphNode]:
        """
        Get all modules that import a given module.

        Args:
            module_path: Path to the module

        Returns:
            List of GraphNode objects for importing modules
        """
        summary_id = self._get_summary_id(f"module:{module_path}:")
        if not summary_id:
            return []

        # Get incoming import relations
        relations = self.database.get_incoming_relations(summary_id, ["imports"])
        results = []

        for rel in relations:
            source_summary = self.database.get_summary_by_id(rel.source_id)
            if source_summary:
                results.append(GraphNode(
                    summary_id=source_summary.id,
                    scope_type=source_summary.scope_type,
                    scope_path=source_summary.scope_path,
                    scope_name=source_summary.scope_name,
                ))

        return results

    def get_subclasses(self, class_name: str) -> list[GraphNode]:
        """
        Get all classes that inherit from a given class.

        Args:
            class_name: Name of the base class

        Returns:
            List of GraphNode objects for subclasses
        """
        # Find the class summary
        summaries = self.database.get_summaries_by_type(self.project_id, "class")
        target_summary = None
        for s in summaries:
            if s.scope_name == class_name:
                target_summary = s
                break

        if not target_summary:
            return []

        # Get incoming inheritance relations
        relations = self.database.get_incoming_relations(target_summary.id, ["inherits"])
        results = []

        for rel in relations:
            source_summary = self.database.get_summary_by_id(rel.source_id)
            if source_summary:
                results.append(GraphNode(
                    summary_id=source_summary.id,
                    scope_type=source_summary.scope_type,
                    scope_path=source_summary.scope_path,
                    scope_name=source_summary.scope_name,
                ))

        return results

    def get_callers(self, function_name: str) -> list[GraphNode]:
        """
        Get all functions that call a given function.

        Args:
            function_name: Name of the function

        Returns:
            List of GraphNode objects for calling functions
        """
        # Find function summaries with this name
        summaries = self.database.get_summaries_by_type(self.project_id, "function")
        target_ids = [s.id for s in summaries if s.scope_name == function_name]

        results = []
        for target_id in target_ids:
            relations = self.database.get_incoming_relations(target_id, ["calls"])
            for rel in relations:
                source_summary = self.database.get_summary_by_id(rel.source_id)
                if source_summary:
                    results.append(GraphNode(
                        summary_id=source_summary.id,
                        scope_type=source_summary.scope_type,
                        scope_path=source_summary.scope_path,
                        scope_name=source_summary.scope_name,
                    ))

        return results

    def _build_summary_cache(self) -> None:
        """Build cache mapping scope keys to summary IDs."""
        self._summary_cache.clear()

        for scope_type in ["package", "module", "class", "function"]:
            summaries = self.database.get_summaries_by_type(self.project_id, scope_type)
            for summary in summaries:
                key = f"{summary.scope_type}:{summary.scope_path}:{summary.scope_name or ''}"
                self._summary_cache[key] = summary.id

    def _get_summary_id(self, key: str) -> Optional[int]:
        """Get summary ID from cache key."""
        return self._summary_cache.get(key)

    def _extract_relations(
        self, file_path: str, content: str
    ) -> list[tuple[str, str, str]]:
        """
        Extract relations from a Python file.

        Returns:
            List of (source_key, target_key, relation_type) tuples
        """
        relations = []

        try:
            tree = ast.parse(content)
        except SyntaxError:
            return relations

        # Track current scope for context
        module_key = f"module:{file_path}:"

        # Extract imports
        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    # Create relation from this module to imported module
                    target_path = self._resolve_import(alias.name, file_path)
                    if target_path:
                        target_key = f"module:{target_path}:"
                        relations.append((module_key, target_key, "imports"))

            elif isinstance(node, ast.ImportFrom):
                if node.module:
                    target_path = self._resolve_import(node.module, file_path)
                    if target_path:
                        target_key = f"module:{target_path}:"
                        relations.append((module_key, target_key, "imports"))

        # Extract class relations
        for node in ast.walk(tree):
            if isinstance(node, ast.ClassDef):
                class_key = f"class:{file_path}:{node.name}"

                # Inheritance relations
                for base in node.bases:
                    if isinstance(base, ast.Name):
                        # Find target class
                        target_key = self._find_class_key(base.id)
                        if target_key:
                            relations.append((class_key, target_key, "inherits"))
                    elif isinstance(base, ast.Attribute):
                        # module.ClassName
                        class_name = base.attr
                        target_key = self._find_class_key(class_name)
                        if target_key:
                            relations.append((class_key, target_key, "inherits"))

                # Method definitions create containment
                for item in node.body:
                    if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
                        method_key = f"function:{file_path}:{item.name}"
                        relations.append((class_key, method_key, "contains"))

        # Extract function call relations (simplified)
        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                func_key = f"function:{file_path}:{node.name}"

                # Find calls within this function
                for child in ast.walk(node):
                    if isinstance(child, ast.Call):
                        if isinstance(child.func, ast.Name):
                            # Direct function call
                            target_key = self._find_function_key(child.func.id)
                            if target_key and target_key != func_key:
                                relations.append((func_key, target_key, "calls"))

        return relations

    def _resolve_import(self, module_name: str, current_file: str) -> Optional[str]:
        """
        Resolve an import to a file path in the project.

        Args:
            module_name: Module name (e.g., 'groknroll.oracle.codebase_indexer')
            current_file: Current file path for relative imports

        Returns:
            Resolved file path or None if not found
        """
        # Convert module name to potential file paths
        parts = module_name.split(".")

        # Try as directory/__init__.py
        potential_paths = [
            "/".join(parts) + "/__init__.py",
            "/".join(parts) + ".py",
        ]

        for path in potential_paths:
            key = f"module:{path}:"
            if key in self._summary_cache:
                return path

        return None

    def _find_class_key(self, class_name: str) -> Optional[str]:
        """Find the cache key for a class by name."""
        for key in self._summary_cache:
            if key.startswith("class:") and key.endswith(f":{class_name}"):
                return key
        return None

    def _find_function_key(self, func_name: str) -> Optional[str]:
        """Find the cache key for a function by name."""
        for key in self._summary_cache:
            if key.startswith("function:") and key.endswith(f":{func_name}"):
                return key
        return None

    def _add_containment_relations(self) -> int:
        """
        Add containment relations from the summary hierarchy.

        Uses parent_id relationships in summaries to create 'contains' relations.

        Returns:
            Number of containment relations added
        """
        count = 0

        # Get all summaries with parent_id
        for scope_type in ["class", "function"]:
            summaries = self.database.get_summaries_by_type(self.project_id, scope_type)
            for summary in summaries:
                if summary.parent_id:
                    self.database.add_relation(
                        summary.parent_id, summary.id, "contains"
                    )
                    count += 1

        return count

    def clear(self) -> None:
        """Clear all relations for this project."""
        self.database.clear_relations(self.project_id)
        self._summary_cache.clear()
