#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""controller_gates.py — THE entry point for this repo's gates. Run from `controller/`:

    python3 scripts/controller_gates.py            # every gate
    python3 scripts/controller_gates.py --fast     # only gates that touch no network and no
                                                   # container runtime (what .githooks/pre-push runs)

Gates, in order (all must pass; **non-zero exit on any failure**):

  1. template-id        every template id/handle referenced by the dashboard resolves
  2. emoji              no emoji in the Hungarian UI (design-system v2)
  3. native-confirm     no native confirm()/prompt() — they freeze browser automation
  4. offbox-rename      the persisted `offbox` key is never re-guessed as `offbox_target`
  5. app-row-dedup      no duplicated app-row markup in the dashboard templates
  6. mojibake           no double-encoded UTF-8 in Hungarian copy
  7. docker-v           every `docker … -v` mount is a named volume or a proven host path
  8. reuse-refs         every path cited by this repo's REUSE.md still resolves

WHY THIS FILE EXISTS (2026-08-02, closing R-29 leg (a) and half of leg (b)).

A census of all thirteen gate scripts across the four felhom repos found one clean correlation:
**every check a CLAUDE.md tells a person to run was passing, and two of the four nobody is told
to run were failing.** Of the seven gates above, this repo's CLAUDE.md named exactly two; four
were reachable only through a line in REUSE.md, and `docker_run_volume_path_gate.py` — RED at the
time of the census — through one line in REUSE.md and nothing else. The fix is not more gates, it
is one place to run them from. `app-catalog-felhom.eu/scripts/catalog_gates.py` is the canonical
shape (R-161) and this copies it deliberately rather than inventing a second one.

THE SHARED CHECKER. `reuse_refs_check.py` lives in ONE place — `felhom.eu/scripts/` — and is
invoked here across the workspace at `<repo-root>/../felhom.eu/scripts/`. It is deliberately NOT
copied into this repo: duplicating it would recreate exactly the drift it exists to detect. If the
sibling clone is absent the gate FAILS and prints the path it tried — fail-closed, because a
runner that quietly skips a gate is the inert-seam failure this project has shipped four times.

EXIT CODES. Each gate returns 0 clean / 1 convicted / 2 inconclusive. This runner exits non-zero
if any gate is non-zero, and reports 2 distinctly as INCONCLUSIVE — an undetermined result is
never a pass, but it is not a conviction either.
"""
import os
import subprocess
import sys

SCRIPTS = os.path.dirname(os.path.abspath(__file__))
CTRL = os.path.dirname(SCRIPTS)                 # <repo>/controller — every gate's cwd
REPO = os.path.dirname(CTRL)                    # <repo> — the root REUSE.md lives here
SHARED_REUSE = os.path.join(os.path.dirname(REPO), "felhom.eu", "scripts", "reuse_refs_check.py")

# (label, absolute script path, args, fast)
GATES = [
    ("template-id",    os.path.join(SCRIPTS, "template_id_gate.py"),            [],       True),
    ("emoji",          os.path.join(SCRIPTS, "emoji_gate.py"),                  [],       True),
    ("native-confirm", os.path.join(SCRIPTS, "native_confirm_gate.py"),         [],       True),
    ("offbox-rename",  os.path.join(SCRIPTS, "offbox_rename_gate.py"),          [],       True),
    ("app-row-dedup",  os.path.join(SCRIPTS, "app_row_dedup_gate.py"),          [],       True),
    ("mojibake",       os.path.join(SCRIPTS, "mojibake_gate.py"),               [],       True),
    ("docker-v",       os.path.join(SCRIPTS, "docker_run_volume_path_gate.py"), [],       True),
    ("reuse-refs",     SHARED_REUSE,                                            [REPO],   True),
]

VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"}


def hooks_armed_note(root):
    """Print a WARNING (never a failure) when this clone's pre-push hook is not switched on.

    core.hooksPath is local config and a clone does not carry it, so an unarmed clone is silent
    by construction — this is the only place it becomes visible.
    """
    try:
        val = subprocess.check_output(["git", "config", "--get", "core.hooksPath"],
                                      cwd=root, stderr=subprocess.DEVNULL).decode().strip()
    except Exception:
        val = ""
    norm = val.replace("\\", "/").rstrip("/")
    if norm == ".githooks" or norm.endswith("/.githooks"):
        return
    print("WARNING: this clone is UNARMED — core.hooksPath is %s, so the pre-push hook will not\n"
          "         run here. Switch it on once with:  git config core.hooksPath .githooks"
          % (("'" + val + "'") if val else "unset"))


def run_gate(label, path, args):
    if not os.path.exists(path):
        print("\nFAIL: gate '%s' is MISSING — tried %s" % (label, path))
        print("      A missing gate is a failure, never a skip (fail-closed). The reuse-refs")
        print("      checker is shared and lives in the felhom.eu sibling clone; it is never copied.")
        return 1
    print("\n" + "=" * 78)
    print("== gate: %s   (%s%s)" % (label, os.path.basename(path),
                                    (" " + " ".join(args)) if args else ""))
    print("=" * 78, flush=True)
    # stream the gate's own output rather than capturing it — its diagnostics are the point.
    return subprocess.call([sys.executable, path] + args, cwd=CTRL)


def main(argv):
    fast = "--fast" in argv
    unknown = [a for a in argv if a != "--fast"]
    if unknown:
        print("unknown argument(s): %s" % " ".join(unknown))
        print("usage: python3 scripts/controller_gates.py [--fast]   (run from controller/)")
        return 2

    selected = [g for g in GATES if g[3] or not fast]
    skipped = [g[0] for g in GATES if not (g[3] or not fast)]
    print("controller_gates — %d gate(s)%s" % (len(selected), "  [--fast]" if fast else ""))
    if skipped:
        print("  --fast SKIPPED (deliberate periodic runs, never in a hook): %s" % ", ".join(skipped))
    hooks_armed_note(REPO)

    results = [(label, run_gate(label, path, args)) for label, path, args, _f in selected]

    print("\n" + "=" * 78)
    print("== summary")
    print("=" * 78)
    worst = 0
    for label, rc in results:
        print("  %-18s %-13s (exit %d)" % (label, VERDICT.get(rc, "ERROR"), rc))
        if rc != 0:
            worst = 1 if rc == 1 or worst == 1 else 2
    if worst == 0:
        print("\nall controller gates OK")
        return 0
    convicted = [l for l, rc in results if rc == 1]
    undecided = [l for l, rc in results if rc not in (0, 1)]
    if convicted:
        print("\nCONVICTED: %s" % ", ".join(convicted))
    if undecided:
        print("UNDETERMINED (never a pass): %s" % ", ".join(undecided))
    return worst


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
