#!/bin/sh
# bullpen-pi-migrate — move a lurker container onto the pi motor.
#
#   bullpen-pi-migrate <nick> [user] [model]
#
# Runs INSIDE the container. Idempotent; re-running is harmless.
# Prerequisite from the parent:
#   incus config device add <container> lan nic network=lanmv name=lan
#
# ------------------------------------------------------------------------
# Every step here pays for a failure on 2026-08-10. None of it is precaution
# "just in case" — each line was missing once and turned the acceptance red:
#
#  1. lan.network missing -> interface DOWN, no address, and the host name is
#     unreachable no matter how well caddy is configured.
#  2. WorkingDirectory left on /root/... -> start fails with CHDIR Permission
#     denied the moment the unit stops running as root.
#  3. PATH without /opt/node/bin -> `#!/usr/bin/env node` picks up the system
#     node 20 and pi dies on `webidl.util.markAsUncloneable is not a function`.
#     THIS HITS THE LURKER AS WELL AS PI-WEB, and it never shows up by hand
#     because `su -` builds a login environment.
#  4. /opt/bullpen-src/lurker/bullpen-lurker is a COPY, not the repo. Without
#     refreshing it the container does not know run_pi and falls back to claude
#     SILENTLY ("No such file or directory: 'claude'").
#  5. The model is not "hossenfelder/big-pickle". pi knows the provider as
#     `hossenfelder-proxy` and the model as `[free] opencode/big-pickle`.
#     Without the provider prefix pi picks its built-in `opencode` and asks for
#     an API key — which reads like an access problem and is a naming one.
#  6. pi-web REGISTERS projects in ~/.pi-web/projects.json; it does not discover
#     them. An empty directory never shows up.
#  7. pi does not read CLAUDE.md. The role description belongs in
#     .pi/SYSTEM.md inside the project, where it REPLACES the default prompt
#     instead of being appended to "you are a coding agent" (usage.md:114).
#  8. .pi/SYSTEM.md requires project trust (security.md:13). Without the entry
#     pi silently does not load it and asks interactively — which a woken
#     lurker never answers.
# ------------------------------------------------------------------------
set -eu

NICK=${1:?nick missing}
U=${2:-agent}
MODEL=${3:-hossenfelder-proxy/big-pickle}
ZEN_ID='[free] opencode/big-pickle'
ALIAS=$(printf '%s' "$MODEL" | sed 's|.*/||')

say() { printf '  %s\n' "$*"; }
NODEPATH=/opt/node/bin

# --- 1) user -------------------------------------------------------------
id "$U" >/dev/null 2>&1 || useradd -m -s /bin/bash "$U"
H=$(getent passwd "$U" | cut -d: -f6)
[ -n "$H" ] || { say "ERROR: no home for $U"; exit 1; }
PROJ="$H/$NICK"
say "user $U, home $H, project $PROJ"

# --- 2) node 22 ----------------------------------------------------------
# NOT via npm install: @jmfederico/pi-web pulls a native module that has to be
# compiled on arm64 — 92 minutes for 31 s of CPU, stuck in blk_mq_get_tag.
# Copying the finished tree from a container that has it takes 5.
if [ -x "$NODEPATH/node" ]; then
    say "node $("$NODEPATH/node" -v) under /opt/node"
else
    say "ERROR: /opt/node/bin/node missing. Copy it from a prepared container:"
    say "  incus exec <source> -- tar -C /opt -cf - node | incus exec $NICK -- tar -C /opt -xf -"
    exit 1
fi

# --- 3) lan.network ------------------------------------------------------
# Do NOT set Hostname: DHCP already announces the nick.
if [ ! -f /etc/systemd/network/lan.network ]; then
    printf '[Match]\nName=lan\n\n[Network]\nDHCP=yes\nDNSSEC=no\n\n[DHCPv4]\nUseDNS=yes\nSendHostname=yes\n' \
        > /etc/systemd/network/lan.network
    systemctl restart systemd-networkd || true
    sleep 5
fi
say "lan: $(ip -4 addr show lan 2>/dev/null | grep -oE 'inet [0-9.]+' || echo 'NO ADDRESS')"

# --- 4) lurker directory and markers -------------------------------------
LD="$H/${NICK}_lurker"
if [ ! -d "$LD" ]; then
    for old in "/root/${NICK}_lurker" /home/*/"${NICK}_lurker"; do
        [ -d "$old" ] && [ "$old" != "$LD" ] && { mv "$old" "$LD"; say "moved: $old -> $LD"; break; }
    done
fi
mkdir -p "$LD" "$PROJ/.pi"
printf 'pi\n' > "$LD/.runtime"
printf '%s\n' "$MODEL" > "$LD/.model"

# --- 5) role prompt: CLAUDE.md -> AGENTS.md + .pi/SYSTEM.md --------------
if [ -f "$LD/CLAUDE.md" ]; then
    cp -a "$LD/CLAUDE.md" "$PROJ/AGENTS.md"
    cp -a "$LD/CLAUDE.md" "$PROJ/.pi/SYSTEM.md"
    say "role prompt from CLAUDE.md ($(wc -c < "$PROJ/.pi/SYSTEM.md") bytes) -> AGENTS.md + .pi/SYSTEM.md"
else
    say "WARNING: $LD/CLAUDE.md missing — $NICK runs on pi's default prompt"
fi

# --- 6) trust and model alias -------------------------------------------
mkdir -p "$H/.pi/agent" "$H/.pi-web" "$H/.config/pi-web"
# trust.json, NOT trusted-projects.json, and the value must be true/false/null.
# Both were wrong once: the wrong name made the file decoration (pi asked
# interactively, which a woken lurker never answers), and the value "always"
# -- the wording of the interactive prompt -- makes readTrustFile THROW, so
# every pi invocation dies before the model is resolved.
python3 - "$H/.pi/agent/trust.json" "$PROJ" <<'PY'
import json, os, sys
path, proj = sys.argv[1:3]
d = {}
if os.path.exists(path):
    try: d = json.load(open(path))
    except Exception: d = {}
d = {k: (v if v in (True, False, None) else True) for k, v in d.items()}
d[proj] = True
json.dump(d, open(path, "w"), indent=2)
PY

# --model matches against `name`, not only `id` (models.md, Per-model
# Overrides) — so an alias is the documented way to make a bracketed id with
# spaces addressable from a config file.
python3 - "$H/.pi/agent/settings.json" "$ALIAS" "$ZEN_ID" <<'PY'
import json, os, sys
path, alias, zid = sys.argv[1:4]
d = {}
if os.path.exists(path):
    try: d = json.load(open(path))
    except Exception: d = {}
d.setdefault("providers", {}).setdefault("hossenfelder-proxy", {}) \
 .setdefault("modelOverrides", {})[zid] = {"name": alias}
json.dump(d, open(path, "w"), indent=2, ensure_ascii=False)
PY
say "modelOverrides: '$ZEN_ID' -> '$ALIAS'"

# --- 7) pi-web config and project registration ---------------------------
PORT=$(ss -tlnp 2>/dev/null | grep -oE '127.0.0.1:[0-9]+ .*node' | grep -oE ':[0-9]+' | head -1 | tr -d :)
PORT=${PORT:-8080}
printf '{\n  "host": "127.0.0.1",\n  "port": %s,\n  "allowedHosts": ["%s.fritz.box"]\n}\n' \
    "$PORT" "$NICK" > "$H/.config/pi-web/config.json"
if ! grep -q "\"path\": \"$PROJ\"" "$H/.pi-web/projects.json" 2>/dev/null; then
    printf '{\n  "projects": [\n    {\n      "id": "%s",\n      "name": "%s",\n      "path": "%s",\n      "createdAt": "%s"\n    }\n  ]\n}\n' \
        "$(cat /proc/sys/kernel/random/uuid)" "$NICK" "$PROJ" \
        "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" > "$H/.pi-web/projects.json"
fi
say "pi-web: port $PORT, project registered"

# --- 8) units ------------------------------------------------------------
LU=/etc/systemd/system/bullpen-lurker.service
if [ -f "$LU" ]; then
    sed -i '/^User=/d; /^Environment=HOME=/d; /^Environment=LURKER_PI_CWD=/d; /^Environment=PATH=/d' "$LU"
    sed -i "s|^WorkingDirectory=.*|WorkingDirectory=$LD|" "$LU"
    sed -i "/^\[Service\]/a User=$U\nEnvironment=HOME=$H\nEnvironment=LURKER_PI_CWD=$PROJ\nEnvironment=PATH=$NODEPATH:$H/.local/bin:/usr/local/bin:/usr/bin:/bin" "$LU"
fi
for n in pi-web-sessiond pi-web; do
    [ "$n" = "pi-web" ] && EXTRA="Wants=pi-web-sessiond.service\nAfter=pi-web-sessiond.service" || EXTRA=""
    [ "$n" = "pi-web" ] && BIN=pi-web-server || BIN=pi-web-sessiond
    printf '[Unit]\nDescription=%s (@%s) — pi, user %s\nAfter=network-online.target\n%b\n\n[Service]\nType=simple\nUser=%s\nEnvironment=PATH=%s:/usr/local/bin:/usr/bin:/bin\nEnvironment=HOME=%s\nWorkingDirectory=%s\nExecStart=/usr/bin/env %s\nRestart=on-failure\nRestartSec=3\n\n[Install]\nWantedBy=multi-user.target\n' \
        "$n" "$NICK" "$U" "$EXTRA" "$U" "$NODEPATH" "$H" "$H" "$BIN" > "/etc/systemd/system/$n.service"
done

# The two keys the user migration orphans. Ownership alone is not enough:
# /etc/bullpen is drwx------ root, and a read fails on the PARENT long before
# the file's own 0600 matters. 0711 grants passage without listing.
if [ -d /etc/bullpen ]; then
    chmod 0711 /etc/bullpen
    [ -d /etc/bullpen/post-secret.d ] && {
        chown -R "$U" /etc/bullpen/post-secret.d
        chmod 0700 /etc/bullpen/post-secret.d
        chmod 0600 /etc/bullpen/post-secret.d/* 2>/dev/null || true
    }
fi
# LMCP_TOKEN lives here. Without it the post never reaches room_say at all --
# the endpoint answers first, and an HTML login page looks nothing like an
# auth error in a lurker log.
[ -f /etc/bullpen-room.env ] && { chown "$U" /etc/bullpen-room.env; chmod 0600 /etc/bullpen-room.env; }
if su - "$U" -c "cat /etc/bullpen/post-secret.d/$NICK" >/dev/null 2>&1; then
    say "posting keys readable by $U"
else
    say "WARNING: $U cannot read /etc/bullpen/post-secret.d/$NICK — posts will be rejected"
fi

chown -R "$U:$U" "$H/.pi" "$H/.pi-web" "$H/.config/pi-web" "$LD" "$PROJ"
systemctl daemon-reload
systemctl enable pi-web-sessiond pi-web >/dev/null 2>&1 || true
systemctl restart pi-web-sessiond pi-web bullpen-lurker
sleep 8
for u in bullpen-lurker pi-web-sessiond pi-web; do
    say "$u: $(systemctl is-active $u)"
done

echo
say "STILL OPEN, to be done from outside:"
say "  1. refresh the lurker copy (otherwise the container does not know run_pi):"
say "     cat repo/lurker/bullpen-lurker | incus exec $NICK -- sh -c 'cat > /opt/bullpen-src/lurker/bullpen-lurker'"
say "  2. certificate: sudo /opt/herding/pki/issue-cert.sh $NICK.fritz.box"
say "     then into the container, chgrp caddy + chmod 640 on the key"
say "  3. caddy: $NICK.fritz.box -> 127.0.0.1:$PORT with that certificate"
