"""Export model for Accountable Record stage 060.

WHY:
    Stage 060 validates machine-readable export artifacts and reconciles element
    exports against the normalized catalog from stage 050. This module defines
    the passive data structures used by discovery, reconciliation, validation,
    and pipeline state.

OBS (element-export scope):
    The registry records element exports only, under data/export/elements/.
    The export tree may also contain non-element exports; those are outside
    catalog-element reconciliation and are not recorded as element export
    records.

OBS (no disk writes):
    Stage 060 validates and models export material. It does not write generated
    files. write_intent() returns a description of what a later emitter stage
    would write.

REQ:
    Models are frozen dataclasses, matching the plain @dataclass(frozen=True)
    style of earlier stage models.
"""

from dataclasses import dataclass


@dataclass(frozen=True)
class ExportElementRecord:
    """One discovered element export, described structurally.

    Attributes:
        category: The on-disk category segment under data/export/elements/.
        name: The on-disk element export directory name.
        rel_dir: Repository-relative path to the element export directory.
        compact_id: The element type id read from the exported element.json
            ([identity].compact_id). The reconciliation key against the catalog.
    """

    category: str
    name: str
    rel_dir: str
    compact_id: str


@dataclass(frozen=True)
class ExportRegistry:
    """The set of discovered element exports, in discovery order.

    Attributes:
        records: Discovered element export records, in discovery order.
    """

    records: tuple[ExportElementRecord, ...] = ()

    def compact_ids(self) -> frozenset[str]:
        """Return the set of element-export compact_ids."""
        return frozenset(record.compact_id for record in self.records)

    @property
    def count(self) -> int:
        """Return the number of element export records."""
        return len(self.records)


@dataclass(frozen=True)
class ExportReconciliation:
    """Result of reconciling element exports against the catalog (1:1).

    OBS:
        Scoped to the element-export namespace. catalog_missing_export are
        catalog entries with no element export; uncataloged_exports are element
        exports with no catalog entry. Non-element exports are not considered.

    Attributes:
        catalog_missing_export: catalog compact_ids with no element export,
            sorted.
        uncataloged_exports: element-export compact_ids with no catalog entry,
            sorted.
    """

    catalog_missing_export: tuple[str, ...] = ()
    uncataloged_exports: tuple[str, ...] = ()

    @property
    def is_consistent(self) -> bool:
        """True when element exports and catalog entries are in 1:1 parity."""
        return not self.catalog_missing_export and not self.uncataloged_exports


@dataclass(frozen=True)
class ExportsArtifact:
    """Resolved exports artifact produced by stage 060.

    WHY:
        This is the artifact stage 060 folds into pipeline state via
        state.with_exports(...). It replaces the stub's placeholder while
        keeping target_root for traceability.

    Attributes:
        target_root: The repository root exports were validated under.
        registry: The discovered element exports.
        reconciliation: The element-export-vs-catalog reconciliation result.
    """

    target_root: str
    registry: ExportRegistry
    reconciliation: ExportReconciliation

    def write_intent(self) -> dict[str, object]:
        """Return a description of what a later emitter stage would write.

        OBS:
            This is write-INTENT modeling, not a write. Stage 060 returns this
            for a later stage to act on; it never writes to disk here.
        """
        return {
            "element_export_count": self.registry.count,
            "element_export_ids": sorted(self.registry.compact_ids()),
        }
