# ============================================================
# src/accountable_record/stage_010_identity/base/constants.py
# ============================================================
"""Identity-stage structural constants.

WHY:
    Stage 010 validates repository identity metadata. This module contains only
    structural constants and schema-driven filename policy used by identity
    validation.

    Diagnostic definitions live in base/diagnostics.py so diagnostic codes,
    severities, and messages stay bound together.

OBS:
    The set of allowed manifest filenames is policy owned by se-manifest-schema,
    not by AR-py. AR-py is one consumer; JR, CR, and the -rs port are others.
    Hardcoding the list here would let consumers drift. So the allowed
    filenames are read at runtime from the schema shipped inside the installed
    se-manifest-schema wheel, under its [manifest] table.

OBS:
    The schema read is lazy, never at import time. Importing this module must
    not do I/O or risk an exception, because downstream modules import it. If
    the schema cannot be read, a built-in fallback is used so identity
    validation degrades to a sane default rather than crashing.
"""

from importlib import resources
import tomllib

from accountable_record.boundary.toml import is_list, is_table

STAGE_NAME = "stage_010_identity"

PYPROJECT_FILENAME = "pyproject.toml"
CITATION_FILENAME = "CITATION.cff"

# WHY: canonical manifest name. The full allowed set comes from the schema
#      via allowed_manifest_filenames(); this is only the preferred/default
#      name and the fallback anchor.
MANIFEST_FILENAME = "SE_MANIFEST.toml"

# WHY: canonical version source fixed by AR policy; one literal everyone shares.
CANONICAL_VERSION_FILE = CITATION_FILENAME

# Built-in fallback used only when the schema resource cannot be read.
_FALLBACK_ALLOWED_MANIFEST_FILENAMES: tuple[str, ...] = (
    "SE_MANIFEST.toml",
    "MANIFEST.toml",
)

# Location of the schema inside the installed se-manifest-schema package.
_SCHEMA_PACKAGE = "se_manifest_schema"
_SCHEMA_RESOURCE = "manifest-schema.toml"

EXPECTED_PROJECT_NAME = "accountable-record"
EXPECTED_PACKAGE_NAME = "accountable_record"
EXPECTED_ORGANIZATION = "structural-explainability"


def _read_schema_manifest_table() -> dict[str, object] | None:
    """Read the [manifest] table from the installed schema, or None on failure.

    OBS:
        Any failure, e.g., package not installed, resource missing, TOML invalid,
        no [manifest] table, returns None so the caller can fall back. This
        function never raises.
    """
    try:
        resource = resources.files(_SCHEMA_PACKAGE).joinpath(_SCHEMA_RESOURCE)
        data = tomllib.loads(resource.read_text(encoding="utf-8"))
    except (
        AttributeError,
        FileNotFoundError,
        ModuleNotFoundError,
        OSError,
        ValueError,
        tomllib.TOMLDecodeError,
    ):
        return None

    manifest = data.get("manifest")
    if not is_table(manifest):
        return None

    return manifest


def allowed_manifest_filenames() -> tuple[str, ...]:
    """Return the allowed manifest filenames, schema-driven with fallback.

    Reads [manifest].allowed_filenames from se-manifest-schema. Falls back to
    the built-in set if the schema cannot be read or the field is missing or
    not a list of strings.
    """
    manifest = _read_schema_manifest_table()
    if manifest is not None:
        allowed = manifest.get("allowed_filenames")
        if is_list(allowed):
            names = tuple(
                name.strip()
                for name in allowed
                if isinstance(name, str) and name.strip()
            )
            if names:
                return names

    return _FALLBACK_ALLOWED_MANIFEST_FILENAMES


def preferred_manifest_filename() -> str:
    """Return the schema's preferred manifest filename, or the default.

    Reads [manifest].filename from se-manifest-schema; falls back to
    MANIFEST_FILENAME.
    """
    manifest = _read_schema_manifest_table()
    if manifest is not None:
        name = manifest.get("filename")
        if isinstance(name, str) and name.strip():
            return name

    return MANIFEST_FILENAME
