"""§1.2 / §1.3 — the AuthorityDecision vocabulary and its receipt (a CORE invariant: the TWO-VOCAB separation).

The AuthorityDecision is the PRE-EXECUTION authorization verdict (may this action run, and under what limits).
It is a SEPARATE 5-value vocabulary from governance's 6-value `DECISION_VOCAB` (a POST-event, prospective
ruling). "Separate" means two INDEPENDENTLY-DEFINED constants — distinct objects, neither derives from or
imports the other, and neither type is ever merged into the other. Their string values are NOT disjoint: by
the design note's own §1.2 listing they share three labels (`ALLOW_WITH_LIMITS`, `REQUIRE_COSIGN`, `ESCALATE`),
so a consumer reads which vocabulary a value belongs to from the carrying SCHEMA / event_type
(`AUTHORITY_DECISION_RECEIPT_V1` vs `GOVERNANCE_DECISION_RECEIPT_V1`), never from the bare value. The
`authority_decision_hash` is DERIVED FROM the canonical content (§1.3) and therefore is NOT present inside that
content; the receipt then carries the hash + signer and projects into an ordinary VEB, mirroring the
governance receipt's `to_veb` binding chain (content → hash → receipt → VEB).
"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from typing import Any, Optional

from cl.canonical.hash_profile import content_hash
from cl.interfaces.identity import IdentityVerifier
from cl.model.veb import InlineCanonicalBody, VebContentV1

# §1.2 — the canonical 5-value AuthorityDecision. A SEPARATE, independently-defined constant from governance's
# DECISION_VOCAB (the two types are never merged; a value's vocabulary is read from the carrying schema, not the
# bare string — see the module docstring). test 1 asserts the two are distinct and the UNIQUE values never cross.
AUTHORITY_DECISION_VALUES = (
    "ALLOW",
    "ALLOW_WITH_LIMITS",
    "REQUIRE_COSIGN",
    "ESCALATE",
    "BLOCK",
)

# The decisions that PERMIT a consequential execution → a FULFILLMENT may be recorded ONLY when an
# AuthorityDecision in this set was VERIFIED. BLOCK and ESCALATE NEVER authorize. REQUIRE_COSIGN is
# CONSERVATIVELY EXCLUDED (fail-closed): it would authorize only once the required co-signature(s) are
# present/verified, but the co-signature model is not wired yet — so a cosign-pending REQUIRE_COSIGN is treated
# as NOT-yet-authorizing and can never slip into a FULFILLMENT. REQUIRE_COSIGN-with-verified-cosign is a tracked
# enhancement (part of #2 production-gate hardening); until it lands, REQUIRE_COSIGN stays out of this set.
AUTHORIZING_DECISIONS = ("ALLOW", "ALLOW_WITH_LIMITS")

DOMAIN_AUTHORITY_DECISION = "COTRUGLI:AUTHORITY_DECISION:CONTENT:V1"

AUTHORITY_DECISION_EVENT_TYPE = "AUTHORITY_DECISION_RECORDED"
AUTHORITY_DECISION_BODY_SCHEMA = "AUTHORITY_DECISION_RECEIPT_V1"


class AuthorityDecisionInvalid(ValueError):
    """An AuthorityDecision content / receipt invariant is violated — fail-closed."""


@dataclass(frozen=True)
class AuthorityDecisionContentV1:
    """A4 — the canonical AuthorityDecision content. The `authority_decision_hash` is DERIVED from this content
    (§1.3) and so is intentionally NOT a field here; `to_content()` MUST NOT contain it (test 11)."""

    action_intent_hash: str                     # hex — the intent this verdict authorizes
    authority_context_manifest_hash: str        # hex — the scoped/signed context
    decision: str                               # ∈ AUTHORITY_DECISION_VALUES
    policy_snapshot_ref: str
    issued_at: str
    valid_until: str
    hash_profile_id: bytes
    signature_suite: str
    authorized_effect_commitment: Optional[str] = None   # hex commitment over the authorized effect
    applied_limits: Optional[dict] = None
    required_cosigners: tuple[str, ...] = ()
    pac_bcvc_ref: Optional[str] = None

    def validate(self) -> None:
        if self.decision not in AUTHORITY_DECISION_VALUES:
            raise AuthorityDecisionInvalid(f"unknown AuthorityDecision: {self.decision!r}")
        for name in (
            "action_intent_hash",
            "authority_context_manifest_hash",
            "policy_snapshot_ref",
            "issued_at",
            "valid_until",
            "signature_suite",
        ):
            if not getattr(self, name):
                raise AuthorityDecisionInvalid(f"AuthorityDecision requires non-empty {name}")

    def to_content(self) -> dict:
        """The canonical content dict — `hash_profile_id` INSIDE (A1), optional fields OMITTED when absent.
        It MUST NOT contain `authority_decision_hash`: the hash is derived FROM this canonicalized content
        (§1.3 / test 11)."""
        self.validate()
        content: dict[str, Any] = {
            "action_intent_hash": self.action_intent_hash,
            "authority_context_manifest_hash": self.authority_context_manifest_hash,
            "decision": self.decision,
            "policy_snapshot_ref": self.policy_snapshot_ref,
            "issued_at": self.issued_at,
            "valid_until": self.valid_until,
            "hash_profile_id": self.hash_profile_id.hex(),  # A1: id inside the hashed content
            "signature_suite": self.signature_suite,
            "required_cosigners": list(self.required_cosigners),
        }
        if self.authorized_effect_commitment is not None:
            content["authorized_effect_commitment"] = self.authorized_effect_commitment
        if self.applied_limits is not None:
            content["applied_limits"] = self.applied_limits
        if self.pac_bcvc_ref is not None:
            content["pac_bcvc_ref"] = self.pac_bcvc_ref
        return content

    def authority_decision_hash(self) -> bytes:
        """A1: `HASH_BY_PROFILE(id, COTRUGLI:AUTHORITY_DECISION:CONTENT:V1 ‖ canonical(content INCLUDING id))`.
        Derived from `to_content()` — which itself never carries this hash (§1.3)."""
        return content_hash(self.hash_profile_id, DOMAIN_AUTHORITY_DECISION, self.to_content())


@dataclass(frozen=True)
class AuthorityDecisionReceiptV1:
    """The signed receipt binding an AuthorityDecision content hash to a signer. Projects into an ordinary VEB
    (mirrors the governance receipt's `to_veb`)."""

    authority_decision_hash: str                # hex of AuthorityDecisionContentV1.authority_decision_hash()
    proof_purpose: str
    signer_ref: str
    signature: str                              # hex

    def validate(self) -> None:
        for name in ("authority_decision_hash", "proof_purpose", "signer_ref", "signature"):
            if not getattr(self, name):
                raise AuthorityDecisionInvalid(f"AuthorityDecision receipt requires non-empty {name}")

    def to_body(self) -> dict:
        """The canonical inline body (validated)."""
        self.validate()
        return {
            "authority_decision_hash": self.authority_decision_hash,
            "proof_purpose": self.proof_purpose,
            "signer_ref": self.signer_ref,
            "signature": self.signature,
        }

    def to_veb(self, hash_profile_id: bytes, *, principal_ref: str, hai5_level: int = 2) -> VebContentV1:
        """Project the receipt into a canonical VEB. The signer is the actor; the receipt LINKS to the decision
        it records (RECORDS relation) without modifying anything — mirrors the governance receipt projection."""
        self.validate()
        return VebContentV1(
            schema_id="veb",
            schema_version=1,
            hash_profile_id=hash_profile_id,
            event_type=AUTHORITY_DECISION_EVENT_TYPE,
            actor_ref=self.signer_ref,
            principal_ref=principal_ref,
            hai5_level=hai5_level,
            semantic_time="",  # the decision's own issued_at lives in its content; the receipt is the record
            body_binding=InlineCanonicalBody(
                AUTHORITY_DECISION_BODY_SCHEMA, self.to_body(), non_personal=True
            ),
            evidence_refs=(self.authority_decision_hash,),
            relations=({"type": "RECORDS", "ref": self.authority_decision_hash},),
        )


def verify_authority_receipt(
    receipt: AuthorityDecisionReceiptV1,
    content: AuthorityDecisionContentV1,
    *,
    expected_action_intent_hash: str,
    verifier: IdentityVerifier,
    signer_key_ref: str,
    signer_registry=None,
    now_iso: Optional[str] = None,
) -> bool:
    """Derive the bridge's `authority_decision_verified` signal from a SIGNED AuthorityDecision, so it is not a
    pure caller assertion (finding #2). FAIL-CLOSED: returns True only if ALL of the following hold, else False
    (it never raises through — any error is a rejection):

      1. the receipt binds THIS content     — receipt.authority_decision_hash == content.authority_decision_hash()
      2. the decision PERMITS execution      — content.decision in AUTHORIZING_DECISIONS (not BLOCK/ESCALATE/COSIGN)
      3. the content binds the SAME intent   — content.action_intent_hash == expected_action_intent_hash
      4. the signature is authentic          — verifier.verify(decision_hash, signature, signer_key_ref) is not None
         (the signer signs the canonical `authority_decision_hash` bytes).

    PR-0 enforcement (each independently opt-in; both on in production, both off = the legacy no-op path for
    the merge=deploy rollout):

      5. `signer_registry` set — the signer is TRUSTED: the verification key is resolved SERVER-SIDE from
         `receipt.signer_ref` via the registry and the caller-supplied `signer_key_ref` is IGNORED; an
         unknown signer_ref is a rejection. A self-issued mandate (own key, own key_ref) can no longer verify.
      6. `now_iso` set — the decision is IN ITS VALIDITY WINDOW: issued_at ≤ now ≤ valid_until, all ISO-8601;
         unparsable or mixed-awareness timestamps reject (fail-closed, via the catch-all below).

    `verifier` is an injected `IdentityVerifier` (e.g. `Ed25519Verifier`).
    """
    try:
        decision_hash = content.authority_decision_hash()
        if receipt.authority_decision_hash != decision_hash.hex():
            return False
        if content.decision not in AUTHORIZING_DECISIONS:
            return False
        if content.action_intent_hash != expected_action_intent_hash:
            return False
        if signer_registry is not None:
            resolved = signer_registry.resolve(receipt.signer_ref)
            if resolved is None:
                return False                # unknown signer: never trusted
            signer_key_ref = resolved       # the caller's key_ref is ignored
        if now_iso is not None:
            now = datetime.fromisoformat(now_iso.replace("Z", "+00:00"))
            issued = datetime.fromisoformat(content.issued_at.replace("Z", "+00:00"))
            until = datetime.fromisoformat(content.valid_until.replace("Z", "+00:00"))
            if not (issued <= now <= until):
                return False                # expired or not-yet-valid mandate
        verified = verifier.verify(decision_hash, bytes.fromhex(receipt.signature), signer_key_ref)
        return verified is not None
    except Exception:  # any verification error is a fail-closed rejection, never a pass-through
        return False
