#!/usr/bin/env bash
#
# ci-local.sh — run the Woodpecker CI/CD pipeline locally, on demand.
#
# Wraps `woodpecker-cli exec` so `.woodpecker.yml` runs in real Docker
# containers against the LOCAL working tree — the same images, the same
# commands the server would run. No server, no push needed.
#
# Reliability model: on the server each step gets its own fresh git-clone, so
# steps never share on-disk state. Locally, `--local --repo-path` bind-mounts
# the SAME repo into every container, so steps that run concurrently collide on
# shared state (the mtest `.gates/` lock, `dist/`, `.coverage`, …). `exec` runs
# independent steps in parallel and has no concurrency knob — so this wrapper
# pre-filters the steps to the ones that run for the chosen event, strips their
# `when:` gate, and chains them into a strict linear order (each `depends_on`
# the previous). Result: one serialized run, no races. Slower than parallel,
# but reliable — which is the point. For a fast inner loop, run a single step.
#
# Safety: which steps run is decided by the simulated event. `publish` is
# `event: tag` only, so a `ci` (push) run physically cannot upload to PyPI.
# Only `release … --publish` ever reads the PyPI token.
#
# Privileged: set CI_LOCAL_PRIVILEGED=1 to run every step with `privileged: true`
# + `--repo-trusted-security`, i.e. full Linux capabilities + host device access.
# Needed for steps that exercise capability / namespace ops (e.g. mgf-hrb's Zig
# privilege-separation tests, which need CAP_SETPCAP). This grants the containers
# host-level access — use only on a repo you trust. Even so, a few tests need
# real hardware (USB) or an exact host cap set that no container provides.
#
# Sibling: set CI_LOCAL_REPO=/path/to/other-repo to drive that repo's pipeline.
#
# Requirements: woodpecker-cli (>=3.x), Docker, and .venv with pyyaml.
#
# Usage:
#   scripts/ci-local.sh ci                 # full validation pipeline (default)
#   scripts/ci-local.sh perf               # the cron perf sweep (perf-nightly)
#   scripts/ci-local.sh step <name>        # run one step in isolation (fast)
#   scripts/ci-local.sh release vX.Y.Z     # dry-run the release flow (NO publish)
#   scripts/ci-local.sh release vX.Y.Z --publish   # REAL release → uploads to PyPI
#   scripts/ci-local.sh lint|typecheck|build|test-3.12|...   # shorthand for `step <name>`
#   CI_LOCAL_PRIVILEGED=1 scripts/ci-local.sh step zig-build-and-test   # full caps
#
set -euo pipefail

# Defaults to this script's repo; set CI_LOCAL_REPO=/path/to/other-repo to
# drive a sibling's .woodpecker.yml from here (its own .venv is used if present).
REPO_ROOT="${CI_LOCAL_REPO:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
PIPELINE="$REPO_ROOT/.woodpecker.yml"
TOKEN_FILE="$REPO_ROOT/my_stuff/keys/pypi_tokens.txt"
PY="$REPO_ROOT/.venv/bin/python"
COMMON=(--local --repo-path "$REPO_ROOT")

die() { echo "ci-local: $*" >&2; exit 2; }
command -v woodpecker-cli >/dev/null || die "woodpecker-cli not on PATH"
command -v docker >/dev/null || die "docker not found"
[[ -f "$PIPELINE" ]] || die "no .woodpecker.yml at $PIPELINE"
[[ -x "$PY" ]] || PY=python3

# Build a serialized, event-filtered pipeline on stdout.
#   prepare <event> [steps-to-drop...]
# Keeps steps whose `when.event` includes <event> (or that have no `when`),
# minus any dropped names; strips `when`; chains them linearly in file order
# (already a valid topological order) so they run strictly sequentially.
prepare() {
  "$PY" - "$PIPELINE" "$@" <<'PY'
import os, sys, yaml
pipeline, event = sys.argv[1], sys.argv[2]
drop = set(sys.argv[3:])
privileged = bool(os.environ.get("CI_LOCAL_PRIVILEGED"))
doc = yaml.safe_load(open(pipeline))

def runs(step):
    if step.get("name") in drop:
        return False
    w = step.get("when")
    if not w:
        return True
    for clause in (w if isinstance(w, list) else [w]):
        ev = clause.get("event")
        if ev is None:
            return True
        if event in (ev if isinstance(ev, list) else [ev]):
            return True
    return False

steps = [s for s in doc.get("steps", []) if runs(s)]
prev = None
for s in steps:
    s.pop("when", None)                       # pre-filtered → gate no longer needed
    if privileged:
        s["privileged"] = True                # paired with --repo-trusted-security
    if prev:
        s["depends_on"] = [prev]              # linear chain → strict serialization
    else:
        s.pop("depends_on", None)
    prev = s["name"]
doc["steps"] = steps
doc.pop("when", None)
yaml.safe_dump(doc, sys.stdout, sort_keys=False)
PY
}

# Single step (gate + deps stripped) on stdout.
one_step() {
  "$PY" - "$PIPELINE" "$1" <<'PY'
import os, sys, yaml
doc = yaml.safe_load(open(sys.argv[1]))
hit = [s for s in doc.get("steps", []) if s.get("name") == sys.argv[2]]
if not hit:
    sys.exit(f"no step named {sys.argv[2]!r}")
s = hit[0]; s.pop("depends_on", None); s.pop("when", None)
if os.environ.get("CI_LOCAL_PRIVILEGED"):
    s["privileged"] = True
yaml.safe_dump({"steps": [s]}, sys.stdout, sort_keys=False)
PY
}

# On the server each step gets a fresh git clone; locally the repo is
# bind-mounted, so build artifacts persist across runs. Reset the ephemeral
# ones so a run starts server-like: a crashed run's mtest lock, and dist/
# (else build's `twine check` chokes on a leftover SBOM / stale wheel).
clear_stale_lock() {
  rm -f "$REPO_ROOT/.gates/.lock" 2>/dev/null || true
  rm -rf "$REPO_ROOT/dist" 2>/dev/null || true
}

resolve_pypi_token() {
  [[ -f "$TOKEN_FILE" ]] || die "no PyPI token file at $TOKEN_FILE"
  local t
  t="$(awk '/Project mfg-common scope:/{c=1;next} c&&/^pypi-/{print;exit}' "$TOKEN_FILE")"
  [[ -n "$t" ]] || t="$(awk '/All projects token:/{f=1;next} f&&/^pypi-/{print;exit}' "$TOKEN_FILE")"
  [[ -n "$t" ]] || die "could not resolve a PyPI token from $TOKEN_FILE"
  printf '%s' "$t"
}

run_serial() {   # run_serial <event> <ref> <tmpfile> [extra exec args...]
  local event="$1" ref="$2" file="$3"; shift 3
  clear_stale_lock
  local trust=()
  if [[ -n "${CI_LOCAL_PRIVILEGED:-}" ]]; then
    trust=(--repo-trusted-security)   # honour the injected `privileged: true`
    echo "  ⚠ privileged: full Linux capabilities + host device access"
  fi
  woodpecker-cli exec "${COMMON[@]}" \
    --pipeline-event "$event" --commit-branch main --commit-ref "$ref" \
    "${trust[@]}" "$@" "$file"
}

cmd="${1:-ci}"; shift || true
TMP=""; cleanup() { [[ -n "$TMP" ]] && rm -f "$TMP"; }; trap cleanup EXIT

case "$cmd" in
  ci|test|all|"")
    echo "▶ full CI validation (push event, serialized — publish gated off)"
    TMP="$(mktemp --suffix=.ci-local.yml)"; prepare push > "$TMP"
    run_serial push refs/heads/main "$TMP"
    ;;

  perf)
    echo "▶ perf sweep (cron event, serialized — runs perf-nightly)"
    TMP="$(mktemp --suffix=.ci-local.yml)"; prepare cron > "$TMP"
    run_serial cron refs/heads/main "$TMP"
    ;;

  step)
    name="${1:?usage: ci-local.sh step <step-name>}"
    echo "▶ step: $name"
    TMP="$(mktemp --suffix=.ci-local.yml)"; one_step "$name" > "$TMP"
    run_serial push refs/heads/main "$TMP"
    ;;

  release)
    version="${1:?usage: ci-local.sh release vX.Y.Z [--publish]}"
    [[ "$version" == v*.*.* ]] || die "version must look like vX.Y.Z (got '$version')"
    ref="refs/tags/${version}"
    TMP="$(mktemp --suffix=.ci-local.yml)"
    if [[ "${2:-}" == "--publish" ]]; then
      prepare tag > "$TMP"
      local_secrets="$(mktemp --suffix=.ci-local.yml)"
      umask 077; printf 'pypi_token: %s\n' "$(resolve_pypi_token)" > "$local_secrets"
      trap 'cleanup; rm -f "$local_secrets"' EXIT
      echo "🚀 REAL RELEASE $version — full tag flow incl. publish (uploads to PyPI)"
      run_serial tag "$ref" "$TMP" --secrets-file "$local_secrets"
    else
      prepare tag publish verify-pypi > "$TMP"
      echo "▶ release DRY-RUN $version (tag flow; publish + verify-pypi stripped — nothing uploaded)"
      echo "  → real release: scripts/ci-local.sh release $version --publish"
      run_serial tag "$ref" "$TMP"
    fi
    ;;

  -h|--help|help)
    sed -n '3,33p' "${BASH_SOURCE[0]}" | sed 's/^#\{0,1\} \{0,1\}//'
    ;;

  *)
    echo "▶ step: $cmd"
    TMP="$(mktemp --suffix=.ci-local.yml)"
    one_step "$cmd" > "$TMP" 2>/dev/null || die "unknown command or step: $cmd (try --help)"
    run_serial push refs/heads/main "$TMP"
    ;;
esac
