"""``mgf-fed agents`` — print the canonical Magogi agent briefing.

The single most reliable way to make an AI agent federation-aware:
`mgf-common` is a dependency of every Magogi project, so this command
is always installed and always works — no file-path assumptions, no
repo-access assumptions, no "did the docs ship in the wheel" doubt.
The briefing is a **source constant** (`AGENTS_BRIEFING`) — it ships
in the wheel because it is code, and it can never fail to resolve.

`AGENTS_BRIEFING` is the **single source of truth** for the briefing
text. `STARTHERE.md` §"For the AI agent in any Magogi project"
embeds the identical text; `tests/unit/cli/test_agents.py` pins the
two together so they cannot drift.

Rule-range references (`WF-01..WF-13`) are computed at module-import
time by counting rule IDs in the packaged standards docs. PAPER-67
(2026-05-26) — when the next WF rule (WF-14) lands in `WORKFLOWS.md`,
the briefing auto-bumps to `WF-01..WF-14` and the STARTHERE pin test
fails until the human-readable mirror is updated. The test failure
IS the maintenance signal; drift can't ship silently.

The instruction to hand any Magogi project's agent collapses to one
line: *"Run ``mgf-fed agents`` and do exactly what it prints."*
"""

from __future__ import annotations

import re
import sys
import tomllib
from pathlib import Path
from typing import TYPE_CHECKING

from mgf.common.cli import EXIT_SUCCESS

from mgf.fed.cli._standards import _resolve_libraries, _resolve_standards_dir

if TYPE_CHECKING:
    import argparse


def _max_rule_id(doc_stem: str, prefix: str) -> int | None:
    """Find the maximum ``<prefix>-NN`` rule ID in the standards doc.

    PAPER-67. Resolution:
      1. Look up the packaged standards dir via `_resolve_standards_dir`.
      2. Read ``<doc_stem>.md`` (e.g., ``WORKFLOWS.md``).
      3. Regex-match ``id="<prefix>-NN"`` (the rule-ID anchor shape
         the standards docs use uniformly).
      4. Return max NN.

    Returns ``None`` if the doc can't be located or parsed — callers
    fall back to a hardcoded value so the briefing never fails to
    render (a broken briefing would break every Magogi project's
    onboarding).
    """
    standards_dir: Path | None = _resolve_standards_dir()
    if standards_dir is None:
        return None
    doc = standards_dir / f"{doc_stem}.md"
    if not doc.is_file():
        return None
    try:
        text = doc.read_text(encoding="utf-8")
    except OSError:
        return None
    matches = re.findall(rf'id="{prefix}-(\d+)"', text)
    if not matches:
        return None
    return max(int(m) for m in matches)


# Compute rule-range references at import time. Hardcoded fallbacks
# match the values as of 2026-05-26; if the parse fails (standards
# dir not packaged, doc moved), the briefing still renders coherently.
_WF_MAX = _max_rule_id("WORKFLOWS", "WF") or 12
_WF_RANGE = f"WF-01..WF-{_WF_MAX:02d}"

# PKG-01..PKG-12 — the Codeberg package-distribution rules (standards
# v2.18 / the mgf-standard 0.1.6 cutover). Same dynamic-bump
# discipline as _WF_RANGE: a new PKG-NN auto-extends the briefing and the
# STARTHERE pin test fails until the mirror is updated.
_PKG_MAX = _max_rule_id("PACKAGE_DISTRIBUTION", "PKG") or 12
_PKG_RANGE = f"PKG-01..PKG-{_PKG_MAX:02d}"

# TS-22..TS-28 is a curated subrange (the test-supervisor rules
# specifically, not the full TS-* taxonomy). If a future TS-29 lands
# IN the supervisor cluster, update both this constant and the
# corresponding mention in TESTING.md §19. Validation: assert each
# rule in the curated range exists in TESTING.md at import time;
# this catches accidental deletion + reminds the maintainer when
# touching TS- rules. (TS-28 — per-test perf attribution — joined the
# cluster in standards v2.13.)
_TS_SUPERVISOR_RANGE = "TS-22..TS-28"


# PAPER-80 (v0.41.3+): federation sibling enumeration is a single
# constant; the briefing renders it via f-string at import time.
# When a new sibling joins the federation, update THIS LIST — the
# briefing's text + the STARTHERE.md mirror's text both refresh on
# the next test run (the pin test catches the diff loudly).
#
# Order: stability-tier first (the framework siblings consumers
# pull most often), then test/infra, then templates. Future
# entries append to keep diffs minimal.
#
# The trailing ellipsis in the briefing's rendered text is
# preserved — there's still a long tail of in-progress siblings
# (vm-* workers, hrb, etc.) that aren't appropriate to surface in
# the canonical onboarding text. The ellipsis tells the reader
# "this is the canonical headline list, not the complete index."
# The complete index lives in `FEDERATION.md §5` per the
# briefing's own cross-reference.
_FEDERATION_SIBLINGS: tuple[str, ...] = (
    "mgf-http",
    "mgf-fastapi",
    "mgf-sqlalchemy",
    "mgf-alembic",
    "mgf-django",
    "mgf-apiprobe",
    "mgf-test-supervisor",
    "mgf-auth",
    "mgf-livepush",
    "mgf-tele-core",
    "mgf-tele-teltonika",
)
_SIBLINGS_RENDERED = ", ".join(f"`{name}`" for name in _FEDERATION_SIBLINGS)


# The canonical agent briefing. Single source of truth — see the
# module docstring. Keep in sync with STARTHERE.md (test-enforced).
# Rule ranges interpolated at import time (PAPER-67); sibling list
# interpolated from _FEDERATION_SIBLINGS (PAPER-80).
AGENTS_BRIEFING = f"""\
**You are an AI agent working inside a Magogi Foundation project.**
The Foundation is a family of Python projects that share one
engineering bar. Writing correct code for this project is only half
your job — the other half is keeping the project a good citizen of
the federation. Concretely:

**1 — Know the shape.** The Foundation has three rings: `mgf-common`
(the cornerstone library + reference implementation, which depends
on `mgf-standard` — the standards + federation-contract source
of truth), the
`mgf-*` sibling libraries ({_SIBLINGS_RENDERED}, …), and consumer
applications that depend on them. Domain logic is built as a library FIRST; applications
consume libraries and never embed reusable domain logic.

**2 — Read the contract before you write code.** `FEDERATION.md` in
the `mgf-standard` repo is the canonical contract: required docs
layout, conformance levels, the federation-first principle,
catch-up cadence, feedback discipline. If you read one document,
read that one. The engineering rules are numbered families (AP, CF,
EH, LG, SC, TS, DP, DOC, …): run `mgf-fed standards` to list
them and `mgf-fed standards <topic>` to print any one. Cite
rules by ID.

**3 — Use the federation; do not reinvent it.** Before writing any
infrastructure code — logging, configuration, typed errors,
retries, vault, process management, HTTP client, DB layer,
rate-limiting, caching — check whether `mgf-common` or a sibling
already ships it. It almost certainly does (`FEDERATION.md §5` has
the "need X → use Y" table). Depend on the federation package, not
a competing third-party one. A hand-rolled version of something the
federation already provides is a defect, not a shortcut.

**4 — Follow the documentation layout + standards.** Every project
ships the universal root-doc baseline (`README`, `LICENSE`,
`CHANGELOG`, `CONTRIBUTING`, `SECURITY`, `FEEDBACK`, `AGENTS`,
`FEDERATION`, `STARTHERE`, `DEPS` — plus a `docs/` directory; for
non-cornerstone projects `FEDERATION.md` is a pointer stub, not a
copy) plus the federation trackers (`MGF_ADOPTION.md` for consumer
apps, `MGF_STANDARDS_CONFORMANCE.md` for every project). Every repo
also keeps committed in-repo memory at `docs/memory/` (a `MEMORY.md`
index + one-fact-per-file notes; AG-17) so durable agent context
travels with the repo, and uses the standardized `.gitignore` that
never commits `my_stuff/` local secrets (AG-18). The project
declares a conformance level — L1 / L2 / L3 — measuring how
strictly it follows the rules. Match the federation's structure;
never invent a parallel one.

**5 — Pick the federation's chosen tools — don't litigate them.**
Three infrastructure choices are already made; you don't redo them
per project. **CI/CD:** Woodpecker CI on Codeberg (rules {_WF_RANGE}
in `mgf-fed standards workflows`) — same `.woodpecker.yml` runs
on Codeberg natively AND locally via `woodpecker-cli exec`. That
`.woodpecker.yml` is a GENERATED artifact (rendered by `mgf-fed
scaffold` from the canonical template; `mgf-fed drift-check`
enforces it) — never hand-edit it (WF-13); see the `run-ci` recipe.
**Testing:** the federation test runner is the published sibling
`mgf-test-supervisor` (rules {_TS_SUPERVISOR_RANGE} in `mgf-fed standards
testing`) — install in dev extras, then `mgf-test-supervisor
--self-check` confirms it's wired. **Packages:** every `mgf-*`
package publishes to and resolves from the Codeberg PyPI registry
(rules {_PKG_RANGE} in `mgf-fed standards pkg`), NOT pypi.org;
wire the `codeberg-mgf` named index + `[tool.uv.sources]` per
PKG-06 (third-party deps still resolve from public PyPI).
Per-project CI tools or hand-rolled test orchestrators are a
defect, not a shortcut.

**6 — Keep the project in sync — new, existing, or behind.** Run
`mgf-fed catch-up` in the project root. It audits drift (stale
dependency pin, missing four-gate CI, missing Woodpecker config,
missing test-supervisor dev dep, missing or stale agent files,
missing federation artifacts); `mgf-fed catch-up --apply` fixes
the mechanical parts. A project that has fallen behind the
federation must be caught up, not worked around. Consistency across
every project — old and new — is a goal, not an accident.

**7 — Leave feedback in the RIGHT place. This is how the federation
improves, and it is the most important item on this list.** When you
hit friction with another project — `mgf-common`, any sibling, or any
project you depend on — a missing capability, a sharp edge, an API
that does not fit, a document that misled you — do NOT silently work
around it, and do NOT record it only in your own repo where the
target's maintainer will never see it. **File a PAPER in the
`FEEDBACK.md` of the project that caused the friction — the target's
own repository.** Routing rule: friction with a specific project →
THAT project's `FEEDBACK.md`; a standards rule, the federation contract, or
the library registry → `mgf-standard`'s `FEEDBACK.md`; other cross-cutting
issues → `mgf-common`'s `FEEDBACK.md`. Give the target enough to **triage and
act without coming back to ask you** — every paper states: **what
hurts** (the concrete friction), **why it matters** (impact / who it
blocks), **concrete evidence** (a repro, `file:line`, commit SHA, and
the version you hit it on), and a **suggested shape** for the fix.
Run `mgf-fed feedback-howto` for the entry template. **You do NOT edit,
PR, or version-bump another project's library yourself — only the
mgf-common supervisor changes a library (AG-13); your job is the
paper, the supervisor's job is the fix + keeping the whole federation
consistent (`MGF_LIBRARIES.md` is the registry of what exists).** A
workaround buried in one consumer helps it once; a well-written paper in the target's
`FEEDBACK.md` improves that project for every consumer, forever. If
you routed around the federation instead of feeding back, the task is
not done — even when the code runs.

**8 — Consistency is the point.** Every Magogi project should read
as though one hand wrote it: the same structure, the same rule
discipline, the same primitives, the same feedback channel. When
you face a choice the federation has already made, make the same
choice.

**9 — You are empowered, not just bound (AG-15).** This corpus is
MUST-heavy so consistency is cheap — but it does NOT ask for blind
compliance. You MAY push back on a rule you believe is wrong or
misapplied here, propose a better path, refine scope up front (confirm
the project mode — greenfield / unconstrained-brownfield /
production-constrained — before substantial autonomous work; do not
apply production guardrails to a throwaway spike), decline a primitive
with recorded rationale, ask a clarifying question, and report
"reviewed; no change needed" as a first-class result. Exercising
judgment WITH a recorded rationale (a FEEDBACK paper per item 7, an
ADR, or a documented decline) is conformant; silent non-compliance is
not. When you hit an ambiguity that is not a risky/irreversible action,
do not stall: record the open question, choose a sensible default, mark
dependent conclusions inferred, and continue. The full license — the
six rights, the mode handshake, the never-block protocol, and what it
does NOT cover — is the **Agent Empowerment Charter** in the standards
corpus (`mgf-fed standards`; `principles/agent-empowerment-charter.md`).

**Startup checklist for this session:**

- **Ensure the standards corpus is installed:** `pip install -U 'mgf-common[standards]'`
  (or `uv add 'mgf-common[standards]'`). The `standards`, `agents` federation
  map, `catch-up` federation-position, `federation`/`design`, and `sync-prompt`
  commands resolve the rules + contract + design + registry from this OPTIONAL
  extra; a plain `mgf-common` install does NOT carry it and those commands
  degrade with an install prompt — never a silent partial answer.
- To bring this project current with the federation, run `mgf-fed sync-prompt`
  and follow it exactly — the enforced comprehend → conform → verify procedure.
- Run `mgf-fed agents` (this briefing — federation-wide duties) and
  `mgf-fed agents --project` (the SAME shape scoped to THIS project:
  its own `AGENTS.md` + `docs/claude/CLAUDE.md`). Every project has the
  project-scoped briefing via this one command.
- Run `mgf-fed catch-up` — audit this project's standing in the
  federation; read the report.
- Run `mgf-fed standards` — the rule topics; read whichever your
  task touches.
- Starting a brand-new project? Run `mgf-fed init-project`.
- **Before you call a change done (AG-14 / DOC-18):** walk the
  change-propagation matrix (`mgf-fed standards DOCUMENTATION` §10.4)
  for every surface you touched and update each dependent IN THE SAME
  commit — a new public symbol ripples to `PUBLIC_API.{{md,json}}` +
  CHANGELOG; a new/changed rule ripples to the README *Index of MUSTs*
  + the `standards`/`agents` CLI; a wire-schema change ripples to every
  parallel copy + its alignment test. Run `mgf-fed app-doctor --standards`
  as the backstop. "Update the docs in a follow-up" is the habit DOC-18
  forbids — leaving a dependent stale is a defect even when nothing is
  visibly broken.

**Canonical references, in order:** `FEDERATION.md` (the contract)
→ `STARTHERE.md` (procedural how-to) → `mgf-fed standards
<topic>` (the rules) → `docs/audience/ai-agents.md` (the
consumer ↔ maintainer feedback-relay protocol) → this project's
`docs/CLAUDE.md` (project-specific state). If anything conflicts,
`FEDERATION.md` wins — it is the canonical contract.
"""


# Project-scoped agent docs, in priority order. `--project` prints the
# first one found in the CWD — the project's own agent briefing. This is
# how EVERY project (incl. pure libraries + non-Python repos that can't
# ship their own CLI) gets a project-scoped `agents` command: the
# universal `mgf-common` tool is installed everywhere mgf-common is a
# dep, and reads the local project's agent docs.
_PROJECT_AGENT_DOCS: tuple[str, ...] = (
    "docs/claude/CLAUDE.md",
    "docs/CLAUDE.md",
    "AGENTS.md",
)


def add_agents_args(subparser: argparse.ArgumentParser) -> None:
    """Register flags for ``mgf-fed agents``.

    ``--project`` switches from the federation-wide briefing to the
    PROJECT-scoped one (the current directory's own agent docs), so
    every project exposes the same ``agents`` capability scoped to
    itself via this one universal command.
    """
    subparser.add_argument(
        "--project",
        "-p",
        action="store_true",
        help=(
            "Print the PROJECT-scoped agent briefing for the current "
            "directory (its docs/claude/CLAUDE.md + AGENTS.md) instead "
            "of the federation-wide briefing."
        ),
    )


def _current_project_name() -> str | None:
    """Best-effort ``[project].name`` of the cwd's ``pyproject.toml`` — to
    locate the agent's own repo in the federation map. ``None`` if there
    is no parseable manifest here."""
    pyproject = Path.cwd() / "pyproject.toml"
    if not pyproject.is_file():
        return None
    try:
        data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
    except (OSError, ValueError):
        return None
    name = data.get("project", {}).get("name")
    return name if isinstance(name, str) else None


def _print_federation_map() -> None:
    """Append the live federation roster + this project's place in it,
    resolved from the installed ``mgf-standard`` registry.

    This is the drift-proof half of federation-awareness: *which other
    projects exist*, their versions, and the dependency graph — resolved
    from the ``mgf-standard`` version this project pins, never a
    hand-copied roster. It degrades **silently** (prints nothing) if the
    registry can't be resolved (an older ``mgf-standard`` without the
    data, or a partial install): the static briefing above must never
    depend on it.
    """
    libs = _resolve_libraries()
    if not libs:
        # Loud, not silent. The briefing (stdout) is the always-works primary
        # output, so `agents` still exits 0 — but a missing federation map is a
        # real degrade (the `mgf-standard` corpus extra isn't installed), so
        # surface it on stderr with the one-line fix instead of vanishing.
        print(
            "warning: federation map unavailable — the `mgf-standard` "
            "corpus extra is not installed, so the live roster / 'you are here' "
            "map is omitted from this briefing. Install it for the full map:\n"
            "    pip install -U 'mgf-common[standards]'",
            file=sys.stderr,
        )
        return
    by_repo = {str(lib.get("repo")): lib for lib in libs}
    cornerstone = by_repo.get("mgf-common", {})
    print()
    print("Federation map — the other projects (live, via mgf-standard)")
    print("-" * 56)
    print(
        f"{len(libs)} libraries · cornerstone mgf-common "
        f"{cornerstone.get('version', '?')}. Resolved from the "
        "mgf-standard version this project pins — not a hand-copied roster."
    )
    entry = by_repo.get(_current_project_name() or "")
    if entry is not None:
        deps = ", ".join(sorted(entry.get("deps", {}))) or "— (federation leaf)"
        used = ", ".join(entry.get("used_by", [])) or "— (no federation consumers yet)"
        print()
        print(f"You are here: {entry.get('repo')} {entry.get('version', '?')}")
        print(f"  depends on (mgf-*): {deps}")
        print(f"  used by   (mgf-*): {used}")
    print()
    print(
        "Full roster + dependency graph: `mgf-fed catch-up`, or `mgf.standard.libraries()` in code."
    )


def run_agents(args: argparse.Namespace) -> int:
    """Print the canonical Magogi agent briefing.

    Default: the federation-wide briefing (a source constant that
    cannot fail to resolve). With ``--project``: the current
    directory's project-scoped agent docs.

    Returns :data:`mgf.common.cli.EXIT_SUCCESS`.
    """
    if getattr(args, "project", False):
        return _run_agents_project()

    print("mgf-common — canonical Magogi Foundation agent briefing")
    print("=" * 56)
    print()
    print(AGENTS_BRIEFING, end="")
    _print_federation_map()
    return EXIT_SUCCESS


def _run_agents_project() -> int:
    """Print the current project's own agent briefing (``--project``)."""
    cwd = Path.cwd()
    print(f"Project-scoped agent briefing — {cwd.name}")
    print("=" * 56)
    print()
    for rel in _PROJECT_AGENT_DOCS:
        doc = cwd / rel
        if doc.is_file():
            print(f"(source: {rel})")
            print()
            try:
                print(doc.read_text(encoding="utf-8").rstrip())
            except OSError as exc:  # pragma: no cover - unreadable file edge
                print(f"(could not read {rel}: {exc})")
            print()
            print("---")
            print("Federation-wide duties: run `mgf-fed agents` (no flag).")
            return EXIT_SUCCESS

    print(
        "No project-scoped agent doc found here (looked for: "
        + ", ".join(_PROJECT_AGENT_DOCS)
        + ")."
    )
    print(
        "Run `mgf-fed catch-up --apply` to create them, or "
        "`mgf-fed init-project` for a brand-new project."
    )
    return EXIT_SUCCESS


__all__ = [
    "AGENTS_BRIEFING",
    "add_agents_args",
    "run_agents",
]
