"""GovernanceDecisionReceiptV1 — the locked typed-VEB governance profile (§2.7, Appendix A.8).

A governance decision is a NEW immutable VEB event (`event_type = GOVERNANCE_DECISION_RECORDED`); it never
modifies/deletes/reclassifies its source, is PROSPECTIVE only, and enters through the SAME ordinary pipeline
as any other VEB (ingress → route → append-only store → batching → anchoring). This module owns ONLY the
receipt model + validation + the locked `governance_evaluation_key`; it does NOT touch the anchoring/
persistence path (§2.6).
"""

from __future__ import annotations

import hashlib
from dataclasses import dataclass
from typing import Optional

from cl.canonical.jcs import canonical_bytes
from cl.model.veb import InlineCanonicalBody, VebContentV1

GOVERNANCE_EVENT_TYPE = "GOVERNANCE_DECISION_RECORDED"
GOVERNANCE_BODY_SCHEMA = "GOVERNANCE_DECISION_RECEIPT_V1"
DOMAIN_EVAL_KEY = "COTRUGLI:GOVERNANCE:EVAL_KEY:V1"

# the locked decision vocabulary
DECISION_VOCAB = (
    "NO_ACTION",
    "RECORD_OBSERVATION",
    "ALLOW_WITH_LIMITS",
    "REQUIRE_COSIGN",
    "ESCALATE",
    "BLOCK_FUTURE_ACTION",
)
# decisions that bind future enforcement → require a precise subject + scope (A7)
SCOPED_DECISIONS = ("BLOCK_FUTURE_ACTION", "ALLOW_WITH_LIMITS")


class GovernanceReceiptInvalid(ValueError):
    """The receipt violates the locked governance contract (A7 / vocabulary) — fail-closed."""


@dataclass(frozen=True)
class DecisionScope:
    action_classes: tuple
    resource_scope: str
    value_scope: Optional[str] = None       # monetary as a STRING (A5: no floats)
    credential_scope: Optional[str] = None

    def to_dict(self) -> dict:
        d: dict = {"action_classes": list(self.action_classes), "resource_scope": self.resource_scope}
        if self.value_scope is not None:
            d["value_scope"] = self.value_scope
        if self.credential_scope is not None:
            d["credential_scope"] = self.credential_scope
        return d

    def is_precise(self) -> bool:
        return bool(self.action_classes) and bool(self.resource_scope)


@dataclass(frozen=True)
class GovernanceDecisionReceiptV1:
    # --- the evaluation-key inputs (locked) ---
    source_event_ref: str
    governance_input_view_ref: str
    policy_snapshot_ref: str
    evaluator_id: str
    evaluator_version: str
    # --- the decision ---
    decision_type: str
    effective_from: str                      # SEMANTIC time the decision takes effect (not operational)
    decision_subject_ref: Optional[str] = None
    decision_scope: Optional[DecisionScope] = None
    triggered_rule_ids: tuple = ()
    reason_codes: tuple = ()
    applied_limits: Optional[dict] = None
    required_cosigners: tuple = ()
    effective_until: Optional[str] = None
    human_approval_ref: Optional[str] = None
    supersedes_decision_ref: Optional[str] = None

    def validate(self) -> None:
        if self.decision_type not in DECISION_VOCAB:
            raise GovernanceReceiptInvalid(f"unknown decision_type: {self.decision_type!r}")
        for name in (
            "source_event_ref",
            "governance_input_view_ref",
            "policy_snapshot_ref",
            "evaluator_id",
            "evaluator_version",
            "effective_from",
        ):
            if not getattr(self, name):
                raise GovernanceReceiptInvalid(f"governance receipt requires non-empty {name}")
        # A7: BLOCK_FUTURE_ACTION / ALLOW_WITH_LIMITS must be precisely targeted
        if self.decision_type in SCOPED_DECISIONS:
            if not self.decision_subject_ref:
                raise GovernanceReceiptInvalid(
                    f"{self.decision_type} requires decision_subject_ref (A7)"
                )
            if self.decision_scope is None or not self.decision_scope.is_precise():
                raise GovernanceReceiptInvalid(
                    f"{self.decision_type} requires a precise decision_scope "
                    "(action_classes + resource_scope) (A7)"
                )

    def to_body(self) -> dict:
        """The canonical inline body (validated; absent optionals omitted for a stable canonical form)."""
        self.validate()
        body: dict = {
            "source_event_ref": self.source_event_ref,
            "governance_input_view_ref": self.governance_input_view_ref,
            "policy_snapshot_ref": self.policy_snapshot_ref,
            "evaluator_id": self.evaluator_id,
            "evaluator_version": self.evaluator_version,
            "decision_type": self.decision_type,
            "effective_from": self.effective_from,
            "triggered_rule_ids": list(self.triggered_rule_ids),
            "reason_codes": list(self.reason_codes),
            "required_cosigners": list(self.required_cosigners),
        }
        if self.decision_subject_ref is not None:
            body["decision_subject_ref"] = self.decision_subject_ref
        if self.decision_scope is not None:
            body["decision_scope"] = self.decision_scope.to_dict()
        if self.applied_limits is not None:
            body["applied_limits"] = self.applied_limits
        if self.effective_until is not None:
            body["effective_until"] = self.effective_until
        if self.human_approval_ref is not None:
            body["human_approval_ref"] = self.human_approval_ref
        if self.supersedes_decision_ref is not None:
            body["supersedes_decision_ref"] = self.supersedes_decision_ref
        return body

    def evaluation_key(self) -> bytes:
        return evaluation_key_of_body(self.to_body())

    def to_veb(self, hash_profile_id: bytes, *, principal_ref: str, hai5_level: int = 2) -> VebContentV1:
        """Project the receipt into a canonical VEB. The evaluator is the actor; the receipt LINKS to its
        source (GOVERNS relation) without modifying it; semantic_time = the decision's effective_from."""
        return VebContentV1(
            schema_id="veb",
            schema_version=1,
            hash_profile_id=hash_profile_id,
            event_type=GOVERNANCE_EVENT_TYPE,
            actor_ref=self.evaluator_id,
            principal_ref=principal_ref,
            hai5_level=hai5_level,
            semantic_time=self.effective_from,
            body_binding=InlineCanonicalBody(GOVERNANCE_BODY_SCHEMA, self.to_body(), non_personal=True),
            evidence_refs=(self.source_event_ref, self.governance_input_view_ref),
            relations=({"type": "GOVERNS", "ref": self.source_event_ref},),
            policy_snapshot_ref=self.policy_snapshot_ref,
        )


def evaluation_key_of_body(body: dict) -> bytes:
    """The locked `governance_evaluation_key` = a deterministic digest of EXACTLY (source_event_ref,
    governance_input_view_ref, policy_snapshot_ref, evaluator_id, evaluator_version). No operational
    timestamps, random values, or retry counters — only semantic inputs."""
    payload = {
        "domain": DOMAIN_EVAL_KEY,
        "source_event_ref": body["source_event_ref"],
        "governance_input_view_ref": body["governance_input_view_ref"],
        "policy_snapshot_ref": body["policy_snapshot_ref"],
        "evaluator_id": body["evaluator_id"],
        "evaluator_version": body["evaluator_version"],
    }
    return hashlib.sha3_256(canonical_bytes(payload)).digest()


def decision_content_of_body(body: dict) -> bytes:
    """A digest of the DECISION content (everything except the eval-key inputs and `supersedes_decision_ref`),
    used to detect a CHANGED decision for the same evaluation key."""
    content = {k: v for k, v in body.items()
               if k not in ("source_event_ref", "governance_input_view_ref", "policy_snapshot_ref",
                            "evaluator_id", "evaluator_version", "supersedes_decision_ref")}
    return hashlib.sha3_256(canonical_bytes(content)).digest()
