#!/usr/bin/env bash
# Workstation-side gate on fleet-wide host-config changes. Sourced by bin/buildbox.
#
# debian2 and debian3 were lost to one change that was validated live, never rebooted,
# and then applied to a second host. Nothing here judges whether a change affects boot:
# the whole payload is the change identity, because deciding that a given file is
# boot-irrelevant is exactly the judgement that failed.

FLEET_LEDGER="${FLEET_LEDGER:-$HOME/.local/state/overdeck/fleet/harden-ledger.jsonl}"

# shellcheck source=../host-config/change-id.sh
. "$(dirname "${BASH_SOURCE[0]}")/../host-config/change-id.sh"
fleet_change_id() { buildbox_change_id "$1"; }

fleet_ledger_record() { # changeId host event [extra-json]
  mkdir -p "$(dirname "$FLEET_LEDGER")"
  python3 -c '
import json,sys,time
extra = json.loads(sys.argv[5]) if len(sys.argv) > 5 and sys.argv[5] else {}
rec = {"ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
       "changeId": sys.argv[2], "host": sys.argv[3], "event": sys.argv[4], **extra}
open(sys.argv[1], "a").write(json.dumps(rec) + "\n")
' "$FLEET_LEDGER" "$1" "$2" "$3" "${4:-}"
}

# Refuses a second host until one host has actually rebooted on this change and been
# reachable afterwards. Re-applying to a host that already carries it is always allowed:
# that host is the one already exposed.
fleet_gate_fanout() { # changeId host
  python3 -c '
import json,os,sys
ledger, change, host = sys.argv[1], sys.argv[2], sys.argv[3]
applied, proven = set(), set()
if os.path.exists(ledger):
    for line in open(ledger):
        line = line.strip()
        if not line: continue
        try: rec = json.loads(line)
        except ValueError: continue
        if rec.get("changeId") != change: continue
        if rec.get("event") == "applied": applied.add(rec.get("host"))
        if rec.get("event") == "reboot-proven": proven.add(rec.get("host"))
if host in applied or not applied or proven:
    print("FLEET fanout-gate ok change=%s applied=%d reboot-proven=%d" % (change, len(applied), len(proven)))
    sys.exit(0)
sys.stderr.write(
    "buildbox: refusing to fan out change %s to %s — it is already on %s and no host has "
    "been rebooted on it. A change that is harmless live can still take a host off the "
    "network at its next cold boot, which is how debian2 and debian3 were lost. Reboot "
    "and prove one host first.\n" % (change, host, ", ".join(sorted(applied))))
sys.exit(1)
' "$FLEET_LEDGER" "$1" "$2"
}

# A door the change provably cannot affect. sshd on the LAN address and sshd on the
# tailnet address are the same door: both are governed by /etc/ssh/sshd_config.d, which
# is what this change class rewrites. tailscaled's own SSH server is not — it is a
# different daemon listening on port 22 of the tailnet address, so it is the only
# independent door available here, and it is proven by using it, not by reading config.
fleet_second_door() { # host ssh_user fleet_ssh_port
  local host="$1" ssh_user="$2" port="$3" ip
  if [ "$port" = 22 ]; then
    echo "buildbox: fleet sshd is on port 22, so a port-22 probe cannot distinguish tailscale SSH from sshd — no independent door can be proven" >&2
    return 1
  fi
  ip="$(tailscale status --json 2>/dev/null | python3 -c '
import json,sys
h = sys.argv[1]
try: st = json.load(sys.stdin)
except ValueError: sys.exit(0)
for peer in st.get("Peer", {}).values():
    names = [peer.get("HostName",""), peer.get("DNSName","").split(".")[0]]
    if h in names and peer.get("TailscaleIPs"):
        print(peer["TailscaleIPs"][0]); break
' "$host")"
  if [ -z "$ip" ]; then
    echo "buildbox: no tailnet address for $host — cannot prove a second door, refusing the change" >&2
    return 1
  fi
  if ssh -F /dev/null -p 22 -o BatchMode=yes -o ConnectTimeout=8 \
       -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
       "$ssh_user@$ip" true 2>/dev/null; then
    echo "FLEET second-door ok tailscale-ssh $ssh_user@$ip:22 (independent of sshd_config.d)"
    return 0
  fi
  echo "buildbox: tailscale SSH to $ip is not answering. Every other way in traverses /etc/ssh/sshd_config.d, which this change rewrites, so the host would have exactly one door and this change would be its only failure point. Refusing." >&2
  return 1
}
