"""stage_050_catalog/build.py. Build the catalog from validated elements (stage 050 op).

WHY:
    build.py turns the s040 ElementRegistry (the validated element set) into a
    Catalog of descriptive entries. It is the op the validation layer drives.
    Like earlier read ops it stays free of diagnostics: it returns a Catalog or
    raises a typed StageError, which the validation layer maps to an AR.CA.*
    code.

OBS (consume, do not re-discover. decision Q2):
    The set of elements comes from state.elements, NOT from re-walking
    data/elements/. For descriptive fields not carried on the ElementRecord
    (label, version, status, canonical_uri, persistent_id, artifact_kind),
    build.py reads the element's element.toml, a file s040 already proved
    present and parseable. Reading an already-validated file to enrich a known
    entry is enrichment, not discovery; the authoritative element set is still
    state.elements.

OBS (boundary):
    Builds an in-memory Catalog only. Writes nothing.
"""

from pathlib import Path
import tomllib

from accountable_record.boundary.toml import is_table
from accountable_record.errors import (
    StageInputMalformed,
    StageInputUnreadable,
)
from accountable_record.stage_040_elements.model import ElementsArtifact
from accountable_record.stage_050_catalog.base import constants
from accountable_record.stage_050_catalog.base.scalars import (
    optional_string,
    require_string,
    require_table,
)
from accountable_record.stage_050_catalog.model import Catalog, CatalogEntry


def build_catalog(elements: ElementsArtifact, repo_root: Path) -> Catalog:
    """Build a Catalog from the validated elements artifact.

    Args:
        elements: The s040 elements artifact (its registry is the source of
            truth for which elements exist).
        repo_root: The target repository root, for resolving element.toml paths.

    Returns:
        A :class:`Catalog` with entries sorted by compact_id for determinism.

    Raises:
        StageInputMalformed: An element's element.toml lacked a descriptive
            field required to build its entry.
        StageInputUnreadable: An element's element.toml could not be read.
    """
    entries: list[CatalogEntry] = []
    for record in elements.registry.records:
        entries.append(_entry_for(record.rel_dir, record.compact_id, repo_root))

    entries.sort(key=lambda entry: entry.compact_id)
    return Catalog(entries=tuple(entries))


def _entry_for(rel_dir: str, compact_id: str, repo_root: Path) -> CatalogEntry:
    """Build one catalog entry by enriching from the element's element.toml.

    Raises:
        StageInputMalformed: A required descriptive field was absent.
        StageInputUnreadable: element.toml could not be read.
    """
    element_path = repo_root / rel_dir / constants.ELEMENT_FILENAME
    rel = f"{rel_dir}/{constants.ELEMENT_FILENAME}"

    data = _load_toml(element_path, rel)

    try:
        identity = require_table(data, constants.TABLE_IDENTITY, rel)
        classification = require_table(data, constants.TABLE_CLASSIFICATION, rel)
        # element.toml [identity] carries compact_id/local_name/label and the
        # URIs; [classification] carries artifact_kind/component_group; [release]
        # carries version/status. release is optional-tolerant: a missing
        # version/status becomes an empty string rather than a hard failure,
        # since those are descriptive, not structural.
        release = data.get(constants.TABLE_RELEASE)
        release_table: dict[str, object] = release if is_table(release) else {}

        entry = CatalogEntry(
            compact_id=require_string(identity, constants.ENTRY_KEY_COMPACT_ID, rel),
            local_name=require_string(identity, constants.ENTRY_KEY_LOCAL_NAME, rel),
            label=optional_string(identity, constants.ENTRY_KEY_LABEL) or "",
            canonical_uri=optional_string(identity, constants.ENTRY_KEY_CANONICAL_URI)
            or "",
            persistent_id=optional_string(identity, constants.ENTRY_KEY_PERSISTENT_ID)
            or "",
            artifact_kind=optional_string(
                classification, constants.ENTRY_KEY_ARTIFACT_KIND
            )
            or "",
            component_group=require_string(
                classification, constants.ENTRY_KEY_COMPONENT_GROUP, rel
            ),
            version=optional_string(release_table, constants.ENTRY_KEY_VERSION) or "",
            status=optional_string(release_table, constants.ENTRY_KEY_STATUS) or "",
            source=rel,
        )
    except ValueError as exc:
        raise StageInputMalformed(str(exc), location=rel) from exc

    # Defensive: the compact_id read from element.toml should match the one s040
    # recorded. A mismatch is structural and would corrupt reconciliation.
    if entry.compact_id != compact_id:
        msg = (
            f"element.toml compact_id {entry.compact_id!r} disagrees with the "
            f"validated id {compact_id!r}"
        )
        raise StageInputMalformed(msg, location=rel)

    return entry


def _load_toml(path: Path, rel: str) -> dict[str, object]:
    """Load and parse an element.toml, raising typed errors naming the path."""
    try:
        text = path.read_text(encoding="utf-8")
    except OSError as exc:
        raise StageInputUnreadable(str(exc), location=rel) from exc
    try:
        return tomllib.loads(text)
    except tomllib.TOMLDecodeError as exc:
        raise StageInputMalformed(f"invalid TOML: {exc}", location=rel) from exc
