"""Error and failure models for ICSD core modules."""

from __future__ import annotations

from collections.abc import Mapping, Sequence
from copy import deepcopy
from dataclasses import dataclass, field
from typing import Any

__all__ = [
    "EvidenceSerialisationError",
    "IcsdError",
    "InvariantFailure",
    "InvariantViolationError",
    "PolicyProfileError",
    "PolicyProfileNotFoundError",
    "PolicyVersionMismatchError",
    "TransitionContextError",
]


class IcsdError(Exception):
    """Base error for ICSD core-level failures."""


class EvidenceSerialisationError(IcsdError):
    """Raised when ICSD evidence data cannot be canonical-JSON serialised."""


@dataclass(frozen=True, slots=True)
class InvariantFailure:
    """Structured output for a failed invariant evaluation."""

    invariant: str
    message: str
    code: str = "invariant_failed"
    details: Mapping[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        object.__setattr__(
            self,
            "invariant",
            _require_non_empty_string("invariant", self.invariant),
        )
        object.__setattr__(
            self,
            "message",
            _require_non_empty_string("message", self.message),
        )
        object.__setattr__(
            self,
            "code",
            _require_non_empty_string("code", self.code),
        )
        if not isinstance(self.details, Mapping):
            msg = "details must be a mapping."
            raise TypeError(msg)
        object.__setattr__(self, "details", deepcopy(dict(self.details)))

    def as_dict(self) -> dict[str, Any]:
        """Return a serialisable payload for logs, APIs, or evidence builders."""
        payload: dict[str, Any] = {
            "invariant": self.invariant,
            "code": self.code,
            "message": self.message,
        }
        if self.details:
            payload["details"] = deepcopy(dict(self.details))
        return payload


class InvariantViolationError(IcsdError):
    """Raised when an invariant set reports one or more failures."""

    def __init__(self, failures: Sequence[InvariantFailure]) -> None:
        self.failures = tuple(failures)
        if not self.failures:
            msg = "InvariantViolationError requires at least one failure."
            raise ValueError(msg)
        super().__init__(self._build_message())

    def _build_message(self) -> str:
        first = self.failures[0]
        if len(self.failures) == 1:
            return f"Invariant violation: [{first.invariant}] {first.message}"
        return (
            f"{len(self.failures)} invariant violations detected "
            f"(first: [{first.invariant}] {first.message})"
        )

    def as_dict(self) -> dict[str, Any]:
        """Return a deterministic structured error payload."""
        return {
            "error": "invariant_violation",
            "failures": [failure.as_dict() for failure in self.failures],
        }


class PolicyProfileError(IcsdError):
    """Base error for invariant policy profile resolution failures."""


class PolicyProfileNotFoundError(PolicyProfileError):
    """Raised when a requested invariant policy profile is unknown."""

    def __init__(self, profile_name: str) -> None:
        self.profile_name = profile_name
        super().__init__(f"Unknown invariant profile: {profile_name!r}.")


class PolicyVersionMismatchError(PolicyProfileError):
    """Raised when the requested policy version does not match the profile."""

    def __init__(
        self,
        *,
        profile_name: str,
        expected_version: str,
        requested_version: str,
    ) -> None:
        self.profile_name = profile_name
        self.expected_version = expected_version
        self.requested_version = requested_version
        super().__init__(
            "Policy version mismatch for profile "
            f"{profile_name!r}: expected {expected_version!r}, got {requested_version!r}."
        )


class TransitionContextError(IcsdError):
    """Raised when a transition context is invalid or unsafe to apply."""


def _require_non_empty_string(field_name: str, value: str) -> str:
    if not isinstance(value, str) or not value.strip():
        msg = f"{field_name} must be a non-empty string."
        raise ValueError(msg)
    return value.strip()
