"""``mgf-fed ci-heal`` — restart pipelines red on the clone step alone (PAPER-118).

The fleet's known clone credential-injection flake (mgf-standard PAPER-109) reds a
pipeline through no fault of the change: Woodpecker's per-build netrc is absent for
the whole step lifetime, so plugin-git's own in-step retries (``attempts: 5``)
cannot recover — only a fresh pipeline start re-injects the credential. The
standing mitigation was a MANUAL restart; this verb automates exactly that, and
nothing more:

- **Detection signature:** a ``failure`` pipeline whose ONLY failed step is
  ``clone`` (every other step skipped/succeeded).
- **Bounded to once:** a pipeline is skipped when ANY later pipeline exists for
  the same commit (a restart — ours or a human's — already happened). A second
  identical failure stays red for a human.
- **Polls Woodpecker only** (self-hosted, unmetered) — never Codeberg
  commit-status (the PAPER-110 rate budget); all calls ride the PAPER-119
  429-backoff helper.

Intended to run from a cron pipeline or the supervisor's cockpit:
``mgf-fed ci-heal [--repo R ...] [--dry-run]``.
"""

from __future__ import annotations

import os
import urllib.error
from typing import TYPE_CHECKING, Any

from mgf.fed._http import request_json
from mgf.fed.cli._ping import _DEFAULT_OWNER, _DEFAULT_WP_URL, _token

if TYPE_CHECKING:
    import argparse

# Recent pipelines examined per repo. The flake reds the HEAD build; deep history
# is a human's archaeology, not a healer's.
_DEFAULT_LIMIT = 10


def add_ci_heal_args(parser: argparse.ArgumentParser) -> None:
    """Register the ``ci-heal`` subcommand's arguments."""
    parser.add_argument(
        "--repo",
        action="append",
        default=None,
        help="repo to scan (repeatable; default: every registry member)",
    )
    parser.add_argument(
        "--owner", default=_DEFAULT_OWNER, help=f"forge owner (default {_DEFAULT_OWNER})"
    )
    parser.add_argument(
        "--url",
        default=os.environ.get("WOODPECKER_URL", _DEFAULT_WP_URL),
        help="Woodpecker base URL",
    )
    parser.add_argument(
        "--token", default=None, help="WP API token (else $WOODPECKER_TOKEN / my_stuff file)"
    )
    parser.add_argument(
        "--limit",
        type=int,
        default=_DEFAULT_LIMIT,
        help=f"recent pipelines examined per repo (default {_DEFAULT_LIMIT})",
    )
    parser.add_argument(
        "--dry-run", action="store_true", help="report what would restart; trigger nothing"
    )


def _steps(pipeline: dict[str, Any]) -> list[dict[str, Any]]:
    """Flatten a pipeline detail's workflow steps (v2 ``workflows[].children[]``,
    with a fallback for the older ``procs`` key)."""
    out: list[dict[str, Any]] = []
    for wf in pipeline.get("workflows") or pipeline.get("procs") or []:
        out.extend(wf.get("children") or [])
    return out


def _is_clone_only_failure(detail: dict[str, Any]) -> bool:
    """The PAPER-109 signature: the pipeline failed and its ONLY failed step is
    ``clone`` (anything else red means a real problem — never auto-restart)."""
    steps = _steps(detail)
    failed = [s for s in steps if s.get("state") == "failure"]
    return bool(failed) and all(str(s.get("name", "")) == "clone" for s in failed)


def _already_retried(pipelines: list[dict[str, Any]], pipe: dict[str, Any]) -> bool:
    """True when a LATER pipeline exists for the same commit — a restart (ours or
    a human's) already happened; the once-bound stops here."""
    return any(
        p.get("commit") == pipe.get("commit") and int(p.get("number", 0)) > int(pipe["number"])
        for p in pipelines
    )


def _heal_repo(
    base: str, owner: str, repo: str, token: str, limit: int, *, dry_run: bool
) -> list[str]:
    """Scan one repo's recent pipelines; restart (once) any clone-only red."""
    try:
        info = request_json(f"{base}/api/repos/lookup/{owner}/{repo}", token=token)
    except urllib.error.HTTPError as e:
        if e.code == 404:
            return [f"SKIP  {repo}: not activated on Woodpecker (404)"]
        if e.code == 401:
            return [f"AUTH  {repo}: 401 — token expired; mint per MGF_INFRA §2"]
        return [f"FAIL  {repo}: lookup HTTP {e.code}"]
    except urllib.error.URLError as e:
        return [f"FAIL  {repo}: lookup unreachable ({e.reason})"]
    repo_id = info.get("id")
    if not repo_id:
        return [f"FAIL  {repo}: lookup returned no id"]

    try:
        listing = request_json(f"{base}/api/repos/{repo_id}/pipelines?perPage={limit}", token=token)
    except (urllib.error.HTTPError, urllib.error.URLError) as e:
        return [f"FAIL  {repo}: pipeline list failed ({e})"]
    # request_json returns a dict; Woodpecker's list endpoint returns an array,
    # which json.loads surfaces as a list — tolerate both shapes.
    pipelines: list[dict[str, Any]] = (
        listing if isinstance(listing, list) else listing.get("pipelines", [])
    )

    lines: list[str] = []
    for pipe in pipelines:
        if pipe.get("status") != "failure" or "number" not in pipe:
            continue
        number = pipe["number"]
        if _already_retried(pipelines, pipe):
            lines.append(f"SKIP  {repo}#{number}: a later pipeline exists for this commit")
            continue
        try:
            detail = request_json(f"{base}/api/repos/{repo_id}/pipelines/{number}", token=token)
        except (urllib.error.HTTPError, urllib.error.URLError) as e:
            lines.append(f"FAIL  {repo}#{number}: detail fetch failed ({e})")
            continue
        if not _is_clone_only_failure(detail):
            continue
        if dry_run:
            lines.append(f"WOULD {repo}#{number}: clone-only red — would restart once")
            continue
        try:
            new = request_json(
                f"{base}/api/repos/{repo_id}/pipelines/{number}", token=token, method="POST"
            )
        except (urllib.error.HTTPError, urllib.error.URLError) as e:
            lines.append(f"FAIL  {repo}#{number}: restart failed ({e})")
            continue
        lines.append(
            f"HEAL  {repo}#{number}: clone-only red → restarted as #{new.get('number', '?')}"
        )
    return lines


def run_ci_heal(args: argparse.Namespace) -> int:
    """Restart (once) every recent pipeline red on the clone step alone."""
    repos: list[str] = list(args.repo or [])
    if not repos:
        from mgf.fed._roster import members

        repos = [m.repo for m in members()]

    token = _token(getattr(args, "token", None))
    if not token:
        print(
            "ci-heal: no token — set $WOODPECKER_TOKEN or "
            "mgf-standard/my_stuff/woodpecker_token.txt (mint per MGF_INFRA §2)."
        )
        return 2

    print(f"ci-heal — scanning {len(repos)} repo(s) for clone-only reds (PAPER-109 flake)")
    healed = failed = 0
    for repo in repos:
        for line in _heal_repo(args.url, args.owner, repo, token, args.limit, dry_run=args.dry_run):
            print(f"  {line}", flush=True)
            healed += line.startswith(("HEAL", "WOULD"))
            failed += line.startswith(("FAIL", "AUTH"))
    print(f"{healed} restart(s){' planned' if args.dry_run else ''}; {failed} error(s).")
    return 1 if failed else 0


__all__ = ["add_ci_heal_args", "run_ci_heal"]
