# ============================================================
# src/accountable_record/stage_080_generated/validation.py
# ============================================================
"""Stage 080 generated-artifact validation for Accountable Record.

WHY:
    Stage 080 validates generated element artifacts using the stage 060 export
    registry and, when present, the stage 070 resolution lock. For AR target
    repositories, generated element artifacts are represented by data/export/.

OBS (profile branch):
    Implementation repositories do not carry target generated material. That
    case is determined from the schema-valid repository profile, not from a
    manifest opt-out. Such a repository yields an empty GeneratedArtifact with
    no warning and no error.

OBS (lock branch):
    Reconciliation against the lock requires a lock to exist in state. On the
    verifier path the lock is present because s070 ran and produced it. On the
    write-lock path the lock may be absent; in that case s080 records the
    generated registry from s060 but performs no lock reconciliation.

OBS (scope):
    The lock is scoped to resolved package-included elements. s080 requires
    locked elements to have generated artifacts; it does not require every
    generated/exported element to be locked.
"""

from dataclasses import dataclass, field

from accountable_record.pipeline.context import PipelineState
from accountable_record.pipeline.diagnostics import Diagnostics
from accountable_record.stage_060_exports.model import ExportsArtifact
from accountable_record.stage_070_resolution.model import (
    ElementsLock,
    ResolutionArtifact,
)
from accountable_record.stage_080_generated.base import diagnostics as dx
from accountable_record.stage_080_generated.compare import reconcile_generated_with_lock
from accountable_record.stage_080_generated.model import (
    GeneratedArtifact,
    GeneratedElementRecord,
    GeneratedReconciliation,
    GeneratedRegistry,
)

__all__ = ["GeneratedValidationResult", "validate_generated_stage"]


@dataclass(frozen=True)
class GeneratedValidationResult:
    """Validation result for stage 080."""

    diagnostics: Diagnostics = field(default_factory=Diagnostics)
    generated: GeneratedArtifact | 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_generated_stage(state: PipelineState) -> GeneratedValidationResult:
    """Validate generated artifacts for the target repository.

    Args:
        state: Accumulated pipeline state. Stage 060 exports must be present.
            Stage 070 resolution is present on the verifier path and absent on
            the write-lock path.

    Returns:
        Generated-artifact validation result.
    """
    diagnostics = Diagnostics()

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

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

    if not profile.validates_target_materials:
        return GeneratedValidationResult(
            diagnostics=diagnostics,
            generated=GeneratedArtifact(
                target_root=str(repo_root),
                registry=GeneratedRegistry(),
                reconciliation=GeneratedReconciliation(),
            ),
        )

    registry = _generated_registry_from_exports(exports)
    _emit_duplicate_diagnostics(registry, diagnostics)

    lock = _lock_from_state(state)
    if lock is not None:
        reconciliation = reconcile_generated_with_lock(registry, lock)
        _emit_reconciliation_diagnostics(reconciliation, diagnostics)
    else:
        reconciliation = GeneratedReconciliation()

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

    return GeneratedValidationResult(
        diagnostics=diagnostics,
        generated=GeneratedArtifact(
            target_root=str(repo_root),
            registry=registry,
            reconciliation=reconciliation,
        ),
    )


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 _generated_registry_from_exports(exports: ExportsArtifact) -> GeneratedRegistry:
    """Build the generated-artifact registry from the stage 060 export registry."""
    return GeneratedRegistry(
        records=tuple(
            GeneratedElementRecord(
                category=record.category,
                name=record.name,
                rel_dir=record.rel_dir,
                compact_id=record.compact_id,
            )
            for record in exports.registry.records
        )
    )


def _lock_from_state(state: PipelineState) -> ElementsLock | None:
    """Return the elements lock from resolution state, if present."""
    if isinstance(state.resolution, ResolutionArtifact):
        return state.resolution.lock
    return None


def _emit_duplicate_diagnostics(
    registry: GeneratedRegistry,
    diagnostics: Diagnostics,
) -> None:
    """Emit a diagnostic for each generated element id appearing more than once."""
    seen: set[str] = set()
    duplicates: set[str] = set()

    for record in registry.records:
        if record.compact_id in seen:
            duplicates.add(record.compact_id)
        seen.add(record.compact_id)

    for compact_id in sorted(duplicates):
        diagnostics.add(
            dx.from_spec(
                dx.GENERATED_DUPLICATE_ID,
                compact_id=compact_id,
            )
        )


def _emit_reconciliation_diagnostics(
    reconciliation: GeneratedReconciliation,
    diagnostics: Diagnostics,
) -> None:
    """Emit lock/generated reconciliation diagnostics."""
    for compact_id in reconciliation.lock_missing_generated:
        diagnostics.add(
            dx.from_spec(
                dx.LOCK_GENERATED_MISSING,
                compact_id=compact_id,
            )
        )

    for compact_id in reconciliation.digest_mismatches:
        diagnostics.add(
            dx.from_spec(
                dx.GENERATED_DIGEST_MISMATCH,
                compact_id=compact_id,
            )
        )
