"""§2.2 — the deterministic router (Appendix A.6): `route(candidate) → VEB | PAC_RO | REJECT`.

Pure and deterministic: the same candidate always routes the same way, with no hidden state. A candidate is
an already-mapped `VebContentV1` (the §2.3 ingress mapper produces it from the lightweight transport
envelope). The router classifies by what the event IS — a PAC-RO iff it carries a valid `PAC_RO_V1` inline
body (the four-element invariant holds), otherwise a general VEB — and REJECTS fail-closed on: unknown
envelope schema, a body that cannot be canonicalized (A5) or violates the A8 binding rule, a PAC-RO missing
any of its four elements, or (when a signature is supplied) an invalid signature/credential.
"""

from __future__ import annotations

import enum
from dataclasses import dataclass
from typing import Optional

from cl.canonical.hash_profile import HashProfileError
from cl.canonical.jcs import CanonicalizationError
from cl.interfaces.identity import AssuranceLevel, IdentityVerifier
from cl.model import hashing
from cl.model.pacro import PAC_RO_V1, PacRoBodyV1, PacRoInvalid
from cl.model.veb import INGRESS_ASSURANCE_V1, BodyBindingError, InlineCanonicalBody, VebContentV1

KNOWN_SCHEMA_ID = "veb"
SUPPORTED_VERSIONS = frozenset({1})


class Decision(enum.Enum):
    VEB = "VEB"
    PAC_RO = "PAC_RO"
    REJECT = "REJECT"


class RejectCode(enum.Enum):
    UNKNOWN_SCHEMA = "unknown_schema"
    CANONICALIZATION_FAIL = "canonicalization_fail"
    BODY_BINDING_FAIL = "body_binding_fail"  # A8 (personal-data inline) etc.
    PAC_RO_INCOMPLETE = "pac_ro_incomplete"  # missing one of the four elements
    BAD_SIGNATURE = "bad_signature"
    INGRESS_ASSURANCE_UNKNOWN = "ingress_assurance_unknown"  # label present but outside INGRESS_ASSURANCE_V1


@dataclass(frozen=True)
class RoutingOutcome:
    decision: Decision
    content_hash: Optional[bytes] = None
    event_id: Optional[str] = None
    assurance: AssuranceLevel = AssuranceLevel.UNSIGNED
    reject_code: Optional[RejectCode] = None
    reason: Optional[str] = None

    @property
    def accepted(self) -> bool:
        return self.decision is not Decision.REJECT


def _reject(code: RejectCode, reason: str) -> RoutingOutcome:
    return RoutingOutcome(decision=Decision.REJECT, reject_code=code, reason=reason)


def route(
    candidate: VebContentV1,
    *,
    signature: Optional[bytes] = None,
    key_ref: Optional[str] = None,
    verifier: Optional[IdentityVerifier] = None,
) -> RoutingOutcome:
    """Classify `candidate`. Deterministic; signature verification is optional (an unsigned candidate routes
    at the lowest assurance — A3 — it is not rejected for being unsigned, only for being *invalidly* signed)."""
    # 1. envelope schema must be known
    if candidate.schema_id != KNOWN_SCHEMA_ID or candidate.schema_version not in SUPPORTED_VERSIONS:
        return _reject(
            RejectCode.UNKNOWN_SCHEMA,
            f"unknown envelope schema {candidate.schema_id!r} v{candidate.schema_version}",
        )

    # 1b. door-provenance label, when present, must come from the registry (fail-closed; the label is
    # assigned by the door, so an unknown value here means a broken door or a spoof attempt)
    if candidate.ingress_assurance is not None and candidate.ingress_assurance not in INGRESS_ASSURANCE_V1:
        return _reject(
            RejectCode.INGRESS_ASSURANCE_UNKNOWN,
            f"ingress_assurance {candidate.ingress_assurance!r} is not in INGRESS_ASSURANCE_V1",
        )

    # 2. the content must canonicalize (A5) and honor the body-binding rule (A8) — this also computes the hash
    try:
        ch = hashing.content_hash(candidate)
    except BodyBindingError as e:
        return _reject(RejectCode.BODY_BINDING_FAIL, str(e))
    except (CanonicalizationError, HashProfileError) as e:
        return _reject(RejectCode.CANONICALIZATION_FAIL, str(e))

    # 3. optional signature/credential check — invalid signature is a REJECT; absent signature is allowed
    assurance = AssuranceLevel.UNSIGNED
    if signature is not None:
        if verifier is None or key_ref is None:
            return _reject(RejectCode.BAD_SIGNATURE, "signature supplied without a verifier/key_ref")
        identity = verifier.verify(ch, signature, key_ref)
        if identity is None:
            return _reject(RejectCode.BAD_SIGNATURE, "signature/credential did not verify")
        assurance = identity.assurance

    eid = hashing.event_id(candidate.hash_profile_id, ch)

    # 4. classify: a valid PAC_RO_V1 inline body → PAC_RO; an invalid one → REJECT; otherwise → VEB
    body = candidate.body_binding
    if isinstance(body, InlineCanonicalBody) and body.body_schema_ref == PAC_RO_V1:
        try:
            PacRoBodyV1.from_body(body.body)
        except PacRoInvalid as e:
            return _reject(RejectCode.PAC_RO_INCOMPLETE, str(e))
        return RoutingOutcome(Decision.PAC_RO, content_hash=ch, event_id=eid, assurance=assurance)

    return RoutingOutcome(Decision.VEB, content_hash=ch, event_id=eid, assurance=assurance)
