"""C — `VerifiedContext`: the signed / scoped identity an authority check runs under.

Identity is the load-bearing seam: principal_ref and tenant come ONLY from a verified token's claims, never
from the request body. An UNSIGNED / None token cannot satisfy an authority check (fail-closed). The
authority-context manifest hash binds the exact scoped claims an ActionIntent / AuthorityDecision was raised
under, through the A1 primitive (id inside the hashed content). A `participant_id` in the body that conflicts
with the token's `sub` is IGNORED — the token wins (test 10).
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Callable, Optional

from cl.canonical.hash_profile import content_hash, with_profile

DOMAIN_AUTHORITY_CONTEXT_MANIFEST = "COTRUGLI:AUTHORITY_CONTEXT_MANIFEST:V1"


class ContextVerificationError(ValueError):
    """The token is unsigned / invalid, or an authority check ran without a verified context — fail-closed."""


@dataclass(frozen=True)
class VerifiedContext:
    """A verified set of token claims. `verified` is True only via `verify_token`; a hand-built context defaults
    to unverified, so it cannot silently satisfy an authority check."""

    iss: str
    aud: str
    sub: str                      # the participant id — the principal, from the token
    tenant: str
    scopes: tuple[str, ...]
    iat: str
    exp: str
    jti: str
    verified: bool = False

    @property
    def participant_id(self) -> str:
        return self.sub


# A token verifier: (token) -> claims mapping, or None / raises if the token is unsigned / invalid.
TokenVerifier = Callable[[str], Optional[dict]]


def verify_token(token: Optional[str], verifier: TokenVerifier) -> VerifiedContext:
    """Verify `token` through the injected `verifier` and project its claims into a `VerifiedContext`. An
    unsigned / None token, or a verifier that returns None / raises, is rejected (fail-closed)."""
    if token is None:
        raise ContextVerificationError("unsigned: no token presented")
    try:
        claims = verifier(token)
    except Exception as e:  # a verifier rejection is a fail-closed rejection, never a pass-through
        raise ContextVerificationError(f"token did not verify: {e}") from e
    if claims is None:
        raise ContextVerificationError("token did not verify")
    return VerifiedContext(
        iss=claims["iss"],
        aud=claims["aud"],
        sub=claims.get("sub") or claims["participant_id"],
        tenant=claims["tenant"],
        scopes=tuple(claims.get("scopes", ())),
        iat=claims["iat"],
        exp=claims["exp"],
        jti=claims["jti"],
        verified=True,
    )


def require_verified_context_for_authority(ctx: Optional[VerifiedContext]) -> VerifiedContext:
    """An authority check MUST run under a verified context. None / unverified raises (test 10): an unsigned
    identity can never satisfy an authority decision."""
    if ctx is None or not ctx.verified:
        raise ContextVerificationError(
            "an authority check requires a verified context (unsigned identity cannot authorize)"
        )
    return ctx


def authority_context_manifest_hash(ctx: VerifiedContext, hash_profile_id: bytes) -> str:
    """A1 hex digest over the scoped claims that bound an ActionIntent / AuthorityDecision."""
    manifest = with_profile(
        {
            "iss": ctx.iss,
            "aud": ctx.aud,
            "sub": ctx.sub,
            "tenant": ctx.tenant,
            "scopes": list(ctx.scopes),
            "jti": ctx.jti,
            "exp": ctx.exp,
        },
        hash_profile_id,
    )
    return content_hash(hash_profile_id, DOMAIN_AUTHORITY_CONTEXT_MANIFEST, manifest).hex()


def resolve_identity(ctx: VerifiedContext, body_claims: Optional[dict[str, Any]] = None) -> tuple[str, str, str]:
    """Resolve `(actor_ref, principal_ref, tenant)`. principal_ref and tenant come ONLY from `ctx`; a body
    `participant_id` that conflicts with `ctx.sub` is IGNORED — the token wins (test 10)."""
    principal_ref = ctx.sub
    tenant = ctx.tenant
    actor_ref = ctx.sub  # the authenticated subject is the actor unless the bridge overrides with a matched id
    return actor_ref, principal_ref, tenant
