"""``mgf-fed gates`` — run the fast static quality gates across a selection of repos.

Composes the **ambient `ruff`** (one pinned version → a uniform fleet-wide lint+format check,
which is the whole point of the `ruff>=0.4,<0.16` pin) as subprocesses. These two gates need
no per-repo dependency install, so they sweep the whole fleet in seconds.

The heavier gates (mypy, pytest, pip-audit) need each repo's deps installed — for those, use
``mgf-fed local-ci`` (the real ``.woodpecker`` pipeline in containers, which installs deps
per step). This keeps ``gates`` fast and dependency-free.
"""

from __future__ import annotations

import shutil
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path

from mgf.fed._roster import members


def _find_ruff() -> str | None:
    """Locate ruff: PATH first, else alongside this interpreter (same venv as mgf-fed)."""
    found = shutil.which("ruff")
    if found:
        return found
    candidate = Path(sys.executable).parent / "ruff"
    return str(candidate) if candidate.is_file() else None


@dataclass(frozen=True, slots=True)
class GateResult:
    repo: str
    ruff_check: bool | None  # None = N/A (no src/tests to lint)
    ruff_format: bool | None
    detail: str

    @property
    def ok(self) -> bool:
        return self.ruff_check is not False and self.ruff_format is not False


def _lint_dirs(repo_dir: Path) -> list[str]:
    return [str(repo_dir / d) for d in ("src", "tests") if (repo_dir / d).is_dir()]


def _run(args: list[str]) -> tuple[bool, str]:
    try:
        p = subprocess.run(args, capture_output=True, text=True, timeout=180)  # noqa: S603
    except (OSError, subprocess.SubprocessError) as exc:
        return False, str(exc)
    return p.returncode == 0, (p.stdout + p.stderr).strip()


def run_gates(*, shape: str = "python", fix: bool = False, root: Path | None = None) -> int:
    ruff = _find_ruff()
    if ruff is None:
        print("ruff not found (PATH or alongside the interpreter) — install mgf-fed[dev].")
        return 2

    results: list[GateResult] = []
    for m in members(root):
        if shape != "all" and m.lang != shape:
            continue
        dirs = _lint_dirs(m.repo_dir)
        if not dirs:
            results.append(GateResult(m.repo, None, None, "no src/tests"))
            continue
        chk_ok, chk_out = _run([ruff, "check", "--no-cache", *(["--fix"] if fix else []), *dirs])
        fmt_ok, _fmt_out = _run(
            [ruff, "format", "--no-cache", *([] if fix else ["--check"]), *dirs]
        )
        detail = ""
        if not chk_ok:
            detail = next(
                (ln for ln in reversed(chk_out.splitlines()) if ln.strip()), "ruff check failed"
            )
        elif not fmt_ok:
            detail = "format diff" if not fix else "reformatted"
        results.append(GateResult(m.repo, chk_ok, fmt_ok, detail))

    _render(results, fix=fix)
    return 1 if any(not r.ok for r in results) else 0


def _glyph(b: bool | None) -> str:
    return "—" if b is None else ("✓" if b else "✗")


def _render(results: list[GateResult], *, fix: bool) -> None:
    hdr = f"{'repo':<22} ruff fmt  notes"
    print(hdr)
    print("─" * max(len(hdr), 40))
    for r in results:
        print(f"{r.repo:<22}  {_glyph(r.ruff_check):>2}  {_glyph(r.ruff_format):>2}  {r.detail}")
    checked = [r for r in results if r.ruff_check is not None]
    failed = [r for r in checked if not r.ok]
    print("─" * max(len(hdr), 40))
    verb = "fixed" if fix else "checked"
    print(
        f"{len(checked)} {verb} · {len(checked) - len(failed)} clean · {len(failed)} failing"
        + (f" → {', '.join(r.repo for r in failed)}" if failed else "")
    )
