# ============================================================
# src/accountable_record/stage_080_generated/base/scalars.py
# ============================================================
"""Scalar/shape helpers for stage 080 generated artifacts.

WHY:
    Mirrors the scalar helper layer of earlier stages: small, dependency-free
    functions that check the minimal structural shape of parsed generated data
    and raise ValueError on shape problems, plus the canonical-form digest
    helpers this stage needs to reconcile a generated artifact against the
    digest the lock declares for it. Keeping these here lets collect/compare
    code read as decisions rather than isinstance-and-hashlib tangles.

OBS (narrow by design):
    Parse and digest helpers only. No filesystem walking, no diagnostics, no
    state mutation, no lock loading, and no graph resolution. The caller
    converts ValueError into a precise AR.GN.* diagnostic with the offending
    path or element label.

OBS (canonical form):
    canonical_json_digest serializes with sorted keys and compact separators
    so the same logical content yields the same digest. The digest_target the
    lock declares is canonical-generated-json; this helper computes over that
    same canonical form. It does not decide which bytes are the digest target;
    the caller supplies the parsed object.
"""

import hashlib
import json

from accountable_record.boundary.toml import is_table


def require_object(value: object, context: str) -> dict[str, object]:
    """Verify a parsed JSON value is an object and return it."""
    if not is_table(value):
        msg = f"{context} must be a JSON object"
        raise ValueError(msg)
    return value


def require_sub_object(
    parent: dict[str, object],
    key: str,
    context: str,
) -> dict[str, object]:
    """Return a required nested object from a parent object."""
    value = parent.get(key)
    if not is_table(value):
        msg = f"{context} requires object field {key!r}"
        raise ValueError(msg)
    return value


def require_string(table: dict[str, object], key: str, context: str) -> str:
    """Return a required non-empty string field."""
    value = table.get(key)
    if not isinstance(value, str) or not value.strip():
        msg = f"{context} requires non-empty string field {key!r}"
        raise ValueError(msg)
    return value.strip()


def require_keys(
    table: dict[str, object],
    keys: tuple[str, ...],
    context: str,
) -> None:
    """Verify that a table contains all required keys."""
    for key in keys:
        if key not in table:
            msg = f"{context} is missing required key {key!r}"
            raise ValueError(msg)


def expect_sha256_hex(value: str, context: str) -> None:
    """Verify a string has the minimal shape of a SHA-256 hex digest."""
    if len(value) != 64:
        msg = f"{context} digest must be 64 hex characters"
        raise ValueError(msg)

    try:
        int(value, 16)
    except ValueError as exc:
        msg = f"{context} digest must be hexadecimal"
        raise ValueError(msg) from exc


def canonical_json_bytes(value: object) -> bytes:
    """Return the canonical-generated-json byte form of a parsed value.

    Canonicalization sorts object keys and uses compact separators so that the
    same logical content always serializes identically.
    """
    return json.dumps(
        value,
        sort_keys=True,
        separators=(",", ":"),
        ensure_ascii=False,
    ).encode("utf-8")


def canonical_json_digest(value: object) -> str:
    """Return the lowercase sha256 hex digest of a value's canonical form."""
    return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
