"""Evaluation metrics for DSPy optimization."""

from __future__ import annotations

import os
import tempfile
from typing import Any


def fix_compiles(example: Any, prediction: Any, trace: Any = None) -> bool:
    """Check if generated fix compiles/parses correctly.

    Args:
        example: DSPy example with ground truth.
        prediction: Model prediction with unified_diff.
        trace: Optional trace for debugging.

    Returns:
        True if the fix code compiles.
    """
    try:
        # Extract new code from diff
        diff = getattr(prediction, "unified_diff", "")
        code = _extract_new_code_from_diff(diff)

        if not code.strip():
            return False

        # Try to compile as Python
        compile(code, "<fix>", "exec")
        return True

    except SyntaxError:
        return False
    except Exception:
        return False


def fix_applies_cleanly(example: Any, prediction: Any, trace: Any = None) -> bool:
    """Check if fix applies without conflicts.

    Args:
        example: DSPy example with code_context.
        prediction: Model prediction with unified_diff.
        trace: Optional trace for debugging.

    Returns:
        True if the patch applies cleanly.
    """
    try:
        import patch_ng
    except ImportError:
        # Can't check without patch-ng
        return True

    try:
        diff = getattr(prediction, "unified_diff", "")
        if not diff.strip():
            return False

        patchset = patch_ng.fromstring(diff.encode())
        if patchset is None:
            return False

        # Try to apply to example code
        code_context = getattr(example, "code_context", "")
        if not code_context:
            return True  # Can't verify without code

        with tempfile.TemporaryDirectory() as tmpdir:
            original_path = os.path.join(tmpdir, "original.py")
            with open(original_path, "w") as f:
                f.write(code_context)

            return patchset.apply(strip=0, root=tmpdir)

    except Exception:
        return False


def fix_is_minimal(example: Any, prediction: Any, trace: Any = None) -> float:
    """Score fix minimality (fewer changes = better).

    Args:
        example: DSPy example.
        prediction: Model prediction with unified_diff.
        trace: Optional trace for debugging.

    Returns:
        Score between 0 and 1 (1 = very minimal).
    """
    try:
        diff = getattr(prediction, "unified_diff", "")

        # Count changed lines (lines starting with + or -)
        lines_changed = sum(
            1 for line in diff.split("\n")
            if (line.startswith("+") or line.startswith("-"))
            and not line.startswith("+++")
            and not line.startswith("---")
        )

        if lines_changed <= 3:
            return 1.0
        elif lines_changed <= 5:
            return 0.9
        elif lines_changed <= 10:
            return 0.7
        elif lines_changed <= 20:
            return 0.5
        elif lines_changed <= 50:
            return 0.3
        else:
            return 0.1

    except Exception:
        return 0.0


def fix_has_explanation(example: Any, prediction: Any, trace: Any = None) -> bool:
    """Check if fix has a meaningful explanation.

    Args:
        example: DSPy example.
        prediction: Model prediction with explanation.
        trace: Optional trace for debugging.

    Returns:
        True if explanation is present and meaningful.
    """
    explanation = getattr(prediction, "explanation", "")

    # Check for minimum length and meaningful content
    if len(explanation) < 20:
        return False

    # Should mention something about the fix
    fix_terms = ["fix", "change", "update", "add", "remove", "modify", "handle", "check"]
    explanation_lower = explanation.lower()

    return any(term in explanation_lower for term in fix_terms)


def fix_matches_category(example: Any, prediction: Any, trace: Any = None) -> bool:
    """Check if fix strategy matches the error category.

    Args:
        example: DSPy example with expected category.
        prediction: Model prediction with fix_strategy.
        trace: Optional trace for debugging.

    Returns:
        True if fix strategy is appropriate for the category.
    """
    # Category to expected fix strategies mapping
    category_strategies = {
        "null_reference": ["null check", "optional", "none", "default", "guard"],
        "type_error": ["type", "cast", "convert", "isinstance"],
        "validation": ["validate", "check", "verify", "assert"],
        "api_error": ["retry", "timeout", "fallback", "error handling"],
        "config": ["config", "setting", "environment", "default"],
    }

    category = getattr(example, "category", None)
    if not category:
        return True  # Can't verify without category

    strategy = getattr(prediction, "fix_strategy", "").lower()
    if not strategy:
        return False

    expected_terms = category_strategies.get(category.lower(), [])
    if not expected_terms:
        return True  # Unknown category

    return any(term in strategy for term in expected_terms)


def combined_metric(example: Any, prediction: Any, trace: Any = None) -> float:
    """Combined metric for overall fix quality.

    Args:
        example: DSPy example.
        prediction: Model prediction.
        trace: Optional trace for debugging.

    Returns:
        Combined score between 0 and 1.
    """
    # Individual metrics with weights
    compiles = fix_compiles(example, prediction, trace)
    applies = fix_applies_cleanly(example, prediction, trace)
    minimal = fix_is_minimal(example, prediction, trace)
    has_explanation = fix_has_explanation(example, prediction, trace)

    # Must compile to get any score
    if not compiles:
        return 0.0

    # Calculate weighted score
    score = (
        0.3 * float(compiles) +
        0.2 * float(applies) +
        0.3 * minimal +
        0.2 * float(has_explanation)
    )

    return score


def _extract_new_code_from_diff(diff: str) -> str:
    """Extract new code from unified diff.

    Args:
        diff: Unified diff string.

    Returns:
        Code after applying the diff (approximation).
    """
    lines = []
    in_hunk = False

    for line in diff.split("\n"):
        # Skip diff headers
        if line.startswith("---") or line.startswith("+++"):
            continue
        if line.startswith("@@"):
            in_hunk = True
            continue

        if in_hunk:
            # Include added lines and context
            if line.startswith("+"):
                lines.append(line[1:])
            elif not line.startswith("-"):
                # Context lines have a leading space in unified diff
                if line.startswith(" "):
                    lines.append(line[1:])
                else:
                    lines.append(line)

    return "\n".join(lines)
