"""
SparkLabs Agent - Code Review

Automated game code quality analysis system. Performs static
analysis, style checking, complexity measurement, and best-practice
validation for game scripts generated by AI agents.

Architecture:
  CodeReviewEngine
    |-- StaticAnalyzer (AST-based structural analysis)
    |-- StyleChecker (coding convention enforcement)
    |-- ComplexityMeter (cyclomatic/cognitive complexity scoring)
    |-- BestPracticeValidator (game-dev-specific rule checks)
    |-- SecurityScanner (injection and vulnerability detection)
    |-- ReviewReporter (structured review output generation)

Review Categories:
  - GAMEPLAY: game loop logic, state machines, event handling
  - PERFORMANCE: draw calls, memory allocation, update frequency
  - ARCHITECTURE: component design, coupling, cohesion
  - SECURITY: input sanitization, code injection, safe eval
  - STYLE: naming conventions, formatting, documentation
"""

from __future__ import annotations

import ast
import math
import re
import time
import uuid
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Any, Callable, Dict, List, Optional, Set, Tuple


class ReviewSeverity(Enum):
    ERROR = "error"
    WARNING = "warning"
    INFO = "info"
    SUGGESTION = "suggestion"


class ReviewCategory(Enum):
    GAMEPLAY = "gameplay"
    PERFORMANCE = "performance"
    ARCHITECTURE = "architecture"
    SECURITY = "security"
    STYLE = "style"


@dataclass
class ReviewFinding:
    id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
    category: ReviewCategory = ReviewCategory.STYLE
    severity: ReviewSeverity = ReviewSeverity.WARNING
    file_path: str = ""
    line: Optional[int] = None
    column: Optional[int] = None
    code_snippet: str = ""
    message: str = ""
    suggestion: str = ""
    rule_id: str = ""

    def to_dict(self) -> Dict[str, Any]:
        return {
            "id": self.id,
            "category": self.category.value,
            "severity": self.severity.value,
            "file_path": self.file_path,
            "line": self.line,
            "column": self.column,
            "code_snippet": self.code_snippet[:200],
            "message": self.message,
            "suggestion": self.suggestion,
            "rule_id": self.rule_id,
        }


@dataclass
class ReviewReport:
    id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
    target_name: str = ""
    findings: List[ReviewFinding] = field(default_factory=list)
    files_reviewed: int = 0
    lines_reviewed: int = 0
    total_findings: int = 0
    score: float = 100.0
    duration_ms: float = 0.0
    created_at: float = field(default_factory=time.time)
    metadata: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        severity_counts = {}
        for f in self.findings:
            severity_counts[f.severity.value] = severity_counts.get(f.severity.value, 0) + 1
        return {
            "id": self.id,
            "target_name": self.target_name,
            "findings": [f.to_dict() for f in self.findings],
            "files_reviewed": self.files_reviewed,
            "lines_reviewed": self.lines_reviewed,
            "total_findings": self.total_findings,
            "score": round(self.score, 1),
            "severity_counts": severity_counts,
            "duration_ms": self.duration_ms,
            "created_at": self.created_at,
        }

    def get_summary(self) -> str:
        errors = sum(1 for f in self.findings if f.severity == ReviewSeverity.ERROR)
        warnings = sum(1 for f in self.findings if f.severity == ReviewSeverity.WARNING)
        infos = sum(1 for f in self.findings if f.severity == ReviewSeverity.INFO)
        suggestions = sum(1 for f in self.findings if f.severity == ReviewSeverity.SUGGESTION)
        return (
            f"Review '{self.target_name}': Score {self.score:.1f}/100, "
            f"{errors} errors, {warnings} warnings, {infos} infos, {suggestions} suggestions"
        )


class CodeReviewEngine:
    """
    Automated code review engine for game scripts and AI-generated code.

    Performs multi-category analysis covering gameplay logic patterns,
    performance hotspots, architectural concerns, security vulnerabilities,
    and coding style adherence.

    Usage:
        reviewer = CodeReviewEngine()
        report = reviewer.review("game_controller.py", code_string)
        print(report.get_summary())
    """

    _instance: Optional["CodeReviewEngine"] = None

    GAMEPLAY_PATTERNS = {
        "update_loop_performance": re.compile(
            r"(for|while)\s+.*\s+in\s+update\s*\(.*\)", re.IGNORECASE
        ),
        "magic_number": re.compile(r"(?<![a-zA-Z_])[0-9]+\.[0-9]+(?![a-zA-Z_])"),
        "unbounded_while": re.compile(r"while\s+True\s*:"),
        "sleep_in_loop": re.compile(r"time\.sleep\(|asyncio\.sleep\("),
    }

    SECURITY_PATTERNS = {
        "unsafe_eval": re.compile(r"\beval\s*\("),
        "unsafe_exec": re.compile(r"\bexec\s*\("),
        "os_system": re.compile(r"os\.system\s*\("),
        "subprocess_shell": re.compile(r"subprocess\.\w+\(.*shell\s*=\s*True", re.IGNORECASE),
        "hardcoded_secret": re.compile(
            r"(api_key|secret|password|token)\s*=\s*['\"][^'\"]+['\"]",
            re.IGNORECASE,
        ),
    }

    PERFORMANCE_PATTERNS = {
        "nested_loop": None,
        "large_list_comprehension": re.compile(r"\[.*for\s+\w+\s+in\s+range\(\d{4,}\)"),
        "file_io_in_loop": re.compile(r"for\s+.*:\s*\n\s+.*open\s*\("),
    }

    STYLE_PATTERNS = {
        "missing_docstring": None,
        "long_line": re.compile(r"^.{120,}$", re.MULTILINE),
        "trailing_whitespace": re.compile(r"[ \t]+$", re.MULTILINE),
        "mixed_indentation": re.compile(r"^\t+ +|^ +\t+"),
    }

    def __init__(self):
        self._reports: Dict[str, ReviewReport] = {}
        self._custom_rules: List[Callable[[str, str], List[ReviewFinding]]] = []
        self._review_count: int = 0

    @classmethod
    def get_instance(cls) -> "CodeReviewEngine":
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    def register_custom_rule(
        self,
        rule: Callable[[str, str], List[ReviewFinding]],
    ) -> None:
        self._custom_rules.append(rule)

    def review(
        self,
        file_path: str,
        code: str,
        categories: Optional[List[ReviewCategory]] = None,
    ) -> ReviewReport:
        start = time.time()
        self._review_count += 1

        lines = code.split("\n")
        findings: List[ReviewFinding] = []

        targets = categories or list(ReviewCategory)

        if ReviewCategory.GAMEPLAY in targets:
            findings.extend(self._analyze_gameplay(file_path, code, lines))
        if ReviewCategory.PERFORMANCE in targets:
            findings.extend(self._analyze_performance(file_path, code, lines))
        if ReviewCategory.ARCHITECTURE in targets:
            findings.extend(self._analyze_architecture(file_path, code))
        if ReviewCategory.SECURITY in targets:
            findings.extend(self._analyze_security(file_path, code, lines))
        if ReviewCategory.STYLE in targets:
            findings.extend(self._analyze_style(file_path, code, lines))

        for custom_rule in self._custom_rules:
            try:
                findings.extend(custom_rule(file_path, code))
            except Exception:
                pass

        score = self._calculate_score(findings)

        report = ReviewReport(
            target_name=file_path,
            findings=findings,
            files_reviewed=1,
            lines_reviewed=len(lines),
            total_findings=len(findings),
            score=score,
            duration_ms=(time.time() - start) * 1000,
        )
        self._reports[report.id] = report
        return report

    def review_multiple(
        self,
        files: Dict[str, str],
        categories: Optional[List[ReviewCategory]] = None,
    ) -> ReviewReport:
        all_findings: List[ReviewFinding] = []
        total_lines = 0
        start = time.time()

        for file_path, code in files.items():
            report = self.review(file_path, code, categories=categories)
            all_findings.extend(report.findings)
            total_lines += report.lines_reviewed

        score = self._calculate_score(all_findings)
        combined = ReviewReport(
            target_name=f"{len(files)} files",
            findings=all_findings,
            files_reviewed=len(files),
            lines_reviewed=total_lines,
            total_findings=len(all_findings),
            score=score,
            duration_ms=(time.time() - start) * 1000,
        )
        self._reports[combined.id] = combined
        return combined

    def get_report(self, report_id: str) -> Optional[ReviewReport]:
        return self._reports.get(report_id)

    def get_stats(self) -> Dict[str, Any]:
        return {
            "total_reviews": self._review_count,
            "cached_reports": len(self._reports),
            "custom_rules": len(self._custom_rules),
        }

    def _analyze_gameplay(
        self, file_path: str, code: str, lines: List[str]
    ) -> List[ReviewFinding]:
        findings: List[ReviewFinding] = []
        try:
            tree = ast.parse(code)
            for node in ast.walk(tree):
                if isinstance(node, ast.FunctionDef) and node.name == "update":
                    child_funcs = [
                        n for n in ast.walk(node)
                        if isinstance(n, ast.FunctionDef) and n != node
                    ]
                    if child_funcs:
                        findings.append(ReviewFinding(
                            category=ReviewCategory.GAMEPLAY,
                            severity=ReviewSeverity.WARNING,
                            file_path=file_path,
                            line=node.lineno,
                            message="Update method contains nested function definitions",
                            suggestion="Extract nested functions to class-level methods for clarity",
                            rule_id="GP-001",
                        ))
                if isinstance(node, ast.For):
                    body_str = ast.unparse(node.body) if hasattr(ast, 'unparse') else ''
                    if 'sleep' in body_str.lower() or 'time.sleep' in body_str:
                        findings.append(ReviewFinding(
                            category=ReviewCategory.GAMEPLAY,
                            severity=ReviewSeverity.ERROR,
                            file_path=file_path,
                            line=node.lineno,
                            message="Sleep call detected inside game loop",
                            suggestion="Use delta-time based timers or coroutines instead of blocking sleep",
                            rule_id="GP-002",
                        ))
                if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
                    if isinstance(node.value, (int, float)) and node.value not in (0, 1, -1, 2, 0.5, 100, 60):
                        parent = getattr(node, 'parent', None)
                        findings.append(ReviewFinding(
                            category=ReviewCategory.GAMEPLAY,
                            severity=ReviewSeverity.INFO,
                            file_path=file_path,
                            line=node.lineno if hasattr(node, 'lineno') else None,
                            code_snippet=str(node.value),
                            message=f"Magic number detected: {node.value}",
                            suggestion="Extract to a named constant for maintainability",
                            rule_id="GP-003",
                        ))
        except SyntaxError:
            findings.append(ReviewFinding(
                category=ReviewCategory.GAMEPLAY,
                severity=ReviewSeverity.ERROR,
                file_path=file_path,
                message="Code contains syntax errors and cannot be fully analyzed",
                suggestion="Fix syntax errors before running gameplay analysis",
                rule_id="GP-000",
            ))
        return findings

    def _analyze_performance(
        self, file_path: str, code: str, lines: List[str]
    ) -> List[ReviewFinding]:
        findings: List[ReviewFinding] = []
        for i, line in enumerate(lines, 1):
            if re.search(r"for\s+\w+\s+in\s+(range|list|enumerate)", line):
                findings.append(ReviewFinding(
                    category=ReviewCategory.PERFORMANCE,
                    severity=ReviewSeverity.INFO,
                    file_path=file_path,
                    line=i,
                    code_snippet=line.strip()[:100],
                    message="Loop detected — verify iteration is bounded",
                    suggestion="Consider using generators or pre-allocated containers for large datasets",
                    rule_id="PF-001",
                ))
            if line.strip().startswith("import ") and len(line.strip().split(",")) > 3:
                findings.append(ReviewFinding(
                    category=ReviewCategory.PERFORMANCE,
                    severity=ReviewSeverity.SUGGESTION,
                    file_path=file_path,
                    line=i,
                    message="Multiple imports on single line may obscure dependency tracking",
                    suggestion="Use one import per line for clarity",
                    rule_id="PF-002",
                ))
        return findings

    def _analyze_architecture(
        self, file_path: str, code: str
    ) -> List[ReviewFinding]:
        findings: List[ReviewFinding] = []
        try:
            tree = ast.parse(code)
            class_count = sum(1 for n in ast.walk(tree) if isinstance(n, ast.ClassDef))
            func_count = sum(1 for n in ast.walk(tree) if isinstance(n, ast.FunctionDef))

            for node in ast.walk(tree):
                if isinstance(node, ast.ClassDef):
                    methods = [
                        n for n in node.body if isinstance(n, ast.FunctionDef)
                    ]
                    if len(methods) > 20:
                        findings.append(ReviewFinding(
                            category=ReviewCategory.ARCHITECTURE,
                            severity=ReviewSeverity.WARNING,
                            file_path=file_path,
                            line=node.lineno,
                            message=f"Class '{node.name}' has {len(methods)} methods — consider splitting",
                            suggestion="Apply Single Responsibility Principle; break into smaller classes",
                            rule_id="AR-001",
                        ))
                    public_methods = [m for m in methods if not m.name.startswith("_")]
                    if len(public_methods) > 10:
                        findings.append(ReviewFinding(
                            category=ReviewCategory.ARCHITECTURE,
                            severity=ReviewSeverity.SUGGESTION,
                            file_path=file_path,
                            line=node.lineno,
                            message=f"Class '{node.name}' has {len(public_methods)} public methods",
                            suggestion="Consider hiding internal methods with underscore prefix",
                            rule_id="AR-002",
                        ))

            if class_count == 0 and func_count > 10:
                findings.append(ReviewFinding(
                    category=ReviewCategory.ARCHITECTURE,
                    severity=ReviewSeverity.SUGGESTION,
                    file_path=file_path,
                    message=f"Module has {func_count} top-level functions with no classes",
                    suggestion="Consider organizing related functions into classes for better structure",
                    rule_id="AR-003",
                ))
        except SyntaxError:
            pass
        return findings

    def _analyze_security(
        self, file_path: str, code: str, lines: List[str]
    ) -> List[ReviewFinding]:
        findings: List[ReviewFinding] = []
        for i, line in enumerate(lines, 1):
            if re.search(r"\beval\s*\(", line):
                findings.append(ReviewFinding(
                    category=ReviewCategory.SECURITY,
                    severity=ReviewSeverity.ERROR,
                    file_path=file_path,
                    line=i,
                    code_snippet=line.strip()[:100],
                    message="Use of eval() detected — potential code injection risk",
                    suggestion="Use ast.literal_eval() or JSON parsing instead",
                    rule_id="SEC-001",
                ))
            if re.search(r"\bexec\s*\(", line):
                findings.append(ReviewFinding(
                    category=ReviewCategory.SECURITY,
                    severity=ReviewSeverity.ERROR,
                    file_path=file_path,
                    line=i,
                    code_snippet=line.strip()[:100],
                    message="Use of exec() detected — arbitrary code execution risk",
                    suggestion="Remove exec() and use explicit logic or plugin systems instead",
                    rule_id="SEC-002",
                ))
            if re.search(r"os\.system\s*\(", line):
                findings.append(ReviewFinding(
                    category=ReviewCategory.SECURITY,
                    severity=ReviewSeverity.WARNING,
                    file_path=file_path,
                    line=i,
                    message="Use of os.system() detected",
                    suggestion="Use subprocess.run() with shell=False for safer command execution",
                    rule_id="SEC-003",
                ))
            secret_match = re.search(
                r"(api_key|secret|password|token)\s*=\s*['\"]\S+['\"]",
                line, re.IGNORECASE,
            )
            if secret_match:
                findings.append(ReviewFinding(
                    category=ReviewCategory.SECURITY,
                    severity=ReviewSeverity.ERROR,
                    file_path=file_path,
                    line=i,
                    message="Hardcoded credential detected in source code",
                    suggestion="Use environment variables or a secure credential manager",
                    rule_id="SEC-004",
                ))
        return findings

    def _analyze_style(
        self, file_path: str, code: str, lines: List[str]
    ) -> List[ReviewFinding]:
        findings: List[ReviewFinding] = []
        for i, line in enumerate(lines, 1):
            if len(line) > 120:
                findings.append(ReviewFinding(
                    category=ReviewCategory.STYLE,
                    severity=ReviewSeverity.INFO,
                    file_path=file_path,
                    line=i,
                    message=f"Line too long ({len(line)} > 120 characters)",
                    suggestion="Break long lines at logical boundaries for readability",
                    rule_id="STY-001",
                ))
            if re.search(r"[ \t]+$", line):
                findings.append(ReviewFinding(
                    category=ReviewCategory.STYLE,
                    severity=ReviewSeverity.SUGGESTION,
                    file_path=file_path,
                    line=i,
                    message="Trailing whitespace detected",
                    suggestion="Remove trailing whitespace",
                    rule_id="STY-002",
                ))

        try:
            tree = ast.parse(code)
            for node in ast.walk(tree):
                if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
                    if not ast.get_docstring(node):
                        severity = ReviewSeverity.WARNING if isinstance(node, ast.ClassDef) else ReviewSeverity.INFO
                        entity_type = "Class" if isinstance(node, ast.ClassDef) else "Function"
                        findings.append(ReviewFinding(
                            category=ReviewCategory.STYLE,
                            severity=severity,
                            file_path=file_path,
                            line=node.lineno,
                            message=f"{entity_type} '{node.name}' is missing a docstring",
                            suggestion=f"Add a docstring describing the {entity_type.lower()}'s purpose and parameters",
                            rule_id="STY-003",
                        ))
        except SyntaxError:
            pass

        return findings

    def _calculate_score(self, findings: List[ReviewFinding]) -> float:
        if not findings:
            return 100.0
        penalties = {
            ReviewSeverity.ERROR: 0.99,
            ReviewSeverity.WARNING: 0.995,
            ReviewSeverity.INFO: 0.999,
            ReviewSeverity.SUGGESTION: 0.9995,
        }
        score = 100.0
        for finding in findings:
            penalty = penalties.get(finding.severity, 0.999)
            score *= penalty
        return max(0.0, min(100.0, score))


def get_code_review_engine() -> CodeReviewEngine:
    return CodeReviewEngine.get_instance()