"""Catalog model for Accountable Record stage 050.

WHY:
    Stage 050 builds a normalized catalog over the elements validated by stage
    040 and reconciles it against the repository's declared element catalog.
    This module is the 050 analog of earlier stage models: frozen dataclasses
    naming a catalog entry, the built catalog, the reconciliation result, and
    the artifact folded into pipeline state. It replaces the stub's placeholder
    CatalogArtifact(target_root: str) with the real catalog.

OBS (entry shape from the real declared catalog, not invented):
    CatalogEntry mirrors the entry fields observed in data/catalog/
    elements.json: compact_id, component_group, local_name, label,
    artifact_kind, canonical_uri, persistent_id, version, status, source.
    compact_id is the reconciliation key; the rest are descriptive, enriched
    from each element's already-validated element.toml.

OBS (no file writes):
    The catalog is an in-memory artifact. serializable() produces the
    {count, elements[], schema} representation matching the declared catalog,
    but stage 050 does not write it to disk; that is a later stage's concern.

OBS (state typing):
    Once this exists, PipelineState.catalog may be narrowed from
    object | None to CatalogArtifact | None (TYPE_CHECKING import to
    avoid a pipeline->stage cycle), as with the earlier stage artifacts.

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

from dataclasses import dataclass

from accountable_record.stage_050_catalog.base.constants import (
    CATALOG_KEY_COUNT,
    CATALOG_KEY_ELEMENTS,
    CATALOG_KEY_SCHEMA,
    CATALOG_SCHEMA_TAG,
    ENTRY_KEY_ARTIFACT_KIND,
    ENTRY_KEY_CANONICAL_URI,
    ENTRY_KEY_COMPACT_ID,
    ENTRY_KEY_COMPONENT_GROUP,
    ENTRY_KEY_LABEL,
    ENTRY_KEY_LOCAL_NAME,
    ENTRY_KEY_PERSISTENT_ID,
    ENTRY_KEY_SOURCE,
    ENTRY_KEY_STATUS,
    ENTRY_KEY_VERSION,
)


@dataclass(frozen=True)
class CatalogEntry:
    """One catalog entry, mirroring the declared catalog's entry shape.

    Attributes:
        compact_id: The element type id; the reconciliation key.
        component_group: The element's component group (== on-disk category).
        local_name: The element's local name.
        label: Human-readable label.
        artifact_kind: The artifact kind (e.g. "element-type").
        canonical_uri: The element's canonical URI.
        persistent_id: The element's persistent identifier.
        version: The element's declared version.
        status: The element's release status.
        source: Repository-relative path to the element.toml the entry was
            built from.
    """

    compact_id: str
    component_group: str
    local_name: str
    label: str
    artifact_kind: str
    canonical_uri: str
    persistent_id: str
    version: str
    status: str
    source: str

    def as_dict(self) -> dict[str, str]:
        """Return the entry as an ordered mapping matching the declared shape."""
        return {
            ENTRY_KEY_ARTIFACT_KIND: self.artifact_kind,
            ENTRY_KEY_CANONICAL_URI: self.canonical_uri,
            ENTRY_KEY_COMPACT_ID: self.compact_id,
            ENTRY_KEY_COMPONENT_GROUP: self.component_group,
            ENTRY_KEY_LABEL: self.label,
            ENTRY_KEY_LOCAL_NAME: self.local_name,
            ENTRY_KEY_PERSISTENT_ID: self.persistent_id,
            ENTRY_KEY_SOURCE: self.source,
            ENTRY_KEY_STATUS: self.status,
            ENTRY_KEY_VERSION: self.version,
        }


@dataclass(frozen=True)
class Catalog:
    """The catalog built over validated elements.

    Attributes:
        entries: Catalog entries, in compact_id-sorted order for determinism.
    """

    entries: tuple[CatalogEntry, ...] = ()

    def compact_ids(self) -> frozenset[str]:
        """Return the set of entry compact_ids."""
        return frozenset(entry.compact_id for entry in self.entries)

    @property
    def count(self) -> int:
        """Return the number of catalog entries."""
        return len(self.entries)

    def serializable(self) -> dict[str, object]:
        """Return the {count, elements[], schema} representation.

        OBS:
            This is the in-memory serializable form matching the declared
            catalog's shape. Stage 050 returns it in the artifact; it does NOT
            write it to disk.
        """
        return {
            CATALOG_KEY_COUNT: self.count,
            CATALOG_KEY_ELEMENTS: [entry.as_dict() for entry in self.entries],
            CATALOG_KEY_SCHEMA: CATALOG_SCHEMA_TAG,
        }


@dataclass(frozen=True)
class CatalogReconciliation:
    """Result of reconciling the built catalog against the declared catalog.

    Attributes:
        declared_present: Whether a declared catalog was found and parsed.
        undeclared: compact_ids built/validated but absent from the declared
            catalog, sorted.
        orphaned: compact_ids declared but with no validated element, sorted.
    """

    declared_present: bool
    undeclared: tuple[str, ...] = ()
    orphaned: tuple[str, ...] = ()

    @property
    def is_consistent(self) -> bool:
        """True when no undeclared or orphaned entries were found."""
        return not self.undeclared and not self.orphaned


@dataclass(frozen=True)
class CatalogArtifact:
    """Resolved catalog artifact produced by stage 050.

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

    Attributes:
        target_root: The repository root the catalog was built under.
        catalog: The built catalog.
        reconciliation: The discovered-vs-declared reconciliation result.
    """

    target_root: str
    catalog: Catalog
    reconciliation: CatalogReconciliation
