# ============================================================
# src/accountable_record/stage_090_docs/validation.py
# ============================================================
"""Stage 090 documentation validation for Accountable Record.

WHY:
    Stage 090 plans and renders documentation for the generated artifacts from
    stage 080, and reconciles the planned page set against the generated set.
    It is the documentation concern of the verifier.

OBS (profile branch):
    Implementation repositories do not carry target documentation material
    derived from generated artifacts. That case is determined from the
    schema-valid repository profile, not from a manifest opt-out. Such a
    repository yields an empty DocsArtifact with no warning and no error,
    mirroring the profile rule used by s070 and s080.

OBS (plan, not write):
    The verifier does not write into docs/. Documentation pages are planned and
    rendered in memory and reconciled against the generated set. Realized
    writes belong to a later emitter, not to verification.
"""

from dataclasses import dataclass, field

from accountable_record.pipeline.context import PipelineState
from accountable_record.pipeline.diagnostics import Diagnostics
from accountable_record.stage_080_generated.model import GeneratedArtifact
from accountable_record.stage_090_docs.base import diagnostics as dx
from accountable_record.stage_090_docs.index import build_documentation
from accountable_record.stage_090_docs.model import (
    DocsArtifact,
    DocsReconciliation,
    DocsRenderPlan,
)

__all__ = ["DocsValidationResult", "validate_docs_stage"]


@dataclass(frozen=True)
class DocsValidationResult:
    """Validation result for stage 090."""

    diagnostics: Diagnostics = field(default_factory=Diagnostics)
    docs: DocsArtifact | None = None

    @property
    def has_errors(self) -> bool:
        """Return true if any error-severity diagnostics are present."""
        return self.diagnostics.has_errors

    @property
    def exit_code(self) -> int:
        """Return 0 if no errors, else 1."""
        return self.diagnostics.exit_code


def validate_docs_stage(state: PipelineState) -> DocsValidationResult:
    """Plan, render, and reconcile documentation for the target repository.

    Args:
        state: Accumulated pipeline state. Stage 080 generated must be present.

    Returns:
        Documentation validation result.
    """
    diagnostics = Diagnostics()

    generated = _generated_from_state(state)
    if generated is None:
        diagnostics.add(dx.from_spec(dx.GENERATED_REQUIRED))
        return DocsValidationResult(diagnostics=diagnostics)

    repo_root = state.context.target_root
    profile = state.context.repository_profile

    if not profile.validates_target_materials:
        return DocsValidationResult(
            diagnostics=diagnostics,
            docs=DocsArtifact(
                target_root=str(repo_root),
                plan=DocsRenderPlan(),
                rendered=(),
                reconciliation=DocsReconciliation(),
            ),
        )

    build = build_documentation(generated.registry)
    _emit_reconciliation_diagnostics(build.reconciliation, diagnostics)

    if diagnostics.has_errors:
        return DocsValidationResult(diagnostics=diagnostics)

    return DocsValidationResult(
        diagnostics=diagnostics,
        docs=DocsArtifact(
            target_root=str(repo_root),
            plan=build.plan,
            rendered=build.rendered,
            reconciliation=build.reconciliation,
        ),
    )


def _generated_from_state(state: PipelineState) -> GeneratedArtifact | None:
    """Narrow the generated state slot to the real GeneratedArtifact."""
    if isinstance(state.generated, GeneratedArtifact):
        return state.generated
    return None


def _emit_reconciliation_diagnostics(
    reconciliation: DocsReconciliation,
    diagnostics: Diagnostics,
) -> None:
    """Emit plan/generated reconciliation diagnostics."""
    for compact_id in reconciliation.generated_missing_page:
        diagnostics.add(
            dx.from_spec(
                dx.GENERATED_PAGE_MISSING,
                compact_id=compact_id,
            )
        )

    for compact_id in reconciliation.page_missing_generated:
        diagnostics.add(
            dx.from_spec(
                dx.PAGE_GENERATED_MISSING,
                compact_id=compact_id,
            )
        )

    for rel_path in reconciliation.duplicate_page_paths:
        diagnostics.add(
            dx.from_spec(
                dx.DOCS_DUPLICATE_PAGE,
                rel_path=rel_path,
            )
        )
