"""
Dependency Graph Builder

Builds component and style dependency graphs for visualization
and analysis of project structure.
"""

import re
import json
from pathlib import Path
from typing import List, Dict, Any, Optional, Set, Tuple
from dataclasses import dataclass, field
from collections import defaultdict


@dataclass
class GraphNode:
    """A node in the dependency graph."""
    id: str
    name: str
    type: str  # 'component', 'style', 'util', 'hook'
    path: str
    size: int = 0  # file size or importance metric
    children: List[str] = field(default_factory=list)
    parents: List[str] = field(default_factory=list)
    metadata: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        return {
            'id': self.id,
            'name': self.name,
            'type': self.type,
            'path': self.path,
            'size': self.size,
            'children': self.children,
            'parents': self.parents,
            'metadata': self.metadata,
        }


@dataclass
class GraphEdge:
    """An edge in the dependency graph."""
    source: str
    target: str
    type: str  # 'import', 'uses', 'styles'
    weight: int = 1

    def to_dict(self) -> Dict[str, Any]:
        return {
            'source': self.source,
            'target': self.target,
            'type': self.type,
            'weight': self.weight,
        }


class DependencyGraph:
    """
    Builds and analyzes dependency graphs for a project.

    Tracks:
    - Component imports/exports
    - Style file dependencies
    - Component usage relationships
    """

    def __init__(self, root_path: str):
        self.root = Path(root_path).resolve()
        self.nodes: Dict[str, GraphNode] = {}
        self.edges: List[GraphEdge] = []

    async def build(self, depth: int = 3) -> Dict[str, Any]:
        """
        Build the full dependency graph.

        Args:
            depth: Maximum depth for traversing dependencies

        Returns:
            Graph representation with nodes and edges
        """
        # Clear existing graph
        self.nodes.clear()
        self.edges.clear()

        # Find all relevant files
        await self._scan_files()

        # Build edges from imports
        await self._build_import_edges()

        # Build edges from component usage
        await self._build_usage_edges()

        return self.to_dict()

    async def _scan_files(self) -> None:
        """Scan project files and create nodes."""
        skip_dirs = {'node_modules', '.git', 'dist', 'build', '.next'}

        # Component files
        for ext in ['*.jsx', '*.tsx']:
            for file_path in self.root.rglob(ext):
                if any(skip in file_path.parts for skip in skip_dirs):
                    continue

                rel_path = str(file_path.relative_to(self.root))
                node_id = self._path_to_id(rel_path)

                self.nodes[node_id] = GraphNode(
                    id=node_id,
                    name=file_path.stem,
                    type='component',
                    path=rel_path,
                    size=file_path.stat().st_size,
                )

        # Style files
        for ext in ['*.css', '*.scss', '*.sass', '*.less']:
            for file_path in self.root.rglob(ext):
                if any(skip in file_path.parts for skip in skip_dirs):
                    continue

                rel_path = str(file_path.relative_to(self.root))
                node_id = self._path_to_id(rel_path)

                self.nodes[node_id] = GraphNode(
                    id=node_id,
                    name=file_path.stem,
                    type='style',
                    path=rel_path,
                    size=file_path.stat().st_size,
                )

        # Utility/Hook files
        for ext in ['*.js', '*.ts']:
            for file_path in self.root.rglob(ext):
                if any(skip in file_path.parts for skip in skip_dirs):
                    continue

                name = file_path.stem.lower()
                rel_path = str(file_path.relative_to(self.root))
                node_id = self._path_to_id(rel_path)

                # Classify file type
                if 'hook' in name or name.startswith('use'):
                    node_type = 'hook'
                elif any(x in name for x in ['util', 'helper', 'lib']):
                    node_type = 'util'
                else:
                    continue  # Skip other JS/TS files

                self.nodes[node_id] = GraphNode(
                    id=node_id,
                    name=file_path.stem,
                    type=node_type,
                    path=rel_path,
                    size=file_path.stat().st_size,
                )

    async def _build_import_edges(self) -> None:
        """Build edges from import statements."""
        import_pattern = re.compile(
            r'import\s+(?:\{[^}]+\}|\*\s+as\s+\w+|\w+)?\s*(?:,\s*\{[^}]+\})?\s*from\s+["\']([^"\']+)["\']',
            re.MULTILINE
        )

        for node_id, node in self.nodes.items():
            if node.type not in ['component', 'hook', 'util']:
                continue

            file_path = self.root / node.path
            if not file_path.exists():
                continue

            try:
                content = file_path.read_text(encoding='utf-8', errors='ignore')

                for match in import_pattern.finditer(content):
                    import_path = match.group(1)

                    # Resolve relative imports
                    target_id = self._resolve_import(node.path, import_path)

                    if target_id and target_id in self.nodes:
                        # Add edge
                        self.edges.append(GraphEdge(
                            source=node_id,
                            target=target_id,
                            type='import',
                        ))

                        # Update parent/child relationships
                        node.children.append(target_id)
                        self.nodes[target_id].parents.append(node_id)

            except Exception:
                continue

    async def _build_usage_edges(self) -> None:
        """Build edges from component usage in JSX."""
        # Pattern to find JSX component usage
        jsx_pattern = re.compile(r'<([A-Z][A-Za-z0-9]*)')

        # Build name -> id mapping for components
        name_to_id = {}
        for node_id, node in self.nodes.items():
            if node.type == 'component':
                name_to_id[node.name] = node_id

        for node_id, node in self.nodes.items():
            if node.type != 'component':
                continue

            file_path = self.root / node.path
            if not file_path.exists():
                continue

            try:
                content = file_path.read_text(encoding='utf-8', errors='ignore')

                used_components = set()
                for match in jsx_pattern.finditer(content):
                    comp_name = match.group(1)
                    if comp_name in name_to_id and name_to_id[comp_name] != node_id:
                        used_components.add(name_to_id[comp_name])

                for target_id in used_components:
                    self.edges.append(GraphEdge(
                        source=node_id,
                        target=target_id,
                        type='uses',
                    ))

            except Exception:
                continue

    def _path_to_id(self, path: str) -> str:
        """Convert file path to node ID."""
        # Remove extension and normalize
        path = re.sub(r'\.(jsx?|tsx?|css|scss|sass|less)$', '', path)
        return path.replace('/', '_').replace('\\', '_').replace('.', '_')

    def _resolve_import(self, source_path: str, import_path: str) -> Optional[str]:
        """Resolve import path to node ID."""
        if not import_path.startswith('.'):
            return None  # Skip node_modules imports

        source_dir = Path(source_path).parent

        # Handle various import patterns
        if import_path.startswith('./'):
            resolved = source_dir / import_path[2:]
        elif import_path.startswith('../'):
            resolved = source_dir / import_path
        else:
            resolved = source_dir / import_path

        # Try to resolve with extensions
        extensions = ['.tsx', '.ts', '.jsx', '.js', '.css', '.scss', '/index.tsx', '/index.ts', '/index.jsx', '/index.js']

        resolved_str = str(resolved)
        for ext in extensions:
            test_id = self._path_to_id(resolved_str + ext)
            if test_id in self.nodes:
                return test_id

        # Try without additional extension (if path already has one)
        test_id = self._path_to_id(resolved_str)
        if test_id in self.nodes:
            return test_id

        return None

    def to_dict(self) -> Dict[str, Any]:
        """Convert graph to dictionary for serialization."""
        return {
            'nodes': [node.to_dict() for node in self.nodes.values()],
            'edges': [edge.to_dict() for edge in self.edges],
            'stats': {
                'total_nodes': len(self.nodes),
                'total_edges': len(self.edges),
                'components': len([n for n in self.nodes.values() if n.type == 'component']),
                'styles': len([n for n in self.nodes.values() if n.type == 'style']),
                'hooks': len([n for n in self.nodes.values() if n.type == 'hook']),
                'utils': len([n for n in self.nodes.values() if n.type == 'util']),
            }
        }

    def to_json(self, pretty: bool = True) -> str:
        """Convert graph to JSON string."""
        return json.dumps(self.to_dict(), indent=2 if pretty else None)

    def get_component_tree(self) -> Dict[str, List[str]]:
        """Get simplified component dependency tree."""
        tree = {}
        for node_id, node in self.nodes.items():
            if node.type == 'component':
                tree[node.name] = [
                    self.nodes[child_id].name
                    for child_id in node.children
                    if child_id in self.nodes and self.nodes[child_id].type == 'component'
                ]
        return tree

    def find_orphans(self) -> List[str]:
        """Find components with no parents (not imported anywhere)."""
        orphans = []
        for node_id, node in self.nodes.items():
            if node.type == 'component' and not node.parents:
                # Exclude entry points (index, App, etc.)
                if node.name.lower() not in ['app', 'index', 'main', 'root']:
                    orphans.append(node.path)
        return orphans

    def find_hubs(self, min_connections: int = 5) -> List[Dict[str, Any]]:
        """Find highly connected nodes (potential refactoring targets)."""
        hubs = []
        for node_id, node in self.nodes.items():
            connections = len(node.children) + len(node.parents)
            if connections >= min_connections:
                hubs.append({
                    'name': node.name,
                    'path': node.path,
                    'type': node.type,
                    'imports': len(node.children),
                    'imported_by': len(node.parents),
                    'total_connections': connections,
                })

        hubs.sort(key=lambda x: x['total_connections'], reverse=True)
        return hubs

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

        def dfs(node_id: str, path: List[str]) -> None:
            visited.add(node_id)
            rec_stack.add(node_id)
            path.append(node_id)

            for child_id in self.nodes.get(node_id, GraphNode('', '', '', '')).children:
                if child_id not in visited:
                    dfs(child_id, path.copy())
                elif child_id in rec_stack:
                    # Found cycle
                    cycle_start = path.index(child_id)
                    cycle = path[cycle_start:] + [child_id]
                    cycles.append([self.nodes[n].name for n in cycle])

            rec_stack.remove(node_id)

        for node_id in self.nodes:
            if node_id not in visited:
                dfs(node_id, [])

        return cycles

    def get_subgraph(self, node_id: str, depth: int = 2) -> Dict[str, Any]:
        """Get subgraph centered on a specific node."""
        if node_id not in self.nodes:
            return {'nodes': [], 'edges': []}

        # BFS to find nodes within depth
        included_nodes = {node_id}
        frontier = {node_id}

        for _ in range(depth):
            new_frontier = set()
            for nid in frontier:
                node = self.nodes.get(nid)
                if node:
                    new_frontier.update(node.children)
                    new_frontier.update(node.parents)
            included_nodes.update(new_frontier)
            frontier = new_frontier

        # Filter nodes and edges
        subgraph_nodes = [
            self.nodes[nid].to_dict()
            for nid in included_nodes
            if nid in self.nodes
        ]

        subgraph_edges = [
            edge.to_dict()
            for edge in self.edges
            if edge.source in included_nodes and edge.target in included_nodes
        ]

        return {
            'nodes': subgraph_nodes,
            'edges': subgraph_edges,
            'center': node_id,
            'depth': depth,
        }

    def get_style_dependencies(self) -> Dict[str, List[str]]:
        """Get mapping of components to their style dependencies."""
        style_deps = {}

        for node_id, node in self.nodes.items():
            if node.type != 'component':
                continue

            style_children = [
                self.nodes[child_id].path
                for child_id in node.children
                if child_id in self.nodes and self.nodes[child_id].type == 'style'
            ]

            if style_children:
                style_deps[node.path] = style_children

        return style_deps
