"""Code DNA analyzer for detecting anomalies against codebase patterns.

This module provides the DNAAnalyzer class which compares code against
a codebase fingerprint to detect naming violations, style drift, missing
type hints, copy-paste patterns, and other anomalies.
"""

import ast
import re
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional, Any, Set
from collections import Counter

from src.dna.extractor import CodebaseFingerprint


class AnomalySeverity(Enum):
    """Severity levels for detected anomalies."""
    INFO = "INFO"
    WARNING = "WARNING"
    STYLE_VIOLATION = "STYLE_VIOLATION"


@dataclass
class Anomaly:
    """Represents a detected anomaly in the code.

    Attributes:
        pattern_name: The type of pattern that was violated.
        expected: What the codebase fingerprint expects.
        actual: What was found in the analyzed code.
        severity: The severity level of this anomaly.
        suggestion: A suggestion for how to fix the anomaly.
        line_number: Optional line number where the anomaly was found.
        context: Optional code context around the anomaly.
    """
    pattern_name: str
    expected: str
    actual: str
    severity: AnomalySeverity
    suggestion: str
    line_number: Optional[int] = None
    context: Optional[str] = None

    def to_dict(self) -> Dict[str, Any]:
        """Convert anomaly to dictionary."""
        return {
            "pattern_name": self.pattern_name,
            "expected": self.expected,
            "actual": self.actual,
            "severity": self.severity.value,
            "suggestion": self.suggestion,
            "line_number": self.line_number,
            "context": self.context,
        }


class DNAAnalyzer:
    """Analyzes code against a codebase DNA fingerprint to detect anomalies.

    Compares individual code snippets or files against the established
    patterns of a codebase to find:
    - Naming convention violations
    - Missing type hints
    - Style drift
    - Copy-paste patterns
    - Outdated idioms
    - Potentially external or AI-generated code
    """

    # Common outdated Python idioms and their modern replacements
    OUTDATED_IDIOMS = {
        r"\.format\(": ("str.format()", "f-string", "Consider using f-strings for string formatting"),
        r"\% *\(": ("% formatting", "f-string", "Consider using f-strings instead of % formatting"),
        r"dict\.keys\(\) *\)": ("dict.keys() in loop", "direct iteration", "Iterate directly over dict instead of dict.keys()"),
        r"== *None": ("== None comparison", "'is None'", "Use 'is None' instead of '== None'"),
        r"!= *None": ("!= None comparison", "'is not None'", "Use 'is not None' instead of '!= None'"),
        r"type\([^)]+\) *== *": ("type() comparison", "isinstance()", "Use isinstance() instead of type() comparison"),
        r"except *:": ("bare except", "specific exception", "Catch specific exceptions instead of bare except"),
        r"\.has_key\(": ("dict.has_key()", "'in' operator", "Use 'key in dict' instead of dict.has_key()"),
        r"print  *['\"]": ("print statement style", "print() function", "Ensure using print() function, not statement"),
        r"from __future__": ("__future__ imports", "modern Python", "May indicate code written for older Python versions"),
    }

    # Patterns that suggest copy-pasted or external code
    COPY_PASTE_INDICATORS = [
        r"# *TODO[: ].*copied",
        r"# *FIXME[: ].*from",
        r"# *Source[: ]",
        r"# *Credit[: ]",
        r"# *Copied from",
        r"# *Taken from",
        r"# *From Stack ?Overflow",
        r"# *Via https?://",
        r"# *Author[: ](?!.*team)",
        r"# *Copyright[: ](?!.*team)",
    ]

    # Patterns suggesting AI-generated code
    AI_GENERATED_INDICATORS = [
        r"# *Generated by",
        r"# *This (code|function|class) was generated",
        r"# *AI-generated",
        r"# *Created with (GPT|Claude|Copilot|ChatGPT)",
        r'""".*\n.*Example usage:',
        r'""".*\n.*Parameters:\n.*-{3,}',
    ]

    def __init__(self, fingerprint: CodebaseFingerprint):
        """Initialize analyzer with a codebase fingerprint.

        Args:
            fingerprint: The CodebaseFingerprint to compare code against.
        """
        self.fingerprint = fingerprint

    def compare(self, code: str, file_path: Optional[str] = None) -> List[Anomaly]:
        """Compare code against the fingerprint and detect anomalies.

        Args:
            code: The code string to analyze.
            file_path: Optional file path for context in error messages.

        Returns:
            List of Anomaly objects representing detected issues.
        """
        anomalies: List[Anomaly] = []

        # Parse the code
        try:
            tree = ast.parse(code)
        except SyntaxError as e:
            anomalies.append(Anomaly(
                pattern_name="syntax_error",
                expected="valid Python syntax",
                actual=f"syntax error at line {e.lineno}",
                severity=AnomalySeverity.WARNING,
                suggestion="Fix the syntax error before analysis",
                line_number=e.lineno,
            ))
            return anomalies

        # Run all detection methods
        anomalies.extend(self._check_naming_conventions(tree, code))
        anomalies.extend(self._check_type_hints(tree))
        anomalies.extend(self._check_docstring_style(tree))
        anomalies.extend(self._check_import_style(tree))
        anomalies.extend(self._check_error_handling(tree))
        anomalies.extend(self._check_function_metrics(tree, code))
        anomalies.extend(self._check_outdated_idioms(code))
        anomalies.extend(self._check_copy_paste_patterns(code))
        anomalies.extend(self._check_external_code_indicators(code))

        return anomalies

    def _detect_style(self, name: str) -> str:
        """Detect the naming style of a single identifier."""
        if re.match(r'^[a-z][a-z0-9_]*$', name):
            return "snake_case"
        elif re.match(r'^[a-z][a-zA-Z0-9]*$', name) and any(c.isupper() for c in name):
            return "camelCase"
        elif re.match(r'^[A-Z][a-zA-Z0-9]*$', name):
            return "PascalCase"
        elif re.match(r'^[A-Z][A-Z0-9_]*$', name):
            return "UPPER_SNAKE_CASE"
        return "unknown"

    def _check_naming_conventions(self, tree: ast.AST, code: str) -> List[Anomaly]:
        """Check for naming convention violations."""
        anomalies: List[Anomaly] = []
        fp = self.fingerprint.naming_conventions

        for node in ast.walk(tree):
            # Check function names
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                if not node.name.startswith("_"):  # Skip private/magic
                    style = self._detect_style(node.name)
                    if style != "unknown" and fp.function_style != "mixed":
                        if style != fp.function_style and not node.name.startswith("test_"):
                            anomalies.append(Anomaly(
                                pattern_name="naming_convention",
                                expected=f"function name in {fp.function_style}",
                                actual=f"'{node.name}' uses {style}",
                                severity=AnomalySeverity.STYLE_VIOLATION,
                                suggestion=f"Rename function to use {fp.function_style}",
                                line_number=node.lineno,
                                context=node.name,
                            ))

            # Check class names
            elif isinstance(node, ast.ClassDef):
                style = self._detect_style(node.name)
                if style != "unknown" and fp.class_style != "mixed":
                    if style != fp.class_style:
                        anomalies.append(Anomaly(
                            pattern_name="naming_convention",
                            expected=f"class name in {fp.class_style}",
                            actual=f"'{node.name}' uses {style}",
                            severity=AnomalySeverity.STYLE_VIOLATION,
                            suggestion=f"Rename class to use {fp.class_style}",
                            line_number=node.lineno,
                            context=node.name,
                        ))

            # Check variable names in assignments
            elif isinstance(node, ast.Assign):
                for target in node.targets:
                    if isinstance(target, ast.Name):
                        name = target.id
                        # Skip constants and private variables
                        if not name.startswith("_") and not name.isupper():
                            style = self._detect_style(name)
                            if style != "unknown" and fp.variable_style != "mixed":
                                if style != fp.variable_style:
                                    anomalies.append(Anomaly(
                                        pattern_name="naming_convention",
                                        expected=f"variable name in {fp.variable_style}",
                                        actual=f"'{name}' uses {style}",
                                        severity=AnomalySeverity.STYLE_VIOLATION,
                                        suggestion=f"Rename variable to use {fp.variable_style}",
                                        line_number=node.lineno,
                                        context=name,
                                    ))

        return anomalies

    def _check_type_hints(self, tree: ast.AST) -> List[Anomaly]:
        """Check for missing type hints based on fingerprint coverage."""
        anomalies: List[Anomaly] = []
        fp = self.fingerprint.type_hint_coverage

        # Only check if codebase has good type hint coverage
        if fp.style in ("full", "partial"):
            for node in ast.walk(tree):
                if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    # Skip test functions and private methods
                    if node.name.startswith("test_") or node.name.startswith("_"):
                        continue

                    has_hints = False
                    missing_params = []

                    # Check return type
                    if node.returns is not None:
                        has_hints = True

                    # Check parameters
                    for arg in node.args.args + node.args.kwonlyargs:
                        if arg.arg not in ("self", "cls"):
                            if arg.annotation is None:
                                missing_params.append(arg.arg)
                            else:
                                has_hints = True

                    # If codebase uses type hints and this function has none
                    if fp.style == "full" and not has_hints and len(node.args.args) > 0:
                        anomalies.append(Anomaly(
                            pattern_name="type_hints",
                            expected=f"type hints (codebase has {fp.coverage_percentage}% coverage)",
                            actual=f"function '{node.name}' has no type hints",
                            severity=AnomalySeverity.WARNING,
                            suggestion="Add type hints to function parameters and return type",
                            line_number=node.lineno,
                            context=node.name,
                        ))
                    elif missing_params and has_hints:
                        anomalies.append(Anomaly(
                            pattern_name="type_hints",
                            expected="consistent type hints on all parameters",
                            actual=f"missing hints for: {', '.join(missing_params)}",
                            severity=AnomalySeverity.INFO,
                            suggestion=f"Add type hints to parameters: {', '.join(missing_params)}",
                            line_number=node.lineno,
                            context=node.name,
                        ))

        return anomalies

    def _check_docstring_style(self, tree: ast.AST) -> List[Anomaly]:
        """Check for docstring style violations."""
        anomalies: List[Anomaly] = []
        fp = self.fingerprint.docstring_style

        # Only check if codebase has good docstring coverage
        if fp.coverage_percentage > 50:
            for node in ast.walk(tree):
                if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
                    # Skip private and test functions
                    if node.name.startswith("_") or node.name.startswith("test_"):
                        continue

                    docstring = ast.get_docstring(node)

                    if not docstring:
                        anomalies.append(Anomaly(
                            pattern_name="docstring_coverage",
                            expected=f"docstring (codebase has {fp.coverage_percentage}% coverage)",
                            actual=f"'{node.name}' has no docstring",
                            severity=AnomalySeverity.INFO,
                            suggestion="Add a docstring describing the function/class",
                            line_number=node.lineno,
                            context=node.name,
                        ))
                    elif fp.format != "simple" and fp.format != "none":
                        # Check if docstring matches expected format
                        has_google = "Args:" in docstring or "Returns:" in docstring
                        has_numpy = "Parameters\n" in docstring
                        has_sphinx = ":param " in docstring

                        format_matches = {
                            "google": has_google,
                            "numpy": has_numpy,
                            "sphinx": has_sphinx,
                        }

                        if fp.format in format_matches and not format_matches[fp.format]:
                            # Only flag if docstring is substantial enough to need formatting
                            if len(docstring) > 50 and isinstance(node, ast.FunctionDef):
                                if len(node.args.args) > 1:  # Has parameters to document
                                    anomalies.append(Anomaly(
                                        pattern_name="docstring_format",
                                        expected=f"{fp.format} docstring format",
                                        actual="different or no docstring format",
                                        severity=AnomalySeverity.STYLE_VIOLATION,
                                        suggestion=f"Use {fp.format} format for docstrings",
                                        line_number=node.lineno,
                                        context=node.name,
                                    ))

        return anomalies

    def _check_import_style(self, tree: ast.AST) -> List[Anomaly]:
        """Check for import style violations."""
        anomalies: List[Anomaly] = []
        fp = self.fingerprint.import_style

        from_imports = 0
        regular_imports = 0
        relative_imports = 0
        total_imports = 0

        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                regular_imports += 1
                total_imports += 1
            elif isinstance(node, ast.ImportFrom):
                from_imports += 1
                total_imports += 1
                if node.level > 0:
                    relative_imports += 1

        if total_imports > 3:
            # Check preference alignment
            prefers_from = from_imports > regular_imports
            if fp.prefers_from_imports and not prefers_from and regular_imports > from_imports:
                anomalies.append(Anomaly(
                    pattern_name="import_style",
                    expected="'from X import Y' style (codebase preference)",
                    actual=f"using {regular_imports} regular imports vs {from_imports} from-imports",
                    severity=AnomalySeverity.STYLE_VIOLATION,
                    suggestion="Consider using 'from X import Y' style for consistency",
                    line_number=1,
                ))

            # Check relative import usage
            if total_imports > 0:
                relative_pct = relative_imports / total_imports * 100
                if fp.relative_import_usage > 20 and relative_pct < 10:
                    anomalies.append(Anomaly(
                        pattern_name="import_style",
                        expected=f"relative imports (~{fp.relative_import_usage}% in codebase)",
                        actual=f"using {relative_pct:.0f}% relative imports",
                        severity=AnomalySeverity.INFO,
                        suggestion="Consider using relative imports for intra-package imports",
                        line_number=1,
                    ))

        return anomalies

    def _check_error_handling(self, tree: ast.AST) -> List[Anomaly]:
        """Check for error handling pattern violations."""
        anomalies: List[Anomaly] = []
        fp = self.fingerprint.error_handling

        for node in ast.walk(tree):
            if isinstance(node, ast.Try):
                for handler in node.handlers:
                    # Check for bare except when codebase avoids them
                    if handler.type is None:
                        if not fp.uses_bare_except:
                            anomalies.append(Anomaly(
                                pattern_name="error_handling",
                                expected="specific exception handling",
                                actual="bare 'except:' clause",
                                severity=AnomalySeverity.WARNING,
                                suggestion="Catch specific exceptions like Exception or a more specific type",
                                line_number=handler.lineno if hasattr(handler, 'lineno') else node.lineno,
                            ))

                    # Check for overly broad Exception when codebase is specific
                    elif isinstance(handler.type, ast.Name):
                        if handler.type.id == "Exception" and fp.exception_specificity == "specific":
                            anomalies.append(Anomaly(
                                pattern_name="error_handling",
                                expected="specific exception types (codebase pattern)",
                                actual="catching broad 'Exception'",
                                severity=AnomalySeverity.INFO,
                                suggestion="Consider catching more specific exception types",
                                line_number=handler.lineno if hasattr(handler, 'lineno') else node.lineno,
                            ))

        return anomalies

    def _check_function_metrics(self, tree: ast.AST, code: str) -> List[Anomaly]:
        """Check for function metric violations."""
        anomalies: List[Anomaly] = []
        fp = self.fingerprint.function_metrics

        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                # Calculate function length
                if hasattr(node, "end_lineno") and node.end_lineno:
                    length = node.end_lineno - node.lineno + 1

                    # Check if significantly longer than average
                    if fp.average_length > 0 and length > fp.average_length * 2:
                        anomalies.append(Anomaly(
                            pattern_name="function_length",
                            expected=f"~{fp.average_length:.0f} lines (codebase average)",
                            actual=f"{length} lines in '{node.name}'",
                            severity=AnomalySeverity.INFO,
                            suggestion="Consider breaking this function into smaller functions",
                            line_number=node.lineno,
                            context=node.name,
                        ))

                # Calculate complexity
                complexity = self._calculate_complexity(node)
                if fp.average_complexity > 0 and complexity > fp.average_complexity * 2:
                    anomalies.append(Anomaly(
                        pattern_name="function_complexity",
                        expected=f"~{fp.average_complexity:.1f} complexity (codebase average)",
                        actual=f"complexity {complexity} in '{node.name}'",
                        severity=AnomalySeverity.WARNING,
                        suggestion="Consider simplifying this function to reduce complexity",
                        line_number=node.lineno,
                        context=node.name,
                    ))

        return anomalies

    def _calculate_complexity(self, node: ast.AST) -> int:
        """Calculate cyclomatic complexity of a function node."""
        complexity = 1

        for child in ast.walk(node):
            if isinstance(child, (ast.If, ast.IfExp)):
                complexity += 1
            elif isinstance(child, (ast.For, ast.AsyncFor, ast.While)):
                complexity += 1
            elif isinstance(child, ast.ExceptHandler):
                complexity += 1
            elif isinstance(child, (ast.With, ast.AsyncWith)):
                complexity += 1
            elif isinstance(child, ast.BoolOp):
                complexity += len(child.values) - 1
            elif isinstance(child, ast.comprehension):
                complexity += 1
                if child.ifs:
                    complexity += len(child.ifs)

        return complexity

    def _check_outdated_idioms(self, code: str) -> List[Anomaly]:
        """Check for outdated Python idioms."""
        anomalies: List[Anomaly] = []
        lines = code.split("\n")

        for line_num, line in enumerate(lines, 1):
            for pattern, (old_desc, new_desc, suggestion) in self.OUTDATED_IDIOMS.items():
                if re.search(pattern, line):
                    anomalies.append(Anomaly(
                        pattern_name="outdated_idiom",
                        expected=new_desc,
                        actual=old_desc,
                        severity=AnomalySeverity.INFO,
                        suggestion=suggestion,
                        line_number=line_num,
                        context=line.strip()[:50],
                    ))

        return anomalies

    def _check_copy_paste_patterns(self, code: str) -> List[Anomaly]:
        """Check for patterns that suggest copy-pasted code."""
        anomalies: List[Anomaly] = []
        lines = code.split("\n")

        for line_num, line in enumerate(lines, 1):
            for pattern in self.COPY_PASTE_INDICATORS:
                if re.search(pattern, line, re.IGNORECASE):
                    anomalies.append(Anomaly(
                        pattern_name="copy_paste_indicator",
                        expected="original or documented code",
                        actual="comment suggests external/copied code",
                        severity=AnomalySeverity.WARNING,
                        suggestion="Ensure copied code is properly attributed and reviewed",
                        line_number=line_num,
                        context=line.strip()[:60],
                    ))
                    break  # Only one match per line

        return anomalies

    def _check_external_code_indicators(self, code: str) -> List[Anomaly]:
        """Check for patterns that suggest AI-generated or external code."""
        anomalies: List[Anomaly] = []
        lines = code.split("\n")

        for line_num, line in enumerate(lines, 1):
            for pattern in self.AI_GENERATED_INDICATORS:
                if re.search(pattern, line, re.IGNORECASE):
                    anomalies.append(Anomaly(
                        pattern_name="ai_generated_indicator",
                        expected="human-written code",
                        actual="comment suggests AI-generated code",
                        severity=AnomalySeverity.INFO,
                        suggestion="Review AI-generated code carefully for correctness and security",
                        line_number=line_num,
                        context=line.strip()[:60],
                    ))
                    break

        # Check for unusually verbose docstrings (common in AI output)
        try:
            tree = ast.parse(code)
            for node in ast.walk(tree):
                if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    docstring = ast.get_docstring(node)
                    if docstring:
                        # Check for unusually long docstring compared to function
                        if hasattr(node, "end_lineno"):
                            func_length = node.end_lineno - node.lineno
                            doc_lines = len(docstring.split("\n"))
                            if doc_lines > func_length and doc_lines > 10:
                                anomalies.append(Anomaly(
                                    pattern_name="ai_generated_indicator",
                                    expected="proportional documentation",
                                    actual=f"docstring ({doc_lines} lines) longer than function ({func_length} lines)",
                                    severity=AnomalySeverity.INFO,
                                    suggestion="Consider if all documentation is necessary",
                                    line_number=node.lineno,
                                    context=node.name,
                                ))
        except SyntaxError:
            pass

        return anomalies

    def get_style_match_percentage(self, code: str) -> float:
        """Calculate how well the code matches the codebase style.

        Args:
            code: The code to analyze.

        Returns:
            Percentage (0-100) of how well the code matches codebase patterns.
        """
        anomalies = self.compare(code)

        if not anomalies:
            return 100.0

        # Weight different severity levels
        weights = {
            AnomalySeverity.INFO: 1,
            AnomalySeverity.STYLE_VIOLATION: 2,
            AnomalySeverity.WARNING: 3,
        }

        total_penalty = sum(weights.get(a.severity, 1) for a in anomalies)

        # Base score with penalties
        # Each penalty point reduces score, capped at 0
        score = max(0, 100 - (total_penalty * 5))

        return round(score, 1)

    def summarize_anomalies(self, anomalies: List[Anomaly]) -> Dict[str, Any]:
        """Generate a summary of detected anomalies.

        Args:
            anomalies: List of anomalies to summarize.

        Returns:
            Dictionary containing summary statistics and grouped anomalies.
        """
        by_severity: Dict[str, List[Anomaly]] = {
            "INFO": [],
            "WARNING": [],
            "STYLE_VIOLATION": [],
        }

        by_pattern: Dict[str, int] = Counter()

        for anomaly in anomalies:
            by_severity[anomaly.severity.value].append(anomaly)
            by_pattern[anomaly.pattern_name] += 1

        return {
            "total": len(anomalies),
            "by_severity": {k: len(v) for k, v in by_severity.items()},
            "by_pattern": dict(by_pattern),
            "top_issues": [
                {
                    "pattern": anomaly.pattern_name,
                    "message": anomaly.suggestion,
                    "severity": anomaly.severity.value,
                    "line": anomaly.line_number,
                }
                for anomaly in anomalies[:5]
            ],
        }
