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

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
collector_dir="$(cd "${script_dir}/../collector" && pwd)"
config_path="${OVERDECK_FRONTDOOR_CONFIG_PATH:-${HOME}/.config/overdeck/config.toml}"
unit_dir="${OVERDECK_FRONTDOOR_UNIT_DIR:-${HOME}/.config/systemd/user}"
front_service="${unit_dir}/overdeck-collector-front.service"
systemctl_bin="${OVERDECK_FRONTDOOR_SYSTEMCTL:-systemctl}"
curl_bin="${OVERDECK_FRONTDOOR_CURL:-curl}"
ss_bin="${OVERDECK_FRONTDOOR_SS:-ss}"
bun_bin="${OVERDECK_FRONTDOOR_BUN:-$(command -v bun || true)}"
original=""
edited=false
post_stop=false
failed_step=""

say_skip() { printf 'frontdoor migration skipped: %s\n' "$1" >&2; exit 0; }
fail() { failed_step="$1"; return 1; }

restore_config() {
  if [[ "${edited}" == true && -n "${original}" && -f "${original}" ]]; then
    mv -f "${original}" "${config_path}"
    edited=false
  fi
}

rollback() {
  status=$?
  trap - ERR
  if [[ "${post_stop}" == true ]]; then
    "${systemctl_bin}" --user stop overdeck-collector-front.service overdeck-collector-front.socket >/dev/null 2>&1 || true
    "${systemctl_bin}" --user disable overdeck-collector-front.service overdeck-collector-front.socket >/dev/null 2>&1 || true
    restore_config
    "${systemctl_bin}" --user start overdeck-collector.service >/dev/null 2>&1 || true
  else
    restore_config
  fi
  printf 'frontdoor migration failed: %s\n' "${failed_step:-unexpected error}" >&2
  exit "${status}"
}
trap rollback ERR

[[ -f "${config_path}" ]] || say_skip "config file missing"
[[ -n "${bun_bin}" ]] || say_skip "bun is unavailable for config verification"

config_values="$(cd "${collector_dir}" && "${bun_bin}" --eval 'import { loadConfig, resolveBindHost } from "./src/config.ts"; const c=loadConfig(process.argv[1]); console.log(JSON.stringify({port:c.port,bind_port:c.bind_port,tailnetBind:c.tailnetBind,host:resolveBindHost(c)}))' "${config_path}" 2>/dev/null)" || say_skip "collector config cannot be loaded"
port="$(printf '%s' "${config_values}" | "${bun_bin}" --eval 'const x=JSON.parse(await Bun.stdin.text()); console.log(x.port)' 2>/dev/null)"
tailnet="$(printf '%s' "${config_values}" | "${bun_bin}" --eval 'const x=JSON.parse(await Bun.stdin.text()); console.log(x.tailnetBind)' 2>/dev/null)"
host="$(printf '%s' "${config_values}" | "${bun_bin}" --eval 'const x=JSON.parse(await Bun.stdin.text()); console.log(x.host)' 2>/dev/null)"
[[ "${port}" == 31338 ]] || say_skip "effective port is not 31338"
[[ "${tailnet}" == false && ( "${host}" == 127.* || "${host}" == localhost || "${host}" == ::1 ) ]] || say_skip "collector bind is not loopback-only"

if [[ "${OVERDECK_FRONTDOOR_NODE+x}" == x ]]; then
  node_bin="${OVERDECK_FRONTDOOR_NODE}"
elif [[ -x "${HOME}/.claude/bin/node" ]]; then
  node_bin="${HOME}/.claude/bin/node"
else
  node_bin="$(command -v node || true)"
fi
# Keep the path as configured — ~/.claude/bin/node is a cpu-guard shim that resolves the
# real binary from its invoked name; readlink -f breaks it (_cpu-guard-shim.sh finds no
# target). The collector unit invokes its bun shim the same way.
[[ -n "${node_bin}" && -x "${node_bin}" ]] || say_skip "node >= v20 is unavailable"
node_major="$("${node_bin}" -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || true)"
[[ "${node_major}" =~ ^[0-9]+$ && "${node_major}" -ge 20 ]] || say_skip "node >= v20 is unavailable"
[[ -f "${front_service}" ]] || say_skip "front service unit is not installed"

if grep -Eq "^[[:space:]]*(bind_port|\"bind_port\"|'bind_port')[[:space:]]*=" "${config_path}"; then
  current_bind="$(printf '%s' "${config_values}" | "${bun_bin}" --eval 'const x=JSON.parse(await Bun.stdin.text()); console.log(x.bind_port)' 2>/dev/null)"
  [[ "${current_bind}" == 31341 ]] || say_skip "existing bind_port is not 31341"
  printf 'frontdoor migration already configured\n'
  trap - ERR
  exit 0
else
  mkdir -p "$(dirname "${config_path}")"
  original="$(mktemp "$(dirname "${config_path}")/.config.toml.frontdoor-original.XXXXXX")"
  cp -p "${config_path}" "${original}"
  candidate="$(mktemp "$(dirname "${config_path}")/.config.toml.frontdoor-candidate.XXXXXX")"
  { printf 'bind_port = 31341\n'; cat "${config_path}"; } >"${candidate}"
  chmod --reference="${config_path}" "${candidate}"
  mv -f "${candidate}" "${config_path}"
  edited=true
fi

verified="$(cd "${collector_dir}" && "${bun_bin}" --eval 'import { loadConfig } from "./src/config.ts"; const c=loadConfig(process.argv[1]); if(c.port!==31338||c.bind_port!==31341) process.exit(1)' "${config_path}")" || fail "config verification"

unit_candidate="$(mktemp "${unit_dir}/.overdeck-collector-front.service.XXXXXX")"
sed "s#__NODE__#${node_bin}#g" "${front_service}" >"${unit_candidate}"
mv -f "${unit_candidate}" "${front_service}"

"${systemctl_bin}" --user daemon-reload || fail "daemon-reload"
post_stop=true
"${systemctl_bin}" --user stop overdeck-collector.service || fail "stop collector"
"${systemctl_bin}" --user enable --now overdeck-collector-front.socket || fail "start front socket"
"${systemctl_bin}" --user enable overdeck-collector-front.service || fail "enable front service"
"${systemctl_bin}" --user start overdeck-collector.service || fail "start collector"
# The live collector needs 50-150s to bind under load (Type=simple returns instantly),
# so verification must wait, bounded. Env overrides are for the test fixtures only.
verify_attempts="${OVERDECK_FRONTDOOR_VERIFY_ATTEMPTS:-100}"
verify_delay="${OVERDECK_FRONTDOOR_VERIFY_DELAY:-3}"
bound=false
for ((i = 0; i < verify_attempts; i++)); do
  if "${ss_bin}" -ltn | grep -Eq '127\.0\.0\.1:31341([[:space:]]|$)'; then bound=true; break; fi
  sleep "${verify_delay}"
done
[[ "${bound}" == true ]] || fail "collector listen verification"
token="$(<"$(dirname "${config_path}")/token")" || fail "read collector token"
# Identity, not liveness: require exactly HTTP 200 with the collector's own token,
# first straight at the backend port, then through the front door. A foreign service
# squatting the backend port (Kanboard's 302 did exactly this on 31339) must fail here
# — 3xx passes curl --fail, so status is compared explicitly.
probe_200() {
  local url=$1 i code
  for ((i = 0; i < verify_attempts; i++)); do
    code="$("${curl_bin}" --silent --output /dev/null --write-out '%{http_code}' --max-time 10 -H "Authorization: Bearer ${token}" "${url}")"
    [[ "${code}" == 200 ]] && return 0
    sleep "${verify_delay}"
  done
  return 1
}
probe_200 http://127.0.0.1:31341/health || fail "collector identity on backend port"
probe_200 http://127.0.0.1:31338/health || fail "front door authenticated request"

if [[ -n "${original}" ]]; then rm -f "${original}"; fi
edited=false
trap - ERR
printf 'frontdoor migration complete\n'
