# ============================================================
# src/accountable_record/stage_060_exports/validation.py
# ============================================================
"""Stage 060 export validation for Accountable Record.

WHY:
    Stage 060 runs after catalog. It validates machine-readable export
    artifacts and reconciles element exports against the normalized catalog
    from stage 050. It consumes state.catalog, returns an
    ExportsValidationResult carrying diagnostics plus an ExportsArtifact, and
    emits CATALOG_REQUIRED when the catalog is absent.

OBS (repository profile):
    Implementation repositories do not carry target export material. That case
    is determined from the schema-valid repository profile, not from a manifest
    opt-out section. Target repositories still fail structurally when required
    export material is absent.

OBS (element-scoped reconciliation):
    Reconciliation is 1:1 within data/export/elements/ only. Non-element
    exports under data/export/ are never treated as orphans against the
    element catalog.

OBS (no writes):
    Validates and models; writes nothing.

OBS (no locks):
    Does not read or reconcile data/locks/.
"""

from dataclasses import dataclass, field

from accountable_record.errors import (
    StageError,
    StageInputMalformed,
    StageInputMissing,
    StageInputUnreadable,
)
from accountable_record.pipeline.context import PipelineState
from accountable_record.pipeline.diagnostics import Diagnostic, Diagnostics
from accountable_record.stage_050_catalog.model import CatalogArtifact
from accountable_record.stage_060_exports.base import diagnostics as dx
from accountable_record.stage_060_exports.index import (
    discover_element_exports,
    reconcile_with_catalog,
)
from accountable_record.stage_060_exports.model import (
    ExportReconciliation,
    ExportRegistry,
    ExportsArtifact,
)
from accountable_record.stage_060_exports.plan import resolve_exports_directive

_ERROR_SPEC_BY_TYPE: tuple[tuple[type[StageError], dx.DiagnosticSpec], ...] = (
    (StageInputMissing, dx.EXPORT_ROOT_MISSING),
    (StageInputMalformed, dx.EXPORT_JSON_INVALID),
    (StageInputUnreadable, dx.EXPORT_JSON_INVALID),
)


@dataclass(frozen=True)
class ExportsValidationResult:
    """Validation result for stage 060."""

    diagnostics: Diagnostics = field(default_factory=Diagnostics)
    exports: ExportsArtifact | 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_exports_stage(state: PipelineState) -> ExportsValidationResult:
    """Validate machine-readable exports for the target repository."""
    diagnostics = Diagnostics()

    catalog = _catalog_from_state(state)
    if catalog is None:
        diagnostics.add(dx.from_spec(dx.CATALOG_REQUIRED))
        return ExportsValidationResult(diagnostics=diagnostics)

    catalog_ids = catalog.catalog.compact_ids()
    repo_root = state.context.target_root
    directive = resolve_exports_directive()
    export_root = repo_root / directive.export_path

    if (
        not state.context.repository_profile.validates_target_materials
        and not export_root.exists()
    ):
        artifact = ExportsArtifact(
            target_root=str(repo_root),
            registry=ExportRegistry(),
            reconciliation=ExportReconciliation(),
        )
        return ExportsValidationResult(
            diagnostics=diagnostics,
            exports=artifact,
        )

    try:
        records = discover_element_exports(repo_root, directive.export_path)
        reconciliation = reconcile_with_catalog(records, catalog_ids)
    except StageError as exc:
        diagnostics.add(_diagnostic_for(exc))
        return ExportsValidationResult(diagnostics=diagnostics)

    _emit_reconciliation_diagnostics(reconciliation, diagnostics)

    artifact = ExportsArtifact(
        target_root=str(repo_root),
        registry=ExportRegistry(records=tuple(records)),
        reconciliation=reconciliation,
    )
    return ExportsValidationResult(diagnostics=diagnostics, exports=artifact)


def _catalog_from_state(state: PipelineState) -> CatalogArtifact | None:
    """Return the stage 050 catalog artifact from state, if present."""
    if isinstance(state.catalog, CatalogArtifact):
        return state.catalog
    return None


def _emit_reconciliation_diagnostics(
    reconciliation: ExportReconciliation,
    diagnostics: Diagnostics,
) -> None:
    """Emit element-scoped reconciliation diagnostics."""
    for compact_id in reconciliation.catalog_missing_export:
        diagnostics.add(
            dx.from_spec(
                dx.CATALOG_ENTRY_EXPORT_MISSING,
                compact_id=compact_id,
            )
        )

    for compact_id in reconciliation.uncataloged_exports:
        diagnostics.add(
            dx.from_spec(
                dx.EXPORT_ELEMENT_UNCATALOGED,
                compact_id=compact_id,
            )
        )


def _diagnostic_for(error: StageError) -> Diagnostic:
    """Map a typed StageError to its AR.EX.* diagnostic."""
    for error_type, spec in _ERROR_SPEC_BY_TYPE:
        if isinstance(error, error_type):
            return dx.from_spec_at(
                spec,
                error.location,
                detail=str(error),
            )

    return dx.from_spec_at(
        dx.EXPORT_JSON_INVALID,
        error.location,
        detail=str(error),
    )
