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

SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
PATCH_FILE="${SCRIPT_DIR}/bobrouter.patch.json5"
BACKUP_DIR="${OPENCLAW_BOBROUTER_BACKUP_DIR:-${HOME}/.openclaw/backups}"
MODE=dry-run
ALLOW_REPLACE=0
CONFIRMED=0
ROLLBACK_FILE=
LOCK_DIR=

usage() {
  cat <<'EOF'
Usage:
  configure-bobrouter.sh [--dry-run]
  configure-bobrouter.sh --apply [--allow-replace]
  configure-bobrouter.sh --rollback BACKUP_FILE [--apply|--confirm]
  configure-bobrouter.sh --uninstall [--apply|--confirm]

The default is --dry-run. The helper never reads or writes an API key.
Set BOBROUTER_API_KEY in the OpenClaw Gateway environment separately.

Options:
  --dry-run       Preview the requested operation (default).
  --apply         Apply a configuration change or confirm rollback/uninstall.
  --confirm       Confirm a rollback/uninstall (non-interactive alias for --apply).
  --uninstall     Remove only models.providers.bobrouter.
  --allow-replace Permit replacing an existing provider during apply.
  --rollback FILE Restore a helper-created backup over the active config.
  -h, --help      Show this help.
EOF
}

die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
note() { printf '%s\n' "$*" >&2; }

while (($#)); do
  case "$1" in
    --dry-run) MODE=dry-run; shift ;;
    --apply)
      CONFIRMED=1
      [[ "$MODE" == dry-run ]] && MODE=apply
      shift
      ;;
    --confirm|--yes) CONFIRMED=1; shift ;;
    --allow-replace) ALLOW_REPLACE=1; shift ;;
    --uninstall) MODE=uninstall; shift ;;
    --rollback)
      [[ $# -ge 2 ]] || die "--rollback requires a backup file"
      MODE=rollback; ROLLBACK_FILE=$2; shift 2 ;;
    -h|--help) usage; exit 0 ;;
    *) die "unknown option: $1" ;;
  esac
done

command -v openclaw >/dev/null 2>&1 || die "openclaw is not installed or not on PATH"
[[ -f "$PATCH_FILE" ]] || die "patch file not found: $PATCH_FILE"
CONFIG_PATH=$(openclaw config file)
[[ -n "$CONFIG_PATH" ]] || die "OpenClaw returned an empty config path"

validate_path() {
  local path=$1 kind=$2
  python3 - "$path" "$kind" <<'PY' || die "unsafe $kind: $path"
import os, sys
path, kind = sys.argv[1:]
parts = os.path.abspath(path).split(os.sep)
cur = os.sep
for part in parts[1:]:
    if not part: continue
    cur = os.path.join(cur, part)
    try: st = os.lstat(cur)
    except FileNotFoundError: continue
    if os.path.islink(cur): raise SystemExit(f"{kind} contains a symlink: {cur}")
    if cur == os.path.abspath(path):
        if kind == "backup directory" and not os.path.isdir(cur): raise SystemExit(f"backup directory is not a directory: {path}")
        if kind == "active config" and not (os.path.isfile(cur) or not os.path.exists(cur)): raise SystemExit(f"active config is not a regular file: {path}")
        if st.st_uid != os.getuid(): raise SystemExit(f"{kind} is not owned by the current user: {path}")
PY
}

validate_active_config() { validate_path "$CONFIG_PATH" "active config"; }
ensure_backup_dir() {
  if [[ -e "$BACKUP_DIR" || -L "$BACKUP_DIR" ]]; then
    validate_path "$BACKUP_DIR" "backup directory"
  else
    mkdir -p -- "$BACKUP_DIR" || die "cannot create backup directory: $BACKUP_DIR"
    validate_path "$BACKUP_DIR" "backup directory"
  fi
  chmod 700 -- "$BACKUP_DIR" || die "cannot secure backup directory: $BACKUP_DIR"
}
acquire_lock() {
  validate_active_config
  LOCK_DIR="${CONFIG_PATH}.bobrouter.lock"
  if ! mkdir -- "$LOCK_DIR" 2>/dev/null; then die "config is locked by another helper: $CONFIG_PATH"; fi
  trap 'rmdir -- "$LOCK_DIR" 2>/dev/null || true' EXIT
  validate_active_config
}

sha256() { sha256sum -- "$1" | awk '{print $1}'; }
write_metadata() {
  local backup=$1 existed=$2 digest=$3
  local meta="${backup}.meta"
  ( set -C; : > "$meta" ) 2>/dev/null || die "metadata path already exists: $meta"
  chmod 600 -- "$meta" || die "cannot secure backup metadata: $meta"
  python3 - "$meta" "$backup" "$CONFIG_PATH" "$existed" "$digest" <<'PY' || die "cannot write backup metadata: $meta"
import json, os, sys
out, backup, config, existed, digest = sys.argv[1:]
data = {"format":"bobrouter-openclaw-backup-v1", "backup":os.path.abspath(backup), "config":os.path.abspath(config), "owner":os.getuid(), "existed":existed == "1", "sha256":digest}
with open(out, "w", encoding="utf-8") as f: json.dump(data, f, sort_keys=True); f.write("\n")
PY
}
backup_config() {
  local label=$1 timestamp base destination existed digest
  ensure_backup_dir
  timestamp=$(date -u +%Y%m%dT%H%M%SZ)
  base="${BACKUP_DIR}/openclaw.json.${timestamp}.${label}.bak"
  destination=$base
  local counter=1
  while ! ( set -C; : > "$destination" ) 2>/dev/null; do
    destination="${base}.${counter}"; ((counter += 1))
  done
  if [[ -f "$CONFIG_PATH" ]]; then
    existed=1; cp -- "$CONFIG_PATH" "$destination" || die "cannot create backup: $destination"; digest=$(sha256 "$destination")
  else
    existed=0; digest=""; : > "$destination"
  fi
  chmod 600 -- "$destination" || die "cannot secure backup: $destination"
  write_metadata "$destination" "$existed" "$digest"
  printf '%s\n' "$destination"
}

atomic_restore() {
  local source=$1
  local dir tmp
  dir=$(dirname -- "$CONFIG_PATH")
  mkdir -p -- "$dir"
  if [[ ! -f "$source" || -L "$source" ]]; then die "restore source is not a regular file: $source"; fi
  tmp=$(mktemp "${dir}/.openclaw-bobrouter-restore.XXXXXX")
  trap 'rm -f -- "${tmp:-}"; rmdir -- "${LOCK_DIR:-}" 2>/dev/null || true' RETURN
  cp -- "$source" "$tmp"
  chmod 600 -- "$tmp"
  validate_active_config
  mv -f -- "$tmp" "$CONFIG_PATH"
  trap - RETURN
}

validate_backup() {
  local backup=$1
  local meta="${backup}.meta"
  [[ -f "$backup" && ! -L "$backup" ]] || die "backup file is missing or not regular: $backup"
  [[ -f "$meta" && ! -L "$meta" ]] || die "backup metadata is missing: $meta"
  python3 - "$meta" "$backup" "$CONFIG_PATH" <<'PY' || die "backup validation failed: $backup"
import hashlib, json, os, stat, sys
meta, backup, config = sys.argv[1:]
bst = os.stat(backup)
if bst.st_uid != os.getuid() or stat.S_IMODE(bst.st_mode) & 0o077: raise SystemExit("backup ownership or permissions are unsafe")
if os.stat(meta).st_uid != os.getuid() or stat.S_IMODE(os.stat(meta).st_mode) & 0o077: raise SystemExit("backup metadata ownership or permissions are unsafe")
try: d=json.load(open(meta, encoding="utf-8"))
except Exception as e: raise SystemExit(f"invalid backup metadata: {e}")
if d.get("format") != "bobrouter-openclaw-backup-v1": raise SystemExit("backup metadata format is not recognized")
if d.get("backup") != os.path.abspath(backup) or d.get("config") != os.path.abspath(config): raise SystemExit("backup metadata is not bound to this backup/config")
if d.get("owner") != os.getuid(): raise SystemExit("backup metadata owner mismatch")
if d.get("existed") and hashlib.sha256(open(backup,"rb").read()).hexdigest() != d.get("sha256"): raise SystemExit("backup checksum mismatch")
PY
}
restore_recorded_state() {
  local backup=$1
  local meta="${backup}.meta" existed
  existed=$(python3 - "$meta" <<'PY'
import json, sys
print("1" if json.load(open(sys.argv[1], encoding="utf-8"))["existed"] else "0")
PY
)
  if [[ "$existed" == 1 ]]; then
    atomic_restore "$backup"
  else
    validate_active_config
    rm -f -- "$CONFIG_PATH"
  fi
}
check_provider_conflict() {
  if openclaw config get models.providers.bobrouter --json >/dev/null 2>&1 && (( ! ALLOW_REPLACE )); then die "models.providers.bobrouter already exists; inspect it first or rerun with --allow-replace"; fi
}

case "$MODE" in
  dry-run)
    validate_active_config; check_provider_conflict; note "Dry run only; no files will be changed."; openclaw config patch --file "$PATCH_FILE" --dry-run ;;
  apply)
    acquire_lock; check_provider_conflict; backup=$(backup_config pre-bobrouter); note "Backup: $backup"
    if ! openclaw config patch --file "$PATCH_FILE" || ! openclaw config validate; then
      note "Apply failed; restoring backup."; restore_recorded_state "$backup"; exit 1
    fi
    note "BobRouter provider applied and config validation passed."
    note "Primary model was not changed. To switch intentionally: openclaw models set bobrouter/gpt-5.6-sol"
    note "Rollback: $0 --rollback '$backup' --apply" ;;
  rollback)
    validate_backup "$ROLLBACK_FILE"; note "Rollback dry run: would restore $ROLLBACK_FILE"
    if (( ! CONFIRMED )); then note "No files changed. Re-run with --rollback '$ROLLBACK_FILE' --apply to confirm."; exit 0; fi
    acquire_lock; validate_backup "$ROLLBACK_FILE"; current_backup=$(backup_config pre-rollback); note "Current config backup: $current_backup"
    restore_recorded_state "$ROLLBACK_FILE"; if ! openclaw config validate; then note "Restored backup did not validate; restoring pre-rollback config."; restore_recorded_state "$current_backup"; exit 1; fi
    note "Rollback restored and validated: $ROLLBACK_FILE" ;;
  uninstall)
    validate_active_config; note "Uninstall dry run: would remove only models.providers.bobrouter"
    if (( ! CONFIRMED )); then openclaw config unset models.providers.bobrouter --dry-run; note "No files changed. Re-run with --uninstall --apply to confirm."; exit 0; fi
    acquire_lock; current_backup=$(backup_config pre-uninstall); note "Current config backup: $current_backup"
    openclaw config unset models.providers.bobrouter
    if ! openclaw config validate; then note "Uninstall validation failed; restoring current config."; restore_recorded_state "$current_backup"; exit 1; fi
    note "Removed only models.providers.bobrouter. Remove BOBROUTER_API_KEY from the Gateway environment separately." ;;
esac
