"""Invariant-bound transition engine that emits evidence bundles."""

from __future__ import annotations

from collections.abc import Callable, Iterable, Mapping, Sequence
from copy import deepcopy
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any
from uuid import UUID, uuid4

from ..adapters import (
    TransparencyLogAdapter,
    TransparencyLogReceipt,
    TransparencyVerification,
    normalise_transparency_receipt,
    normalise_transparency_verification,
)
from ..adapters.authority_adapter import (
    AuthorityAdapter,
    AuthorityProposal,
    normalise_authority_proposal,
)
from ..adapters.transparency_stream import (
    TransparencyStreamAdapter,
    normalise_transparency_stream_receipt,
)
from .errors import PolicyProfileNotFoundError, TransitionContextError
from .evidence import (
    AuthoritySource,
    EvidenceBundle,
    _transparency_log_payload_from_mapping,
    normalise_authority_sources,
)
from .invariants import Invariant, InvariantProfile, InvariantProfileRegistry, InvariantSet
from .serialise import canonical_json_dumps

__all__ = ["AuthorityValidationHook", "Transition", "TransitionContext", "TransitionResult"]

TransitionMutator = Callable[[dict[str, Any]], Mapping[str, Any] | None]
AuthorityValidationHook = Callable[[str, tuple[AuthoritySource, ...]], None]


@dataclass(frozen=True, slots=True)
class TransitionResult:
    """Transition output containing immutable snapshots and generated evidence."""

    transition_name: str
    before_state: dict[str, Any]
    after_state: dict[str, Any]
    evidence: EvidenceBundle

    @property
    def evidence_json(self) -> str:
        """Return canonical JSON text for the generated evidence payload."""
        return canonical_json_dumps(self.evidence.as_dict())


@dataclass(frozen=True, slots=True)
class TransitionContext:
    """Validated authority/policy context for safe transition application."""

    authority: str
    authority_sources: Iterable[AuthoritySource | Mapping[str, Any]] = ()
    profile: str | None = None
    policy_version: str | None = None
    active_at: str | None = None

    def __post_init__(self) -> None:
        if not isinstance(self.authority, str) or not self.authority.strip():
            msg = "authority must be a non-empty string."
            raise TransitionContextError(msg)
        object.__setattr__(self, "authority", self.authority.strip())
        try:
            normalised_sources = normalise_authority_sources(self.authority_sources)
        except ValueError as exc:
            raise TransitionContextError(str(exc)) from exc
        object.__setattr__(self, "authority_sources", normalised_sources)
        object.__setattr__(
            self,
            "profile",
            _normalise_optional_string(self.profile, field_name="profile"),
        )
        object.__setattr__(
            self,
            "policy_version",
            _normalise_optional_string(self.policy_version, field_name="policy_version"),
        )
        object.__setattr__(
            self,
            "active_at",
            _normalise_optional_timestamp(self.active_at, field_name="active_at"),
        )


class Transition:
    """Apply a state mutation while enforcing invariant preservation."""

    __slots__ = (
        "_authority_validator",
        "_default_profile",
        "_authority_adapter",
        "_invariants",
        "_mutate",
        "_name",
        "_profile_registry",
        "_transparency_log",
        "_transparency_stream",
        "_verify_transparency_after_append",
    )

    def __init__(
        self,
        *,
        name: str,
        mutate: TransitionMutator,
        invariants: InvariantSet | Iterable[Invariant] | None = None,
        profile_registry: InvariantProfileRegistry | None = None,
        default_profile: str | None = None,
        authority_adapter: AuthorityAdapter | None = None,
        authority_validator: AuthorityValidationHook | None = None,
        transparency_log: TransparencyLogAdapter | None = None,
        transparency_stream: TransparencyStreamAdapter | None = None,
        verify_transparency_after_append: bool = False,
    ) -> None:
        if not isinstance(name, str) or not name.strip():
            msg = "Transition name must be a non-empty string."
            raise ValueError(msg)
        if not callable(mutate):
            msg = "Transition mutate must be callable."
            raise TypeError(msg)
        if invariants is None and profile_registry is None:
            msg = "Transition requires invariants or profile_registry."
            raise ValueError(msg)
        if invariants is not None and profile_registry is not None:
            msg = "Provide either invariants or profile_registry, not both."
            raise ValueError(msg)
        if profile_registry is None and default_profile is not None:
            msg = "default_profile requires profile_registry."
            raise ValueError(msg)
        if default_profile is not None and (
            not isinstance(default_profile, str) or not default_profile.strip()
        ):
            msg = "default_profile must be a non-empty string."
            raise ValueError(msg)
        if authority_validator is not None and not callable(authority_validator):
            msg = "authority_validator must be callable."
            raise TypeError(msg)
        if authority_adapter is not None and not isinstance(authority_adapter, AuthorityAdapter):
            msg = "authority_adapter must implement AuthorityAdapter."
            raise TypeError(msg)
        if transparency_log is not None and not isinstance(
            transparency_log, TransparencyLogAdapter
        ):
            msg = "transparency_log must implement TransparencyLogAdapter."
            raise TypeError(msg)
        if transparency_stream is not None and not isinstance(
            transparency_stream, TransparencyStreamAdapter
        ):
            msg = "transparency_stream must implement TransparencyStreamAdapter."
            raise TypeError(msg)
        if not isinstance(verify_transparency_after_append, bool):
            msg = "verify_transparency_after_append must be a boolean."
            raise TypeError(msg)
        if verify_transparency_after_append and transparency_log is None:
            msg = "verify_transparency_after_append requires transparency_log."
            raise ValueError(msg)
        self._name = name
        self._mutate = mutate
        self._authority_adapter = authority_adapter
        self._authority_validator = authority_validator
        self._transparency_log = transparency_log
        self._transparency_stream = transparency_stream
        self._verify_transparency_after_append = verify_transparency_after_append
        self._default_profile = (
            default_profile.strip() if isinstance(default_profile, str) else None
        )
        if isinstance(invariants, InvariantSet):
            self._invariants = invariants
            self._profile_registry = None
        elif invariants is not None:
            self._invariants = InvariantSet(invariants)
            self._profile_registry = None
        else:
            if not isinstance(profile_registry, InvariantProfileRegistry):
                msg = "profile_registry must be an InvariantProfileRegistry instance."
                raise TypeError(msg)
            if self._default_profile is not None:
                if self._default_profile not in profile_registry.profile_names:
                    raise PolicyProfileNotFoundError(self._default_profile)
            self._invariants = None
            self._profile_registry = profile_registry

    @classmethod
    def simple(
        cls,
        *,
        name: str,
        mutate: TransitionMutator,
        invariants: InvariantSet | Iterable[Invariant],
        authority_adapter: AuthorityAdapter | None = None,
        authority_validator: AuthorityValidationHook | None = None,
        transparency_log: TransparencyLogAdapter | None = None,
        transparency_stream: TransparencyStreamAdapter | None = None,
        verify_transparency_after_append: bool = False,
    ) -> Transition:
        """Construct a direct-invariant transition with optional adapter hooks."""
        if invariants is None:
            msg = "Transition.simple requires invariants."
            raise ValueError(msg)
        return cls(
            name=name,
            mutate=mutate,
            invariants=invariants,
            authority_adapter=authority_adapter,
            authority_validator=authority_validator,
            transparency_log=transparency_log,
            transparency_stream=transparency_stream,
            verify_transparency_after_append=verify_transparency_after_append,
        )

    @property
    def name(self) -> str:
        """Return transition name."""
        return self._name

    @property
    def supports_policy_profiles(self) -> bool:
        """Return True when this transition resolves invariants via profile registry."""
        return self._profile_registry is not None

    def build_context(
        self,
        *,
        authority: str | None = None,
        authority_sources: Iterable[AuthoritySource | Mapping[str, Any]] | None = None,
        profile: str | None = None,
        policy_version: str | None = None,
        active_at: datetime | str | None = None,
    ) -> TransitionContext:
        """Build a validated transition context before constructing transition artefacts."""
        resolved_active_at = _normalise_optional_timestamp(active_at, field_name="active_at")
        _, resolved_authority, _ = self._resolve_invariants_and_authority(
            authority=authority,
            profile=profile,
            policy_version=policy_version,
            active_at=resolved_active_at,
        )
        resolved_authority_sources = normalise_authority_sources(authority_sources)
        self._validate_authority_sources(
            authority=resolved_authority,
            authority_sources=resolved_authority_sources,
        )
        resolved_profile = profile if self._profile_registry is not None else None
        if resolved_profile is None and self._profile_registry is not None:
            resolved_profile = self._default_profile
        return TransitionContext(
            authority=resolved_authority,
            authority_sources=resolved_authority_sources,
            profile=resolved_profile,
            policy_version=policy_version,
            active_at=resolved_active_at,
        )

    def apply(
        self,
        *,
        state: Mapping[str, Any],
        entity_type: str,
        entity_id: str,
        actor: str,
        authority: str | None = None,
        authority_sources: Iterable[AuthoritySource | Mapping[str, Any]] | None = None,
        profile: str | None = None,
        policy_version: str | None = None,
        authority_response: Mapping[str, Any] | None = None,
        transition_id: str | UUID | None = None,
        timestamp: datetime | str | None = None,
    ) -> TransitionResult:
        """Run transition, enforcing invariants and emitting an evidence bundle."""
        effective_timestamp = _normalise_timestamp(timestamp, field_name="timestamp")
        context = self.build_context(
            authority=authority,
            authority_sources=authority_sources,
            profile=profile,
            policy_version=policy_version,
            active_at=effective_timestamp,
        )
        return self.apply_with_context(
            state=state,
            entity_type=entity_type,
            entity_id=entity_id,
            actor=actor,
            context=context,
            authority_response=authority_response,
            transition_id=transition_id,
            timestamp=effective_timestamp,
        )

    def apply_with_context(
        self,
        *,
        state: Mapping[str, Any],
        entity_type: str,
        entity_id: str,
        actor: str,
        context: TransitionContext,
        authority_response: Mapping[str, Any] | None = None,
        transition_id: str | UUID | None = None,
        timestamp: datetime | str | None = None,
    ) -> TransitionResult:
        """Apply transition using a pre-validated context object."""
        if not isinstance(context, TransitionContext):
            msg = "context must be a TransitionContext instance."
            raise TransitionContextError(msg)
        if timestamp is not None and context.active_at is not None:
            candidate_timestamp = _normalise_timestamp(timestamp, field_name="timestamp")
            if candidate_timestamp != context.active_at:
                msg = "timestamp must match context.active_at when both are provided."
                raise TransitionContextError(msg)
        effective_timestamp = _normalise_timestamp(
            context.active_at if context.active_at is not None else timestamp,
            field_name="timestamp",
        )
        invariants, resolved_authority, resolved_profile = self._resolve_invariants_and_authority(
            authority=context.authority,
            profile=context.profile,
            policy_version=context.policy_version,
            active_at=effective_timestamp,
        )
        resolved_authority_sources = normalise_authority_sources(context.authority_sources)
        self._validate_authority_sources(
            authority=resolved_authority,
            authority_sources=resolved_authority_sources,
        )
        before_state = invariants.construct_state(state, field_name="state")
        accepted_authority_proposal = self._validate_authority_response(
            invariants=invariants,
            current_state=before_state,
            authority_response=authority_response,
        )
        working_state = deepcopy(before_state)
        mutated_state = self._mutate(working_state)
        after_state = _normalise_after_state(mutated_state, fallback_state=working_state)
        after_state = invariants.construct_state(after_state, field_name="mutate return")

        transition_identifier = transition_id if transition_id is not None else uuid4()
        provenance_entries: list[dict[str, Any]] = []
        profile_provenance = self._profile_provenance_entries(
            profile=resolved_profile,
            active_at=effective_timestamp,
        )
        if profile_provenance is not None:
            provenance_entries.extend(profile_provenance)
        authority_proposal_provenance = _authority_proposal_provenance_entry(
            accepted_authority_proposal,
            timestamp=effective_timestamp,
        )
        if authority_proposal_provenance is not None:
            provenance_entries.append(authority_proposal_provenance)
        evidence = EvidenceBundle.from_states(
            transition_id=transition_identifier,
            entity_type=entity_type,
            entity_id=entity_id,
            actor=actor,
            authority=resolved_authority,
            authority_sources=resolved_authority_sources,
            before_state=before_state,
            after_state=after_state,
            timestamp=effective_timestamp,
            provenance=provenance_entries or None,
        )
        evidence = self._append_transparency_receipt_provenance(evidence)
        if self._verify_transparency_after_append:
            verification = self.verify_transparency(evidence)
            if not verification.verified:
                reason = verification.reason or "adapter returned unverified result."
                msg = f"Transparency log verification failed: {reason}"
                raise ValueError(msg)
        self._emit_transparency_stream_event(evidence)
        return TransitionResult(
            transition_name=self._name,
            before_state=before_state,
            after_state=after_state,
            evidence=evidence,
        )

    def verify_transparency(
        self,
        evidence: EvidenceBundle | Mapping[str, Any],
    ) -> TransparencyVerification:
        """Verify evidence transparency-log inclusion using the configured adapter."""
        if self._transparency_log is None:
            msg = "Transition has no transparency_log adapter configured."
            raise ValueError(msg)
        if isinstance(evidence, EvidenceBundle):
            raw_payload = evidence.as_dict()
            payload = evidence.transparency_log_payload()
        else:
            raw_payload = _copy_mapping(evidence, field_name="evidence")
            payload = _transparency_log_payload_from_mapping(raw_payload)
        receipt = _extract_transparency_receipt(raw_payload)
        result = self._transparency_log.verify(
            evidence_payload=payload,
            receipt=receipt,
        )
        return normalise_transparency_verification(result)

    def _resolve_invariants_and_authority(
        self,
        *,
        authority: str | None,
        profile: str | None,
        policy_version: str | None,
        active_at: datetime | str | None,
    ) -> tuple[InvariantSet, str, InvariantProfile | None]:
        if self._profile_registry is None:
            if profile is not None:
                msg = "profile requires profile_registry."
                raise ValueError(msg)
            if policy_version is not None:
                msg = "policy_version requires profile_registry."
                raise ValueError(msg)
            if authority is None:
                msg = "authority must be provided when Transition uses direct invariants."
                raise ValueError(msg)
            if self._invariants is None:
                msg = "Transition invariants are not configured."
                raise RuntimeError(msg)
            return self._invariants, authority, None
        profile_name = profile if profile is not None else self._default_profile
        if profile_name is None:
            msg = "profile must be provided when Transition uses a profile registry."
            raise ValueError(msg)
        resolved_profile = self._profile_registry.resolve(
            profile=profile_name,
            policy_version=policy_version,
            active_at=active_at,
        )
        resolved_authority = authority if authority is not None else resolved_profile.authority
        return resolved_profile.invariants, resolved_authority, resolved_profile

    def _validate_authority_sources(
        self,
        *,
        authority: str,
        authority_sources: tuple[AuthoritySource, ...],
    ) -> None:
        if self._authority_validator is None:
            return
        self._authority_validator(authority, authority_sources)

    def _validate_authority_response(
        self,
        *,
        invariants: InvariantSet,
        current_state: dict[str, Any],
        authority_response: Mapping[str, Any] | None,
    ) -> AuthorityProposal | None:
        if authority_response is None:
            return None
        if self._authority_adapter is None:
            msg = "authority_response requires authority_adapter."
            raise ValueError(msg)
        proposal_response = _copy_mapping(
            authority_response,
            field_name="authority_response",
        )
        proposal = normalise_authority_proposal(
            self._authority_adapter.propose(
                current_state=deepcopy(current_state),
                authority_response=proposal_response,
            )
        )
        invariants.construct_state_from_proposal(
            current_state=current_state,
            proposal_changes=proposal.changes,
            field_name="authority proposal",
        )
        return proposal

    def _profile_provenance_entries(
        self,
        *,
        profile: InvariantProfile | None,
        active_at: str,
    ) -> list[dict[str, Any]] | None:
        if profile is None:
            return None
        details: dict[str, Any] = {
            "profile": profile.name,
            "policy": profile.policy,
            "policy_version": profile.policy_version,
            "active_at": active_at,
        }
        if profile.active_from is not None:
            details["active_from"] = profile.active_from
        if profile.active_until is not None:
            details["active_until"] = profile.active_until
        return [
            {
                "source": f"policy-profile/{profile.name}",
                "reference": profile.authority,
                "timestamp": active_at,
                "details": details,
            }
        ]

    def _append_transparency_receipt_provenance(
        self,
        evidence: EvidenceBundle,
    ) -> EvidenceBundle:
        if self._transparency_log is None:
            return evidence
        receipt = normalise_transparency_receipt(
            self._transparency_log.append(evidence_payload=evidence.transparency_log_payload())
        )
        if receipt is None:
            return evidence
        payload = evidence.as_dict()
        provenance = list(payload.get("provenance", []))
        provenance.append(_transparency_receipt_provenance_entry(receipt))
        payload["provenance"] = provenance
        return EvidenceBundle.from_dict(payload)

    def _emit_transparency_stream_event(self, evidence: EvidenceBundle) -> None:
        if self._transparency_stream is None:
            return
        stream_event = evidence.as_stream_event_payload(transition_name=self._name)
        normalise_transparency_stream_receipt(
            self._transparency_stream.emit(event_payload=stream_event)
        )

    @staticmethod
    def require_authority_sources(*, minimum: int = 1) -> AuthorityValidationHook:
        """Return a validator that requires at least `minimum` authority sources."""
        if minimum < 1:
            msg = "minimum must be greater than or equal to 1."
            raise ValueError(msg)

        def _validator(authority: str, authority_sources: tuple[AuthoritySource, ...]) -> None:
            if len(authority_sources) < minimum:
                msg = (
                    "authority validation failed for "
                    f"{authority!r}: expected at least {minimum} authority source(s)."
                )
                raise ValueError(msg)

        return _validator


def _normalise_after_state(
    state: Mapping[str, Any] | None,
    *,
    fallback_state: dict[str, Any],
) -> dict[str, Any]:
    if state is None:
        return deepcopy(fallback_state)
    return _copy_mapping(state, field_name="mutate return")


def _copy_mapping(value: Mapping[str, Any], *, field_name: str) -> dict[str, Any]:
    if not isinstance(value, Mapping):
        msg = f"{field_name} must be a mapping."
        raise TypeError(msg)
    return deepcopy(dict(value))


def _normalise_optional_string(value: str | None, *, field_name: str) -> str | None:
    if value is None:
        return None
    if not isinstance(value, str) or not value.strip():
        msg = f"{field_name} must be a non-empty string when provided."
        raise TransitionContextError(msg)
    return value.strip()


def _normalise_optional_timestamp(
    value: datetime | str | None,
    *,
    field_name: str,
) -> str | None:
    if value is None:
        return None
    return _normalise_timestamp(value, field_name=field_name)


def _normalise_timestamp(
    value: datetime | str | None,
    *,
    field_name: str,
) -> str:
    if value is None:
        timestamp = datetime.now(timezone.utc)
    elif isinstance(value, datetime):
        timestamp = value
    elif isinstance(value, str):
        return _parse_timestamp(value, field_name=field_name).isoformat().replace("+00:00", "Z")
    else:
        msg = f"{field_name} must be a datetime, ISO-8601 UTC string, or None."
        raise TransitionContextError(msg)
    if timestamp.tzinfo is None:
        timestamp = timestamp.replace(tzinfo=timezone.utc)
    else:
        timestamp = timestamp.astimezone(timezone.utc)
    return timestamp.isoformat().replace("+00:00", "Z")


def _parse_timestamp(raw_value: str, *, field_name: str) -> datetime:
    value = raw_value.strip()
    if value.endswith("Z"):
        value = f"{value[:-1]}+00:00"
    try:
        parsed = datetime.fromisoformat(value)
    except ValueError as exc:
        msg = f"{field_name} must be a valid ISO-8601 UTC string; got {raw_value!r}."
        raise TransitionContextError(msg) from exc
    if parsed.tzinfo is None or parsed.utcoffset() != timezone.utc.utcoffset(None):
        msg = f"{field_name} must include a UTC offset; got {raw_value!r}."
        raise TransitionContextError(msg)
    return parsed.astimezone(timezone.utc)


def _extract_transparency_receipt(
    payload: Mapping[str, Any],
) -> TransparencyLogReceipt | None:
    provenance = payload.get("provenance")
    if not isinstance(provenance, Sequence) or isinstance(provenance, (str, bytes, bytearray)):
        return None
    for item in provenance:
        if not isinstance(item, Mapping):
            continue
        source = item.get("source")
        reference = item.get("reference")
        if not isinstance(source, str) or not source.startswith("transparency-log/"):
            continue
        adapter_name = source.partition("/")[2].strip()
        if not adapter_name or not isinstance(reference, str) or not reference.strip():
            continue
        receipt_payload: dict[str, Any] = {
            "adapter": adapter_name,
            "reference": reference,
        }
        if "timestamp" in item:
            receipt_payload["timestamp"] = item["timestamp"]
        if "details" in item:
            receipt_payload["details"] = item["details"]
        try:
            return normalise_transparency_receipt(receipt_payload)
        except ValueError:
            continue
    return None


def _transparency_receipt_provenance_entry(
    receipt: TransparencyLogReceipt,
) -> dict[str, Any]:
    provenance_entry: dict[str, Any] = {
        "source": f"transparency-log/{receipt.adapter}",
        "reference": receipt.reference,
    }
    if receipt.timestamp is not None:
        provenance_entry["timestamp"] = receipt.timestamp
    if receipt.details:
        provenance_entry["details"] = dict(receipt.details)
    return provenance_entry


def _authority_proposal_provenance_entry(
    proposal: AuthorityProposal | None,
    *,
    timestamp: str,
) -> dict[str, Any] | None:
    if proposal is None or proposal.source is None or proposal.reference is None:
        return None
    details: dict[str, Any] = {
        "proposal_changes": deepcopy(dict(proposal.changes)),
    }
    if proposal.details:
        details["proposal_details"] = deepcopy(dict(proposal.details))
    return {
        "source": proposal.source,
        "reference": proposal.reference,
        "timestamp": timestamp,
        "details": details,
    }
