# ============================================================
# src/accountable_record/stage_070_resolution/model.py
# ============================================================
"""Resolution model for Accountable Record stage 070.

WHY:
    Stage 070 validates resolution and lock material after exports are known.
    This module defines the frozen structural records used by lock loading,
    graph construction, verification, and the final artifact folded into
    pipeline state.

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

OBS (scope):
    This stage models resolution locks, especially data/locks/elements.lock.json.
    It does not rebuild exports, validate authored element quartets, or validate
    generated artifact freshness.
"""

from dataclasses import dataclass


@dataclass(frozen=True)
class ResolutionPolicy:
    """Resolution policy declared by the lock file.

    Attributes:
        one_major_version_per_identity_per_graph: Whether each identity may
            resolve to only one major version within a graph.
        range_conflicts_are_errors: Whether incompatible version ranges are
            structural errors.
    """

    one_major_version_per_identity_per_graph: bool
    range_conflicts_are_errors: bool


@dataclass(frozen=True)
class PackageLockRecord:
    """One package record from the elements lock.

    Attributes:
        compact_id: Package compact identifier.
        version: Locked package version.
        canonical_uri: Canonical package URI.
        digest_algorithm: Digest algorithm declared by the lock.
        digest_target: Digest target declared by the lock.
        digest: Digest string declared by the lock.
        source: Repository-relative authored source path for the package.
    """

    compact_id: str
    version: str
    canonical_uri: str
    digest_algorithm: str
    digest_target: str
    digest: str
    source: str

    @property
    def versioned_id(self) -> str:
        """Return compact_id@version."""
        return f"{self.compact_id}@{self.version}"


@dataclass(frozen=True)
class ElementLockRecord:
    """One element record from the elements lock.

    Attributes:
        compact_id: Element compact identifier.
        version: Locked element version.
        included_by: Versioned package ids that include this element.
        digest_algorithm: Digest algorithm declared by the lock.
        digest_target: Digest target declared by the lock.
        digest: Digest string declared by the lock.
    """

    compact_id: str
    version: str
    included_by: tuple[str, ...]
    digest_algorithm: str
    digest_target: str
    digest: str

    @property
    def versioned_id(self) -> str:
        """Return compact_id@version."""
        return f"{self.compact_id}@{self.version}"


@dataclass(frozen=True)
class ElementsLock:
    """Parsed elements lock.

    Attributes:
        target_root: Repository root the lock was loaded from.
        rel_path: Repository-relative lock path.
        schema: Lock schema tag.
        resolved_at: Lock resolution timestamp string.
        policy: Declared resolution policy.
        packages: Package lock records.
        elements: Element lock records.
    """

    target_root: str
    rel_path: str
    schema: str
    resolved_at: str
    policy: ResolutionPolicy
    packages: tuple[PackageLockRecord, ...]
    elements: tuple[ElementLockRecord, ...]

    def package_ids(self) -> frozenset[str]:
        """Return locked package compact ids."""
        return frozenset(record.compact_id for record in self.packages)

    def package_versioned_ids(self) -> frozenset[str]:
        """Return locked package compact_id@version ids."""
        return frozenset(record.versioned_id for record in self.packages)

    def element_ids(self) -> frozenset[str]:
        """Return locked element compact ids."""
        return frozenset(record.compact_id for record in self.elements)

    def element_versioned_ids(self) -> frozenset[str]:
        """Return locked element compact_id@version ids."""
        return frozenset(record.versioned_id for record in self.elements)


@dataclass(frozen=True)
class ResolutionGraph:
    """Derived package/element inclusion graph.

    Attributes:
        package_to_elements: Mapping from versioned package id to locked element
            compact ids included by that package.
        element_to_packages: Mapping from element compact id to versioned package
            ids that include that element.
        unknown_package_refs: Versioned package refs named by elements but not
            declared in the package lock section.
        duplicate_package_ids: Duplicate package compact ids.
        duplicate_element_ids: Duplicate element compact ids.
    """

    package_to_elements: dict[str, tuple[str, ...]]
    element_to_packages: dict[str, tuple[str, ...]]
    unknown_package_refs: tuple[str, ...] = ()
    duplicate_package_ids: tuple[str, ...] = ()
    duplicate_element_ids: tuple[str, ...] = ()

    @property
    def is_consistent(self) -> bool:
        """Return true when graph-level structural checks found no issues."""
        return (
            not self.unknown_package_refs
            and not self.duplicate_package_ids
            and not self.duplicate_element_ids
        )


@dataclass(frozen=True)
class LockExportReconciliation:
    """Element lock/export reconciliation result.

    OBS:
        This is scoped to locked elements and element exports only. It does not
        compare package locks to exports and does not recompute digests.

    OBS:
        The element lock is scoped to the resolved package graph, not every
        element exported under data/export/. Therefore s070 requires every
        locked element to have an export, but it does not require every exported
        element to have a lock entry.

    Attributes:
        lock_missing_export: Locked element compact ids with no export.
    """

    lock_missing_export: tuple[str, ...] = ()

    @property
    def is_consistent(self) -> bool:
        """Return true when every locked element has an export artifact."""
        return not self.lock_missing_export


@dataclass(frozen=True)
class ResolutionArtifact:
    """Resolution artifact produced by stage 070.

    Attributes:
        target_root: Repository root resolution was validated under.
        lock: Parsed elements lock
        graph: Derived resolution graph
        reconciliation: Lock/export reconciliation, absent until verify layer
            supplies it.
    """

    target_root: str
    lock: ElementsLock | None
    graph: ResolutionGraph | None
    reconciliation: LockExportReconciliation | None = None

    @property
    def locked_element_count(self) -> int:
        """Return the number of locked element records."""
        if self.lock is None:
            return 0
        return len(self.lock.elements)

    @property
    def locked_package_count(self) -> int:
        """Return the number of locked package records."""
        if self.lock is None:
            return 0
        return len(self.lock.packages)
