"""PR-0 — the trusted signer registry: the trust anchor for AuthorityDecision receipts.

`verify_authority_receipt` used to receive the signer's key_ref FROM THE CALLER: anyone holding a valid
ingest token could sign a mandate with their OWN key and present their own key_ref — a self-issued mandate
verified (recon finding, 2026-07-23). The registry closes that hole: with enforcement ON, the verification
key is resolved SERVER-SIDE from the receipt's `signer_ref` through this registry, the caller-supplied
key_ref is ignored, and an unknown signer_ref is a rejection. The same switch turns on the
issued_at/valid_until window check in `verify_authority_receipt`.

NO-OP ROLLOUT (this repo is merge=deploy): with no registry configured the legacy caller-supplied path is
byte-identical — deploying this change alters nothing for existing callers. Enforcement is a per-deployment
decision via one env var:

    CL_AUTHORITY_SIGNER_REGISTRY="urn:authority:kernel=<hex-ed25519-pubkey>,<ref2>=<key2>"   (inline)
    CL_AUTHORITY_SIGNER_REGISTRY="@/etc/cl/authority_signers.json"                            (flat JSON file)

Same law as POHAT on the NEOva side: WHO may sign is decided by a server-held registry, never by the
requester. (Token-claim enforcement — tenant/scopes/jti over signed JWTs — is the gateway track's PR A;
this module deliberately covers only the authority-receipt trust anchor.)
"""

from __future__ import annotations

import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Mapping, Optional


class SignerRegistryInvalid(ValueError):
    """A malformed registry configuration — fail loudly at boot, never at verify time."""


@dataclass(frozen=True)
class SignerRegistry:
    """Immutable signer_ref → key_ref bindings (key_ref = the raw hex public key the configured
    `IdentityVerifier` consumes, e.g. Ed25519 raw-bytes hex)."""

    bindings: Mapping[str, str] = field(default_factory=dict)

    def resolve(self, signer_ref: str) -> Optional[str]:
        """The server-side key for this signer, or None — and None is a REJECTION upstream."""
        return self.bindings.get(signer_ref)


def registry_from_env(env: Optional[Mapping[str, str]] = None) -> Optional[SignerRegistry]:
    """Build the registry from CL_AUTHORITY_SIGNER_REGISTRY, or None when unset (enforcement off — the
    no-op rollout default). A SET-but-malformed value raises: a deployment that ASKED for enforcement
    must never boot silently without it."""
    e = os.environ if env is None else env
    raw = (e.get("CL_AUTHORITY_SIGNER_REGISTRY") or "").strip()
    if not raw:
        return None
    if raw.startswith("@"):
        data = json.loads(Path(raw[1:]).read_text())
        if not isinstance(data, dict) or not all(
            isinstance(k, str) and isinstance(v, str) and k and v for k, v in data.items()
        ):
            raise SignerRegistryInvalid("registry file must be a flat non-empty {signer_ref: key_ref} object")
        bindings = dict(data)
    else:
        bindings = {}
        for pair in raw.split(","):
            pair = pair.strip()
            if not pair:
                continue
            if "=" not in pair:
                raise SignerRegistryInvalid(f"bad registry entry {pair!r} (want signer_ref=key_ref)")
            ref, key = pair.split("=", 1)
            if not ref.strip() or not key.strip():
                raise SignerRegistryInvalid(f"bad registry entry {pair!r} (empty ref or key)")
            bindings[ref.strip()] = key.strip()
    if not bindings:
        raise SignerRegistryInvalid("CL_AUTHORITY_SIGNER_REGISTRY is set but parsed to an empty registry")
    return SignerRegistry(bindings=bindings)
