# ============================================================
# src/accountable_record/stage_080_generated/model.py
# ============================================================
"""Generated-artifact model for Accountable Record stage 080.

WHY:
    Stage 080 validates generated artifacts after exports and resolution are
    known. This module defines the frozen structural records used by generated
    collection, lock reconciliation, and the final artifact folded into
    pipeline state via state.with_generated(...).

OBS:
    Models are passive. They do not read files, compute digests, mutate state,
    or decide diagnostics. They only name the collected generated structures
    and the derived reconciliation facts.

OBS (scope):
    This stage models element generated artifacts under data/generated/elements/
    and their parity with the locked element records from stage 070. It does
    not rebuild exports, re-derive the resolution graph, or render docs.

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

from dataclasses import dataclass

__all__ = [
    "GeneratedElementRecord",
    "GeneratedRegistry",
    "GeneratedReconciliation",
    "GeneratedArtifact",
]


@dataclass(frozen=True)
class GeneratedElementRecord:
    """One generated element artifact considered by stage 080.

    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 generated element compact id.
    """

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


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

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

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

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

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


@dataclass(frozen=True)
class GeneratedReconciliation:
    """Result of reconciling generated artifacts against the lock.

    OBS:
        Scoped to element generated artifacts and locked element records.
        digest_mismatches is populated only when the validation layer
        recomputes digests; presence/parity checks do not require it.

    Attributes:
        lock_missing_generated: Locked element compact_ids with no generated
            artifact, sorted.
        generated_missing_lock: Generated-artifact compact_ids with no locked
            element record, sorted.
        digest_mismatches: Element compact_ids whose recomputed digest did not
            match the locked digest, sorted.
    """

    lock_missing_generated: tuple[str, ...] = ()
    generated_missing_lock: tuple[str, ...] = ()
    digest_mismatches: tuple[str, ...] = ()

    @property
    def is_consistent(self) -> bool:
        """True when generated artifacts and locked elements are in parity."""
        return (
            not self.lock_missing_generated
            and not self.generated_missing_lock
            and not self.digest_mismatches
        )


@dataclass(frozen=True)
class GeneratedArtifact:
    """Resolved generated-artifact result produced by stage 080.

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

    Attributes:
        target_root: The repository root generated artifacts were validated
            under.
        registry: The discovered generated element artifacts.
        reconciliation: The generated-vs-lock reconciliation result.
    """

    target_root: str
    registry: GeneratedRegistry
    reconciliation: GeneratedReconciliation

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