"""Eval suite helpers — load, validate, run, and compare against golden baselines."""

from __future__ import annotations

import json
import math
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from abi_control_core.sdk.validate import validate_json


def load_eval_suite(path: Path) -> dict[str, Any]:
    """Load and validate an eval suite from a JSON file.

    Args:
        path: Path to the eval suite JSON file.

    Returns:
        Validated eval suite dict.

    Raises:
        SchemaValidationError: If the suite fails schema validation.
    """
    with open(path, encoding="utf-8") as f:
        suite = json.load(f)
    validate_json(suite, "eval_suite.schema.json")
    return suite  # type: ignore[no-any-return]


def write_eval_result(
    path: Path,
    suite_id: str,
    run_id: str,
    results: list[dict[str, Any]],
) -> dict[str, Any]:
    """Build, validate, and write an eval result.

    Args:
        path: Output path for the eval result JSON.
        suite_id: ID of the eval suite that was run.
        run_id: Run ID (8 hex chars).
        results: List of per-case result dicts with at least 'case_id' and 'status'.

    Returns:
        The validated eval result dict.

    Raises:
        SchemaValidationError: If the result fails schema validation.
    """
    passed = sum(1 for r in results if r["status"] == "pass")
    failed = sum(1 for r in results if r["status"] == "fail")
    errors = sum(1 for r in results if r["status"] == "error")
    skipped = sum(1 for r in results if r["status"] == "skipped")

    result = {
        "schema_version": "eval-result/1.0",
        "suite_id": suite_id,
        "run_id": run_id,
        "evaluated_at": datetime.now(UTC).isoformat(),
        "results": results,
        "summary": {
            "total": len(results),
            "passed": passed,
            "failed": failed,
            "errors": errors,
            "skipped": skipped,
        },
    }

    validate_json(result, "eval_result.schema.json")
    path.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    return result


def compare_exact(actual: Any, expected: Any) -> bool:
    """Exact comparison — values must be identical."""
    return actual == expected


def compare_numeric_tolerance(actual: Any, expected: Any, tolerance: float = 1e-6) -> bool:
    """Numeric comparison with absolute tolerance.

    Recursively compares structures. Numbers are compared with tolerance;
    all other types use exact equality.
    """
    if isinstance(expected, (int, float)) and isinstance(actual, (int, float)):
        if math.isnan(expected) and math.isnan(actual):
            return True
        return abs(actual - expected) <= tolerance

    if isinstance(expected, dict) and isinstance(actual, dict):
        if set(expected.keys()) != set(actual.keys()):
            return False
        return all(
            compare_numeric_tolerance(actual[k], expected[k], tolerance)
            for k in expected
        )

    if isinstance(expected, list) and isinstance(actual, list):
        if len(expected) != len(actual):
            return False
        return all(
            compare_numeric_tolerance(a, e, tolerance)
            for a, e in zip(actual, expected, strict=True)
        )

    return actual == expected


def compare_structural(actual: Any, expected: Any) -> bool:
    """Structural comparison — expected keys must be present with matching types.

    This is a minimal schema-aware comparison: every key in ``expected``
    must exist in ``actual`` with the same type. Extra keys in ``actual``
    are permitted. For leaf values, types must match but values can differ.
    """
    if isinstance(expected, dict):
        if not isinstance(actual, dict):
            return False
        for key in expected:
            if key not in actual:
                return False
            if not compare_structural(actual[key], expected[key]):
                return False
        return True

    if isinstance(expected, list):
        if not isinstance(actual, list):
            return False
        if len(expected) == 0:
            return True
        if len(actual) == 0:
            return False
        # Compare first element structurally as a representative
        return compare_structural(actual[0], expected[0])

    # Leaf: just check type matches
    return type(actual) is type(expected)


COMPARISON_MODES = {
    "exact": compare_exact,
    "numeric_tolerance": compare_numeric_tolerance,
    "structural": compare_structural,
}
