# ============================================================
# src/accountable_record/stage_020_contract/base/diagnostics.py
# ============================================================
"""Stage 020 contract 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.CT.* diagnostics stage 020 may emit. It does
    not read manifests, validate contract declarations, 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 accountable_record.pipeline.diagnostics import (
    Concern,
    DiagnosticCode,
    DiagnosticSpec,
    Severity,
    ar_code,
    from_spec,
)
from accountable_record.stage_020_contract.base.constants import (
    CONTRACT_TABLE,
    EXPECTED_CONTRACT_NAME,
    MANIFEST_FILENAME,
    SE_SPEC_NAME,
)

__all__ = [
    "CONTRACT_INVALID",
    "CONTRACT_MISSING",
    "CONTRACT_NAME_MISSING",
    "CONTRACT_VERSION_MISMATCH",
    "CONTRACT_VERSION_MISSING",
    "DiagnosticSpec",
    "SE_CONFORMANCE_UNDECLARED",
    "UNEXPECTED_CONTRACT_NAME",
    "from_spec",
]


def _contract_missing_message() -> str:
    """Return a message for an absent contract declaration."""
    return (
        f"{MANIFEST_FILENAME} declares no [{CONTRACT_TABLE}] table; "
        f"the target does not declare the contract it targets"
    )


def _contract_invalid_message(*, detail: str) -> str:
    """Return a message for a malformed contract declaration."""
    return f"{MANIFEST_FILENAME} [{CONTRACT_TABLE}] is malformed: {detail}"


def _contract_name_missing_message() -> str:
    """Return a message for a contract declaration missing its name."""
    return f"{MANIFEST_FILENAME} [{CONTRACT_TABLE}] declares no contract name"


def _contract_version_missing_message(*, contract_name: str) -> str:
    """Return a message for a named contract that declares no version."""
    return (
        f"{MANIFEST_FILENAME} [{CONTRACT_TABLE}] names contract "
        f"{contract_name!r} but declares no version"
    )


def _contract_version_mismatch_message(
    *,
    contract_version: str,
    identity_version: str,
) -> str:
    """Return a message for contract/identity version disagreement."""
    return (
        f"{MANIFEST_FILENAME} [{CONTRACT_TABLE}] version "
        f"{contract_version!r} disagrees with the established identity "
        f"version {identity_version!r}; CITATION.cff is authoritative"
    )


def _unexpected_contract_name_message(*, contract_name: str) -> str:
    """Return a message for an unexpected contract name."""
    return (
        f"Expected {MANIFEST_FILENAME} [{CONTRACT_TABLE}] to target "
        f"{EXPECTED_CONTRACT_NAME!r}, got {contract_name!r}"
    )


def _se_conformance_undeclared_message(*, contract_name: str) -> str:
    """Return a message for a contract that omits SE conformance."""
    return (
        f"Contract {contract_name!r} does not declare conformance with the "
        f"{SE_SPEC_NAME!r} specification (AR.CONFORMANCE.SE.REQUIRED)"
    )


# --- AR.CT.* : base contract diagnostics ------------------------------------

_CONTRACT_MISSING_CODE: DiagnosticCode = ar_code(
    Concern.CONTRACT,
    "CONTRACT_MISSING",
)
_CONTRACT_INVALID_CODE: DiagnosticCode = ar_code(
    Concern.CONTRACT,
    "CONTRACT_INVALID",
)
_CONTRACT_NAME_MISSING_CODE: DiagnosticCode = ar_code(
    Concern.CONTRACT,
    "CONTRACT_NAME_MISSING",
)
_CONTRACT_VERSION_MISSING_CODE: DiagnosticCode = ar_code(
    Concern.CONTRACT,
    "CONTRACT_VERSION_MISSING",
)
_CONTRACT_VERSION_MISMATCH_CODE: DiagnosticCode = ar_code(
    Concern.CONTRACT,
    "CONTRACT_VERSION_MISMATCH",
)
_UNEXPECTED_CONTRACT_NAME_CODE: DiagnosticCode = ar_code(
    Concern.CONTRACT,
    "UNEXPECTED_CONTRACT_NAME",
)
_SE_CONFORMANCE_UNDECLARED_CODE: DiagnosticCode = ar_code(
    Concern.CONTRACT,
    "SE_CONFORMANCE_UNDECLARED",
)


CONTRACT_MISSING = DiagnosticSpec(
    code=_CONTRACT_MISSING_CODE,
    severity=Severity.ERROR,
    message=_contract_missing_message,
    description="No contract declaration was found in the target manifest.",
)

CONTRACT_INVALID = DiagnosticSpec(
    code=_CONTRACT_INVALID_CODE,
    severity=Severity.ERROR,
    message=_contract_invalid_message,
    description="A contract declaration is present but malformed or unparseable.",
)

CONTRACT_NAME_MISSING = DiagnosticSpec(
    code=_CONTRACT_NAME_MISSING_CODE,
    severity=Severity.ERROR,
    message=_contract_name_missing_message,
    description="The contract declaration does not name the contract it targets.",
)

CONTRACT_VERSION_MISSING = DiagnosticSpec(
    code=_CONTRACT_VERSION_MISSING_CODE,
    severity=Severity.ERROR,
    message=_contract_version_missing_message,
    description="The contract declaration names a contract but declares no version.",
)

CONTRACT_VERSION_MISMATCH = DiagnosticSpec(
    code=_CONTRACT_VERSION_MISMATCH_CODE,
    severity=Severity.ERROR,
    message=_contract_version_mismatch_message,
    description=(
        "The declared contract version disagrees with the established identity version."
    ),
)

UNEXPECTED_CONTRACT_NAME = DiagnosticSpec(
    code=_UNEXPECTED_CONTRACT_NAME_CODE,
    severity=Severity.ERROR,
    message=_unexpected_contract_name_message,
    description="The target declares a contract name other than the expected AR contract.",
)

SE_CONFORMANCE_UNDECLARED = DiagnosticSpec(
    code=_SE_CONFORMANCE_UNDECLARED_CODE,
    severity=Severity.ERROR,
    message=_se_conformance_undeclared_message,
    description=(
        "The target contract declaration does not reference the Structural "
        "Explainability specification."
    ),
)
