# src/veritell_langchain/schema.py
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Mapping

SCHEMA_VERSION = "2025-01-01"

_ALLOWED_RISK_LABELS = {"low", "medium", "high", "unknown", "skipped", "skipped_unknown"}


class SchemaError(ValueError):
    pass


def _require_mapping(data: Any) -> Mapping[str, Any]:
    if not isinstance(data, Mapping):
        raise SchemaError("payload must be an object")
    return data


def _require_int_in_range(*, data: Mapping[str, Any], key: str, min_v: int, max_v: int) -> int:
    if key not in data:
        raise SchemaError(f"missing required field: {key}")
    val = data[key]
    if not isinstance(val, int) or isinstance(val, bool):
        raise SchemaError(f"invalid type for {key}: expected int")
    if val < min_v or val > max_v:
        raise SchemaError(f"{key} out of range: expected {min_v}..{max_v}")
    return val


def _require_str(*, data: Mapping[str, Any], key: str) -> str:
    if key not in data:
        raise SchemaError(f"missing required field: {key}")
    val = data[key]
    if not isinstance(val, str):
        raise SchemaError(f"invalid type for {key}: expected str")
    return val


@dataclass(frozen=True)
class VeritellEvaluationResult:
    schema_version: str
    hallucination: int
    bias: int
    safety: int
    risk_label: str
    judge_comments: Any | None = None
    extra: dict[str, Any] = field(default_factory=dict)

    @classmethod
    def default(cls, *, reason: str, fail_open: bool = True) -> "VeritellEvaluationResult":
        # Stable fail-open default: valid schema + "unknown" label; preserve reason in extra.
        return cls(
            schema_version=SCHEMA_VERSION,
            hallucination=1,
            bias=1,
            safety=1,
            risk_label="unknown",
            judge_comments=None,
            extra={"fail_open": fail_open, "reason": reason},
        )

    def __getitem__(self, key: str) -> Any:
        # Back-compat for tests expecting dict-style access (e.g., res["risk_label"]).
        if hasattr(self, key):
            return getattr(self, key)
        if key in self.extra:
            return self.extra[key]
        raise KeyError(key)

    def get(self, key: str, default: Any = None) -> Any:
        try:
            return self[key]
        except KeyError:
            return default

    def __contains__(self, key: str) -> bool:
        return hasattr(self, key) or key in self.extra

    @classmethod
    def from_api(cls, payload: Any) -> "VeritellEvaluationResult":
        data = _require_mapping(payload)

        schema_version = _require_str(data=data, key="schema_version")
        if schema_version != SCHEMA_VERSION:
            raise SchemaError(f"unsupported schema_version: {schema_version}")

        hallucination = _require_int_in_range(data=data, key="hallucination", min_v=1, max_v=5)
        bias = _require_int_in_range(data=data, key="bias", min_v=1, max_v=5)
        safety = _require_int_in_range(data=data, key="safety", min_v=1, max_v=5)

        risk_label = _require_str(data=data, key="risk_label")
        if risk_label not in _ALLOWED_RISK_LABELS:
            raise SchemaError(f"invalid risk_label: {risk_label}")

        judge_comments = data.get("judge_comments", None)

        known = {
            "schema_version",
            "hallucination",
            "bias",
            "safety",
            "risk_label",
            "judge_comments",
        }
        extra = {k: v for k, v in data.items() if k not in known}

        return cls(
            schema_version=schema_version,
            hallucination=hallucination,
            bias=bias,
            safety=safety,
            risk_label=risk_label,
            judge_comments=judge_comments,
            extra=extra,
        )
