"""Cyclomatic complexity analyzer using Python AST.

Calculates real cyclomatic complexity by counting decision points in code.
Complexity = 1 + number_of_decision_points

Decision points: if, elif, for, while, except, with, and, or,
                 assert, ternary (IfExp), list/set/dict comprehensions.
"""

from __future__ import annotations

import ast
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any


@dataclass
class FunctionComplexity:
    """Complexity result for a single function/method."""

    name: str
    file_path: str
    line_number: int
    complexity: int
    line_count: int
    is_method: bool = False
    class_name: str = ""

    @property
    def rating(self) -> str:
        """Human-readable complexity rating."""
        if self.complexity <= 5:
            return "simple"
        elif self.complexity <= 10:
            return "moderate"
        elif self.complexity <= 20:
            return "complex"
        elif self.complexity <= 50:
            return "very_complex"
        return "unmaintainable"

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "file_path": self.file_path,
            "line_number": self.line_number,
            "complexity": self.complexity,
            "line_count": self.line_count,
            "rating": self.rating,
            "is_method": self.is_method,
            "class_name": self.class_name,
        }


@dataclass
class FileComplexity:
    """Aggregate complexity for a file."""

    file_path: str
    functions: list[FunctionComplexity] = field(default_factory=list)
    parse_error: str | None = None

    @property
    def max_complexity(self) -> int:
        if not self.functions:
            return 0
        return max(f.complexity for f in self.functions)

    @property
    def avg_complexity(self) -> float:
        if not self.functions:
            return 0.0
        return sum(f.complexity for f in self.functions) / len(self.functions)

    @property
    def total_complexity(self) -> int:
        return sum(f.complexity for f in self.functions)

    def to_dict(self) -> dict[str, Any]:
        return {
            "file_path": self.file_path,
            "function_count": len(self.functions),
            "max_complexity": self.max_complexity,
            "avg_complexity": round(self.avg_complexity, 2),
            "total_complexity": self.total_complexity,
            "functions": [f.to_dict() for f in self.functions],
            "parse_error": self.parse_error,
        }


class _ComplexityVisitor(ast.NodeVisitor):
    """AST visitor that calculates cyclomatic complexity for each function."""

    def __init__(self, file_path: str) -> None:
        self.file_path = file_path
        self.results: list[FunctionComplexity] = []
        self._class_stack: list[str] = []

    def visit_ClassDef(self, node: ast.ClassDef) -> None:
        self._class_stack.append(node.name)
        self.generic_visit(node)
        self._class_stack.pop()

    def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
        self._process_function(node)

    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
        self._process_function(node)

    def _process_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
        complexity = self._calculate_complexity(node)
        end_line = getattr(node, "end_lineno", node.lineno)
        line_count = end_line - node.lineno + 1

        is_method = len(self._class_stack) > 0
        class_name = self._class_stack[-1] if is_method else ""
        full_name = f"{class_name}.{node.name}" if is_method else node.name

        self.results.append(
            FunctionComplexity(
                name=full_name,
                file_path=self.file_path,
                line_number=node.lineno,
                complexity=complexity,
                line_count=line_count,
                is_method=is_method,
                class_name=class_name,
            )
        )
        # Visit nested functions/classes
        self.generic_visit(node)

    def _calculate_complexity(self, node: ast.AST) -> int:
        """Count decision points within a function body.

        Cyclomatic complexity = 1 + decision_points
        """
        complexity = 1  # Base complexity

        for child in ast.walk(node):
            # Branching statements
            if isinstance(child, ast.If):
                complexity += 1
            elif isinstance(child, ast.For):
                complexity += 1
            elif isinstance(child, ast.While):
                complexity += 1
            elif isinstance(child, ast.ExceptHandler):
                complexity += 1
            elif isinstance(child, ast.With):
                complexity += 1
            elif isinstance(child, ast.Assert):
                complexity += 1
            # Boolean operators (each 'and'/'or' adds a path)
            elif isinstance(child, ast.BoolOp):
                # 'a and b and c' has 2 operators -> 2 extra paths
                complexity += len(child.values) - 1
            # Ternary expression: x if cond else y
            elif isinstance(child, ast.IfExp):
                complexity += 1
            # Comprehensions contain implicit loops
            elif isinstance(child, (ast.ListComp, ast.SetComp, ast.GeneratorExp, ast.DictComp)):
                for generator in child.generators:
                    complexity += 1  # The for clause
                    complexity += len(generator.ifs)  # Each if filter

        return complexity


class ComplexityAnalyzer:
    """Analyze cyclomatic complexity of Python source files."""

    SUPPORTED_EXTENSIONS = {".py"}

    def analyze_file(self, file_path: str | Path) -> FileComplexity:
        """Analyze a single Python file for function complexity."""
        file_path = Path(file_path)
        result = FileComplexity(file_path=str(file_path))

        if file_path.suffix not in self.SUPPORTED_EXTENSIONS:
            return result

        try:
            source = file_path.read_text(encoding="utf-8", errors="replace")
        except OSError as e:
            result.parse_error = f"Cannot read file: {e}"
            return result

        try:
            tree = ast.parse(source, filename=str(file_path))
        except SyntaxError as e:
            result.parse_error = f"Syntax error at line {e.lineno}: {e.msg}"
            return result

        visitor = _ComplexityVisitor(str(file_path))
        visitor.visit(tree)
        result.functions = visitor.results
        return result

    def analyze_directory(
        self,
        directory: str | Path,
        exclude_patterns: list[str] | None = None,
    ) -> list[FileComplexity]:
        """Analyze all Python files in a directory tree."""
        directory = Path(directory)
        exclude_patterns = exclude_patterns or [
            "venv", "node_modules", ".git", "__pycache__", ".tox", ".eggs",
            "build", "dist", ".mypy_cache",
        ]
        results: list[FileComplexity] = []

        for py_file in directory.rglob("*.py"):
            # Skip excluded directories
            parts = py_file.relative_to(directory).parts
            if any(excl in parts for excl in exclude_patterns):
                continue
            results.append(self.analyze_file(py_file))

        return results

    def get_hotspots(
        self,
        results: list[FileComplexity],
        threshold: int = 10,
        limit: int = 20,
    ) -> list[FunctionComplexity]:
        """Get the most complex functions across all analyzed files.

        Args:
            results: List of FileComplexity from analyze_directory.
            threshold: Minimum complexity to be considered a hotspot.
            limit: Max number of results.

        Returns:
            Functions sorted by complexity descending.
        """
        all_funcs: list[FunctionComplexity] = []
        for fc in results:
            for fn in fc.functions:
                if fn.complexity >= threshold:
                    all_funcs.append(fn)

        all_funcs.sort(key=lambda f: f.complexity, reverse=True)
        return all_funcs[:limit]

    def summary(self, results: list[FileComplexity]) -> dict[str, Any]:
        """Generate a summary of complexity analysis."""
        all_funcs = [fn for fc in results for fn in fc.functions]
        if not all_funcs:
            return {
                "total_functions": 0,
                "avg_complexity": 0,
                "max_complexity": 0,
                "simple_count": 0,
                "moderate_count": 0,
                "complex_count": 0,
                "very_complex_count": 0,
                "unmaintainable_count": 0,
            }

        ratings = {"simple": 0, "moderate": 0, "complex": 0, "very_complex": 0, "unmaintainable": 0}
        for fn in all_funcs:
            ratings[fn.rating] += 1

        return {
            "total_functions": len(all_funcs),
            "avg_complexity": round(sum(f.complexity for f in all_funcs) / len(all_funcs), 2),
            "max_complexity": max(f.complexity for f in all_funcs),
            "simple_count": ratings["simple"],
            "moderate_count": ratings["moderate"],
            "complex_count": ratings["complex"],
            "very_complex_count": ratings["very_complex"],
            "unmaintainable_count": ratings["unmaintainable"],
        }
