"""Eval-output scoring contract — shared between eval, optimizer, and fitness function.

Validates and normalizes verifier rewards into scalar scores.
NOT in scope: task directory validation (task_contract.py) or experiment
output validation (output_schema.py).
"""

from __future__ import annotations


class InvalidEvalOutput(Exception):
    """Raised when eval output violates the reward contract.

    This is a contract violation — fail-fast, no zero-score fallback.
    """
    pass


def extract_score(rewards: object) -> float:
    """Extract scalar score from rewards dict.

    Score policy: rewards["score"] then rewards["reward"].
    Non-numeric values or missing keys are hard failures.

    Raises:
        InvalidEvalOutput: On any contract violation.
    """
    if not isinstance(rewards, dict):
        raise InvalidEvalOutput(
            f"rewards must be a dict, got {type(rewards).__name__}: {rewards!r}"
        )

    # Check all values are numeric
    for key, val in rewards.items():
        if not isinstance(val, (int, float)):
            raise InvalidEvalOutput(
                f"rewards[{key!r}] must be numeric, got {type(val).__name__}: {val!r}"
            )

    # Extract scalar score, clamped to [-1.0, 1.0]
    raw: float | None = None
    if "score" in rewards:
        raw = float(rewards["score"])
    elif "reward" in rewards:
        raw = float(rewards["reward"])
    else:
        raise InvalidEvalOutput(
            f"rewards has neither 'score' nor 'reward' key. Keys: {list(rewards.keys())}"
        )
    return max(-1.0, min(1.0, raw))


def validate_side_info(side_info: dict, task_name: str) -> list[str]:
    """Check side_info for malformed entries. Returns list of warnings."""
    warnings: list[str] = []
    if not isinstance(side_info, dict):
        warnings.append(f"[{task_name}] side_info is not a dict: {type(side_info).__name__}")
        return warnings

    feedback = side_info.get("Feedback")
    if feedback is not None and not isinstance(feedback, dict):
        warnings.append(f"[{task_name}] side_info['Feedback'] is not a dict: {type(feedback).__name__}")

    gen_outputs = side_info.get("Generated Outputs")
    if gen_outputs is not None and not isinstance(gen_outputs, dict):
        warnings.append(f"[{task_name}] side_info['Generated Outputs'] is not a dict: {type(gen_outputs).__name__}")

    return warnings
