# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-normalize-cross-protocol-identity-assertions-before-appending-to-an-audit-log.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-normalize-cross-protocol-identity-assertions-before-appending-to-an-audit-log.warrant.json
# Markdown SHA-256:   446db3316d43bf97bdb115fa0852562722894a32ed3f81f0cfa8154775c3bdfa
# Code AST SHA-256:   5152ac1fd99b248390fb378dbbaf10716e5f70ba9fd60762e718dab638232a94
# Verified Classes:   SemanticForgeError, Assertion
# Defensive Guards:   3 fail-closed invariants (zero external deps)

import hashlib
import json
from dataclasses import dataclass
from typing import Optional


class SemanticForgeError(RuntimeError):
    """Raised when an assertion cannot be mapped injectively to a canonical subject.

    Appending such an assertion to the tamper-evident chain would certify a
    subject that the normalizer cannot prove, making the chain semantically
    forgeable even though it remains byte-integrity-sound.
    """


@dataclass(frozen=True)
class Assertion:
    protocol: str          # "mcp" | "a2a" | "aaif"
    issuer: str
    audience: str
    subject_claim: str
    nbf: int
    exp: int


# Injective map: (protocol, issuer, subject_claim) -> canonical subject.
# Must be total over the deployment's known issuers; unknown issuer => fail.
_SUBJECT_MAP = {
    ("mcp",  "https://idp.corp.example",  "alice@corp"): "did:web:corp.example:alice",
    ("aaif", "did:web:corp.example",      "did:web:corp.example:alice"): "did:web:corp.example:alice",
    ("a2a",  "https://broker.corp.example", "broker-7"): "agent:broker.corp.example:broker-7",
}


def normalize_for_chain(
    assertion: Assertion,
    ingesting_audience: str,
    reference_clock: int,
    skew_ms: int = 90_000,
) -> dict:
    # 1. Audience binding: reject before normalization. This is an authz
    #    failure, not a normalization failure; never fold into subject.
    if assertion.audience != ingesting_audience:
        raise SemanticForgeError(
            f"audience mismatch: assertion aud={assertion.audience!r} "
            f"ingesting aud={ingesting_audience!r}"
        )

    # 2. Validity window with explicit, logged skew.
    if reference_clock < assertion.nbf - skew_ms:
        raise SemanticForgeError("assertion not yet valid at reference clock")
    validity = "expired" if reference_clock > assertion.exp + skew_ms else "valid"

    # 3. Injective subject mapping. Unknown => fail closed; do NOT coerce
    #    to None/"unknown", which would launder the principal into a bucket.
    key = (assertion.protocol, assertion.issuer, assertion.subject_claim)
    canonical = _SUBJECT_MAP.get(key)
    if canonical is None:
        raise SemanticForgeError(
            f"unmappable subject: protocol={assertion.protocol!r} "
            f"issuer={assertion.issuer!r} subject={assertion.subject_claim!r}; "
            "quarantine required, chain append refused"
        )

    entry_body = {
        "subject": canonical,
        "protocol": assertion.protocol,
        "issuer": assertion.issuer,
        "audience": assertion.audience,
        "validity": validity,
        "reference_clock": reference_clock,
        "skew_ms": skew_ms,
    }
    entry_digest = hashlib.sha256(
        json.dumps(entry_body, sort_keys=True).encode()
    ).hexdigest()
    return {**entry_body, "digest": entry_digest}


def append_to_chain(chain_tail: str, entry: dict) -> str:
    return hashlib.sha256((chain_tail + entry["digest"]).encode()).hexdigest()
