"""Deterministic single-process batch validation helpers."""

from __future__ import annotations

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

from .errors import InvariantFailure
from .invariants import InvariantSet

__all__ = ["BatchReport", "BatchStateResult", "validate_states"]

NON_MAPPING_INVARIANT = "state_mapping_required"
NON_MAPPING_CODE = "state_not_mapping"
NON_MAPPING_MESSAGE = "State must be a mapping."


@dataclass(frozen=True, slots=True)
class BatchStateResult:
    """Per-index validation outcome for a batch item."""

    index: int
    valid: bool
    failures: tuple[InvariantFailure, ...] = ()

    def __post_init__(self) -> None:
        if not isinstance(self.index, int) or self.index < 0:
            msg = "index must be an integer greater than or equal to zero."
            raise ValueError(msg)
        if not isinstance(self.valid, bool):
            msg = "valid must be a boolean."
            raise TypeError(msg)
        if any(not isinstance(item, InvariantFailure) for item in self.failures):
            msg = "failures must contain only InvariantFailure values."
            raise TypeError(msg)
        if self.valid and self.failures:
            msg = "valid results must not include failures."
            raise ValueError(msg)
        if not self.valid and not self.failures:
            msg = "invalid results must include at least one failure."
            raise ValueError(msg)

    def as_dict(self) -> dict[str, Any]:
        """Return deterministic serialisable payload."""
        payload: dict[str, Any] = {
            "index": self.index,
            "status": "ok" if self.valid else "failed",
            "failure_count": len(self.failures),
        }
        if self.failures:
            payload["failures"] = [failure.as_dict() for failure in self.failures]
        return payload


@dataclass(frozen=True, slots=True)
class BatchReport:
    """Deterministic report payload for a batch validation run."""

    results: tuple[BatchStateResult, ...]
    context: Mapping[str, Any] | None = None

    def __post_init__(self) -> None:
        if any(not isinstance(result, BatchStateResult) for result in self.results):
            msg = "results must contain only BatchStateResult values."
            raise TypeError(msg)
        if self.context is None:
            return
        if not isinstance(self.context, Mapping):
            msg = "context must be a mapping when provided."
            raise TypeError(msg)
        object.__setattr__(self, "context", deepcopy(dict(self.context)))

    @property
    def total_count(self) -> int:
        """Return total number of processed batch items."""
        return len(self.results)

    @property
    def passed_count(self) -> int:
        """Return number of valid batch items."""
        return sum(1 for result in self.results if result.valid)

    @property
    def failed_count(self) -> int:
        """Return number of invalid batch items."""
        return self.total_count - self.passed_count

    def as_dict(self) -> dict[str, Any]:
        """Return deterministic serialisable report."""
        payload: dict[str, Any] = {}
        if self.context is not None:
            payload["context"] = deepcopy(dict(self.context))
        payload["total_count"] = self.total_count
        payload["passed_count"] = self.passed_count
        payload["failed_count"] = self.failed_count
        payload["results"] = [result.as_dict() for result in self.results]
        return payload


def validate_states(
    invariant_set: InvariantSet,
    states: Iterable[Any],
    *,
    context: Mapping[str, Any] | None = None,
) -> BatchReport:
    """Validate a batch of states in declaration order without raising on failures."""
    if not isinstance(invariant_set, InvariantSet):
        msg = "invariant_set must be an InvariantSet instance."
        raise TypeError(msg)
    if isinstance(states, (str, bytes, bytearray)):
        msg = "states must be an iterable of state values."
        raise TypeError(msg)
    if context is not None and not isinstance(context, Mapping):
        msg = "context must be a mapping when provided."
        raise TypeError(msg)

    results: list[BatchStateResult] = []
    for index, state in enumerate(states):
        if not isinstance(state, Mapping):
            failures = (_non_mapping_state_failure(state),)
            results.append(BatchStateResult(index=index, valid=False, failures=failures))
            continue
        candidate_state = deepcopy(dict(state))
        failures = invariant_set.validate(candidate_state)
        results.append(BatchStateResult(index=index, valid=not failures, failures=failures))
    return BatchReport(results=tuple(results), context=context)


def _non_mapping_state_failure(state: Any) -> InvariantFailure:
    return InvariantFailure(
        invariant=NON_MAPPING_INVARIANT,
        code=NON_MAPPING_CODE,
        message=NON_MAPPING_MESSAGE,
        details={"state_type": type(state).__name__},
    )
