# ============================================================
# src/accountable_record/stage_040_elements/base/diagnostics.py
# ============================================================
"""Stage 040 element-quartet 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.EL.* diagnostics stage 040 may emit. It does
    not discover elements, parse quartet files, 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_040_elements.base.constants import ELEMENTS_DIRNAME

__all__ = [
    "CHECKS_TOML_INVALID",
    "ELEMENTS_DECLARED_NONE_BUT_PRESENT",
    "ELEMENTS_DIR_EMPTY",
    "ELEMENTS_DIR_MISSING",
    "ELEMENT_IDENTITY_MISMATCH",
    "ELEMENT_TOML_INVALID",
    "EXAMPLE_JSON_INVALID",
    "QUARTET_MEMBER_MISSING",
    "SCHEMA_JSON_INVALID",
    "SOURCES_REQUIRED",
    "from_spec",
    "from_spec_at",
]


def _sources_required_message() -> str:
    """Return a message for a missing upstream sources artifact."""
    return "s040_elements requires s030_sources to complete first."


def _elements_dir_missing_message() -> str:
    """Return a message for an absent data/elements/ directory."""
    return (
        f"{ELEMENTS_DIRNAME}/ does not exist; "
        "no authored element source quartets to validate"
    )


def _elements_dir_empty_message() -> str:
    """Return a message for a present but element-free data/elements/."""
    return (
        f"{ELEMENTS_DIRNAME}/ exists but no element directories "
        "(quartets) were discovered beneath it"
    )


def _quartet_member_missing_message(*, element_label: str, member_path: str) -> str:
    """Return a message for a missing quartet member."""
    return (
        f"Element {element_label!r} is missing required quartet member {member_path!r}"
    )


def _parse_failure_message(*, file_path: str, detail: str) -> str:
    """Return a message for a file that failed to parse."""
    return f"{file_path} could not be parsed: {detail}"


def _identity_mismatch_message(*, detail: str) -> str:
    """Return a message for a failed structural cross-link."""
    return detail


def _elements_declared_none_but_present_message() -> str:
    """Return a message for contradictory elements opt-out/material presence."""
    return (
        '[elements].declarations = "none" but data/elements/ exists; '
        "the declaration and filesystem disagree"
    )


def _stage_error_message(*, detail: str) -> str:
    """Return a generic structural element-stage error message."""
    return detail


SOURCES_REQUIRED = DiagnosticSpec(
    code=ar_code(Concern.ELEMENTS, "SOURCES_REQUIRED"),
    severity=Severity.ERROR,
    message=_sources_required_message,
    description="Stage 040 requires the stage 030 sources artifact to be present.",
)

ELEMENTS_DIR_MISSING = DiagnosticSpec(
    code=ar_code(Concern.ELEMENTS, "ELEMENTS_DIR_MISSING"),
    severity=Severity.ERROR,
    message=_elements_dir_missing_message,
    description="The target has no data/elements directory.",
)

ELEMENTS_DIR_EMPTY = DiagnosticSpec(
    code=ar_code(Concern.ELEMENTS, "ELEMENTS_DIR_EMPTY"),
    severity=Severity.ERROR,
    message=_elements_dir_empty_message,
    description="data/elements exists but no element quartets were discovered.",
)

QUARTET_MEMBER_MISSING = DiagnosticSpec(
    code=ar_code(Concern.ELEMENTS, "QUARTET_MEMBER_MISSING"),
    severity=Severity.ERROR,
    message=_quartet_member_missing_message,
    description="An element directory is missing a required quartet member.",
)

ELEMENT_TOML_INVALID = DiagnosticSpec(
    code=ar_code(Concern.ELEMENTS, "ELEMENT_TOML_INVALID"),
    severity=Severity.ERROR,
    message=_stage_error_message,
    description="element.toml could not be parsed or lacks minimal observed shape.",
)

CHECKS_TOML_INVALID = DiagnosticSpec(
    code=ar_code(Concern.ELEMENTS, "CHECKS_TOML_INVALID"),
    severity=Severity.ERROR,
    message=_parse_failure_message,
    description="checks.toml could not be parsed or lacks minimal observed shape.",
)

SCHEMA_JSON_INVALID = DiagnosticSpec(
    code=ar_code(Concern.ELEMENTS, "SCHEMA_JSON_INVALID"),
    severity=Severity.ERROR,
    message=_parse_failure_message,
    description="schema.schema.json could not be parsed or lacks minimal observed shape.",
)

EXAMPLE_JSON_INVALID = DiagnosticSpec(
    code=ar_code(Concern.ELEMENTS, "EXAMPLE_JSON_INVALID"),
    severity=Severity.ERROR,
    message=_parse_failure_message,
    description="examples/valid.json could not be parsed or is not a JSON object.",
)

ELEMENT_IDENTITY_MISMATCH = DiagnosticSpec(
    code=ar_code(Concern.ELEMENTS, "ELEMENT_IDENTITY_MISMATCH"),
    severity=Severity.ERROR,
    message=_identity_mismatch_message,
    description="A structural element identity cross-link did not hold.",
)

ELEMENTS_DECLARED_NONE_BUT_PRESENT = DiagnosticSpec(
    code=ar_code(Concern.ELEMENTS, "ELEMENTS_DECLARED_NONE_BUT_PRESENT"),
    severity=Severity.ERROR,
    message=_elements_declared_none_but_present_message,
    description=(
        '[elements].declarations = "none" but data/elements material is present.'
    ),
)
