"""Failure Distiller — converts classified failures into eval regression tests."""
from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from abi_evals.models import EvalCase


@dataclass
class FailureRecord:
    """Represents a single failure record from failures.json."""
    failure_id: str
    failure_class: str
    evidence: dict[str, Any]
    repo: str = ""
    task_category: str = ""
    task_id: str = ""


@dataclass
class RegressionEvalCase:
    """Extended eval case with regression metadata."""
    eval_id: str
    name: str
    source_failure_id: str
    check_type: str
    target_files: list[str]
    expected_outcome: str = "pass"
    config: dict[str, Any] = field(default_factory=dict)
    tags: list[str] = field(default_factory=list)

    def to_eval_case(self) -> EvalCase:
        """Convert to standard EvalCase format."""
        return EvalCase(
            case_id=self.eval_id,
            description=self.name,
            input={"target_files": self.target_files, "config": self.config},
            expected={"status": self.expected_outcome},
            comparison="exact",
            tags=self.tags + ["regression", f"failure_class:{self.check_type}"],
            config=self.config,
        )

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary format for JSON serialization."""
        return {
            "eval_id": self.eval_id,
            "name": self.name,
            "source_failure_id": self.source_failure_id,
            "check_type": self.check_type,
            "target_files": self.target_files,
            "expected_outcome": self.expected_outcome,
            "config": self.config,
            "tags": self.tags,
        }


class FailureDistiller:
    """Converts classified failures into regression eval test cases."""

    def __init__(self) -> None:
        """Initialize the failure distiller."""
        self.distilled_cases: list[RegressionEvalCase] = []

    @staticmethod
    def _generate_eval_id(failure_record: FailureRecord, check_type: str) -> str:
        """Generate a unique eval ID based on failure content.

        Args:
            failure_record: The failure record to generate an ID for
            check_type: The type of check being performed

        Returns:
            A unique eval ID string
        """
        # Create a deterministic hash from failure characteristics
        content = f"{failure_record.failure_class}:{failure_record.repo}:{check_type}"
        content += f":{json.dumps(failure_record.evidence, sort_keys=True)}"
        hash_digest = hashlib.sha256(content.encode()).hexdigest()[:12]
        return f"regression_{failure_record.failure_class}_{hash_digest}"

    @staticmethod
    def _extract_target_files(evidence: dict[str, Any]) -> list[str]:
        """Extract target files from failure evidence.

        Args:
            evidence: The evidence dictionary from the failure record

        Returns:
            List of file paths mentioned in the evidence
        """
        files = []

        # Common evidence keys that might contain file paths
        for key in ["file", "files", "path", "paths", "module", "modules", "location"]:
            value = evidence.get(key)
            if value:
                if isinstance(value, str):
                    files.append(value)
                elif isinstance(value, list):
                    files.extend(str(v) for v in value)

        # Check for file paths in error messages or stack traces
        if "error_message" in evidence:
            # Simple heuristic: look for patterns like "path/to/file.py"
            import re
            msg = str(evidence["error_message"])
            # Match common file patterns (*.py, *.ts, *.js, etc.)
            file_patterns = re.findall(r'[\w\-./\\]+\.\w+', msg)
            files.extend(file_patterns)

        # Remove duplicates while preserving order
        seen = set()
        unique_files = []
        for f in files:
            if f not in seen:
                seen.add(f)
                unique_files.append(f)

        return unique_files

    def _distill_import_chain_break(self, failure: FailureRecord) -> RegressionEvalCase:
        """Create eval case for import chain break failures.

        Args:
            failure: The failure record

        Returns:
            A regression eval case that checks import resolution
        """
        target_files = self._extract_target_files(failure.evidence)
        eval_id = self._generate_eval_id(failure, "import_chain")

        return RegressionEvalCase(
            eval_id=eval_id,
            name=f"regression_import_chain_{failure.task_id or 'unknown'}",
            source_failure_id=failure.failure_id,
            check_type="static_analysis",
            target_files=target_files,
            expected_outcome="pass",
            config={
                "check": "import_resolution",
                "method": "ast_parse_and_importlib",
                "evidence": failure.evidence,
            },
            tags=["import_chain", "static"],
        )

    def _distill_schema_drift(self, failure: FailureRecord) -> RegressionEvalCase:
        """Create eval case for schema drift failures.

        Args:
            failure: The failure record

        Returns:
            A regression eval case that validates schema compliance
        """
        target_files = self._extract_target_files(failure.evidence)
        eval_id = self._generate_eval_id(failure, "schema")

        # Extract schema from evidence if available
        schema = failure.evidence.get("expected_schema", {})

        return RegressionEvalCase(
            eval_id=eval_id,
            name=f"regression_schema_drift_{failure.task_id or 'unknown'}",
            source_failure_id=failure.failure_id,
            check_type="schema_validation",
            target_files=target_files,
            expected_outcome="pass",
            config={
                "check": "schema_validation",
                "schema": schema,
                "evidence": failure.evidence,
            },
            tags=["schema", "validation"],
        )

    def _distill_hallucinated_path(self, failure: FailureRecord) -> RegressionEvalCase:
        """Create eval case for hallucinated path failures.

        Args:
            failure: The failure record

        Returns:
            A regression eval case that checks file existence
        """
        target_files = self._extract_target_files(failure.evidence)
        eval_id = self._generate_eval_id(failure, "path_existence")

        return RegressionEvalCase(
            eval_id=eval_id,
            name=f"regression_hallucinated_path_{failure.task_id or 'unknown'}",
            source_failure_id=failure.failure_id,
            check_type="file_existence",
            target_files=target_files,
            expected_outcome="pass",
            config={
                "check": "file_existence",
                "must_exist": True,
                "evidence": failure.evidence,
            },
            tags=["path", "existence"],
        )

    def _distill_stub_not_wired(self, failure: FailureRecord) -> RegressionEvalCase:
        """Create eval case for stub not wired failures.

        Args:
            failure: The failure record

        Returns:
            A regression eval case that checks for stub patterns
        """
        target_files = self._extract_target_files(failure.evidence)
        eval_id = self._generate_eval_id(failure, "stub_check")

        # Common stub/placeholder patterns
        forbidden_patterns = [
            "Not implemented",
            "not implemented",
            "TODO",
            "FIXME",
            "placeholder",
            "PLACEHOLDER",
            "raise NotImplementedError",
            "pass  # stub",
        ]

        return RegressionEvalCase(
            eval_id=eval_id,
            name=f"regression_stub_not_wired_{failure.task_id or 'unknown'}",
            source_failure_id=failure.failure_id,
            check_type="pattern_search",
            target_files=target_files,
            expected_outcome="pass",
            config={
                "check": "forbidden_patterns",
                "patterns": forbidden_patterns,
                "evidence": failure.evidence,
            },
            tags=["stub", "implementation"],
        )

    def _distill_config_naming_drift(self, failure: FailureRecord) -> RegressionEvalCase:
        """Create eval case for config naming drift failures.

        Args:
            failure: The failure record

        Returns:
            A regression eval case that checks config naming conventions
        """
        target_files = self._extract_target_files(failure.evidence)
        eval_id = self._generate_eval_id(failure, "config_naming")

        # Extract naming convention from evidence
        expected_pattern = failure.evidence.get("expected_pattern", r"^[a-z_]+\.config\.(json|yaml|yml)$")

        return RegressionEvalCase(
            eval_id=eval_id,
            name=f"regression_config_naming_{failure.task_id or 'unknown'}",
            source_failure_id=failure.failure_id,
            check_type="pattern_search",
            target_files=target_files,
            expected_outcome="pass",
            config={
                "check": "naming_convention",
                "pattern": expected_pattern,
                "evidence": failure.evidence,
            },
            tags=["config", "naming"],
        )

    def _distill_event_payload_mismatch(self, failure: FailureRecord) -> RegressionEvalCase:
        """Create eval case for event payload mismatch failures.

        Args:
            failure: The failure record

        Returns:
            A regression eval case that checks event type alignment
        """
        target_files = self._extract_target_files(failure.evidence)
        eval_id = self._generate_eval_id(failure, "event_alignment")

        return RegressionEvalCase(
            eval_id=eval_id,
            name=f"regression_event_payload_{failure.task_id or 'unknown'}",
            source_failure_id=failure.failure_id,
            check_type="static_analysis",
            target_files=target_files,
            expected_outcome="pass",
            config={
                "check": "event_type_alignment",
                "emitter_handler_pairs": failure.evidence.get("pairs", []),
                "evidence": failure.evidence,
            },
            tags=["event", "type_safety"],
        )

    def _distill_invariant_violation(self, failure: FailureRecord) -> RegressionEvalCase:
        """Create eval case for invariant violation failures.

        Args:
            failure: The failure record

        Returns:
            A regression eval case that runs the specific invariant test
        """
        target_files = self._extract_target_files(failure.evidence)
        eval_id = self._generate_eval_id(failure, "invariant")

        invariant_name = failure.evidence.get("invariant", "unknown")

        return RegressionEvalCase(
            eval_id=eval_id,
            name=f"regression_invariant_{invariant_name}_{failure.task_id or 'unknown'}",
            source_failure_id=failure.failure_id,
            check_type="static_analysis",
            target_files=target_files,
            expected_outcome="pass",
            config={
                "check": "invariant_test",
                "invariant": invariant_name,
                "evidence": failure.evidence,
            },
            tags=["invariant", "constraint"],
        )

    def _distill_other(self, failure: FailureRecord) -> RegressionEvalCase:
        """Create generic eval case for unclassified failures.

        Args:
            failure: The failure record

        Returns:
            A generic regression eval case
        """
        target_files = self._extract_target_files(failure.evidence)
        eval_id = self._generate_eval_id(failure, "generic")

        return RegressionEvalCase(
            eval_id=eval_id,
            name=f"regression_generic_{failure.task_id or 'unknown'}",
            source_failure_id=failure.failure_id,
            check_type="file_existence",
            target_files=target_files,
            expected_outcome="pass",
            config={
                "check": "no_regression",
                "description": "Generic regression check - verify files haven't regressed",
                "evidence": failure.evidence,
            },
            tags=["generic", "regression"],
        )

    def distill_failure(self, failure: FailureRecord) -> RegressionEvalCase:
        """Distill a single failure into an eval case.

        Args:
            failure: The failure record to distill

        Returns:
            A regression eval case
        """
        distillers = {
            "import_chain_break": self._distill_import_chain_break,
            "schema_drift": self._distill_schema_drift,
            "hallucinated_path": self._distill_hallucinated_path,
            "stub_not_wired": self._distill_stub_not_wired,
            "config_naming_drift": self._distill_config_naming_drift,
            "event_payload_mismatch": self._distill_event_payload_mismatch,
            "invariant_violation": self._distill_invariant_violation,
        }

        distiller = distillers.get(failure.failure_class, self._distill_other)
        return distiller(failure)

    def distill(self, failures_json_path: str | Path) -> list[RegressionEvalCase]:
        """Read a failures.json file and generate eval cases from each failure.

        Args:
            failures_json_path: Path to the failures.json file

        Returns:
            List of RegressionEvalCase objects

        Raises:
            FileNotFoundError: If the failures.json file doesn't exist
            json.JSONDecodeError: If the file contains invalid JSON
        """
        failures_path = Path(failures_json_path)
        if not failures_path.exists():
            msg = f"Failures file not found: {failures_path}"
            raise FileNotFoundError(msg)

        with open(failures_path, encoding="utf-8") as f:
            data = json.load(f)

        # Handle both list and dict formats
        failures_data = data if isinstance(data, list) else data.get("failures", [])

        self.distilled_cases = []
        for item in failures_data:
            failure = FailureRecord(
                failure_id=item.get("failure_id", ""),
                failure_class=item.get("failure_class", "other"),
                evidence=item.get("evidence", {}),
                repo=item.get("repo", ""),
                task_category=item.get("task_category", ""),
                task_id=item.get("task_id", ""),
            )

            eval_case = self.distill_failure(failure)
            self.distilled_cases.append(eval_case)

        return self.distilled_cases
