"""``mgf-fed scaffold`` — apply the canonical template to a repo (the inverse of ``drift-check``).

What it can apply automatically: the policy's **byte-exact shared files** (e.g.
``scripts/verify_codeberg.py``) — copied verbatim from ``mgf-standard/templates/``. Everything
else the drift policy requires (SCOPE.md, PUBLIC_API.json, the ledger, the ``[standards]`` extra,
CI steps, the ruff pin) is **bespoke per repo**, so scaffold reports it as residual drift to fix
by hand (or generate a fresh project from ``templates/cookiecutter-mgf-project``). ``--dry-run``
previews without writing.
"""

from __future__ import annotations

from pathlib import Path

from mgf.fed import _drift, _woodpecker
from mgf.fed._roster import members
from mgf.fed._template import canonical_text, load_policy

# Drift kinds that scaffold APPLIES (so they are not reported as residual manual
# work): the byte-exact files, and — when woodpecker generation is on — both the
# generation-currency item and its fallback name-list item (the .woodpecker.yml
# is (re)written from the canonical render below).
_SCAFFOLD_APPLIES = {"exact"}
_WOODPECKER_KINDS = {"ci-generated", "ci-step"}


def run_scaffold(repos: list[str], *, dry_run: bool = False, root: Path | None = None) -> int:
    policy = load_policy(root)
    exact = policy.get("exact", {}) or {}
    generate_woodpecker = bool(policy.get("woodpecker_generated"))
    woodpecker_exempt = set((policy.get("exempt", {}) or {}).get("woodpecker", []))
    by_repo = {m.repo: m for m in members(root)}

    rc = 0
    for r in repos:
        m = by_repo.get(r)
        if m is None:
            print(f"  ! unknown repo: {r}")
            rc = 1
            continue
        d = m.repo_dir
        # A legitimately-bespoke pipeline ([exempt].woodpecker, WF-13) is left alone:
        # render nothing, write nothing — but still apply the other [exact] files and
        # report its name-list fallback drift (a missing required step there is manual).
        gen_wp = generate_woodpecker and m.repo not in woodpecker_exempt
        applied: list[str] = []
        for rel, tmpl in exact.items():
            dest = d / rel
            want = canonical_text(tmpl, root)
            if dest.is_file() and dest.read_text(encoding="utf-8") == want:
                continue  # already matches the template
            applied.append(rel)
            if not dry_run:
                dest.parent.mkdir(parents=True, exist_ok=True)
                dest.write_text(want, encoding="utf-8")

        # Generated .woodpecker.yml — render the canonical pipeline and write it
        # when the on-disk file is missing or diverges. The render is byte-stable,
        # so an already-current repo is a no-op. If the render raises (e.g. an
        # unparseable pyproject.toml), leave the file to the drift fallback rather
        # than overwriting it with a broken/partial render.
        if gen_wp:
            wp = d / ".woodpecker.yml"
            try:
                want_wp = _woodpecker.render_woodpecker(m, root)
            except Exception as exc:  # surface, don't crash the sweep
                print(f"      ! cannot render .woodpecker.yml ({type(exc).__name__}: {exc})")
            else:
                current = wp.read_text(encoding="utf-8") if wp.is_file() else None
                if current != want_wp:
                    applied.append(".woodpecker.yml (generated)")
                    if not dry_run:
                        wp.parent.mkdir(parents=True, exist_ok=True)
                        wp.write_text(want_wp, encoding="utf-8")

        residual = [
            it
            for it in _drift.check(m, policy, root).items
            if it.kind not in _SCAFFOLD_APPLIES and not (gen_wp and it.kind in _WOODPECKER_KINDS)
        ]
        verb = "would apply" if dry_run else "applied"
        suffix = f": {', '.join(applied)}" if applied else " (template files already current)"
        print(f"  {r}: {verb} {len(applied)} byte-exact file(s){suffix}")
        for it in residual:
            print(f"      manual [{it.kind}] {it.detail}")
        if residual:
            rc = 1
    return rc
