#!/usr/bin/env bash
set -euo pipefail

# Apply Cascadia Nostr durability patches to an upgraded OpenClaw install on a target host.
# Usage:
#   scripts/apply-to-agent.sh agent@host [ssh-key]
# Example:
#   scripts/apply-to-agent.sh agent@192.168.40.110 ~/.ssh/core01

TARGET="${1:-}"
SSH_KEY="${2:-}"

if [[ -z "$TARGET" ]]; then
  echo "usage: $0 user@host [ssh-key]" >&2
  exit 1
fi

SSH=(ssh)
if [[ -n "$SSH_KEY" ]]; then
  SSH+=( -i "$SSH_KEY" )
fi
SSH+=( "$TARGET" )

"${SSH[@]}" 'bash -s' <<'REMOTE'
set -euo pipefail

OPENCLAW_ROOT="/home/agent/.npm-global/lib/node_modules/openclaw"
CHANNEL_FILE="$OPENCLAW_ROOT/dist/channel-NyXEePse.js"
SETUP_FILE="$OPENCLAW_ROOT/dist/setup-surface-DMovXYFN.js"
NOSTR_TOOLS_ESM="$OPENCLAW_ROOT/node_modules/nostr-tools/lib/esm/abstract-relay.js"
NOSTR_TOOLS_BUNDLE="$OPENCLAW_ROOT/node_modules/nostr-tools/lib/nostr.bundle.js"
START_SCRIPT="/home/agent/openclaw-start.sh"
CONFIG_FILE="/home/agent/.openclaw/openclaw.json"

python3 - <<'PY'
from pathlib import Path
import json

channel = Path("/home/agent/.npm-global/lib/node_modules/openclaw/dist/channel-NyXEePse.js")
setup = Path("/home/agent/.npm-global/lib/node_modules/openclaw/dist/setup-surface-DMovXYFN.js")
esm = Path("/home/agent/.npm-global/lib/node_modules/openclaw/node_modules/nostr-tools/lib/esm/abstract-relay.js")
bundle = Path("/home/agent/.npm-global/lib/node_modules/openclaw/node_modules/nostr-tools/lib/nostr.bundle.js")
start_script = Path("/home/agent/openclaw-start.sh")
config_file = Path("/home/agent/.openclaw/openclaw.json")

# 1) Nostr filter-shape fix in setup-surface bundle.
s = setup.read_text()
old = "const sub = pool.subscribeMany(relays, [{\n\t\tkinds: [4],\n\t\t\"#p\": [pk],\n\t\tsince\n\t}], {"
new = "const sub = pool.subscribeMany(relays, {\n\t\tkinds: [4],\n\t\t\"#p\": [pk],\n\t\tsince\n\t}, {"
if old in s:
    s = s.replace(old, new, 1)
setup.write_text(s)

# 2) Non-fatal subscription close in bus callback.
s = setup.read_text()
old = "\t\tonclose: (reason) => {\n\t\t\tfor (const relay of relays) {\n\t\t\t\tmetrics.emit(\"relay.message.closed\", 1, { relay });\n\t\t\t\toptions.onDisconnect?.(relay);\n\t\t\t}\n\t\t\tonError?.(/* @__PURE__ */ new Error(`Subscription closed: ${reason.join(\", \")}`), \"subscription\");\n\t\t}"
new = "\t\tonclose: (reason) => {\n\t\t\tfor (const relay of relays) {\n\t\t\t\tmetrics.emit(\"relay.message.closed\", 1, { relay });\n\t\t\t\toptions.onDisconnect?.(relay);\n\t\t\t}\n\t\t\toptions.onDebug?.(`subscription closed (non-fatal): ${reason.join(\", \")}`);\n\t\t}"
if old in s:
    s = s.replace(old, new, 1)
setup.write_text(s)

# 3) Keep Nostr provider resident until abort.
s = channel.read_text()
old = "\tbusHandle = bus;\n\tactiveBuses.set(account.accountId, bus);\n\tctx.log?.info?.(`[${account.accountId}] Nostr provider started, connected to ${account.relays.length} relay(s)`);\n\treturn { stop: () => {\n\t\tbus.close();\n\t\tactiveBuses.delete(account.accountId);\n\t\tmetricsSnapshots.delete(account.accountId);\n\t\tctx.log?.info?.(`[${account.accountId}] Nostr provider stopped`);\n\t} };\n};"
new = "\tbusHandle = bus;\n\tactiveBuses.set(account.accountId, bus);\n\tctx.log?.info?.(`[${account.accountId}] Nostr provider started, connected to ${account.relays.length} relay(s)`);\n\tawait new Promise((resolve) => {\n\t\tif (ctx.abortSignal?.aborted) return resolve();\n\t\tctx.abortSignal?.addEventListener(\"abort\", () => resolve(), { once: true });\n\t});\n\tbus.close();\n\tactiveBuses.delete(account.accountId);\n\tmetricsSnapshots.delete(account.accountId);\n\tctx.log?.info?.(`[${account.accountId}] Nostr provider stopped`);\n\treturn { stop: () => {} };\n};"
if old in s:
    s = s.replace(old, new, 1)
channel.write_text(s)

# 4) nostr-tools reconnect patch (ws.onerror should not disable reconnect).
s = esm.read_text()
old = "this.skipReconnection = true;\n        this.onclose?.();\n        this.handleHardClose(\"relay connection failed\");"
new = "this.onclose?.();\n        this.handleHardClose(\"relay connection failed\");"
if old in s:
    s = s.replace(old, new)
esm.write_text(s)

s = bundle.read_text()
old = "this.ws.onerror = () => {\n          clearTimeout(connectionTimeoutHandle);\n          reject(\"connection failed\");\n          this.connectionPromise = void 0;\n          this.skipReconnection = true;\n          this.onclose?.();\n          this.handleHardClose(\"relay connection failed\");\n        };"
new = "this.ws.onerror = () => {\n          clearTimeout(connectionTimeoutHandle);\n          reject(\"connection failed\");\n          this.connectionPromise = void 0;\n          this.onclose?.();\n          this.handleHardClose(\"relay connection failed\");\n        };"
if old in s:
    s = s.replace(old, new, 1)
bundle.write_text(s)

# 5) Remove stale local startup nip29 patch invocation.
if start_script.exists():
    s = start_script.read_text()
    old = "python3 /home/agent/nip29-patch.py 2>&1 | logger -t openclaw-patch\n"
    if old in s:
        s = s.replace(old, "")
        start_script.write_text(s)

# 6) Remove obsolete config key that breaks newer OpenClaw schema.
if config_file.exists():
    d = json.loads(config_file.read_text())
    nostr = d.get("channels", {}).get("nostr", {})
    if isinstance(nostr, dict) and "nostr_channels" in nostr:
        nostr.pop("nostr_channels", None)
        config_file.write_text(json.dumps(d, indent=2) + "\n")
PY

rm -f /tmp/jiti/*.cjs 2>/dev/null || true
systemctl --user restart openclaw-gateway
sleep 5
systemctl --user status openclaw-gateway --no-pager -l | sed -n '1,80p'
REMOTE
