"""φ_PRG — HMAC-SHA-512 anchor map for v7 § 9.10 (ticket #000035).

The v7 plastic-training spec proposes φ_PRG as the M1 mitigation for
the soft-hash covert channel analyzed in ticket #000018: the anchor
map MUST be a cryptographic PRG independent of parameter geometry,
so that a data-poisoning or gradient-shaping adversary cannot bias
the training signal toward SHA-256 buckets they control.

Construction — HMAC-SHA-512 in NIST SP 800-108 KDF counter mode:

    Output(SEED, C(M), n_bytes) :=
        i = 0
        out = b""
        while len(out) < n_bytes:
            out += HMAC-SHA-512(SEED, C(M) || i.to_bytes(4, 'little'))
            i += 1
        return out[:n_bytes]

    Float conversion (uint32_le → [-1, 1)):
        f(u32) := 2 * (u32 / 2**32) - 1

Byte order is **little-endian** throughout — matching v7's canonical
TLV encoding (verified 2026-05-11 against ``merkle-agi-dag_v7.txt``
§A1: TLV length prefixes ``to_bytes(4, "little")``, ``enc_int`` is
``to_bytes(8, "little")``, tensors are ``<i8``). dav1d's 2026-05-11
§3.4 review: "if v7 TLV canonical integer encoding is little-endian,
flip §3.4 to little-endian before KAT freeze." Done — this is the
``-le`` variant; ``PHI_PRG_VERSION`` carries the ``-le`` suffix.

Security: HMAC-SHA-512 is a PRF under the standard SHA-512 + HMAC
assumption. Distinguishing advantage from random is bounded by the
SHA-512 collision-resistance bound (~2^256), which structurally
matches the substrate's SHA-256 hard-hash family. See ticket #000035
§2.1 for the full reasoning, §2.2 for why this construction won over
AES-256-CTR and ChaCha20.

Hard rules (per #000035 §2.4 + §3.4, dav1d-reviewed 2026-05-11):

- The seed is committed in the v7 boot manifest as
  ``anchor_prg_seed`` (purpose-scoped name; the function argument
  here is just ``seed`` / ``phi_prg_seed``-as-local-alias). It is
  **published** — secrecy is NOT the security property; the
  property is computational indistinguishability of the OUTPUT from
  random, which holds even with a public seed.
- The seed MUST be generated independently of the model checkpoint
  and training data, and MUST NOT be adversary-selected. It is
  **single-purpose** — never reuse ``anchor_prg_seed`` for any
  other PRG domain (no domain-separation tag in v1, so reuse would
  break the PRF argument).
- Per-checkpoint seed rotation is the M2 mitigation, orthogonal to
  this module — leave it to the v7 manifest layer.
- 32-byte hard-hash input matches the substrate's SHA-256 surface;
  shorter inputs raise ``ValueError`` rather than silently padding.
- ``dim_h ≤ 16 · 2^32``: a 4-byte counter admits 2^32 HMAC-SHA-512
  blocks of 64 bytes each (= 16 float32-words). A larger request
  raises ``ValueError`` rather than overflowing the counter.
"""

from __future__ import annotations

import hashlib
import hmac

# v7 manifest will publish the canonical seed when the spec lands.
# Until then, callers MUST pass a seed explicitly; the module-level
# constant exists so that test fixtures and KAT data have a stable
# placeholder to reference. Bytes-literal so accidentally substituting
# a string raises a clear TypeError at hmac.new() time.
PHI_PRG_VERSION = "phi-prg-v1-hmac-sha512-le"

# Maximum dim_h: a 4-byte counter admits 2^32 HMAC-SHA-512 blocks of
# 64 bytes = 16 float32-words each (#000035 §3.4). Astronomically above
# real deployments, but the guard makes the construction total.
_MAX_DIM_H = 16 * (1 << 32)

# Placeholder seed for tests + KAT generation. Replaced at deployment
# time with the v7 manifest's ``anchor_prg_seed`` field. The placeholder
# is 32 bytes so it matches the deployment shape; the value itself is
# the SHA-256 of a fixed string for reproducibility, NOT a security
# claim. Callers in production code path should pass their own seed.
PLACEHOLDER_SEED: bytes = hashlib.sha256(
    b"arborist v7 phi_prg placeholder seed -- ticket #000035"
).digest()


def phi_prg(
    hard_hash_32: bytes,
    dim_h: int,
    *,
    seed: bytes = PLACEHOLDER_SEED,
) -> list[float]:
    """Compute the anchor vector φ_PRG(C(M), dim_h) from a 32-byte hard hash.

    Parameters
    ----------
    hard_hash_32
        The committed hard-hash ``C(M)`` of the model (32 bytes,
        SHA-256 output). ``ValueError`` if not exactly 32 bytes —
        accepting shorter inputs would silently pad and break the
        PRF security argument.
    dim_h
        Length of the output anchor vector. Must be a positive int
        with ``dim_h ≤ 16 · 2^32`` (the 4-byte-counter ceiling).
    seed
        Published HMAC key — the v7 manifest's ``anchor_prg_seed``.
        Defaults to ``PLACEHOLDER_SEED``; deployment code must
        override with the committed manifest seed. Must be generated
        independently of the model and not adversary-selected
        (#000035 §3.4); single-purpose — do not reuse for other PRGs.

    Returns
    -------
    list[float]
        ``dim_h`` floats uniform over a 2^32-point grid in
        ``[-1, 1)`` (``-1.0`` reachable, ``+1.0`` not; finite-grid
        mean ``−2^−32`` — negligible, but not exactly zero),
        deterministically derived from ``(seed, hard_hash_32)``.

    Raises
    ------
    ValueError
        If ``hard_hash_32`` is not exactly 32 bytes, or ``dim_h``
        is not a positive integer, or ``dim_h > 16 · 2^32``.
    """
    if not isinstance(hard_hash_32, (bytes, bytearray)) or len(hard_hash_32) != 32:
        raise ValueError(
            "hard_hash_32 must be exactly 32 bytes (SHA-256 output); "
            f"got {len(hard_hash_32)!r} bytes"
            if isinstance(hard_hash_32, (bytes, bytearray))
            else f"got {type(hard_hash_32).__name__}"
        )
    if not isinstance(dim_h, int) or isinstance(dim_h, bool) or dim_h <= 0:
        raise ValueError(f"dim_h must be a positive int; got {dim_h!r}")
    if dim_h > _MAX_DIM_H:
        raise ValueError(
            f"dim_h must be ≤ 16·2^32 = {_MAX_DIM_H} (4-byte counter "
            f"ceiling, #000035 §3.4); got {dim_h}"
        )

    raw = _expand(seed, bytes(hard_hash_32), dim_h * 4)
    return _bytes_to_floats(raw)


def _expand(seed: bytes, hard_hash: bytes, n_bytes: int) -> bytes:
    """SP 800-108 counter-mode KDF over HMAC-SHA-512.

    Block size is the HMAC-SHA-512 output (64 bytes); we ceil-divide
    to the smallest counter range that yields ``n_bytes`` output, then
    truncate the last block. Counter is **little-endian** 4-byte
    unsigned, starting at 0 — matching v7's TLV integer encoding
    (#000035 §3.4, dav1d review 2026-05-11). The ``dim_h ≤ 16·2^32``
    guard in ``phi_prg`` keeps the counter from exhausting; if a bug
    ever drove it past 2^32 blocks, ``int.to_bytes(4, 'little')``
    would raise ``OverflowError`` rather than silently wrapping.
    """
    out = bytearray()
    counter = 0
    while len(out) < n_bytes:
        msg = hard_hash + counter.to_bytes(4, "little")
        out += hmac.new(seed, msg, hashlib.sha512).digest()
        counter += 1
    return bytes(out[:n_bytes])


def _bytes_to_floats(raw: bytes) -> list[float]:
    """Map each little-endian uint32 to a float in [-1, 1).

    Per #000035 §2.3 / §3.4: ``f(u32) = 2 * (u32 / 2**32) - 1``. Words
    are read **little-endian** to match v7's TLV integer convention
    (dav1d review 2026-05-11). Output is uniform over a 2^32-point grid
    in ``[-1, 1)`` with step ``2^-31``: ``f(0) = -1.0`` (reachable),
    ``f(2^32-1) = 1 - 2^-31`` (so ``+1.0`` is never reached). The
    finite-grid mean is ``−2^−32`` — negligible for the anchor-map use
    case, but **not exactly zero-mean**. If exact zero-mean is ever
    required, switch to the midpoint map ``f(u32) = 2*((u32+0.5)/2**32)
    - 1`` (range ``(-1, 1)``, exact zero mean) with a ``PHI_PRG_VERSION``
    bump + new KATs — never change the formula silently.
    """
    n = len(raw) // 4
    if len(raw) != n * 4:
        raise ValueError(
            f"raw byte length {len(raw)} not a multiple of 4; "
            "indicates an upstream bug in _expand truncation"
        )
    floats: list[float] = []
    inv_2_32 = 1.0 / (1 << 32)
    for i in range(n):
        u32 = int.from_bytes(raw[4 * i : 4 * (i + 1)], "little")
        floats.append(2.0 * (u32 * inv_2_32) - 1.0)
    return floats


__all__ = ["phi_prg", "PHI_PRG_VERSION", "PLACEHOLDER_SEED"]
