"""PAC_RO_V1 — the consequential-transactional subtype (Appendix A.5).

PAC-RO is carried as a VEB with `body_binding = INLINE_CANONICAL_BODY`, `body_schema_ref = PAC_RO_V1`, and is
the SINGLE source of truth for the transaction (no parallel `payload_hash` + separate facts/evidence, A6).
HARD INVARIANT: a PAC-RO is INVALID unless all four of `facts`, `evidence`, `signature_policy`,
`exception_semantics` are present and coherent. A8: any PERSONAL parts of facts/evidence are carried as
ENCRYPTED_EXTERNAL evidence references / commitments — never raw personal data inline — so the inline body is
provably non-personal and remains crypto-shreddable.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

from cl.model.accounting_binding import validate_facts_accounting_binding
from cl.model.veb import InlineCanonicalBody, VebContentV1

PAC_RO_V1 = "PAC_RO_V1"

EVENT_CLASSES = ("COMMITMENT", "FULFILLMENT", "ACCEPTANCE", "SETTLEMENT", "EXCEPTION")

# The four inseparable elements (the PAC-RO four-element invariant).
FOUR_ELEMENTS = ("facts", "evidence", "signature_policy", "exception_semantics")


class PacRoInvalid(ValueError):
    """The PAC-RO four-element invariant (or event_class) is violated — fail-closed (routes to REJECT)."""


@dataclass(frozen=True)
class PacRoBodyV1:
    event_class: str
    lifecycle_state: str
    pac_bcvc_ref: str
    facts: dict
    evidence: dict
    signature_policy: dict
    exception_semantics: dict
    co_signers: tuple[str, ...] = ()
    parent_receipt_ref: Optional[str] = None
    exception_of_ref: Optional[str] = None
    batch_metadata: Optional[dict] = None
    governance_context: Optional[dict] = None

    def validate(self) -> None:
        if self.event_class not in EVENT_CLASSES:
            raise PacRoInvalid(f"unknown event_class: {self.event_class!r}")
        for name in FOUR_ELEMENTS:
            value = getattr(self, name)
            # present AND coherent: a non-empty mapping (the structural minimum; richer coherence is per-schema)
            if not isinstance(value, dict) or not value:
                raise PacRoInvalid(
                    f"PAC-RO four-element invariant: '{name}' must be present and non-empty"
                )
        # ERP_ACCOUNTING_V1 (additive): an absent binding changes nothing; a present one must conform.
        validate_facts_accounting_binding(self.facts)

    def to_body(self) -> dict:
        """The canonical PAC-RO body dict (validated; absent optionals omitted for a stable canonical form)."""
        self.validate()
        body: dict = {
            "event_class": self.event_class,
            "lifecycle_state": self.lifecycle_state,
            "pac_bcvc_ref": self.pac_bcvc_ref,
            "facts": self.facts,
            "evidence": self.evidence,
            "signature_policy": self.signature_policy,
            "exception_semantics": self.exception_semantics,
            "co_signers": list(self.co_signers),
        }
        for opt in ("parent_receipt_ref", "exception_of_ref", "batch_metadata", "governance_context"):
            value = getattr(self, opt)
            if value is not None:
                body[opt] = value
        return body


    @classmethod
    def from_body(cls, body: dict) -> "PacRoBodyV1":
        """Parse + validate a PAC-RO body dict (e.g. from an inline VEB body during routing). Raises
        `PacRoInvalid` on an unknown key, a missing required field, or a four-element violation."""
        required = {"event_class", "lifecycle_state", "pac_bcvc_ref", *FOUR_ELEMENTS}
        optional = {"co_signers", "parent_receipt_ref", "exception_of_ref", "batch_metadata", "governance_context"}
        unknown = set(body.keys()) - required - optional
        if unknown:
            raise PacRoInvalid(f"unknown PAC-RO body fields: {sorted(unknown)}")
        missing = required - set(body.keys())
        if missing:
            raise PacRoInvalid(f"missing PAC-RO body fields: {sorted(missing)}")
        pacro = cls(
            event_class=body["event_class"],
            lifecycle_state=body["lifecycle_state"],
            pac_bcvc_ref=body["pac_bcvc_ref"],
            facts=body["facts"],
            evidence=body["evidence"],
            signature_policy=body["signature_policy"],
            exception_semantics=body["exception_semantics"],
            co_signers=tuple(body.get("co_signers", ())),
            parent_receipt_ref=body.get("parent_receipt_ref"),
            exception_of_ref=body.get("exception_of_ref"),
            batch_metadata=body.get("batch_metadata"),
            governance_context=body.get("governance_context"),
        )
        pacro.validate()
        return pacro


def pacro_inline_body(pacro: PacRoBodyV1) -> InlineCanonicalBody:
    """Wrap a validated PAC-RO body as the VEB inline body (body_schema_ref = PAC_RO_V1). `non_personal=True`
    is asserted because PAC-RO personal data lives in ENCRYPTED_EXTERNAL evidence, not inline (A8)."""
    return InlineCanonicalBody(body_schema_ref=PAC_RO_V1, body=pacro.to_body(), non_personal=True)


def is_pac_ro(veb: VebContentV1) -> bool:
    body = veb.body_binding
    return isinstance(body, InlineCanonicalBody) and body.body_schema_ref == PAC_RO_V1
