"""Governance receipt ingestion (§2.7) — validates a GovernanceDecisionReceipt and admits it as a NEW
immutable VEB through the SAME ordinary pipeline (`cl.pipeline.ingest`): ingress → route → append-only store
→ (then the shared batching/anchoring path, untouched here). Idempotent by the locked
`governance_evaluation_key`. This module makes NO direct NEO call and owns NO anchoring/persistence state.
"""

from __future__ import annotations

import enum
import json
from dataclasses import dataclass
from typing import Optional

from cl.governance.cosign import CoSignPolicy, CoSignShortfall, cosign_proofs, verify_cosignatures
from cl.governance.receipt import (
    GOVERNANCE_EVENT_TYPE,
    GovernanceDecisionReceiptV1,
    GovernanceReceiptInvalid,
    decision_content_of_body,
    evaluation_key_of_body,
)
from cl.identity.registry import VerifierRegistry
from cl.ingress.envelope import IngressEnvelope, map_to_veb
from cl.interfaces.storage import StorageBackend, StoredEvent
from cl.model import hashing
from cl.model.veb import PIPELINE_TRUSTED
from cl.model.veb import InlineCanonicalBody
from cl.pipeline import IngestResult, ingest
from cl.governance.receipt import GOVERNANCE_BODY_SCHEMA


@dataclass(frozen=True)
class GovernancePolicy:
    """Operational policy for the governance ingestion path. `enabled=False` disables governance ENTIRELY
    without touching ERP/VEB ingestion or anchoring (they never depend on this module). `persist_no_action`
    records NO_ACTION only when the active policy demands an audit trail (A7)."""

    enabled: bool = True
    persist_no_action: bool = False
    # G1: k-of-n co-sign gate; None = pre-G1 behavior unchanged (activation = registering real
    # signer keys per the key runbook — an owner/config act, not a code change).
    cosign: Optional[CoSignPolicy] = None


class Outcome(enum.Enum):
    ACCEPTED = "ACCEPTED"          # new immutable governance event stored
    IDEMPOTENT = "IDEMPOTENT"      # the exact same receipt already exists → existing returned
    REJECTED = "REJECTED"         # invalid, or a changed decision for an unchanged eval key w/o supersession
    NOT_PERSISTED = "NOT_PERSISTED"  # NO_ACTION with no audit-record policy
    DISABLED = "DISABLED"         # governance is switched off


@dataclass(frozen=True)
class GovernanceIngestResult:
    outcome: Outcome
    stored: Optional[StoredEvent] = None
    reason: Optional[str] = None

    @property
    def accepted(self) -> bool:
        return self.outcome in (Outcome.ACCEPTED, Outcome.IDEMPOTENT)


def _envelope(receipt: GovernanceDecisionReceiptV1, principal_ref: str, hai5_level: int) -> IngressEnvelope:
    # transport fields are NEVER hashed (purity) — a deterministic, semantic-free idempotency hint is fine
    key_hex = receipt.evaluation_key().hex()
    return IngressEnvelope(
        event_type=GOVERNANCE_EVENT_TYPE,
        actor_ref=receipt.evaluator_id,
        principal_ref=principal_ref,
        hai5_level=hai5_level,
        semantic_time=receipt.effective_from,
        body_binding=InlineCanonicalBody(GOVERNANCE_BODY_SCHEMA, receipt.to_body(), non_personal=True),
        evidence_refs=(receipt.source_event_ref, receipt.governance_input_view_ref),
        relations=({"type": "GOVERNS", "ref": receipt.source_event_ref},),
        policy_snapshot_ref=receipt.policy_snapshot_ref,
        ingress_request_id=f"governance:{key_hex}",
        ingress_idempotency_key=key_hex,
        received_at=receipt.effective_from,
    )


def _conflicting_prior_decision(store: StorageBackend, eval_key: bytes, new_decision: bytes) -> bool:
    """True iff a stored governance event has the SAME evaluation key but a DIFFERENT decision content — a
    changed decision for unchanged inputs (only legitimate via supersession)."""
    for ev in store.scan_from(0):
        try:
            doc = json.loads(ev.canonical_bytes)
        except (json.JSONDecodeError, UnicodeDecodeError):
            continue
        if doc.get("event_type") != GOVERNANCE_EVENT_TYPE:
            continue
        body = doc.get("body_binding", {}).get("body")
        if not isinstance(body, dict):
            continue
        if evaluation_key_of_body(body) == eval_key and decision_content_of_body(body) != new_decision:
            return True
    return False


def ingest_governance_receipt(
    receipt: GovernanceDecisionReceiptV1,
    store: StorageBackend,
    hash_profile_id: bytes,
    *,
    principal_ref: str,
    policy: GovernancePolicy = GovernancePolicy(),
    hai5_level: int = 2,
    registry: Optional[VerifierRegistry] = None,
    cosignatures: tuple = (),
) -> GovernanceIngestResult:
    if not policy.enabled:
        return GovernanceIngestResult(Outcome.DISABLED, reason="governance disabled")

    # 1. validate the locked contract (A7 + vocabulary); invalid → REJECTED (fail-closed)
    try:
        receipt.validate()
    except GovernanceReceiptInvalid as e:
        return GovernanceIngestResult(Outcome.REJECTED, reason=str(e))

    # 1b. G1 co-sign gate (GOVERNANCE_ROOT_EVOLUTION faza G1): when a cosign policy is active, the
    # decision needs k-of-n distinct registered signatures over its canonical decision content —
    # shortfall REJECTS fail-closed. No policy → behavior unchanged (additive rollout, like the label).
    extra_proofs: tuple = ()
    if policy.cosign is not None:
        try:
            verified = verify_cosignatures(receipt.to_body(), cosignatures, policy.cosign)
        except CoSignShortfall as e:
            return GovernanceIngestResult(Outcome.REJECTED, reason=str(e))
        extra_proofs = cosign_proofs(verified, policy.cosign.registry)

    # 2. NO_ACTION is not persisted unless the active policy requires an audit record (A7)
    if receipt.decision_type == "NO_ACTION" and not policy.persist_no_action:
        return GovernanceIngestResult(Outcome.NOT_PERSISTED, reason="NO_ACTION not persisted by policy")

    env = _envelope(receipt, principal_ref, hai5_level)
    # Governance rides the library path → PIPELINE_TRUSTED (spec §5.3: no third registry value in
    # V1). The idempotency pre-check MUST mirror the label the ingest below will assign, or the two
    # hashes diverge and dedup silently breaks.
    content_hash = hashing.content_hash(map_to_veb(env, hash_profile_id, PIPELINE_TRUSTED))

    # 3. exact duplicate (content-addressed) → return the existing event (idempotent)
    existing = store.get_by_content_hash(content_hash)
    if existing is not None:
        return GovernanceIngestResult(Outcome.IDEMPOTENT, stored=existing)

    # 4. a CHANGED decision for an UNCHANGED evaluation key is only legitimate via supersession
    eval_key = receipt.evaluation_key()
    new_decision = decision_content_of_body(receipt.to_body())
    if receipt.supersedes_decision_ref is None and _conflicting_prior_decision(store, eval_key, new_decision):
        return GovernanceIngestResult(
            Outcome.REJECTED,
            reason="changed decision for an unchanged evaluation key requires a changed policy/evaluator/"
            "view or a supersedes_decision_ref",
        )

    # 5. admit through the SAME ordinary pipeline (ingress → route → append-only store)
    res: IngestResult = ingest(env, store, hash_profile_id, registry, extra_proofs=extra_proofs)
    if not res.accepted:
        return GovernanceIngestResult(
            Outcome.REJECTED, reason=f"ordinary pipeline rejected: {res.outcome.reject_code}"
        )
    return GovernanceIngestResult(Outcome.ACCEPTED, stored=res.stored)
