"""Routing quality evaluators.

Validates routing decision records against schema, tests deterministic
replay (same inputs + same budget snapshot => same decision), and
evaluates fallback logic correctness.
"""

from __future__ import annotations

import hashlib
import json
from collections.abc import Callable
from typing import Any

# Fields required in every routing decision record.
REQUIRED_FIELDS = frozenset(
    {
        "schema_version",
        "task_id",
        "selected_model",
        "selected_channel",
        "record_hash",
    }
)


def validate_decision_record(record: dict[str, Any]) -> tuple[bool, str]:
    """Check that *record* contains all required fields and a valid schema_version.

    Returns ``(True, "ok")`` on success, or ``(False, reason)`` on failure.
    """
    missing = REQUIRED_FIELDS - record.keys()
    if missing:
        return False, f"missing required fields: {sorted(missing)}"

    sv = record.get("schema_version", "")
    if not isinstance(sv, str) or not sv:
        return False, "schema_version must be a non-empty string"

    return True, "ok"


def check_deterministic_replay(
    engine_fn: Callable[[dict[str, Any]], dict[str, Any]],
    task: dict[str, Any],
    n: int = 10,
) -> tuple[bool, str]:
    """Call *engine_fn(task)* *n* times and assert all record_hashes are identical.

    Returns ``(True, "ok")`` when every invocation produces the same hash,
    or ``(False, reason)`` when a mismatch is detected.
    """
    hashes: list[str] = []
    for _ in range(n):
        result = engine_fn(task)
        h = result.get("record_hash")
        if h is None:
            # Compute a deterministic hash from the full record
            h = hashlib.sha256(json.dumps(result, sort_keys=True).encode()).hexdigest()
        hashes.append(h)

    unique = set(hashes)
    if len(unique) == 1:
        return True, "ok"
    return False, f"non-deterministic: {len(unique)} distinct hashes across {n} replays"


def check_fallback_correctness(
    record: dict[str, Any],
    expected_primary: str,
) -> tuple[bool, str]:
    """Validate fallback logic in a routing decision record.

    If *fallback_used* is True in the record, *fallback_reason* must also
    be present and non-empty.

    Returns ``(True, "ok")`` on success, or ``(False, reason)`` on failure.
    """
    fallback_used = record.get("fallback_used", False)
    if not fallback_used:
        return True, "ok"

    fallback_reason = record.get("fallback_reason", "")
    if not fallback_reason:
        return False, "fallback_used is True but fallback_reason is missing or empty"

    return True, "ok"


def evaluate_routing_quality(records: list[dict[str, Any]]) -> dict[str, Any]:
    """Produce an eval summary for a batch of routing decision records.

    Returns a dict with pass/fail counts and per-record details.
    """
    total = len(records)
    passed = 0
    failed = 0
    details: list[dict[str, Any]] = []

    for i, record in enumerate(records):
        valid, msg = validate_decision_record(record)
        entry: dict[str, Any] = {"index": i, "valid": valid, "message": msg}

        if valid:
            passed += 1
        else:
            failed += 1

        details.append(entry)

    return {
        "total": total,
        "passed": passed,
        "failed": failed,
        "pass_rate": passed / total if total else 0.0,
        "details": details,
    }
