"""``mgf.standard`` — the Magogi Foundation engineering-standards corpus.

This package is the federation's **source of truth** for the engineering
standards, the federation contract (``FEDERATION.md``), and the
foundation-level docs. The human-readable docs live in ``docs/`` at the
repo root; the standards corpus (``docs/standards/``) is *also* shipped as
wheel data at ``mgf/standard/_standards/`` so any installed consumer
can read the canonical rules through :func:`standards_dir` without
vendoring or path-hacking.

``mgf-common`` depends on this package and surfaces the corpus through its
``mgf-common standards`` / ``mgf-common agents`` CLI — those commands call
:func:`standards_dir` here rather than reaching into the private
``_standards`` data directory, so the federation's closed-box rule (a
consumer reaches only a sibling's documented public surface) stays intact.

Public surface:

  - :func:`standards_dir` — the directory holding the standards docs.
  - :func:`list_standards` — the canonical topic names (file stems).
  - :func:`read_standard` — the text of one topic by name.
  - :func:`recipes_dir` — the directory holding the federation-operations
    recipes + governance templates (the ``/sync`` prompt, cohort-release,
    sibling-rename, the per-repo doc templates, …).
  - :func:`list_recipes` — the recipe names (file stems).
  - :func:`read_recipe` — the text of one recipe by name, so a CLI can
    surface a governance recipe (e.g. the federation sync prompt) from the
    version a consumer pins rather than a hand-copied command body.
  - :func:`libraries` — the federation library registry (every ``mgf-*``
    member with its version, dependencies, reverse-deps, and purpose), so
    an agent resolves the current federation map from the dependency it
    already pins rather than a copied list that drifts.
  - :func:`federation` — the full text of the federation **contract**
    (``FEDERATION.md``, DOC-17), so an agent resolves the canonical duties
    offline from the pinned version rather than a checkout / Codeberg URL.
  - :func:`design_dir` / :func:`list_design` / :func:`read_design` — the
    foundation **design** papers (``docs/design/``, the federation-pattern
    paper), so the *whole* picture (rules + contract + design) is resolvable
    from the package, not only the rules.
"""

from __future__ import annotations

import json
from importlib.resources import as_file, files
from pathlib import Path
from typing import Any

__all__ = [
    "__version__",
    "design_dir",
    "federation",
    "libraries",
    "list_design",
    "list_recipes",
    "list_scaffold",
    "list_standards",
    "read_design",
    "read_recipe",
    "read_scaffold",
    "read_standard",
    "recipes_dir",
    "release_waves",
    "standards_dir",
]

__version__ = "0.1.61"


def _docs_data_dir(name: str) -> Path:
    """Resolve a shipped docs data directory (``docs/<name>/``).

    Both the standards corpus and the recipes corpus are ``docs/<name>/``
    subtrees that hatchling ``force-include``s into the wheel at
    ``mgf/standard/_<name>/``. This shared resolver keeps the two
    public seams (:func:`standards_dir`, :func:`recipes_dir`) byte-for-byte
    consistent.

    Resolution order:

      1. Wheel-shipped package data (``mgf/standard/_<name>/``).
      2. Repo-relative fallback for editable installs:
         ``<repo-root>/docs/<name>/``.

    A directory counts only if it contains the canonical ``README.md``
    index.

    Raises:
        FileNotFoundError: if neither location resolves.
    """
    # 1. Wheel-shipped data. Anchor on the package (which has an
    #    ``__init__``) and navigate into the force-included data dir;
    #    robust whether or not ``_<name>`` is itself importable.
    try:
        ref = files("mgf.standard").joinpath(f"_{name}")
    except (ModuleNotFoundError, ImportError):
        ref = None
    if ref is not None:
        try:
            with as_file(ref / "README.md") as readme:
                if readme.is_file():
                    return readme.parent
        except (FileNotFoundError, ValueError):
            pass

    # 2. Editable-install repo-relative path: src/mgf/standard ->
    #    repo root -> docs/<name>.
    pkg_root = Path(__file__).resolve().parent  # src/mgf/standard
    candidate = pkg_root.parent.parent.parent / "docs" / name
    if candidate.is_dir() and (candidate / "README.md").is_file():
        return candidate

    raise FileNotFoundError(
        f"could not locate the mgf-standard {name!r} docs corpus: "
        f"neither the packaged wheel data (mgf/standard/_{name}/) nor "
        f"an editable-install docs/{name}/ directory was found."
    )


def _is_unsafe_stem(name: str) -> bool:
    """Return ``True`` if ``name`` is not a bare doc stem (SC-15 guard).

    The public readers resolve ``<corpus-dir>/<name>.md``. A corpus topic /
    recipe / design stem is always a **bare filename** (``"LOGGING"``,
    ``"federation-sync-prompt"``) — never a path. A ``name`` carrying a path
    separator, a parent-dir reference, or an absolute path could escape the
    corpus directory and read an arbitrary ``.md`` file elsewhere on disk
    (directory traversal, rule **SC-15**). Anything that is not its own
    basename — or contains a separator or ``..`` — is rejected, so the readers
    can only ever reach a file *inside* their corpus directory.
    """
    return (
        not name
        or name in {".", ".."}
        or "/" in name
        or "\\" in name
        or ".." in name
        or name != Path(name).name
    )


def standards_dir() -> Path:
    """Return the directory holding the engineering-standards docs.

    Resolves wheel-shipped data (``mgf/standard/_standards/``,
    force-included from ``docs/standards/`` at build time) first, then the
    editable-install repo path ``<repo-root>/docs/standards/``.

    Returns:
        The resolved directory as a :class:`pathlib.Path`.

    Raises:
        FileNotFoundError: if neither location resolves to a directory
            containing the canonical ``README.md`` index.
    """
    return _docs_data_dir("standards")


def list_standards() -> list[str]:
    """Return the canonical topic names (Markdown file stems), sorted."""
    return sorted(p.stem for p in standards_dir().glob("*.md"))


def read_standard(name: str) -> str:
    """Return the text of one standards doc by file stem.

    Args:
        name: A topic file stem, e.g. ``"LOGGING"`` or ``"ERROR_HANDLING"``.

    Returns:
        The full UTF-8 text of ``<name>.md``.

    Raises:
        FileNotFoundError: if ``name`` is not a known topic.
    """
    target = standards_dir() / f"{name}.md"
    if _is_unsafe_stem(name) or not target.is_file():
        raise FileNotFoundError(
            f"unknown standards topic {name!r}; available: {', '.join(list_standards())}."
        )
    return target.read_text(encoding="utf-8")


def recipes_dir() -> Path:
    """Return the directory holding the federation-operations recipes.

    The recipes corpus (``docs/recipes/``) carries the governance how-tos
    and per-repo doc templates — the ``/sync`` federation sync prompt,
    cohort-release, sibling-rename, the pointer-stub / adoption /
    conformance / DEPS / FEEDBACK templates. Like the standards corpus it
    is shipped as wheel data (``mgf/standard/_recipes/``) so a consumer
    (e.g. the ``mgf-common`` CLI) can surface a recipe from the version it
    pins instead of a hand-copied command body that drifts.

    Returns:
        The resolved directory as a :class:`pathlib.Path`.

    Raises:
        FileNotFoundError: if neither the wheel data nor an
            editable-install ``docs/recipes/`` directory resolves.
    """
    return _docs_data_dir("recipes")


def list_recipes() -> list[str]:
    """Return the recipe names (Markdown file stems), sorted."""
    return sorted(p.stem for p in recipes_dir().glob("*.md"))


def read_recipe(name: str) -> str:
    """Return the text of one recipe by file stem.

    Args:
        name: A recipe file stem, e.g. ``"federation-sync-prompt"`` or
            ``"cohort-release-wave"``.

    Returns:
        The full UTF-8 text of ``<name>.md``.

    Raises:
        FileNotFoundError: if ``name`` is not a known recipe.
    """
    target = recipes_dir() / f"{name}.md"
    if _is_unsafe_stem(name) or not target.is_file():
        raise FileNotFoundError(f"unknown recipe {name!r}; available: {', '.join(list_recipes())}.")
    return target.read_text(encoding="utf-8")


def design_dir() -> Path:
    """Return the directory holding the foundation design papers.

    The design corpus (``docs/design/``) carries the federation-pattern
    paper and any foundation-level design notes. Like the standards and
    recipes corpora it is shipped as wheel data (``mgf/standard/_design/``)
    so a consumer resolves the *design* rationale — not only the rules —
    from the version it pins rather than a checkout.

    Returns:
        The resolved directory as a :class:`pathlib.Path`.

    Raises:
        FileNotFoundError: if neither the wheel data nor an
            editable-install ``docs/design/`` directory resolves.
    """
    return _docs_data_dir("design")


def list_design() -> list[str]:
    """Return the design-paper names (Markdown file stems), sorted."""
    return sorted(p.stem for p in design_dir().glob("*.md"))


def read_design(name: str) -> str:
    """Return the text of one design paper by file stem.

    Args:
        name: A design-paper file stem, e.g. ``"federation"``.

    Returns:
        The full UTF-8 text of ``<name>.md``.

    Raises:
        FileNotFoundError: if ``name`` is not a known design paper.
    """
    target = design_dir() / f"{name}.md"
    if _is_unsafe_stem(name) or not target.is_file():
        raise FileNotFoundError(
            f"unknown design paper {name!r}; available: {', '.join(list_design())}."
        )
    return target.read_text(encoding="utf-8")


def federation() -> str:
    """Return the full text of the federation contract (``FEDERATION.md``).

    The contract (DOC-17) is the single source of truth for federation
    duties — required docs, conformance levels, the federation-first
    principle, catch-up cadence, feedback discipline, onboarding. It is
    shipped as wheel data (``mgf/standard/_FEDERATION.md``, force-included
    from the repo-root ``FEDERATION.md``) so an agent resolves the canonical
    contract from the version it pins — offline, no checkout or Codeberg URL.

    Resolution mirrors :func:`libraries`: wheel-shipped data first, then the
    editable-install repo file (``FEDERATION.md`` at the repo root).

    Returns:
        The full UTF-8 text of ``FEDERATION.md``.

    Raises:
        FileNotFoundError: if neither location resolves.
    """
    # 1. Wheel-shipped data (force-included from repo-root ``FEDERATION.md``).
    try:
        return files("mgf.standard").joinpath("_FEDERATION.md").read_text(encoding="utf-8")
    except (ModuleNotFoundError, ImportError, FileNotFoundError, OSError):
        pass
    # 2. Editable-install fallback: ``<repo-root>/FEDERATION.md``.
    repo_file = Path(__file__).resolve().parents[3] / "FEDERATION.md"
    if repo_file.is_file():
        return repo_file.read_text(encoding="utf-8")
    raise FileNotFoundError(
        "could not locate the mgf-standard federation contract: "
        "neither packaged wheel data (mgf/standard/_FEDERATION.md) nor "
        "an editable-install FEDERATION.md was found."
    )


def _load_registry() -> dict[str, Any]:
    """Load the machine-readable federation registry (``_libraries.json``).

    Resolution mirrors :func:`standards_dir`: wheel-shipped data first,
    then the editable-install repo file (``MGF_LIBRARIES.json`` at the
    repo root).

    Raises:
        FileNotFoundError: if neither location resolves.
    """
    text: str | None = None
    # 1. Wheel-shipped data (force-included from ``MGF_LIBRARIES.json``).
    try:
        text = files("mgf.standard").joinpath("_libraries.json").read_text(encoding="utf-8")
    except (ModuleNotFoundError, ImportError, FileNotFoundError, OSError):
        text = None
    # 2. Editable-install fallback: ``<repo-root>/MGF_LIBRARIES.json``.
    if text is None:
        repo_json = Path(__file__).resolve().parents[3] / "MGF_LIBRARIES.json"
        if repo_json.is_file():
            text = repo_json.read_text(encoding="utf-8")
    if text is None:
        raise FileNotFoundError(
            "could not locate the mgf-standard federation registry: "
            "neither packaged wheel data (mgf/standard/_libraries.json) "
            "nor an editable-install MGF_LIBRARIES.json was found."
        )
    data: dict[str, Any] = json.loads(text)
    return data


def libraries() -> list[dict[str, Any]]:
    """Return the federation library registry — the roster of every project.

    Each entry is a dict describing one ``mgf-*`` member: ``repo``,
    ``lang``, ``version``, ``requires_python``, ``deps`` (the ``mgf-*``
    pins it depends on), ``used_by`` (the ``mgf-*`` that depend on it),
    ``extras``, ``third_party_count``, and ``purpose``.

    The data is generated by ``scripts/gen_mgf_libraries.py`` and shipped
    as wheel data, so an agent resolves the *current* federation map (who
    exists, their versions, the dependency graph) from the
    ``mgf-standard`` version its project pins — not a copied list that
    drifts. This is the reliable replacement for chasing the roster via
    catch-up/sync.

    Returns:
        The list of member records, sorted by repo name.

    Raises:
        FileNotFoundError: if the registry cannot be resolved.
    """
    libs = _load_registry().get("libraries", [])
    return list(libs) if isinstance(libs, list) else []


def release_waves() -> list[list[str]]:
    """Return the dependency-ordered release waves — the order to walk the
    federation for any cross-cutting update.

    Each wave is a list of ``mgf-*`` repo names with no unshipped mgf
    dependency among them; wave 0 is the roots (the cornerstone + the leaves
    nothing depends on), and a repo may ship only once every wave below it is
    published + live. Walking **roots → leaves** is the invariant order for a
    cornerstone bump, a standards-corpus change, or a cohort pin-walk. The
    ``mgf-test-supervisor`` (dev/test) and ``mgf-standard``
    (``[standards]`` extra) edges are deliberately not treated as build-order
    constraints.

    Generated by ``scripts/gen_mgf_libraries.py`` (via the shared wave
    computer ``scripts/fed_waves.py``) and shipped as wheel data, so an agent
    resolves the *current* walk order from the version its project pins — not a
    hand-maintained ordering that drifts. See
    ``docs/recipes/federation-update-strategy.md`` for the full procedure.

    Returns:
        The list of waves, roots first; each wave sorted. Empty if the
        registry predates the ``release_waves`` field.

    Raises:
        FileNotFoundError: if the registry cannot be resolved.
    """
    waves = _load_registry().get("release_waves", [])
    if not isinstance(waves, list):
        return []
    return [list(w) for w in waves if isinstance(w, list)]


# Canonical standardized git/CI scaffold artifacts. Name -> editable-install
# source path (relative to the repo root); in a wheel these are force-included
# flat into ``mgf/standard/_scaffold/``. The single source of truth is the
# repo's own working copy (``.githooks/`` + ``scripts/``), so a consumer that
# writes these into a repo (``mgf-common catch-up``) gets the canonical text
# rather than a drift-prone parallel copy.
_SCAFFOLD_FILES = {
    "prepare-commit-msg": ".githooks/prepare-commit-msg",
    "check_commit_trailers.py": "scripts/check_commit_trailers.py",
}


def list_scaffold() -> list[str]:
    """Return the canonical scaffold artifact names (the AG-03 hook + CI checker), sorted."""
    return sorted(_SCAFFOLD_FILES)


def read_scaffold(name: str) -> str:
    """Return the text of a canonical scaffold artifact by name.

    The federation's standardized git/CI scaffolding — the AG-03
    ``prepare-commit-msg`` hook and the ``check_commit_trailers.py`` CI trailer
    checker — shipped as wheel data so a consumer (e.g. ``mgf-common catch-up``)
    writes the *canonical* text into a repo instead of carrying a drift-prone
    copy. The single source of truth is mgf-standard's own ``.githooks/`` +
    ``scripts/`` working files.

    Args:
        name: A scaffold artifact name, e.g. ``"prepare-commit-msg"`` or
            ``"check_commit_trailers.py"``.

    Returns:
        The full UTF-8 text of the artifact.

    Raises:
        FileNotFoundError: if ``name`` is not a known scaffold artifact, or the
            artifact cannot be resolved.
    """
    if name not in _SCAFFOLD_FILES:
        raise FileNotFoundError(
            f"unknown scaffold artifact {name!r}; available: {', '.join(list_scaffold())}."
        )
    # 1. Wheel-shipped data (``mgf/standard/_scaffold/<name>``).
    try:
        return files("mgf.standard").joinpath("_scaffold", name).read_text(encoding="utf-8")
    except (ModuleNotFoundError, ImportError, FileNotFoundError, OSError):
        pass
    # 2. Editable-install fallback: the repo's own working file.
    repo_file = Path(__file__).resolve().parents[3] / _SCAFFOLD_FILES[name]
    if repo_file.is_file():
        return repo_file.read_text(encoding="utf-8")
    raise FileNotFoundError(
        f"could not locate scaffold artifact {name!r}: neither packaged wheel data "
        f"(mgf/standard/_scaffold/{name}) nor the editable-install source was found."
    )
