# ============================================================
# src/accountable_record/stage_040_elements/validation.py
# ============================================================
"""Stage 040 element-quartet validation for Accountable Record.

WHY:
    Stage 040 validates authored element source quartets when target material
    is expected. It discovers element directories under data/elements/,
    validates each quartet's presence, parseability, minimal shape, and
    structural cross-links, registers them by compact_id, and folds the result
    into pipeline state.

OBS (repository profile):
    Implementation repositories do not carry target element 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
    data/elements/ material is absent.

OBS (scope):
    Source-side data/elements/ only. No data/export/elements/, no lock
    reconciliation, and no generated artifact validation.

OBS (typed errors):
    The ops (discover_elements, build_registry) raise typed StageErrors. This
    layer maps each error type to an AR.EL.* diagnostic spec.
"""

from dataclasses import dataclass, field
from pathlib import Path

from accountable_record.errors import (
    StageError,
    StageInputDuplicate,
    StageInputMalformed,
    StageInputMissing,
    StageInputUnreadable,
)
from accountable_record.pipeline.context import PipelineState
from accountable_record.pipeline.diagnostics import (
    Diagnostic,
    Diagnostics,
    DiagnosticSpec,
)
from accountable_record.stage_030_sources.model import SourcesArtifact
from accountable_record.stage_040_elements.base import constants
from accountable_record.stage_040_elements.base import diagnostics as dx
from accountable_record.stage_040_elements.discover import discover_elements
from accountable_record.stage_040_elements.load import resolve_elements_directive
from accountable_record.stage_040_elements.model import (
    ElementRegistry,
    ElementsArtifact,
)
from accountable_record.stage_040_elements.registry import build_registry

_ERROR_SPEC_BY_TYPE: tuple[tuple[type[StageError], DiagnosticSpec], ...] = (
    (StageInputMissing, dx.ELEMENTS_DIR_MISSING),
    (StageInputMalformed, dx.ELEMENT_TOML_INVALID),
    (StageInputUnreadable, dx.ELEMENT_TOML_INVALID),
    (StageInputDuplicate, dx.ELEMENT_IDENTITY_MISMATCH),
)


@dataclass(frozen=True)
class ElementsValidationResult:
    """Validation result for stage 040."""

    diagnostics: Diagnostics = field(default_factory=Diagnostics)
    elements: ElementsArtifact | 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_elements_stage(state: PipelineState) -> ElementsValidationResult:
    """Validate authored element source quartets for the target repository."""
    diagnostics = Diagnostics()

    sources = _sources_from_state(state)
    if sources is None:
        diagnostics.add(dx.from_spec(dx.SOURCES_REQUIRED))
        return ElementsValidationResult(diagnostics=diagnostics)

    repo_root: Path = state.context.target_root
    elements_root: Path = repo_root / constants.ELEMENTS_DIRNAME

    if (
        not state.context.repository_profile.validates_target_materials
        and not elements_root.exists()
    ):
        elements = ElementsArtifact(
            target_root=str(repo_root),
            registry=ElementRegistry(),
        )
        return ElementsValidationResult(
            diagnostics=diagnostics,
            elements=elements,
        )

    directive = resolve_elements_directive(repo_root)

    try:
        records = discover_elements(
            repo_root,
            elements_dirname=directive.declarations_path,
        )
        registry = build_registry(list(records))
    except StageError as exc:
        diagnostics.add(_diagnostic_for(exc))
        return ElementsValidationResult(diagnostics=diagnostics)

    elements = ElementsArtifact(
        target_root=str(repo_root),
        registry=registry,
    )
    return ElementsValidationResult(diagnostics=diagnostics, elements=elements)


def empty_registry() -> ElementRegistry:
    """Return an empty element registry."""
    return ElementRegistry()


def _sources_from_state(state: PipelineState) -> SourcesArtifact | None:
    """Return the stage 030 sources artifact from state, if present."""
    if isinstance(state.sources, SourcesArtifact):
        return state.sources
    return None


def _diagnostic_for(error: StageError) -> Diagnostic:
    """Map a typed StageError to its AR.EL.* diagnostic."""
    for error_type, spec in _ERROR_SPEC_BY_TYPE:
        if isinstance(error, error_type):
            return _diagnostic_from_stage_error(spec, error)

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


def _diagnostic_from_stage_error(
    spec: DiagnosticSpec,
    error: StageError,
) -> Diagnostic:
    """Build a diagnostic for a typed element-stage error."""
    if spec is dx.ELEMENTS_DIR_MISSING:
        return dx.from_spec_at(
            spec,
            error.location,
        )

    if spec is dx.ELEMENTS_DIR_EMPTY:
        return dx.from_spec_at(
            spec,
            error.location,
        )

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