# ============================================================
# src/accountable_record/stage_070_resolution/validation.py
# ============================================================
"""Stage 070 resolution validation for Accountable Record.

WHY:
    Stage 070 validates target repository resolution locks and derives the
    lock-based package/element inclusion graph. It checks lock structure,
    graph consistency, and coverage from locked elements to generated export
    artifacts.

OBS:
    This stage does not recompute canonical JSON digests. It validates lock
    structure and locked-element export coverage only.

OBS:
    Exports are required because locked element records refer to canonical
    generated JSON digest targets. s070 does not require every exported element
    to be package-included or locked. Full generated-artifact validation belongs
    to s080.
"""

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_060_exports.model import ExportsArtifact
from accountable_record.stage_070_resolution.base import constants
from accountable_record.stage_070_resolution.base import diagnostics as dx
from accountable_record.stage_070_resolution.graph import build_resolution_graph
from accountable_record.stage_070_resolution.lock import load_elements_lock
from accountable_record.stage_070_resolution.model import (
    LockExportReconciliation,
    ResolutionArtifact,
    ResolutionGraph,
)
from accountable_record.stage_070_resolution.resolve import (
    resolve_resolution_directive,
)
from accountable_record.stage_070_resolution.verify import (
    reconcile_lock_with_exports,
)

__all__ = ["ResolutionValidationResult", "validate_resolution_stage"]

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


@dataclass(frozen=True)
class ResolutionValidationResult:
    """Validation result for stage 070."""

    diagnostics: Diagnostics = field(default_factory=Diagnostics)
    resolution: ResolutionArtifact | 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_resolution_stage(state: PipelineState) -> ResolutionValidationResult:
    """Validate resolution and lock state for the target repository.

    Args:
        state: Accumulated pipeline state. Stage 060 exports must be present.

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

    exports = _exports_from_state(state)
    if exports is None:
        diagnostics.add(dx.from_spec(dx.EXPORTS_REQUIRED))
        return ResolutionValidationResult(diagnostics=diagnostics)

    repo_root = state.context.target_root
    directive = resolve_resolution_directive(repo_root)

    try:
        lock = load_elements_lock(
            repo_root,
            f"{directive.locks_path}/{constants.ELEMENTS_LOCK_FILENAME}",
        )
    except StageError as exc:
        diagnostics.add(_diagnostic_for(exc))
        return ResolutionValidationResult(diagnostics=diagnostics)

    graph = build_resolution_graph(lock)
    reconciliation = reconcile_lock_with_exports(lock, exports)

    _emit_graph_diagnostics(graph, diagnostics)
    _emit_reconciliation_diagnostics(reconciliation, diagnostics)

    artifact = ResolutionArtifact(
        target_root=str(repo_root),
        lock=lock,
        graph=graph,
        reconciliation=reconciliation,
    )
    return ResolutionValidationResult(
        diagnostics=diagnostics,
        resolution=artifact,
    )


def _exports_from_state(state: PipelineState) -> ExportsArtifact | None:
    """Return the stage 060 exports artifact from state, if present."""
    if isinstance(state.exports, ExportsArtifact):
        return state.exports
    return None


def _emit_graph_diagnostics(graph: ResolutionGraph, diagnostics: Diagnostics) -> None:
    """Emit graph-level structural diagnostics."""
    for compact_id in graph.duplicate_package_ids:
        diagnostics.add(
            dx.from_spec(
                dx.LOCK_DUPLICATE_ID,
                section="packages",
                compact_id=compact_id,
            )
        )

    for compact_id in graph.duplicate_element_ids:
        diagnostics.add(
            dx.from_spec(
                dx.LOCK_DUPLICATE_ID,
                section="elements",
                compact_id=compact_id,
            )
        )

    for package_ref in graph.unknown_package_refs:
        diagnostics.add(
            dx.from_spec(
                dx.LOCK_PACKAGE_INVALID,
                compact_id=package_ref,
                detail=(
                    f"element lock records reference undeclared package {package_ref!r}"
                ),
            )
        )


def _emit_reconciliation_diagnostics(
    reconciliation: LockExportReconciliation,
    diagnostics: Diagnostics,
) -> None:
    """Emit locked-element export-coverage diagnostics."""
    for compact_id in reconciliation.lock_missing_export:
        diagnostics.add(
            dx.from_spec(
                dx.LOCK_EXPORT_MISSING,
                compact_id=compact_id,
            )
        )


def _diagnostic_for(error: StageError) -> Diagnostic:
    """Map a typed StageError to its AR.RS.* diagnostic."""
    for error_type, spec in _ERROR_SPEC_BY_TYPE:
        if isinstance(error, error_type):
            if spec is dx.LOCK_ROOT_MISSING:
                return dx.from_spec_at(
                    spec,
                    error.location,
                    lock_path=error.location or constants.LOCKS_DIRNAME,
                )

            return dx.from_spec_at(
                spec,
                error.location,
                file_path=error.location or constants.ELEMENTS_LOCK_PATH,
                detail=str(error),
            )

    return dx.from_spec_at(
        dx.LOCK_JSON_INVALID,
        error.location,
        file_path=error.location or constants.ELEMENTS_LOCK_PATH,
        detail=str(error),
    )
