"""LLM Evaluation Framework for Fix Quality.

Uses DeepEval metrics to measure:
1. Correctness - Does the fix address the root cause?
2. Relevance - Is the fix relevant to the error?
3. Faithfulness - Does the fix follow the code style?
4. Toxicity - Does the fix introduce anti-patterns?

GitHub: https://github.com/confident-ai/deepeval
"""
from __future__ import annotations

import logging
import re
from dataclasses import dataclass, field
from typing import Any, Dict, List

logger = logging.getLogger(__name__)

# Try to import DeepEval
_DEEPEVAL_AVAILABLE = False

try:
    from deepeval import evaluate
    from deepeval.metrics import (
        AnswerRelevancyMetric,
        FaithfulnessMetric,
    )
    from deepeval.test_case import LLMTestCase

    _DEEPEVAL_AVAILABLE = True
except ImportError:
    evaluate = None
    AnswerRelevancyMetric = None
    FaithfulnessMetric = None
    LLMTestCase = None


@dataclass
class FixEvaluation:
    """Evaluation results for a generated fix."""

    fix_id: str
    overall_score: float  # 0-1
    correctness_score: float
    relevance_score: float
    faithfulness_score: float
    toxicity_score: float  # Lower is better (anti-pattern detection)
    passed: bool
    feedback: List[str] = field(default_factory=list)
    details: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            "fix_id": self.fix_id,
            "overall_score": self.overall_score,
            "correctness_score": self.correctness_score,
            "relevance_score": self.relevance_score,
            "faithfulness_score": self.faithfulness_score,
            "toxicity_score": self.toxicity_score,
            "passed": self.passed,
            "feedback": self.feedback,
            "details": self.details,
        }


class FixEvaluator:
    """Evaluate AI-generated fixes using LLM-as-judge.

    Metrics:
    1. Correctness: Fix addresses the actual root cause
    2. Relevance: Fix is relevant to the error context
    3. Faithfulness: Fix follows coding conventions
    4. No Anti-patterns: Fix doesn't suppress errors

    Usage:
        evaluator = FixEvaluator()
        result = await evaluator.evaluate_fix(
            fix_code=diff,
            original_code=code,
            error_message=err,
            root_cause=cause,
            test_name=name,
        )
        if result.passed:
            print("Fix looks good!")
    """

    PASS_THRESHOLD = 0.7

    # Anti-patterns that indicate symptom suppression
    ANTI_PATTERNS = [
        (r"except\s*:\s*pass", "Bare except: pass"),
        (r"except\s+Exception\s*:\s*pass", "Catch-all Exception with pass"),
        (r"except\s*.*?:\s*\.\.\.", "Exception with ellipsis"),
        (r"except\s*.*?:\s*return\s+None\s*$", "Exception returning None"),
        (r"except\s+BaseException", "Catching BaseException"),
        (r"# type:\s*ignore", "Type ignore comment"),
        (r"# noqa", "Noqa comment"),
        (r"@pytest\.mark\.skip", "Skip marker without condition"),
        (r"time\.sleep\(\d+\)", "Hardcoded sleep"),
        (r"while\s+True:", "Infinite loop"),
    ]

    def __init__(self, model: str = "gpt-4", use_deepeval: bool = True):
        """Initialize evaluator.

        Args:
            model: Model to use for evaluation
            use_deepeval: Whether to use DeepEval (if available)
        """
        self.model = model
        self.use_deepeval = use_deepeval and _DEEPEVAL_AVAILABLE
        self._metrics_initialized = False

    @property
    def deepeval_available(self) -> bool:
        """Check if DeepEval is available."""
        return _DEEPEVAL_AVAILABLE

    def _init_metrics(self) -> None:
        """Initialize DeepEval metrics."""
        if not self.use_deepeval or self._metrics_initialized:
            return

        try:
            self.relevance_metric = AnswerRelevancyMetric(model=self.model)
            self.faithfulness_metric = FaithfulnessMetric(model=self.model)
            self._metrics_initialized = True
        except Exception as e:
            logger.warning(f"Failed to initialize DeepEval metrics: {e}")
            self.use_deepeval = False

    async def evaluate_fix(
        self,
        fix_code: str,
        original_code: str,
        error_message: str,
        root_cause: str,
        test_name: str,
        fix_id: str = "",
    ) -> FixEvaluation:
        """Evaluate a generated fix.

        Args:
            fix_code: The generated fix (diff or full code)
            original_code: Original failing code
            error_message: The error that occurred
            root_cause: AI-generated root cause analysis
            test_name: Name of the test
            fix_id: Optional fix identifier

        Returns:
            FixEvaluation with scores
        """
        scores: Dict[str, float] = {}
        feedback: List[str] = []
        details: Dict[str, Any] = {}

        # 1. Check for anti-patterns (toxicity)
        toxicity_score, toxicity_feedback = self._check_anti_patterns(fix_code)
        scores["toxicity"] = toxicity_score
        feedback.extend(toxicity_feedback)

        # 2. Check correctness (root cause alignment)
        correctness_score, correctness_feedback = self._evaluate_correctness(
            fix_code, error_message, root_cause
        )
        scores["correctness"] = correctness_score
        feedback.extend(correctness_feedback)

        # 3. Use DeepEval for relevance and faithfulness
        if self.use_deepeval:
            self._init_metrics()

            if self._metrics_initialized:
                deepeval_scores = await self._run_deepeval(
                    fix_code, original_code, error_message, root_cause
                )
                scores.update(deepeval_scores)
        else:
            # Fallback heuristics
            scores["relevance"] = self._heuristic_relevance(
                fix_code, error_message, original_code
            )
            scores["faithfulness"] = self._heuristic_faithfulness(
                fix_code, original_code
            )

        # Calculate overall score
        overall = (
            scores.get("correctness", 0.5) * 0.4
            + scores.get("relevance", 0.5) * 0.3
            + scores.get("faithfulness", 0.5) * 0.2
            + (1 - scores.get("toxicity", 0)) * 0.1
        )

        passed = overall >= self.PASS_THRESHOLD and toxicity_score < 0.3

        return FixEvaluation(
            fix_id=fix_id,
            overall_score=overall,
            correctness_score=scores.get("correctness", 0.5),
            relevance_score=scores.get("relevance", 0.5),
            faithfulness_score=scores.get("faithfulness", 0.5),
            toxicity_score=toxicity_score,
            passed=passed,
            feedback=feedback,
            details=details,
        )

    def _check_anti_patterns(self, fix_code: str) -> tuple[float, List[str]]:
        """Check for anti-patterns in the fix.

        Args:
            fix_code: The fix code/diff

        Returns:
            Tuple of (toxicity_score, feedback_list)
        """
        found_patterns: List[str] = []

        for pattern, description in self.ANTI_PATTERNS:
            if re.search(pattern, fix_code, re.MULTILINE | re.IGNORECASE):
                found_patterns.append(f"Anti-pattern: {description}")

        # Score based on number of anti-patterns found
        if not found_patterns:
            return 0.0, []
        elif len(found_patterns) == 1:
            return 0.3, found_patterns
        elif len(found_patterns) == 2:
            return 0.6, found_patterns
        else:
            return 0.9, found_patterns

    def _evaluate_correctness(
        self,
        fix_code: str,
        error_message: str,
        root_cause: str,
    ) -> tuple[float, List[str]]:
        """Evaluate if fix addresses root cause.

        Args:
            fix_code: The fix code/diff
            error_message: Original error
            root_cause: Root cause analysis

        Returns:
            Tuple of (correctness_score, feedback_list)
        """
        feedback: List[str] = []
        score = 0.5  # Base score

        # Check if fix mentions key terms from root cause
        root_cause_lower = root_cause.lower()
        fix_lower = fix_code.lower()

        # Flakiness-related keywords
        fix_keywords = {
            "order by": 0.15,
            "await": 0.1,
            "timeout": 0.1,
            "mock": 0.1,
            "fixture": 0.1,
            "sorted": 0.1,
            "lock": 0.1,
            "sleep": -0.1,  # Negative - often symptom suppression
        }

        for keyword, weight in fix_keywords.items():
            if keyword in root_cause_lower and keyword in fix_lower:
                score += weight
                if weight > 0:
                    feedback.append(f"Fix addresses '{keyword}' from root cause")

        # Check if fix targets the right location
        # (This is a simplified check)
        if "line" in root_cause_lower or "file" in root_cause_lower:
            # Extract line numbers from root cause
            line_matches = re.findall(r"line\s*(\d+)", root_cause_lower)
            for line_num in line_matches:
                if line_num in fix_code:
                    score += 0.1
                    feedback.append(f"Fix targets line {line_num} mentioned in root cause")

        return min(1.0, max(0.0, score)), feedback

    async def _run_deepeval(
        self,
        fix_code: str,
        original_code: str,
        error_message: str,
        root_cause: str,
    ) -> Dict[str, float]:
        """Run DeepEval metrics.

        Args:
            fix_code: The fix
            original_code: Original code
            error_message: Error message
            root_cause: Root cause

        Returns:
            Dict of metric scores
        """
        scores: Dict[str, float] = {}

        if not self._metrics_initialized:
            return scores

        # Create test case
        test_case = LLMTestCase(
            input=f"Fix this error: {error_message}\nRoot cause: {root_cause}",
            actual_output=fix_code,
            context=[original_code, root_cause],
            retrieval_context=[original_code],
        )

        # Relevance
        try:
            self.relevance_metric.measure(test_case)
            scores["relevance"] = self.relevance_metric.score
        except Exception as e:
            logger.warning(f"Relevance evaluation failed: {e}")
            scores["relevance"] = 0.5

        # Faithfulness
        try:
            self.faithfulness_metric.measure(test_case)
            scores["faithfulness"] = self.faithfulness_metric.score
        except Exception as e:
            logger.warning(f"Faithfulness evaluation failed: {e}")
            scores["faithfulness"] = 0.5

        return scores

    def _heuristic_relevance(
        self,
        fix_code: str,
        error_message: str,
        original_code: str,
    ) -> float:
        """Heuristic relevance check when DeepEval unavailable.

        Args:
            fix_code: The fix
            error_message: Error message
            original_code: Original code

        Returns:
            Relevance score (0-1)
        """
        score = 0.5

        # Check if fix references parts of the original code
        fix_lines = set(fix_code.split("\n"))
        original_lines = set(original_code.split("\n"))

        # Intersection suggests relevance
        common = fix_lines & original_lines
        if len(common) > 0:
            score += 0.2

        # Check if error type is addressed
        error_types = ["KeyError", "TypeError", "ValueError", "AttributeError", "IndexError"]
        for error_type in error_types:
            if error_type in error_message and error_type.lower() in fix_code.lower():
                score += 0.15

        return min(1.0, score)

    def _heuristic_faithfulness(
        self,
        fix_code: str,
        original_code: str,
    ) -> float:
        """Heuristic faithfulness check when DeepEval unavailable.

        Checks if fix follows coding style of original.

        Args:
            fix_code: The fix
            original_code: Original code

        Returns:
            Faithfulness score (0-1)
        """
        score = 0.7  # Start with assumption of reasonable faithfulness

        # Check indentation consistency
        original_indent = self._detect_indent(original_code)
        fix_indent = self._detect_indent(fix_code)

        if original_indent == fix_indent:
            score += 0.1

        # Check quote style consistency
        original_quotes = "'" if original_code.count("'") > original_code.count('"') else '"'
        fix_quotes = "'" if fix_code.count("'") > fix_code.count('"') else '"'

        if original_quotes == fix_quotes:
            score += 0.1

        # Penalize if fix introduces completely new patterns
        if "import" in fix_code and "import" not in original_code:
            score -= 0.1  # New imports might break things

        return min(1.0, max(0.0, score))

    def _detect_indent(self, code: str) -> str:
        """Detect indentation style (spaces vs tabs)."""
        lines = code.split("\n")
        for line in lines:
            if line.startswith("    "):
                return "spaces"
            elif line.startswith("\t"):
                return "tabs"
        return "spaces"

    def evaluate_batch(
        self,
        fixes: List[Dict[str, Any]],
    ) -> List[FixEvaluation]:
        """Evaluate multiple fixes.

        Args:
            fixes: List of fix dicts with keys:
                - fix_code, original_code, error_message, root_cause, test_name

        Returns:
            List of FixEvaluation results
        """
        import asyncio

        async def _eval_all():
            results = []
            for i, fix in enumerate(fixes):
                result = await self.evaluate_fix(
                    fix_code=fix.get("fix_code", ""),
                    original_code=fix.get("original_code", ""),
                    error_message=fix.get("error_message", ""),
                    root_cause=fix.get("root_cause", ""),
                    test_name=fix.get("test_name", ""),
                    fix_id=fix.get("fix_id", str(i)),
                )
                results.append(result)
            return results

        return asyncio.run(_eval_all())


async def evaluate_fix(
    fix_code: str,
    original_code: str,
    error_message: str,
    root_cause: str,
    test_name: str,
    fix_id: str = "",
) -> FixEvaluation:
    """Convenience function to evaluate a fix.

    Args:
        fix_code: The generated fix
        original_code: Original code
        error_message: Error message
        root_cause: Root cause analysis
        test_name: Test name
        fix_id: Optional fix ID

    Returns:
        FixEvaluation
    """
    evaluator = FixEvaluator()
    return await evaluator.evaluate_fix(
        fix_code=fix_code,
        original_code=original_code,
        error_message=error_message,
        root_cause=root_cause,
        test_name=test_name,
        fix_id=fix_id,
    )
