"""A1 — hash-by-profile, with the profile id *inside* the hashed content.

The single hashing primitive for the whole Ledger. Per amendment A1 (AnchoredRoot MUST#2):

    content_hash = HASH_BY_PROFILE(hash_profile_id, DOMAIN ‖ canonical(content INCLUDING hash_profile_id))

The `hash_profile_id` is folded INTO the canonical content that gets hashed — never carried as metadata
beside the digest. That removes algorithm-confusion: a verifier cannot be tricked about which algorithm
produced a digest, and a hash-widening (sha-256 → sha3-384 …) is a clean NEW profile version on the one
versioning axis, not a breaking re-interpretation of existing bytes.

NOTE (A5 seam): `canonical()` here is PROVISIONAL — it MUST be replaced by the migrated RFC 8785 (JCS) KEEP
module from the adapter before any golden vectors are locked. The structural A1 invariants below do not
depend on the exact canonical bytes; the byte-level golden-vector gate (A5) is added once JCS is migrated and
the VEB/PAC-RO field set is finalized.
"""

from __future__ import annotations

import hashlib
from dataclasses import dataclass
from typing import Any, Mapping

from cl.canonical.jcs import canonical_bytes as _canonical_bytes


class HashProfileError(ValueError):
    """A1 invariant violation — fail-closed, never a silent fallback."""


@dataclass(frozen=True)
class HashProfile:
    """A versioned hash profile. The id is content-addressed over this descriptor (algorithm + digest width +
    domain + version_token), so the id *means* the descriptor and any auditor reproduces it. The
    `version_token` is the human-readable, profile-UNIQUE label used in `event_id` URNs (CONFIRM-WITH-CHANGE
    #1): a bare algorithm name would collide if two profiles ever shared an algorithm with different
    parameters; the token is bound INTO the id, so it is precise as the profile_id yet readable like 'v1'."""

    algorithm: str
    digest_bytes: int
    version_token: str = "v1"
    domain: str = "NEO::HASH_PROFILE::V1"

    def descriptor(self) -> dict[str, Any]:
        # the canonical descriptor whose hash is the profile id (deterministic, sorted)
        return {
            "algorithm": self.algorithm,
            "digest_bytes": self.digest_bytes,
            "version_token": self.version_token,
            "domain": self.domain,
        }

    def _hasher(self):
        if self.algorithm == "sha3-256":
            return hashlib.sha3_256()
        if self.algorithm == "sha-256":
            return hashlib.sha256()
        if self.algorithm == "sha3-384":
            return hashlib.sha3_384()
        raise HashProfileError(f"unknown hash algorithm: {self.algorithm!r}")

    def digest(self, data: bytes) -> bytes:
        h = self._hasher()
        h.update(data)
        out = h.digest()
        if len(out) != self.digest_bytes:
            raise HashProfileError(
                f"profile {self.algorithm} produced {len(out)} bytes, declared {self.digest_bytes}"
            )
        return out

    def id(self) -> bytes:
        """The content-addressed profile id: sha-256 of the canonical descriptor (the bootstrap hash used to
        *name* a profile is fixed to sha-256 so profile ids are stable across widenings of the data hash)."""
        return hashlib.sha256(_canonical_bytes(self.descriptor())).digest()


# --- Canon Lock v1 (docs/contracts/CANON_LOCK_V1.md §1.3) ------------------------------------------------
# Two self-describing profiles, BOTH registered. SHA-256 is the default for new L3 writes (alignment with the
# live deployed stack: adapter + SLA8ra are SHA-256). SHA3-256 is retained verify-only (dormant for L3 write),
# with its EXACT meaning unchanged — never redefined. A widening adds a new HashProfile (e.g. sha3-384/48)
# with a new id — existing ids are never re-interpreted (A1 / AnchoredRoot MUST#2).

# Retained verify-only profile (SHA3-256 / 32B). version_token DEFAULT "v1" so PROFILE_SHA3_256_V1.id() (and
# the event_id/URN "v1" token) is byte-identical to the historical PROFILE_V1 — old golden vectors/ids are
# unchanged.
PROFILE_SHA3_256_V1 = HashProfile(algorithm="sha3-256", digest_bytes=32)

# Default profile for NEW writes (SHA-256 / 32B). A DISTINCT version_token ("sha256-v1") so its id and its
# event_id/URN token do not collide with the sha3 "v1".
PROFILE_SHA256_V1 = HashProfile(algorithm="sha-256", digest_bytes=32, version_token="sha256-v1")

# The default hash profile for every new L3 write (Canon Lock v1).
DEFAULT_PROFILE = PROFILE_SHA256_V1

# DEPRECATED back-compat alias. Value UNCHANGED = sha3-256 (do NOT redefine its meaning); kept so existing
# imports keep working. New-write call sites use DEFAULT_PROFILE; verify-only paths may name this directly.
PROFILE_V1 = PROFILE_SHA3_256_V1

# Both profiles registered so resolve_profile() finds either; a truly-unknown id still fails closed.
_REGISTRY: dict = {
    PROFILE_SHA3_256_V1.id(): PROFILE_SHA3_256_V1,
    PROFILE_SHA256_V1.id(): PROFILE_SHA256_V1,
}


def resolve_profile(hash_profile_id: bytes) -> HashProfile:
    profile = _REGISTRY.get(hash_profile_id)
    if profile is None:
        raise HashProfileError(f"unregistered hash_profile_id: {hash_profile_id.hex()}")
    return profile


def content_hash(hash_profile_id: bytes, domain: str, content: Mapping[str, Any]) -> bytes:
    """A1 — the one hashing primitive.

    The `hash_profile_id` MUST already be present inside `content` and MUST equal the argument (fail-closed):
    the id is part of what is hashed, so a digest is inseparable from its algorithm. Then:
    `digest_by_profile(domain ‖ canonical(content))`.
    """
    present = content.get("hash_profile_id")
    if present is None:
        raise HashProfileError("content must carry 'hash_profile_id' (A1: id is hashed, not metadata)")
    present_bytes = bytes.fromhex(present) if isinstance(present, str) else present
    if present_bytes != hash_profile_id:
        raise HashProfileError("content.hash_profile_id != hash_profile_id argument (A1 mismatch)")

    profile = resolve_profile(hash_profile_id)
    preimage = domain.encode("utf-8") + b"\x1f" + _canonical_bytes(content)  # ‖ via a unit separator
    return profile.digest(preimage)


def with_profile(content: dict[str, Any], hash_profile_id: bytes) -> dict[str, Any]:
    """Return `content` with `hash_profile_id` set (hex) — the canonical way to make content A1-hashable."""
    out = dict(content)
    out["hash_profile_id"] = hash_profile_id.hex()
    return out
