# ============================================================
# src/accountable_record/stage_050_catalog/base/diagnostics.py
# ============================================================
"""Stage 050 catalog diagnostic definitions.

WHY:
    Diagnostic codes, severities, and messages belong together. Keeping each
    diagnostic as one definition prevents code constants and message builders
    from drifting independently.

OBS:
    This module defines the AR.CA.* diagnostics stage 050 may emit. It does
    not build catalogs, reconcile declared catalogs, mutate pipeline state, or
    decide when diagnostics are emitted.

REQ:
    Public diagnostic names in this module are DiagnosticSpec objects. The
    underlying DiagnosticCode values are private implementation details held
    by each spec.
"""

from collections.abc import Callable
from dataclasses import dataclass

from accountable_record.pipeline.diagnostics import (
    Concern,
    Diagnostic,
    DiagnosticCode,
    Severity,
    ar_code,
)
from accountable_record.stage_050_catalog.base.constants import (
    DECLARED_CATALOG_PATH,
)

__all__ = [
    "CATALOG_BUILD_FAILED",
    "CATALOG_ENTRY_ORPHANED",
    "CATALOG_ENTRY_UNDECLARED",
    "DECLARED_CATALOG_INVALID",
    "DECLARED_CATALOG_MISSING",
    "DiagnosticSpec",
    "ELEMENTS_REQUIRED",
    "from_spec",
]


@dataclass(frozen=True)
class DiagnosticSpec:
    """Definition of one diagnostic emitted by stage 050."""

    code: DiagnosticCode
    severity: Severity
    message: Callable[..., str]
    description: str


def from_spec(
    spec: DiagnosticSpec,
    *args: object,
    location: str | None = None,
    **kwargs: object,
) -> Diagnostic:
    """Build a Diagnostic from a stage diagnostic spec."""
    return Diagnostic(
        code=spec.code,
        severity=spec.severity,
        message=spec.message(*args, **kwargs),
        location=location,
    )


def _elements_required_message() -> str:
    """Return a message for a missing upstream elements artifact."""
    return "s050_catalog requires s040_elements to complete first."


def _catalog_build_failed_message(*, detail: str) -> str:
    """Return a message for a catalog build failure."""
    return f"Could not build catalog: {detail}"


def _declared_catalog_missing_message() -> str:
    """Return a message for an absent declared catalog."""
    return (
        f"{DECLARED_CATALOG_PATH} not found; "
        "skipping discovered-vs-declared catalog reconciliation"
    )


def _declared_catalog_invalid_message(*, detail: str) -> str:
    """Return a message for a malformed declared catalog."""
    return f"{DECLARED_CATALOG_PATH} is invalid: {detail}"


def _catalog_entry_undeclared_message(*, compact_id: str) -> str:
    """Return a message for a discovered element absent from the declared catalog."""
    return (
        f"Element {compact_id!r} is validated but not present in "
        f"{DECLARED_CATALOG_PATH}"
    )


def _catalog_entry_orphaned_message(*, compact_id: str) -> str:
    """Return a message for a declared entry with no discovered element."""
    return (
        f"{DECLARED_CATALOG_PATH} declares {compact_id!r} but no validated "
        "element was discovered for it"
    )


# --- AR.CA.* : base catalog diagnostics -------------------------------------

_ELEMENTS_REQUIRED_CODE: DiagnosticCode = ar_code(
    Concern.CATALOG,
    "ELEMENTS_REQUIRED",
)
_CATALOG_BUILD_FAILED_CODE: DiagnosticCode = ar_code(
    Concern.CATALOG,
    "CATALOG_BUILD_FAILED",
)
_DECLARED_CATALOG_MISSING_CODE: DiagnosticCode = ar_code(
    Concern.CATALOG,
    "DECLARED_CATALOG_MISSING",
)
_DECLARED_CATALOG_INVALID_CODE: DiagnosticCode = ar_code(
    Concern.CATALOG,
    "DECLARED_CATALOG_INVALID",
)
_CATALOG_ENTRY_UNDECLARED_CODE: DiagnosticCode = ar_code(
    Concern.CATALOG,
    "CATALOG_ENTRY_UNDECLARED",
)
_CATALOG_ENTRY_ORPHANED_CODE: DiagnosticCode = ar_code(
    Concern.CATALOG,
    "CATALOG_ENTRY_ORPHANED",
)


ELEMENTS_REQUIRED = DiagnosticSpec(
    code=_ELEMENTS_REQUIRED_CODE,
    severity=Severity.ERROR,
    message=_elements_required_message,
    description="Stage 050 requires the stage 040 elements artifact to be present.",
)

CATALOG_BUILD_FAILED = DiagnosticSpec(
    code=_CATALOG_BUILD_FAILED_CODE,
    severity=Severity.ERROR,
    message=_catalog_build_failed_message,
    description="A catalog entry could not be built from validated element data.",
)

DECLARED_CATALOG_MISSING = DiagnosticSpec(
    code=_DECLARED_CATALOG_MISSING_CODE,
    severity=Severity.WARNING,
    message=_declared_catalog_missing_message,
    description="The declared catalog is absent and reconciliation is skipped.",
)

DECLARED_CATALOG_INVALID = DiagnosticSpec(
    code=_DECLARED_CATALOG_INVALID_CODE,
    severity=Severity.ERROR,
    message=_declared_catalog_invalid_message,
    description="The declared catalog is present but malformed or unparseable.",
)

CATALOG_ENTRY_UNDECLARED = DiagnosticSpec(
    code=_CATALOG_ENTRY_UNDECLARED_CODE,
    severity=Severity.ERROR,
    message=_catalog_entry_undeclared_message,
    description="A validated element is not present in the declared catalog.",
)

CATALOG_ENTRY_ORPHANED = DiagnosticSpec(
    code=_CATALOG_ENTRY_ORPHANED_CODE,
    severity=Severity.ERROR,
    message=_catalog_entry_orphaned_message,
    description="A declared catalog entry has no corresponding validated element.",
)
