# ============================================================
# src/accountable_record/stage_090_docs/model.py
# ============================================================
"""Documentation model for Accountable Record stage 090.

WHY:
    Stage 090 plans and renders documentation after generated artifacts are
    known. This module defines the frozen structural records used by page
    planning, page/generated reconciliation, in-memory rendering, and the final
    artifact folded into pipeline state via state.with_docs(...).

OBS:
    Models are passive. They do not read files, render markup, write to disk,
    mutate state, or decide diagnostics. They only name the planned and
    rendered documentation structures and the derived reconciliation facts.

OBS (plan, not write):
    A DocsRenderPlan describes the pages a later emitter would render; a
    RenderedPage carries in-memory page content for inspection. Neither implies
    a file was written. This matches the write-intent convention of stage 060.

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

from dataclasses import dataclass

__all__ = [
    "PlannedPage",
    "DocsRenderPlan",
    "RenderedPage",
    "DocsReconciliation",
    "DocsArtifact",
]


@dataclass(frozen=True)
class PlannedPage:
    """One documentation page planned for a generated element.

    Attributes:
        compact_id: The element id the page documents.
        rel_path: Repository-relative path the page would be rendered to.
        title: The page title.
    """

    compact_id: str
    rel_path: str
    title: str


@dataclass(frozen=True)
class DocsRenderPlan:
    """The set of documentation pages planned from the generated artifacts.

    Attributes:
        pages: Planned pages, in sorted (rel_path) order.
    """

    pages: tuple[PlannedPage, ...] = ()

    def compact_ids(self) -> frozenset[str]:
        """Return the set of element compact_ids the plan documents."""
        return frozenset(page.compact_id for page in self.pages)

    def rel_paths(self) -> tuple[str, ...]:
        """Return the planned page paths, in order."""
        return tuple(page.rel_path for page in self.pages)

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


@dataclass(frozen=True)
class RenderedPage:
    """One rendered documentation page held in memory.

    OBS:
        Content is realized for inspection, not written to disk.

    Attributes:
        rel_path: Repository-relative path the page would be written to.
        content: The rendered Markdown content.
    """

    rel_path: str
    content: str


@dataclass(frozen=True)
class DocsReconciliation:
    """Result of reconciling the page plan against the generated artifacts.

    OBS:
        Scoped to element pages and generated element records.

    Attributes:
        generated_missing_page: Generated element compact_ids with no planned
            page, sorted.
        page_missing_generated: Planned-page compact_ids with no generated
            element, sorted.
        duplicate_page_paths: Repository-relative page paths that two planned
            pages share, sorted.
    """

    generated_missing_page: tuple[str, ...] = ()
    page_missing_generated: tuple[str, ...] = ()
    duplicate_page_paths: tuple[str, ...] = ()

    @property
    def is_consistent(self) -> bool:
        """True when planned pages and generated elements are in parity."""
        return (
            not self.generated_missing_page
            and not self.page_missing_generated
            and not self.duplicate_page_paths
        )


@dataclass(frozen=True)
class DocsArtifact:
    """Resolved documentation result produced by stage 090.

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

    Attributes:
        target_root: The repository root documentation was planned under.
        plan: The documentation render plan.
        rendered: The in-memory rendered pages.
        reconciliation: The plan-vs-generated reconciliation result.
    """

    target_root: str
    plan: DocsRenderPlan
    rendered: tuple[RenderedPage, ...]
    reconciliation: DocsReconciliation

    @property
    def planned_page_count(self) -> int:
        """Return the number of planned documentation pages."""
        return self.plan.count
