"""Code analyzer for identifying issues and patterns.

This module provides static analysis capabilities for code review.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Optional, Any


class Severity(Enum):
    """Severity levels for review issues."""

    CRITICAL = "critical"  # Must fix before merge
    HIGH = "high"  # Should fix
    MEDIUM = "medium"  # Consider fixing
    LOW = "low"  # Nice to have
    INFO = "info"  # Informational


class IssueCategory(Enum):
    """Categories for review issues."""

    SECURITY = "security"
    PERFORMANCE = "performance"
    MAINTAINABILITY = "maintainability"
    BUG_RISK = "bug_risk"
    STYLE = "style"
    DOCUMENTATION = "documentation"
    TEST = "test"
    COMPLEXITY = "complexity"


@dataclass
class ReviewIssue:
    """Represents a code review issue."""

    severity: Severity
    category: IssueCategory
    title: str
    description: str
    file_path: str
    line: int
    end_line: Optional[int] = None
    suggestion: Optional[str] = None
    code_snippet: Optional[str] = None
    rule_id: Optional[str] = None

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        return {
            "severity": self.severity.value,
            "category": self.category.value,
            "title": self.title,
            "description": self.description,
            "file_path": self.file_path,
            "line": self.line,
            "end_line": self.end_line,
            "suggestion": self.suggestion,
            "code_snippet": self.code_snippet,
            "rule_id": self.rule_id,
        }


class CodeAnalyzer:
    """Analyzes code for common issues."""

    # Pattern-based checks
    PATTERNS = {
        "python": {
            "bare_except": {
                "pattern": r"except\s*:",
                "severity": Severity.MEDIUM,
                "category": IssueCategory.BUG_RISK,
                "title": "Bare except clause",
                "description": "Catching all exceptions can hide bugs. Catch specific exceptions.",
                "suggestion": "Use 'except Exception:' or a more specific exception type.",
            },
            "todo_fixme": {
                "pattern": r"#\s*(TODO|FIXME|XXX|HACK)",
                "severity": Severity.INFO,
                "category": IssueCategory.MAINTAINABILITY,
                "title": "TODO/FIXME comment found",
                "description": "Consider resolving or tracking this TODO item.",
            },
            "print_statement": {
                "pattern": r"^\s*print\s*\(",
                "severity": Severity.LOW,
                "category": IssueCategory.STYLE,
                "title": "Print statement in code",
                "description": "Consider using logging instead of print statements.",
                "suggestion": "Use the logging module for production code.",
            },
            "magic_number": {
                "pattern": r"(?<!['\"])\b(?<!\.)\d{2,}(?!\d)(?!['\"])",
                "severity": Severity.LOW,
                "category": IssueCategory.MAINTAINABILITY,
                "title": "Magic number",
                "description": "Consider using a named constant for this number.",
            },
            "long_line": {
                "pattern": r".{120,}",
                "severity": Severity.LOW,
                "category": IssueCategory.STYLE,
                "title": "Line too long",
                "description": "Line exceeds 120 characters.",
            },
            "empty_docstring": {
                "pattern": r'(def|class)\s+\w+.*:\s*\n\s*["\']["\']["\'][\s]*["\']["\']["\']',
                "severity": Severity.LOW,
                "category": IssueCategory.DOCUMENTATION,
                "title": "Empty docstring",
                "description": "Docstring is empty. Add meaningful documentation.",
            },
        },
        "cpp": {
            "todo_fixme": {
                "pattern": r"//\s*(TODO|FIXME|XXX|HACK)",
                "severity": Severity.INFO,
                "category": IssueCategory.MAINTAINABILITY,
                "title": "TODO/FIXME comment found",
                "description": "Consider resolving or tracking this TODO item.",
            },
            "raw_pointer_new": {
                "pattern": r"\bnew\s+\w+(?!\s*\[)",
                "severity": Severity.MEDIUM,
                "category": IssueCategory.BUG_RISK,
                "title": "Raw pointer allocation",
                "description": "Consider using smart pointers (unique_ptr, shared_ptr).",
                "suggestion": "Use std::make_unique or std::make_shared.",
            },
            "c_style_cast": {
                "pattern": r"\(\s*\w+\s*\*?\s*\)\s*\w+",
                "severity": Severity.LOW,
                "category": IssueCategory.STYLE,
                "title": "C-style cast",
                "description": "Prefer C++ casts (static_cast, dynamic_cast).",
            },
            "magic_number": {
                "pattern": r"(?<!['\"])\b(?<!\.)\d{2,}(?!\d)(?!['\"])",
                "severity": Severity.LOW,
                "category": IssueCategory.MAINTAINABILITY,
                "title": "Magic number",
                "description": "Consider using a named constant.",
            },
            "empty_catch": {
                "pattern": r"catch\s*\([^)]*\)\s*\{\s*\}",
                "severity": Severity.MEDIUM,
                "category": IssueCategory.BUG_RISK,
                "title": "Empty catch block",
                "description": "Empty catch blocks can hide errors. At least log the exception.",
            },
        },
    }

    # Complexity thresholds
    COMPLEXITY_THRESHOLDS = {
        "function_lines": 50,
        "file_lines": 500,
        "parameters": 5,
        "nesting_depth": 4,
    }

    def __init__(self):
        """Initialize the analyzer."""
        pass

    def analyze_file(self, file_path: str | Path) -> list[ReviewIssue]:
        """Analyze a file for issues.

        Args:
            file_path: Path to the file.

        Returns:
            List of issues found.
        """
        file_path = Path(file_path)
        issues = []

        try:
            with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
                content = f.read()
                lines = content.split("\n")
        except Exception:
            return issues

        # Determine language
        language = self._detect_language(file_path)

        # Run pattern checks
        issues.extend(self._check_patterns(content, lines, str(file_path), language))

        # Run complexity checks
        issues.extend(self._check_complexity(content, lines, str(file_path), language))

        return issues

    def analyze_diff(self, diff: str, file_path: str) -> list[ReviewIssue]:
        """Analyze a diff for issues.

        Args:
            diff: Git diff content.
            file_path: Path to the file.

        Returns:
            List of issues in changed lines.
        """
        issues = []
        language = self._detect_language(Path(file_path))

        # Parse diff to find changed lines
        changed_lines = self._parse_diff_lines(diff)

        # Check patterns in changed content
        for line_num, line_content in changed_lines:
            line_issues = self._check_line_patterns(
                line_content, line_num, file_path, language
            )
            issues.extend(line_issues)

        return issues

    def _detect_language(self, file_path: Path) -> str:
        """Detect programming language from file extension."""
        suffix = file_path.suffix.lower()

        if suffix in (".py", ".pyi", ".pyw"):
            return "python"
        elif suffix in (".cpp", ".cxx", ".cc", ".c", ".h", ".hpp"):
            return "cpp"
        elif suffix in (".js", ".jsx"):
            return "javascript"
        elif suffix in (".ts", ".tsx"):
            return "typescript"

        return "unknown"

    def _check_patterns(
        self,
        content: str,
        lines: list[str],
        file_path: str,
        language: str,
    ) -> list[ReviewIssue]:
        """Check content against patterns."""
        issues = []
        patterns = self.PATTERNS.get(language, {})

        for rule_id, rule in patterns.items():
            pattern = re.compile(rule["pattern"], re.MULTILINE)

            for i, line in enumerate(lines, 1):
                if pattern.search(line):
                    issues.append(ReviewIssue(
                        severity=rule["severity"],
                        category=rule["category"],
                        title=rule["title"],
                        description=rule["description"],
                        file_path=file_path,
                        line=i,
                        suggestion=rule.get("suggestion"),
                        code_snippet=line.strip()[:100],
                        rule_id=rule_id,
                    ))

        return issues

    def _check_line_patterns(
        self,
        line: str,
        line_num: int,
        file_path: str,
        language: str,
    ) -> list[ReviewIssue]:
        """Check a single line against patterns."""
        issues = []
        patterns = self.PATTERNS.get(language, {})

        for rule_id, rule in patterns.items():
            if re.search(rule["pattern"], line):
                issues.append(ReviewIssue(
                    severity=rule["severity"],
                    category=rule["category"],
                    title=rule["title"],
                    description=rule["description"],
                    file_path=file_path,
                    line=line_num,
                    suggestion=rule.get("suggestion"),
                    code_snippet=line.strip()[:100],
                    rule_id=rule_id,
                ))

        return issues

    def _check_complexity(
        self,
        content: str,
        lines: list[str],
        file_path: str,
        language: str,
    ) -> list[ReviewIssue]:
        """Check for complexity issues."""
        issues = []

        # Check file length
        if len(lines) > self.COMPLEXITY_THRESHOLDS["file_lines"]:
            issues.append(ReviewIssue(
                severity=Severity.MEDIUM,
                category=IssueCategory.COMPLEXITY,
                title="File too long",
                description=f"File has {len(lines)} lines. Consider splitting into smaller modules.",
                file_path=file_path,
                line=1,
                rule_id="file_length",
            ))

        # Check function lengths (simplified)
        if language == "python":
            func_pattern = re.compile(r"^\s*def\s+(\w+)\s*\(")
        else:
            func_pattern = re.compile(r"^\s*(?:[\w:*&]+\s+)?(\w+)\s*\([^)]*\)\s*\{?")

        func_starts = []
        for i, line in enumerate(lines, 1):
            match = func_pattern.match(line)
            if match:
                func_starts.append((i, match.group(1)))

        # Estimate function lengths
        for idx, (start, name) in enumerate(func_starts):
            if idx + 1 < len(func_starts):
                end = func_starts[idx + 1][0]
            else:
                end = len(lines)

            length = end - start
            if length > self.COMPLEXITY_THRESHOLDS["function_lines"]:
                issues.append(ReviewIssue(
                    severity=Severity.MEDIUM,
                    category=IssueCategory.COMPLEXITY,
                    title=f"Function '{name}' too long",
                    description=f"Function has ~{length} lines. Consider breaking into smaller functions.",
                    file_path=file_path,
                    line=start,
                    end_line=end,
                    rule_id="function_length",
                ))

        # Check nesting depth
        issues.extend(self._check_nesting_depth(lines, file_path, language))

        return issues

    def _check_nesting_depth(
        self,
        lines: list[str],
        file_path: str,
        language: str,
    ) -> list[ReviewIssue]:
        """Check for deep nesting."""
        issues = []
        threshold = self.COMPLEXITY_THRESHOLDS["nesting_depth"]

        # Simple indent-based nesting detection
        for i, line in enumerate(lines, 1):
            if not line.strip():
                continue

            # Count leading whitespace
            stripped = line.lstrip()
            indent = len(line) - len(stripped)

            # Estimate nesting level
            if language == "python":
                level = indent // 4  # Assume 4-space indent
            else:
                level = indent // 4  # Simplified

            if level > threshold:
                issues.append(ReviewIssue(
                    severity=Severity.MEDIUM,
                    category=IssueCategory.COMPLEXITY,
                    title="Deep nesting",
                    description=f"Code is nested {level} levels deep. Consider extracting functions.",
                    file_path=file_path,
                    line=i,
                    rule_id="nesting_depth",
                ))

        return issues

    def _parse_diff_lines(self, diff: str) -> list[tuple[int, str]]:
        """Parse diff to extract added/modified lines.

        Args:
            diff: Git diff content.

        Returns:
            List of (line_number, content) tuples.
        """
        changed_lines = []
        current_line = 0

        for line in diff.split("\n"):
            # Parse hunk header for line numbers
            if line.startswith("@@"):
                match = re.search(r"\+(\d+)", line)
                if match:
                    current_line = int(match.group(1))
                continue

            # Track added/modified lines
            if line.startswith("+") and not line.startswith("+++"):
                changed_lines.append((current_line, line[1:]))
                current_line += 1
            elif line.startswith(" "):
                current_line += 1
            # Deleted lines (starting with -) don't increment

        return changed_lines

    def calculate_complexity_score(self, file_path: str | Path) -> dict:
        """Calculate complexity metrics for a file.

        Args:
            file_path: Path to the file.

        Returns:
            Dictionary with complexity metrics.
        """
        try:
            with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
                content = f.read()
                lines = content.split("\n")
        except Exception:
            return {"error": "Could not read file"}

        non_empty_lines = [l for l in lines if l.strip()]
        comment_lines = [l for l in lines if l.strip().startswith(("#", "//", "/*", "*"))]

        return {
            "total_lines": len(lines),
            "code_lines": len(non_empty_lines) - len(comment_lines),
            "comment_lines": len(comment_lines),
            "blank_lines": len(lines) - len(non_empty_lines),
            "comment_ratio": len(comment_lines) / max(len(non_empty_lines), 1),
        }
