# ============================================================
# src/accountable_record/stage_030_sources/base/diagnostics.py
# ============================================================
"""Stage 030 source-material 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.SR.* diagnostics stage 030 may emit. It does
    not load sources, inspect paths, build registries, mutate pipeline state,
    or decide when diagnostics are emitted.

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

from accountable_record.pipeline.diagnostics import (
    Concern,
    DiagnosticSpec,
    Severity,
    ar_code,
    from_spec,
    from_spec_at,
)
from accountable_record.stage_030_sources.base.constants import (
    DEFAULT_SOURCES_DIRNAME,
    SOURCES_TABLE,
)

__all__ = [
    "IDENTITY_REQUIRED",
    "SOURCE_DECLARATION_INVALID",
    "SOURCE_ID_DUPLICATE",
    "SOURCE_MISSING",
    "SOURCE_OUT_OF_ROOT",
    "SOURCE_UNREADABLE",
    "SOURCES_TABLE_MISSING",
    "from_spec",
    "from_spec_at",
]


def _identity_required_message() -> str:
    """Return a message for a missing upstream identity artifact."""
    return "s030_sources requires s010_identity to complete first."


def _sources_table_missing_message(*, manifest_filename: str) -> str:
    """Return a message for no declared sources and no conventional directory."""
    return (
        f"{manifest_filename} declares no [{SOURCES_TABLE}] table and no "
        f"{DEFAULT_SOURCES_DIRNAME!r} directory is present; "
        "no source materials to record"
    )


def _source_missing_message(*, source_id: str, path: str) -> str:
    """Return a message for a declared source path that does not exist."""
    return f"Source {source_id!r} declares path {path!r}, which does not exist"


def _source_unreadable_message(
    *,
    source_id: str,
    path: str,
    detail: str,
) -> str:
    """Return a message for a present source whose presence could not be read."""
    return (
        f"Source {source_id!r} path {path!r} exists but could not be read "
        f"(structural check only): {detail}"
    )


def _source_declaration_invalid_message(*, detail: str) -> str:
    """Return a message for a structurally malformed source declaration."""
    return f"Source declaration is malformed: {detail}"


def _source_out_of_root_message(*, source_id: str, path: str) -> str:
    """Return a message for a source path that escapes the target root."""
    return (
        f"Source {source_id!r} path {path!r} resolves outside the target "
        "repository root"
    )


def _source_id_duplicate_message(*, source_id: str) -> str:
    """Return a message for two source declarations sharing one identifier."""
    return (
        f"Source identifier {source_id!r} is declared more than once; "
        "identifiers must be unique"
    )


IDENTITY_REQUIRED = DiagnosticSpec(
    code=ar_code(Concern.SOURCES, "IDENTITY_REQUIRED"),
    severity=Severity.ERROR,
    message=_identity_required_message,
    description="Stage 030 requires the stage 010 identity artifact to be present.",
)

SOURCES_TABLE_MISSING = DiagnosticSpec(
    code=ar_code(Concern.SOURCES, "SOURCES_TABLE_MISSING"),
    severity=Severity.ERROR,
    message=_sources_table_missing_message,
    description=(
        "The target declares no sources table and has no conventional sources "
        "directory."
    ),
)

SOURCE_MISSING = DiagnosticSpec(
    code=ar_code(Concern.SOURCES, "SOURCE_MISSING"),
    severity=Severity.ERROR,
    message=_source_missing_message,
    description="A declared source path does not exist in the target repository.",
)

SOURCE_UNREADABLE = DiagnosticSpec(
    code=ar_code(Concern.SOURCES, "SOURCE_UNREADABLE"),
    severity=Severity.ERROR,
    message=_source_unreadable_message,
    description=(
        "A declared source path exists but its presence could not be established."
    ),
)

SOURCE_DECLARATION_INVALID = DiagnosticSpec(
    code=ar_code(Concern.SOURCES, "SOURCE_DECLARATION_INVALID"),
    severity=Severity.ERROR,
    message=_source_declaration_invalid_message,
    description="A source declaration is structurally malformed.",
)

SOURCE_OUT_OF_ROOT = DiagnosticSpec(
    code=ar_code(Concern.SOURCES, "SOURCE_OUT_OF_ROOT"),
    severity=Severity.ERROR,
    message=_source_out_of_root_message,
    description="A declared or discovered source path resolves outside the target root.",
)

SOURCE_ID_DUPLICATE = DiagnosticSpec(
    code=ar_code(Concern.SOURCES, "SOURCE_ID_DUPLICATE"),
    severity=Severity.ERROR,
    message=_source_id_duplicate_message,
    description="Two source declarations share one stable identifier.",
)
