"""Deterministic, zero-dependency renderer for the canonical ``.woodpecker.yml``.

Each member's CI pipeline is a GENERATED artifact, not a hand-pasted copy. The
single source of truth is ``mgf-standard/templates/woodpecker/`` — a canonical
``base.yml`` (the standard Python-library pipeline) plus opt-in fragments
(``frag-*.yml``) spliced in at ``# <<INSERT:NAME>>`` markers, and ``base-zig.yml``
for the Zig shape. ``render_woodpecker(member)`` derives the per-repo parameters
from the member's metadata + on-disk layout and returns the pipeline text.

Pure text, no jinja/cookiecutter (mgf-fed has no templating engine at runtime):
the only operations are ``str.replace`` for ``{{TOKEN}}`` substitution and
marker-line replacement for the ``# <<INSERT:NAME>>`` fragments.
"""

from __future__ import annotations

import difflib
import re
import sys
from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path

from mgf.fed._roster import Member, federation_root
from mgf.fed._template import canonical_text, template_dir

# Marker name -> fragment file, in the order they appear in base.yml. A marker
# whose gate (in ``_gates``) is False has its line removed; otherwise the line
# is replaced by the fragment's text verbatim (fragments are pre-indented to
# their insertion column, so no runtime re-indent is needed).
_FRAGMENTS: dict[str, str] = {
    "cron-event": "frag-cron-event.yml",
    "no-pypi-surface": "frag-no-pypi-surface.yml",
    "commit-trailers": "frag-commit-trailers.yml",
    "supervisor": "frag-supervisor.yml",
    "cornerstone": "frag-cornerstone.yml",
    "cornerstone-publish-dep": "frag-cornerstone-publish-dep.yml",
    "console-script-smoke": "frag-console-script-smoke.yml",
    # DP-01 layering gate — only spliced in when the repo actually declares an
    # import-linter contract (a .importlinter file OR [tool.importlinter]); a repo
    # without one would otherwise fast-fail the lint step on `lint-imports`'s
    # "Could not read any configuration".
    "lint-imports": "frag-lint-imports.yml",
    # TS-06 / WUI-07 browser install — only spliced into repos that declare
    # [tool.mgf-fed] browser = true (a suite that drives a real Chromium via
    # Playwright). python:*-slim ships neither the browser nor its system libs, so
    # a browser-driving suite otherwise cannot run — and cannot reach its coverage
    # floor — in CI. Opt-in, so no other member pays the download.
    "playwright-install": "frag-playwright-install.yml",
    # WUI-07 UI gate — only spliced into repos that depend on mgf-uitest-web (a
    # console with a browser-UI spec tier under tests/e2e/). A dedicated step because
    # every console's pytest addopts deselect `e2e` by default, so UI specs inside a
    # normal test job would be silently deselected — present, and never run.
    "ui-gate": "frag-ui-gate.yml",
    # D-GEN freshness gate — only spliced in when the repo carries
    # fed-resources/source/project.toml (the fedres convention). Regenerates
    # fed-resources/generated/ and byte-compares; nonzero on drift.
    "fedres-check": "frag-fedres-check.yml",
    # AP-16 federation pin-window safety net — only spliced into repos that carry a
    # versioned mgf-common dependency (never the cornerstone). Re-pins mgf-common
    # DOWN to the declared floor and re-runs the suite, so a real break at the floor
    # reds CI; the upper end (latest-in-pin = cornerstone) is the normal test step.
    "mgf-common-matrix": "frag-mgf-common-matrix.yml",
    # AP-16 B-lite ping (cornerstone only) — mgf-common's release pipeline triggers
    # every consumer's compat-matrix after publish (see frag-cornerstone-ping.yml).
    "cornerstone-ping": "frag-cornerstone-ping.yml",
}

# Fleet-wide pip-audit ignores applied to EVERY generated pipeline, on top of any
# per-repo [tool.mgf-fed].vuln_ignore. CVE-2025-71176 is a pytest <9.0.3 local
# /tmp predictable-dir DoS: pytest is a universal dev/CI-only dependency, never in
# any shipped wheel, so the vector is unreachable for consumers — the same triage
# the whole fleet carried per-repo. Drop entries here once the CI base image ships
# the fixed version (pytest >= 9.0.3).
_FLEET_VULN_IGNORES: tuple[str, ...] = ("CVE-2025-71176",)

# Default coverage floor for the 3.12 gate when the repo declares no floor at
# all (neither [tool.mgf-fed].coverage_floor nor [tool.coverage.report].fail_under).
# THE single-sourced default (M2 §3.3) — every floor consumer reads this constant;
# never repeat the number elsewhere. AGGRESSIVE picks 85 (the fleet's hot target)
# deliberately: a repo with no declared floor that lands below 85 SHOULD red-line
# the sweep and get a measured floor set first — see derive_params + the
# COVERAGE-FLOOR risk list.
_DEFAULT_COVERAGE_FLOOR = 85

# The self-hosted devpi index (Phase 2 cutover, 2026-06-19). magogi/prod serves
# PUBLISHED mgf-* releases LOCAL-ONLY (mirror_whitelist empty → a public-PyPI
# release of a name we ALREADY HOST can never be served) and proxies all public
# deps through its root/pypi pull-through cache, so a SINGLE --index-url resolves
# everything. Hence: no --extra-index-url. On a transient failure the install
# retries the SAME steered command (see `_steered`), never a bare index-less install.
#
# NOT dependency-confusion-proof yet (CIR-01, proven live 2026-07-17). The
# mirror-block only covers names devpi HAS a release for; a name with NO devpi
# release — a typo'd or pre-declared mgf-* dep — falls through `bases: [root/pypi]`
# to public PyPI (`+simple/mgf-doc-standard/` → 200; mgf-standard PAPER-123). The
# hole closes only when root/pypi is severed from magogi/prod (or mgf-*/sooth*
# fall-through is denied); decision package pending. Do NOT restore a "structurally
# impossible" claim until then.
#
# M2 §3.4 (PAPER-113 companion): generated pipelines NEVER embed the endpoint —
# every package-consuming install references the Woodpecker SERVER-level variable
# ``$DEVPI_URL`` (MGF_INFRA.md §2; injected into every step, live since
# 2026-07-04), preceded by the same fail-fast guard line the template fragments
# carry. Host-side tooling resolves the same fact via ``mgf.fed._index.devpi_url``
# — the one resolver. A devpi host move = server variable + resolver default +
# fleet re-render; never a hand-search across committed pipelines.
_INDEX_FLAGS = '--index-url "$DEVPI_URL"'
_DEVPI_GUARD = (
    '\'[ -n "$DEVPI_URL" ] || { echo "DEVPI_URL is unset -- set the Woodpecker '
    "server-level variable (MGF_INFRA.md §2; PAPER-113)\"; exit 1; }'"
)


@dataclass(frozen=True, slots=True)
class WoodpeckerParams:
    """Everything the template needs, derived from one member."""

    import_name: str  # e.g. "mgf.common" / "mgf.vm.core"
    dist_name: str  # underscored wheel/sdist stem, e.g. "mgf_vm_core"
    dist_name_dash: str  # dashed project name, e.g. "mgf-vm-core"
    extras_install: str  # editable install target, e.g. ".[dev,observability,vault]"
    sbom_install: str  # consumer-surface install, e.g. ".[observability,vault]" or "."
    coverage_floor: int
    lang: str
    is_cli: bool
    console_script: str  # the [project.scripts] entry-point name ("" when not a CLI)
    has_supervisor: bool
    is_cornerstone: bool
    # --- declared [tool.mgf-fed] carve-outs (faithful generation, no regressions) ---
    # (the dead ``skip_editable`` knob was deleted in M2 §3.3: it was parsed but
    # never read — ``--skip-editable`` is intrinsic to vuln_audit_cmd; declaring
    # the key now draws the unknown-key note from ``_mgf_ci``.)
    vuln_ignore: tuple[str, ...] = ()  # pip-audit --ignore-vuln <CVE> per entry
    # System packages apt-installed before every package-BUILDING install
    # (lint/typecheck/test/vuln-scan/sbom). For repos whose deps include a
    # C-extension that compiles from source — e.g. mgf-vm-libvirt / mgf-vm-sdk
    # pull libvirt-python, which needs gcc + libc6-dev + pkg-config + libvirt-dev,
    # none of which python:*-slim ships. Empty (the default) renders nothing.
    apt_packages: tuple[str, ...] = ()
    # Index steering (the devpi ``$DEVPI_URL`` index) on EVERY package-
    # consuming install — lint/typecheck/test/vuln-scan editable installs AND
    # sbom/tag-coherence/smoke. DEFAULT-ON: an mgf-* member resolves its mgf-*
    # deps (which are in its ``dependencies`` / ``[dev]``, not on pypi.org)
    # from the devpi index; without it those installs fail in a fresh CI
    # container. ``[tool.mgf-fed] extra_index = false`` opts out (the
    # cornerstone mgf-common has no mgf-* deps, and mgf-fastapi pins them
    # editable, so neither needs it).
    extra_index: bool = True
    # Whether the repo declares an import-linter contract (.importlinter file or
    # [tool.importlinter]) — gates the `lint-imports` line in the lint step. A repo
    # without a contract skips it rather than fast-failing on missing config.
    has_importlinter: bool = False
    # Whether this repo consumes the browser-UI harness (``mgf-uitest-web``) — i.e.
    # carries the WUI-07 UI spec tier under the canonical ``tests/e2e/`` (TS-20).
    # Gates the standalone `ui-gate` step. Detected from the dependency rather than
    # declared, exactly like ``has_importlinter``: the specs cannot exist without the
    # harness, so a console cannot add the tier and forget to turn its gate on.
    has_ui_tests: bool = False
    # Whether this repo IS mgf-fedres — the generator that the D-GEN freshness gate
    # runs. It must check itself with its OWN code (`-e .`), never the published
    # release: verifying the generator with the previous generator deadlocks it — any
    # change to a projector's output reds its own gate (the committed tree no longer
    # matches what the OLD release regenerates) and the fix cannot publish until that
    # gate is green. Same shape as the cornerstone's circular-dep carve-out.
    is_fedres: bool = False
    # Whether this repo's suite drives a real browser (Playwright) and so needs the
    # browser + its system libs installed before the test steps. Declared per-repo
    # via ``[tool.mgf-fed] browser = true``. The first member to need it is
    # mgf-uitest-web, whose product IS driving a console in a real Chromium: without
    # the browser its test job cannot run at all, let alone meet the TS-06 floor.
    browser: bool = False
    # Whether the repo carries a fed-resources/source/project.toml — gates the
    # standalone `fedres check` freshness step. A repo without the convention
    # skips it (no fed-resources/ to verify).
    has_fed_resources: bool = False
    # AP-16 federation pin-window: the MAJOR.MINOR floor of the repo's mgf-common pin
    # (e.g. "0.46" from `mgf-common>=0.46,<0.50`), which the compat-matrix step re-pins
    # to. Empty when the repo declares no versioned mgf-common dep — the matrix then
    # stays off (no floor to bound); the cornerstone (mgf-common itself) is excluded.
    mgf_common_floor: str = ""

    @property
    def gates(self) -> dict[str, bool]:
        """Which ``# <<INSERT:NAME>>`` markers are active for this member."""
        return {
            "cron-event": self.is_cornerstone,
            "no-pypi-surface": self.is_cornerstone,
            # AG-03 trailer gate — every repo's commits need both trailers.
            "commit-trailers": True,
            "supervisor": self.has_supervisor,
            "cornerstone": self.is_cornerstone,
            # The cornerstone's compat-matrix is a release gate: publish MUST
            # wait on it (a public-API regression vs example_app blocks the
            # upload). Preserved as a load-bearing carve-out, not normalised away.
            "cornerstone-publish-dep": self.is_cornerstone,
            "console-script-smoke": self.is_cli,
            # DP-01 layering check — only when the repo declares a contract.
            "lint-imports": self.has_importlinter,
            # WUI-07 — the UI gate, only for consoles carrying a tests/ui/ tier.
            "ui-gate": self.has_ui_tests,
            # TS-06 — install the pinned Chromium before the test steps run, but
            # only for repos whose suite actually drives one.
            "playwright-install": self.browser,
            # D-GEN freshness gate — only when the repo carries fed-resources/.
            "fedres-check": self.has_fed_resources,
            # AP-16 pin-window safety net — only when the repo carries a versioned
            # mgf-common dep (a floor to bound) and isn't the cornerstone itself.
            "mgf-common-matrix": bool(self.mgf_common_floor) and not self.is_cornerstone,
            # AP-16 B-lite ping — only the cornerstone's release fans out to siblings.
            "cornerstone-ping": self.is_cornerstone,
        }

    @property
    def vuln_audit_cmd(self) -> str:
        """The ``pip-audit`` invocation line.

        ``--skip-editable --desc`` with one retry on transient OSV-database fetch
        errors. ``--skip-editable`` is intrinsic, not a carve-out: every mgf-*
        repo installs ITSELF ``-e`` for the audit (so the mgf-* deps resolve), and
        the repo's own package is unpublishable on PyPI. NOTE: no ``--strict`` —
        ``--strict`` treats the deliberately-skipped editable self as an
        un-auditable dep and hard-fails ("distribution marked as editable"); the
        default still exits non-zero on any real advisory, which is the gate.
        ``vuln_ignore`` appends ``--ignore-vuln <CVE>`` per embargoed-but-unfixed
        advisory.
        """
        flags = "--skip-editable --desc"
        seen: set[str] = set()
        for cve in (*_FLEET_VULN_IGNORES, *self.vuln_ignore):
            if cve not in seen:
                seen.add(cve)
                flags += f" --ignore-vuln {cve}"
        cmd = f"pip-audit {flags}"
        return f"{cmd} || (sleep 30 && {cmd})"

    @property
    def index_flags(self) -> str:
        """Index-steering flags for package-consuming installs (empty unless opted in).

        The flags reference the ``$DEVPI_URL`` server-level variable (M2 §3.4) —
        the endpoint is resolved by CI at run time, never embedded in the render.
        """
        return f" {_INDEX_FLAGS}" if self.extra_index else ""

    @property
    def devpi_guard_prefix(self) -> str:
        """The ``$DEVPI_URL`` fail-fast guard as its own command list-item, or ``""``.

        Every step whose install references ``$DEVPI_URL`` carries the guard
        (MGF_INFRA.md §2: "a fail-fast guard line in each consuming step") —
        byte-identical to the guard the template fragments ship, so the whole
        fleet has ONE greppable guard shape. Rendered before the apt prefix /
        install via the same two-list-items trick as ``apt_prefix`` (the value
        carries the next item's ``\\n      - `` separator itself). Empty when
        ``extra_index`` is opted out (no ``$DEVPI_URL`` consumption to guard).
        """
        if not self.extra_index:
            return ""
        return f"{_DEVPI_GUARD}\n      - "

    def _steered(self, base: str) -> str:
        """Wrap a package-consuming install with the devpi index flags + a
        transient-failure retry of the SAME steered command.

        Returns ``base`` unchanged when ``extra_index`` is opted out. Otherwise:
        ``<base><flags> || (sleep 15 && <base><flags>)``. The retry re-runs the
        index-steered command — it NEVER falls back to a bare (index-less) install.
        A bare retry would resolve the mgf-* deps from public PyPI (dependency-
        confusion + stale-version testing), and since those deps live on the
        devpi index a public resolution is *always* wrong, never a useful
        fallback — so the retry buys transient-network resilience without the hole.
        """
        if not self.extra_index:
            return base
        steered = f"{base}{self.index_flags}"
        return f"{steered} || (sleep 15 && {steered})"

    @property
    def apt_prefix(self) -> str:
        """An ``apt-get install`` list-item rendered before a build install, or ``""``.

        The value expands the single ``      - {{..._INSTALL...}}`` template line
        into TWO YAML list items — the apt-get line, then the pip install — by
        carrying the next item's ``\\n      - `` prefix itself. Empty (no declared
        ``apt_packages``) leaves the install line untouched.
        """
        if not self.apt_packages:
            return ""
        pkgs = " ".join(self.apt_packages)
        return (
            f"apt-get update -qq && apt-get install -y -qq --no-install-recommends {pkgs}\n      - "
        )

    @property
    def fedres_install(self) -> str:
        """What the D-GEN freshness step installs to get the ``fedres`` generator.

        ``-e .`` for mgf-fedres itself (check the generator with the code under
        review — see ``is_fedres``), the published distribution for everyone else.
        """
        return "-e ." if self.is_fedres else "mgf-fedres"

    @property
    def extras_install_cmd(self) -> str:
        """The lint/typecheck/test editable-install line (``.[dev,...]``).

        The mgf-* deps in ``dependencies`` / ``[dev]`` resolve from the devpi
        index, so these install steps carry the ``$DEVPI_URL`` guard + the index
        flags + a steered retry (see ``_steered``). ``extra_index = false``
        collapses it to the plain editable install.
        """
        base = f'pip install --quiet -e "{self.extras_install}"'
        return f"{self.devpi_guard_prefix}{self.apt_prefix}{self._steered(base)}"

    @property
    def vuln_pkg_install(self) -> str:
        """The vuln-scan step's install line (``pip-audit`` + the editable surface).

        Same guarded, index-steered shape as the other install steps; auditing the
        editable closure needs the mgf-* deps resolvable from devpi too.
        """
        base = f'pip install --quiet pip-audit -e "{self.extras_install}"'
        return f"{self.devpi_guard_prefix}{self.apt_prefix}{self._steered(base)}"

    @property
    def sbom_pkg_install(self) -> str:
        """The sbom step's consumer-surface install command line."""
        base = f'pip install --quiet "{self.sbom_install}"'
        return f"{self.devpi_guard_prefix}{self.apt_prefix}{self._steered(base)}"

    @property
    def tagcoh_install(self) -> str:
        """The tag-coherence step's editable install command line."""
        return f"{self.devpi_guard_prefix}{self._steered('pip install --quiet -e .')}"

    @property
    def smoke_install(self) -> str:
        """The smoke step's wheel install command line."""
        wheel = f"dist/{self.dist_name}-${{CI_COMMIT_TAG#v}}-py3-none-any.whl"
        # /tmp/smoke is the canonical smoke-venv path INSIDE the CI container
        # (created by `python -m venv /tmp/smoke` in base.yml) — it is a literal
        # in generated YAML, not a host-side temp file this process touches.
        base = "/tmp/smoke/bin/pip install --no-cache-dir"  # noqa: S108
        if not self.extra_index:
            return f"{base} {wheel}"
        return f"{self.devpi_guard_prefix}{base}{self.index_flags} {wheel}"


def _shallowest_import_name(repo_dir: Path) -> str:
    """The package import name == the shallowest ``__init__.py`` under ``src/``.

    ``src/mgf/common/__init__.py`` -> ``mgf.common``;
    ``src/mgf/vm/core/__init__.py`` -> ``mgf.vm.core`` (the single-level glob
    ``src/mgf/*/__init__.py`` misses this — mgf-vm-core has no ``src/mgf/vm``
    ``__init__.py`` — so resolve by shallowest leaf instead).
    """
    src = repo_dir / "src"
    inits = sorted(
        src.rglob("__init__.py"),
        key=lambda p: (len(p.relative_to(src).parts), str(p)),
    )
    if not inits:
        raise FileNotFoundError(f"no src/**/__init__.py under {src} — cannot derive import name")
    pkg_dir = inits[0].parent.relative_to(src)
    return ".".join(pkg_dir.parts)


def _read_pyproject(repo_dir: Path) -> dict[str, object]:
    import tomllib

    path = repo_dir / "pyproject.toml"
    if not path.is_file():
        return {}
    data: dict[str, object] = tomllib.loads(path.read_text(encoding="utf-8"))
    return data


def _project_table(pyproject: dict[str, object]) -> dict[str, object]:
    project = pyproject.get("project")
    return project if isinstance(project, dict) else {}


def _optional_dep_names(project: dict[str, object]) -> list[str]:
    opt = project.get("optional-dependencies")
    return list(opt.keys()) if isinstance(opt, dict) else []


def _depends_on(project: dict[str, object], dist: str) -> bool:
    """Whether ``project`` requires ``dist`` — in core deps or in ANY extra.

    Matches the distribution name at a requirement boundary (``mgf-uitest-web``,
    ``mgf-uitest-web>=0.1,<0.2``, ``mgf-uitest-web[x]``) without matching a longer
    name that merely starts with it (``mgf-uitest-web-extras``).
    """
    reqs: list[str] = []
    core = project.get("dependencies")
    if isinstance(core, list):
        reqs += [r for r in core if isinstance(r, str)]
    opt = project.get("optional-dependencies")
    if isinstance(opt, dict):
        for group in opt.values():
            if isinstance(group, list):
                reqs += [r for r in group if isinstance(r, str)]
    pattern = re.compile(rf"^\s*{re.escape(dist)}(?![\w.-])", re.IGNORECASE)
    return any(pattern.match(r) for r in reqs)


def _tool_table(pyproject: dict[str, object], name: str) -> dict[str, object]:
    """Return ``[tool.<name>]`` as a dict (empty when absent / malformed)."""
    tool = pyproject.get("tool")
    if isinstance(tool, dict):
        table = tool.get(name)
        if isinstance(table, dict):
            return table
    return {}


# The carve-out keys derive_params reads. Anything else in [tool.mgf-fed] draws
# the unknown-key note below — the tombstone check that keeps deleted knobs dead
# (M2 §3.3: `skip_editable` was parsed-but-never-read and is gone; --skip-editable
# is intrinsic to vuln_audit_cmd).
_KNOWN_CI_KEYS = frozenset(
    {"browser", "coverage_floor", "vuln_ignore", "extra_index", "apt_packages", "sbom_extras"}
)

#: Plausible-but-wrong knobs, mapped to the real key that does the job. A typo is
#: caught lexically by difflib; THESE are not typos — they are the wrong *concept*,
#: so no edit distance finds them. ``qt = true`` (mgf-uitest-desktop, 2026-07-29)
#: reads like ``browser = true`` and was written by analogy with it; it parsed, drew
#: the stderr note, and rendered a pipeline with no Qt libs — three red runs before
#: anyone looked at the note. Add an entry here the first time a knob is invented,
#: not the second.
_CI_KEY_ALIASES = {
    "qt": "apt_packages",  # Qt needs system libs: apt_packages = ["libgl1", ...]
    "gui": "apt_packages",
    "headless": "browser",
    "chromium": "browser",
    "playwright": "browser",
    "system_packages": "apt_packages",
    "apt": "apt_packages",
    "packages": "apt_packages",
    "coverage": "coverage_floor",
    "fail_under": "coverage_floor",
    "min_coverage": "coverage_floor",
    "index": "extra_index",
    "devpi": "extra_index",
    "ignore_vulns": "vuln_ignore",
    "vuln_ignores": "vuln_ignore",
}

#: Knobs that once existed and were deliberately deleted. A repo still declaring one
#: is not typing badly — it is running on a stale mental model, so it gets the reason
#: rather than a suggestion (M2 §3.3).
_CI_KEY_TOMBSTONES = {
    "skip_editable": "removed in M2 §3.3 — `--skip-editable` is intrinsic to vuln_audit_cmd",
}


def _key_hint(key: str) -> str:
    """Return a ``(did you mean …)`` clause for an unrecognised carve-out key.

    Three sources, most-specific first: a tombstone (the knob was deleted, and why),
    a semantic alias (the wrong concept — ``qt`` is not a typo for anything), then
    a lexical near-match for genuine typos (``apt_packges``).
    """
    if (why := _CI_KEY_TOMBSTONES.get(key)) is not None:
        return f" ({why})"
    if (alias := _CI_KEY_ALIASES.get(key)) is not None:
        return f" (did you mean `{alias}`?)"
    near = difflib.get_close_matches(key, sorted(_KNOWN_CI_KEYS), n=1, cutoff=0.7)
    return f" (did you mean `{near[0]}`?)" if near else ""


def unknown_ci_keys(pyproject: dict[str, object]) -> tuple[str, ...]:
    """Return the ``[tool.mgf-fed]`` keys the CI renderer does not read, sorted.

    Pure — no printing, no side effects — so callers choose how loud to be:
    :func:`_mgf_ci` keeps its stderr note (the render must stay usable), while
    ``drift-check`` raises the same fact to a **finding**, which is what makes it
    fleet-visible and CI-enforceable. A note on stderr during a 46-repo sweep is
    not a signal; a finding is.
    """
    table = _tool_table(pyproject, "mgf-fed")
    # `policy` is the fleet-policy subtable in mgf-standard's own pyproject
    # ([tool.mgf-fed.policy], read by _template.load_policy) — not a carve-out.
    return tuple(sorted(k for k in table if k not in _KNOWN_CI_KEYS and k != "policy"))


def unknown_ci_key_detail(keys: Iterable[str]) -> str:
    """Render the human-facing sentence for a set of unrecognised carve-out keys."""
    listed = ", ".join(f"`{k}`{_key_hint(k)}" for k in keys)
    return (
        f"[tool.mgf-fed] key(s) not read by the CI renderer: {listed}"
        f" — the rendered pipeline is NOT shaped by them."
        f" Known keys: {', '.join(sorted(_KNOWN_CI_KEYS))}"
    )


def _mgf_ci(pyproject: dict[str, object]) -> dict[str, object]:
    """Return the ``[tool.mgf-fed]`` woodpecker CI carve-out table (empty when absent).

    The carve-outs live in the dedicated ``[tool.mgf-fed]`` namespace, NOT
    ``[tool.mgf]`` — the latter is mgf-common's ``MgfSettings`` config source
    (``extra="forbid"``), so a subtable there breaks ``MgfSettings()``/``bootstrap()``.
    The transitional ``[tool.mgf.ci]`` fallback was dropped once the fleet finished
    migrating (2026-06-15) — every repo now declares ``[tool.mgf-fed]``.

    Unknown keys do not fail the render (it must stay usable), but they are NOT
    merely noted any more. The note here is the courtesy copy; the enforcement lives
    in ``drift-check``, which reports the same keys as a ``ci-unknown-key`` finding
    and so fails ``scaffold`` and CI. mgf-uitest-desktop declared ``qt = true`` for
    weeks: it parsed, printed this note into the middle of a 46-repo sweep, reported
    success, and rendered a pipeline with no Qt libraries. A warning that only ever
    reaches stderr is a warning nobody receives.
    """
    table = _tool_table(pyproject, "mgf-fed")
    if unknown := unknown_ci_keys(pyproject):
        print(f"note: {unknown_ci_key_detail(unknown)}", file=sys.stderr)
    return table


def _coverage_floor(pyproject: dict[str, object]) -> int:
    """Derive the 3.12 coverage gate floor.

    Precedence: ``[tool.mgf-fed].coverage_floor`` → ``[tool.coverage.report]
    .fail_under`` → ``_DEFAULT_COVERAGE_FLOOR`` (the single-sourced default). The
    ``[tool.mgf-fed]`` override lets a repo pin a CI gate that differs from its
    pyproject fail_under (e.g. mgf-common runs the gate at 85 while its pyproject
    fail_under is 80).
    """
    ci = _mgf_ci(pyproject)
    floor = ci.get("coverage_floor")
    if isinstance(floor, (int, float)) and not isinstance(floor, bool):
        return int(floor)
    report = _tool_table(pyproject, "coverage").get("report")
    if isinstance(report, dict):
        report_floor = report.get("fail_under")
        if isinstance(report_floor, (int, float)) and not isinstance(report_floor, bool):
            return int(report_floor)
    return _DEFAULT_COVERAGE_FLOOR


def _str_list(value: object) -> tuple[str, ...]:
    """Coerce a TOML value to a tuple of strings (empty when absent / wrong type)."""
    if isinstance(value, list):
        return tuple(str(x) for x in value)
    return ()


_MGF_COMMON_PIN = re.compile(r"mgf-common(?:\[[^\]]*\])?\s*>=\s*(\d+\.\d+)")
# devpi's oldest served mgf-common: the Phase-2 cutover did NOT backfill <0.44.5. The
# compat-matrix can only re-pin to a version devpi actually serves, so the matrix floor
# is clamped UP to this — an older declared pin (0.41-0.43) resolves to >=0.44.5 in
# practice anyway (those releases are un-installable), so nothing real is lost. Raise
# this if devpi is ever backfilled or its floor moves.
_DEVPI_MIN_MGF_COMMON = (0, 44)


def _floor_in(deps: object) -> str:
    """The clamped MAJOR.MINOR of the first versioned mgf-common ``>=`` pin in a
    dependency list (empty when none / wrong type). Shared by core-deps and
    runtime-extra scanning."""
    if not isinstance(deps, list):
        return ""
    for dep in deps:
        if isinstance(dep, str):
            m = _MGF_COMMON_PIN.search(dep)
            if m:
                floor = tuple(int(x) for x in m.group(1).split("."))
                if floor < _DEVPI_MIN_MGF_COMMON:
                    return f"{_DEVPI_MIN_MGF_COMMON[0]}.{_DEVPI_MIN_MGF_COMMON[1]}"
                return m.group(1)
    return ""


def _mgf_common_floor(project: dict[str, object], runtime_extras: tuple[str, ...] = ()) -> str:
    """The MAJOR.MINOR the AP-16 compat-matrix re-pins mgf-common to: the repo's
    declared ``>=`` floor, clamped UP to the oldest version devpi serves (0.44). So
    ``mgf-common[standards]>=0.46,<0.50`` -> ``0.46``, but ``>=0.41`` -> ``0.44``
    (0.41-0.43 are not on devpi — the matrix couldn't install them). A patch-level
    floor (``>=0.46.2``) yields the MINOR series ``0.46``. Empty when the repo declares
    no versioned mgf-common ``>=`` dependency (the matrix stays off).

    The pin is read from core ``dependencies`` first; if none is declared there it
    falls back to the repo's *runtime* extras — the same surface the matrix step
    installs (``.[dev,<runtime_extras>]``, i.e. every extra except ``dev``/``standards``).
    That covers repos whose runtime mgf-common lives only in an optional extra —
    notably ``mgf-fed``, whose core is zero-dependency and whose real (24-import)
    mgf-common dep sits in ``[cli]``. A ``standards``-only conformance pin is NOT a
    runtime dep (excluded from ``runtime_extras``), so it correctly leaves the matrix
    OFF — no runtime floor to guard."""
    project_get = project.get
    core = _floor_in(project_get("dependencies", []))
    if core:
        return core
    optional = project_get("optional-dependencies", {})
    if isinstance(optional, dict):
        for extra in runtime_extras:
            floor = _floor_in(optional.get(extra, []))
            if floor:
                return floor
    return ""


def derive_params(member: Member, root: Path | None = None) -> WoodpeckerParams:
    """Derive the template parameters for ``member`` from metadata + on-disk layout."""
    root = root or federation_root()
    repo_dir = root / member.repo
    pyproject = _read_pyproject(repo_dir)
    project = _project_table(pyproject)

    dist_dash = member.repo  # repo slug == published project name (== dashed dist)
    dist_under = dist_dash.replace("-", "_")

    ci = _mgf_ci(pyproject)

    # Optional-dependency surface, minus the always-installed `dev` and the
    # mgf-standard `standards` extra (a conformance helper, not a runtime dep).
    opt_names = _optional_dep_names(project)
    runtime_extras = [e for e in opt_names if e not in ("dev", "standards")]

    extras_install = ".[" + ",".join(["dev", *runtime_extras]) + "]"

    # SBOM consumer surface: the derived runtime extras by default, OR the
    # explicit [tool.mgf-fed].sbom_extras override (e.g. fastapi excludes the
    # `testing` extra — uvicorn/httpx are test-only, not consumer runtime deps).
    sbom_override = _str_list(ci.get("sbom_extras"))
    sbom_extras = list(sbom_override) if "sbom_extras" in ci else runtime_extras
    sbom_install = (".[" + ",".join(sbom_extras) + "]") if sbom_extras else "."

    scripts = project.get("scripts")
    console_script = next(iter(scripts), "") if isinstance(scripts, dict) and scripts else ""

    # Zig (and any other non-Python) members ship no ``src/**/__init__.py``;
    # their template (base-zig.yml) carries no import-name token, so a missing
    # package is expected, not an error.
    import_name = "" if member.lang != "python" else _shallowest_import_name(repo_dir)

    return WoodpeckerParams(
        import_name=import_name,
        dist_name=dist_under,
        dist_name_dash=dist_dash,
        extras_install=extras_install,
        sbom_install=sbom_install,
        coverage_floor=_coverage_floor(pyproject),
        lang=member.lang,
        is_cli=bool(console_script),
        console_script=console_script,
        has_supervisor="mgf-test-supervisor" in member.deps,
        is_cornerstone=member.repo == "mgf-common",
        is_fedres=member.repo == "mgf-fedres",
        vuln_ignore=_str_list(ci.get("vuln_ignore")),
        extra_index=bool(ci.get("extra_index", True)),
        apt_packages=_str_list(ci.get("apt_packages")),
        # TS-06: a suite that drives a real Chromium needs the browser installed in
        # the test job. Opt-in per repo — nobody else pays the download.
        browser=bool(ci.get("browser", False)),
        # A contract via either the standalone .importlinter file (mgf-common) or
        # [tool.importlinter] in pyproject (mgf-cloud-app) gates the lint-imports line.
        has_importlinter=(repo_dir / ".importlinter").is_file()
        or bool(_tool_table(pyproject, "importlinter")),
        # WUI-07: depending on the browser-UI harness IS the declaration of a UI spec
        # tier — the specs cannot exist without it (their conftest imports
        # ``mgf.uitest_web``), so the gate cannot be added-and-forgotten. Read from
        # THIS repo's pyproject rather than the registry's deps: the registry is
        # regenerated on its own cadence, and a stale entry would silently render the
        # gate away — the exact failure this gate exists to catch.
        has_ui_tests=_depends_on(project, "mgf-uitest-web"),
        # The fedres convention (a generated fed-resources/ with an authoring
        # source/project.toml) gates the `fedres check` freshness step.
        has_fed_resources=(repo_dir / "fed-resources" / "source" / "project.toml").is_file(),
        # AP-16: the MAJOR.MINOR floor of this repo's mgf-common pin (compat-matrix).
        # Read from core deps, falling back to the runtime extras the matrix installs
        # (so mgf-fed's `[cli]`-only mgf-common floor is guarded like everyone else's).
        mgf_common_floor=_mgf_common_floor(project, tuple(runtime_extras)),
    )


def _substitute(text: str, params: WoodpeckerParams) -> str:
    """Replace every ``{{TOKEN}}`` in ``text`` with the derived value."""
    mapping = {
        "{{IMPORT_NAME}}": params.import_name,
        "{{DIST_NAME}}": params.dist_name,
        "{{DIST_NAME_DASH}}": params.dist_name_dash,
        "{{EXTRAS_INSTALL}}": params.extras_install,
        "{{FEDRES_INSTALL}}": params.fedres_install,
        "{{SBOM_INSTALL}}": params.sbom_install,
        "{{COVERAGE_FLOOR}}": str(params.coverage_floor),
        "{{CONSOLE_SCRIPT}}": params.console_script,
        # Carve-out-aware whole-command tokens ([tool.mgf-fed]).
        "{{EXTRAS_INSTALL_CMD}}": params.extras_install_cmd,
        "{{VULN_PKG_INSTALL}}": params.vuln_pkg_install,
        "{{VULN_AUDIT_CMD}}": params.vuln_audit_cmd,
        "{{SBOM_PKG_INSTALL}}": params.sbom_pkg_install,
        "{{TAGCOH_INSTALL}}": params.tagcoh_install,
        "{{SMOKE_INSTALL}}": params.smoke_install,
        # AP-16 compat-matrix floor (e.g. "0.46"); "" when the repo has no mgf-common
        # pin, in which case the matrix fragment is gated off and never references it.
        "{{MGF_COMMON_FLOOR}}": params.mgf_common_floor,
    }
    # Replace longest token first so a token that is a prefix of another
    # ({{EXTRAS_INSTALL}} ⊂ {{EXTRAS_INSTALL_CMD}}, {{DIST_NAME}} ⊂
    # {{DIST_NAME_DASH}}) cannot corrupt the longer one.
    for token in sorted(mapping, key=len, reverse=True):
        text = text.replace(token, mapping[token])
    return text


def _apply_markers(text: str, params: WoodpeckerParams, root: Path | None) -> str:
    """Replace each ``# <<INSERT:NAME>>`` marker line with its fragment, or drop it.

    The marker occupies its own line. When the gate is True the whole line is
    replaced by the fragment text (already token-substituted + pre-indented);
    when False the line (and its trailing newline) is removed.
    """
    gates = params.gates
    out_lines: list[str] = []
    for line in text.splitlines(keepends=True):
        stripped = line.strip()
        if stripped.startswith("# <<INSERT:") and stripped.endswith(">>"):
            name = stripped[len("# <<INSERT:") : -len(">>")]
            if gates.get(name, False):
                fragment = canonical_text(f"woodpecker/{_FRAGMENTS[name]}", root)
                out_lines.append(_substitute(fragment, params))
            # gate False (or unknown marker): drop the marker line entirely.
            continue
        out_lines.append(line)
    return "".join(out_lines)


def render_woodpecker(member: Member, root: Path | None = None) -> str:
    """Render ``member``'s canonical ``.woodpecker.yml`` as text.

    Deterministic and pure-text: reads the template via ``_template.canonical_text``,
    substitutes the ``{{TOKEN}}`` parameters, and splices opt-in fragments at the
    ``# <<INSERT:NAME>>`` markers. Zig members render from ``base-zig.yml`` through
    the same substitute+markers path (uniform contract — since the zig fedres
    fragment landed, the old verbatim short-circuit would drop opt-in gates).
    """
    root = root or federation_root()
    params = derive_params(member, root)

    template = "woodpecker/base-zig.yml" if params.lang == "zig" else "woodpecker/base.yml"
    base = canonical_text(template, root)
    base = _substitute(base, params)
    return _apply_markers(base, params, root)


def _template_root(root: Path | None = None) -> Path:
    """The directory holding the woodpecker template files (for tooling/tests)."""
    return template_dir(root) / "woodpecker"
