"""Element-quartet model for Accountable Record stage 040.

WHY:
    Stage 040 establishes the target's authored element source quartets. This
    module is the 040 analog of stage 030's source model: frozen dataclasses
    naming a discovered element, the registry of all elements, and the artifact
    folded into pipeline state. It replaces the stub's placeholder
    ElementsArtifact(target_root: str) with the real registry.

OBS (data-driven requirement):
    category and name are read from the on-disk path
    (data/elements/<category>/<element>/), not from any hardcoded list. An
    ElementRecord therefore describes a discovered directory by its structural
    coordinates, not by membership in a Python enumeration.

OBS:
    Once these types exist, PipelineState.elements should be narrowed from
    object | None to ElementsArtifact | None (the same TYPE_CHECKING /
    string-annotation approach noted for sources, to avoid a pipeline->stage
    import cycle).

OBS (reconciliation seam, NOT folded in):
    Lock reconciliation against data/locks/elements.lock.json is deferred to a
    later export/generated stage (the lock digests generated JSON).
    The registry exposes the discovered set keyed by the element's compact_id so a
    later stage can reconcile against the lock's compact_id set WITHOUT s040
    reading the lock. That is the seam: a query method here, the reconciliation
    logic elsewhere.

REQ:
    Frozen dataclasses, matching the plain @dataclass(frozen=True) style of
    the other stage models, so pipeline state carries them immutably.
"""

from dataclasses import dataclass


@dataclass(frozen=True)
class ElementRecord:
    """One discovered authored element quartet, described structurally.

    Attributes:
        category: The on-disk category segment (e.g. identity). Read from
            the path, not a hardcoded set.
        name: The on-disk element directory name (e.g. version).
        rel_dir: Repository-relative path to the element directory.
        compact_id: The element type id declared in element.toml
            ([identity].compact_id). Recorded for the later reconciliation
            seam; absence is itself a structural failure caught upstream.
    """

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


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

    Attributes:
        records: All discovered element records, in discovery order.
    """

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

    def by_compact_id(self) -> dict[str, ElementRecord]:
        """Return a mapping of element compact_id to record.

        OBS (reconciliation seam):
            This is the query a later stage uses to reconcile the discovered
            authored set against data/locks/elements.lock.json's compact_id
            list. Built fresh on call to keep the dataclass frozen.
        """
        return {record.compact_id: record for record in self.records}

    def category_name_pairs(self) -> tuple[tuple[str, str], ...]:
        """Return discovered (category, name) pairs, in discovery order.

        OBS:
            Reports what was discovered from the filesystem path, satisfying
            the data-driven requirement to report category/name from the path
            rather than from a constant.
        """
        return tuple((r.category, r.name) for r in self.records)

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


@dataclass(frozen=True)
class ElementsArtifact:
    """Resolved element-quartet artifact produced by stage 040.

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

    Attributes:
        target_root: The repository root elements were discovered under.
        registry: The discovered element quartets.
    """

    target_root: str
    registry: ElementRegistry
