"""Scalar/shape parsing helpers for stage 040 element quartets.

WHY:
    Mirrors the scalar helpers in stages 010, 020, and 030: small,
    dependency-free functions that check the minimal structural shape of a
    parsed quartet member and raise ValueError on shape problems, so the
    load/validation code reads as decisions rather than isinstance tangles.

OBS (structural only):
    These helpers verify presence and type of structural keys. They do NOT
    interpret rule semantics, validate examples against their schema, or judge
    element meaning. They raise ValueError; the calling op converts that into
    a precise diagnostic naming the offending file.
"""

from accountable_record.boundary.toml import is_table


def require_table(
    parent: dict[str, object], key: str, context: str
) -> dict[str, object]:
    """Return a required sub-table (mapping) from a parent table.

    Args:
        parent: Parsed mapping.
        key: Sub-table name to read.
        context: Human-readable description, for error messages.

    Returns:
        The sub-table as a dict.

    Raises:
        ValueError: If the key is missing or not a table.
    """
    value = parent.get(key)
    if not is_table(value):
        msg = f"{context} requires a [{key}] table"
        raise ValueError(msg)
    return value


def require_string(table: dict[str, object], key: str, context: str) -> str:
    """Return a required non-empty string field.

    Args:
        table: Parsed mapping.
        key: Field name to read.
        context: Human-readable description, for error messages.

    Returns:
        The stripped string value.

    Raises:
        ValueError: If the field is missing, not a string, or empty.
    """
    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.

    WHY:
        A single call for "these keys must all be present", used for the
        minimal observed key sets. Reports the first missing key with context.

    Args:
        table: Parsed mapping.
        keys: Required key names.
        context: Human-readable description, for error messages.

    Raises:
        ValueError: If any required key is absent.
    """
    for key in keys:
        if key not in table:
            msg = f"{context} is missing required key {key!r}"
            raise ValueError(msg)


def expect_schema_tag(
    table: dict[str, object],
    expected: str,
    context: str,
) -> None:
    """Verify a top-level schema tag matches the observed value.

    OBS:
        Structural, not semantic: it checks the declared schema tag equals the
        value every real example carries, which catches a wrong or absent tag
        without interpreting the schema itself.

    Args:
        table: Parsed mapping (the file root).
        expected: The observed schema tag value.
        context: Human-readable description, for error messages.

    Raises:
        ValueError: If the schema tag is absent or does not match.
    """
    actual = table.get("schema")
    if actual != expected:
        msg = f"{context} expected schema tag {expected!r}, got {actual!r}"
        raise ValueError(msg)


def expect_object(value: object, context: str) -> dict[str, object]:
    """Verify a parsed JSON value is an object (mapping) and return it.

    Args:
        value: A parsed JSON value.
        context: Human-readable description, for error messages.

    Returns:
        The value as a dict.

    Raises:
        ValueError: If the value is not a JSON object.
    """
    if not is_table(value):
        msg = f"{context} must be a JSON object"
        raise ValueError(msg)
    return value
