#!/usr/bin/env bash
# termtether one-line installer. Sets up the local terminal (ttyd+tmux), the mobilebar proxy, the
# relay connector, and the `termtether` CLI — then you run `termtether login <handle>`.
#
#   Linux:  curl -fsSL https://termtether.io/install | bash      (run as YOUR user — it sudo's only for packages)
#   macOS:  curl -fsSL https://termtether.io/install | bash      (Homebrew refuses root; installs via launchd)
#
# Verified on: Ubuntu 24.04 / 22.04, Debian 12, Rocky 9 (RHEL family), Arch. Auto-detects the package
# manager (apt/dnf/pacman/apk/zypper); ttyd falls back to the official static binary where unpackaged.
# Requires systemd (Alpine/OpenRC is out of scope). On macOS it uses Homebrew + launchd instead
# (see container/setup-macos.sh); the agent runs as a headless-capable LaunchDaemon.
#
# Env overrides:
#   RELAY_BASE_DOMAIN  relay base domain                          (default: termtether.io)
#   TT_BROKER_HOST   broker hostname (must be in its TLS cert)  (default: broker.<base>)
#   TERMTETHER_YES     set to skip the package-confirmation prompt (non-interactive installs)
#   SKIP_AGENT_CLIS    set to skip installing the claude/codex CLIs
#   TERMTETHER_SRC     use an existing checkout instead of fetching
#   TERMTETHER_REPO    git URL to clone when no SRC               (default: the termtether repo)
set -euo pipefail

# --- source integrity (audit Finding 4) -------------------------------------------------------------
# The agent source tarball is signed at release time (ECDSA P-256) and the SIGNATURE is verified here,
# before it's extracted + run — integrity beyond TLS. The signing PUBLIC key is pinned below (rotate by
# replacing this block + the SOURCE_SIGNING_KEY release secret). ECDSA-P256 + `openssl dgst` so it
# verifies on both OpenSSL and macOS's LibreSSL.
#   Verification is REQUIRED by default: a published signature must verify with openssl present, and an
#   unsigned release (or a host without openssl) is REFUSED. Set TT_ALLOW_UNSIGNED=1 ONLY for a
#   development / self-host build to proceed without a verified signature.
SOURCE_SIGNING_PUB='-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ4w81P2nNPuAAdUYCrw4YcIs3jp9
FBgH/T2pVterp+WLgc1i+uJ4Pd7ex0riLFB89ILiYD3QoVJ8VHvRs/oGxQ==
-----END PUBLIC KEY-----'
# fetch_source <url-base> <dest-dir>: download source.tgz (+ .sig), verify, then extract to dest.
fetch_source() {
  local urlbase="$1" dest="$2" tmp; tmp="$(mktemp -d)"
  curl -fsSL "$urlbase/source.tgz" -o "$tmp/source.tgz" || { echo "termtether: could not download source.tgz" >&2; rm -rf "$tmp"; exit 1; }
  local sig=""; curl -fsSL "$urlbase/source.tgz.sig" -o "$tmp/source.tgz.sig" 2>/dev/null && sig="$tmp/source.tgz.sig"
  if [ -n "$sig" ]; then
    if command -v openssl >/dev/null 2>&1; then
      printf '%s\n' "$SOURCE_SIGNING_PUB" > "$tmp/pub.pem"
      if openssl dgst -sha256 -verify "$tmp/pub.pem" -signature "$sig" "$tmp/source.tgz" >/dev/null 2>&1; then
        echo "==> source.tgz signature verified ✓"
      else
        echo "termtether: SOURCE SIGNATURE VERIFICATION FAILED — refusing to install a possibly-tampered tarball." >&2
        rm -rf "$tmp"; exit 1
      fi
    elif [ -n "${TT_ALLOW_UNSIGNED:-}" ]; then
      echo "==> warning: openssl not found — skipping source.tgz signature check (TT_ALLOW_UNSIGNED set)" >&2
    else
      echo "termtether: openssl not found — cannot verify source.tgz. Install openssl, or set TT_ALLOW_UNSIGNED=1 for a dev/self-host build." >&2; rm -rf "$tmp"; exit 1
    fi
  elif [ -n "${TT_ALLOW_UNSIGNED:-}" ]; then
    echo "==> note: source.tgz is unsigned — proceeding WITHOUT verification (TT_ALLOW_UNSIGNED set)" >&2
  else
    echo "termtether: no source.tgz.sig published — refusing to install unverified source. Set TT_ALLOW_UNSIGNED=1 for a dev/self-host build." >&2; rm -rf "$tmp"; exit 1
  fi
  install -d "$dest"
  tar xz -C "$dest" -f "$tmp/source.tgz" || { echo "termtether: source extract failed" >&2; rm -rf "$tmp"; exit 1; }
  rm -rf "$tmp"
}

# ---- macOS: Homebrew + launchd path (runs as the user, NOT root). Branch before the Linux root check.
if [ "$(uname -s)" = "Darwin" ]; then
  [ "$(id -u)" != 0 ] || { echo "termtether: on macOS run WITHOUT sudo (as your own user) — Homebrew refuses to run as root"; exit 1; }
  MAC_USER="${TT_USER:-$(id -un)}"
  BASE="${RELAY_BASE_DOMAIN:-termtether.io}"
  BROKER_HOST="${TT_BROKER_HOST:-broker.$BASE}"
  echo "==> termtether installer (macOS, user=$MAC_USER, base=$BASE)"

  if   [ -x /opt/homebrew/bin/brew ]; then BREW=/opt/homebrew/bin/brew          # Apple Silicon
  elif [ -x /usr/local/bin/brew  ]; then BREW=/usr/local/bin/brew               # Intel
  elif command -v brew >/dev/null;  then BREW="$(command -v brew)"
  else
    echo "==> installing Homebrew (pulls Command Line Tools if missing)"
    NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
    if [ -x /opt/homebrew/bin/brew ]; then BREW=/opt/homebrew/bin/brew; else BREW=/usr/local/bin/brew; fi
  fi
  BREW_BIN="$(dirname "$BREW")"
  need=""; for c in ttyd tmux node; do [ -x "$BREW_BIN/$c" ] || command -v "$c" >/dev/null 2>&1 || need="$need $c"; done
  if [ -n "$need" ]; then echo "==> deps:$need"; "$BREW" install $need; else echo "==> deps present (ttyd tmux node)"; fi

  SRC="${TERMTETHER_SRC:-}"
  if [ -z "$SRC" ]; then
    SRC="$(mktemp -d /tmp/termtether-src.XXXXXX)"
    echo "==> fetching agent source from https://$BASE/install/source.tgz"
    fetch_source "https://$BASE/install" "$SRC"
  fi
  [ -f "$SRC/container/setup-macos.sh" ] || { echo "termtether: source not found at $SRC (set TERMTETHER_SRC)"; exit 1; }
  # setup-macos.sh derives the product from RELAY_BASE_DOMAIN (termtether.io -> termtether,
  # termtether.dev -> termtether-dev). The .dev install.sh is published with the base baked in.
  exec env TT_USER="$MAC_USER" RELAY_BASE_DOMAIN="$BASE" TT_BROKER_HOST="$BROKER_HOST" \
       ${TT_LISTEN_PORT:+TT_LISTEN_PORT="$TT_LISTEN_PORT"} \
       ${TT_CHANNEL:+TT_CHANNEL="$TT_CHANNEL"} ${TT_HANDLE:+TT_HANDLE="$TT_HANDLE"} \
       bash "$SRC/container/setup-macos.sh"
fi

# Install AS THE INVOKING USER — not root (mirrors the macOS path). The sub-installers sudo only for
# package install (with confirmation) and boot-linger; everything else lands in your ~/.local + ~/.config.
BASE="${RELAY_BASE_DOMAIN:-termtether.io}"
BROKER_HOST="${TT_BROKER_HOST:-broker.$BASE}"
SRC="${TERMTETHER_SRC:-}"
REPO="${TERMTETHER_REPO:-}"   # set only for a git-based (dev/private) install; default fetches the broker tarball
# Product is baked from the apex (publish-pages rewrites the default base on .dev). Each product is a
# separate command + tree; they share ONLY the user's tmux / termtether-term.
case "$(printf '%s' "$BASE" | tr '[:upper:]' '[:lower:]')" in
  ''|termtether.io) PRODUCT=termtether ;;
  termtether.dev) PRODUCT=termtether-dev ;;
  *.termtether.io) PRODUCT="termtether-${BASE%.termtether.io}" ;;
  *) PRODUCT="termtether-$(printf '%s' "$BASE" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-' | sed 's/-\+/-/g; s/^-//; s/-$//')" ;;
esac
INSTALL_ROOT="$HOME/.local/share/$PRODUCT"
CONFIG_DIR="$HOME/.config/$PRODUCT"
CLI_NAME="$PRODUCT"

if [ "$(id -u)" = 0 ]; then
  # No sudo needed — the install is user-scoped and elevates itself only where required. If sudo was used
  # out of habit, transparently re-run as the invoking user instead of dead-ending with an error.
  self="${BASH_SOURCE:-$0}"
  if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != root ] && [ -f "$self" ]; then
    echo "==> you used sudo; re-running as '$SUDO_USER' (termtether installs per-user, sudo'ing internally where needed)"
    exec sudo -u "$SUDO_USER" -H env RELAY_BASE_DOMAIN="$BASE" TT_BROKER_HOST="$BROKER_HOST" \
      ${SRC:+TERMTETHER_SRC="$SRC"} ${REPO:+TERMTETHER_REPO="$REPO"} \
      ${TERMTETHER_YES:+TERMTETHER_YES="$TERMTETHER_YES"} ${SKIP_AGENT_CLIS:+SKIP_AGENT_CLIS="$SKIP_AGENT_CLIS"} \
      ${TT_ALLOWED:+TT_ALLOWED="$TT_ALLOWED"} \
      bash "$self"
  fi
  # Piped as `curl | sudo bash` (no script file to re-run), or a genuine root-only box: guide clearly.
  echo "termtether: run this WITHOUT sudo — as the user the agent should run as:" >&2
  echo "    curl -fsSL https://$BASE/install | bash" >&2
  echo "  It elevates internally only for system packages + boot-linger. If you piped to 'sudo bash', just" >&2
  echo "  drop the sudo. On a root-only box, create a user first (adduser me && usermod -aG sudo me)." >&2
  exit 1
fi

echo "==> termtether installer (user=$USER, base=$BASE, product=$PRODUCT)"

if [ -z "$SRC" ]; then
  SRC="$INSTALL_ROOT/src"
  install -d "$SRC"
  if [ -n "$REPO" ]; then                                  # explicit git source (dev / private repo)
    command -v git >/dev/null || { echo "termtether: git needed for a REPO install — install it first."; exit 1; }
    if [ -d "$SRC/.git" ]; then git -C "$SRC" pull -q; else echo "==> cloning $REPO"; git clone -q "$REPO" "$SRC"; fi
  else                                                     # default: the open agent source the broker serves (no auth, no git)
    command -v curl >/dev/null && command -v tar >/dev/null || { echo "termtether: need curl + tar to fetch the source."; exit 1; }
    echo "==> fetching agent source from https://$BASE/install/source.tgz"
    fetch_source "https://$BASE/install" "$SRC"
  fi
fi
[ -f "$SRC/container/setup.sh" ] || { echo "termtether: source not found at $SRC (set TERMTETHER_SRC or TERMTETHER_REPO)"; exit 1; }

# --- product tree (docs/MULTI-INSTANCE.md) -----------------------------------------------------------
# Each apex is its own command. Prod is `termtether` at ~/.config/termtether (no sibling keys).
# Dev is `termtether-dev` at ~/.config/termtether-dev. They share ONLY termtether-term / the user's tmux.
install -d "$CONFIG_DIR" "$INSTALL_ROOT"
chmod 700 "$CONFIG_DIR" 2>/dev/null || true
RELAY_ENV="$CONFIG_DIR/relay.env"

# Lift a leftover --base-keyed slot (~/.config/termtether/<base_slug>/) into this product's own tree.
# Prod flattens termtether-io/ up into ~/.config/termtether/. Dev moves termtether-dev/ out to
# ~/.config/termtether-dev/. Does not touch the other product's files. Identity is per product:
# prod keeps the existing box-identity.json; a new dev tree generates its own on first proxy start.
migrate_keyed_slot() {
  local old_slug old_dir f src
  old_slug="$(printf '%s' "$BASE" | tr '[:upper:]' '[:lower:]' | tr '.' '-')"
  old_dir="$HOME/.config/termtether/$old_slug"
  [ -f "$old_dir/relay.env" ] || return 0
  if [ -f "$RELAY_ENV" ] && grep -q '^TT_HANDLE=' "$RELAY_ENV" 2>/dev/null; then
    echo "==> keyed leftover $old_dir (product $PRODUCT already has $RELAY_ENV — leaving it)"
    return 0
  fi
  echo "==> migrating keyed '$old_slug' -> product '$PRODUCT' ($CONFIG_DIR)"
  for f in relay.env site.env webpush.env notify.env; do
    [ -e "$old_dir/$f" ] || continue
    [ -e "$CONFIG_DIR/$f" ] && continue
    if [ -L "$old_dir/$f" ]; then
      src="$(readlink -f "$old_dir/$f" 2>/dev/null || true)"
      if [ -n "$src" ] && [ -f "$src" ]; then cp -a "$src" "$CONFIG_DIR/$f"
      else cp -a "$old_dir/$f" "$CONFIG_DIR/$f"
      fi
    else
      mv "$old_dir/$f" "$CONFIG_DIR/$f"
    fi
  done
  [ -d "$old_dir/tls" ] && [ ! -e "$CONFIG_DIR/tls" ] && mv "$old_dir/tls" "$CONFIG_DIR/tls"
  rmdir "$old_dir" 2>/dev/null || true
  for u in "termtether-mobilebar-${old_slug}.service" "termtether-mobilebar-${PRODUCT}.service"; do
    systemctl --user disable --now "$u" 2>/dev/null || true
    rm -f "$HOME/.config/systemd/user/$u"
  done
  systemctl --user daemon-reload 2>/dev/null || true
  echo "  ✓ config now at $RELAY_ENV; old keyed unit retired"
}
migrate_keyed_slot
# Carry the shared auto-update flag onto a newly split non-prod tree (prod keeps the original file).
if [ "$PRODUCT" != termtether ] && [ -f "$HOME/.config/termtether/auto-update" ] && [ ! -f "$CONFIG_DIR/auto-update" ]; then
  cp -a "$HOME/.config/termtether/auto-update" "$CONFIG_DIR/auto-update"
fi

# Pick this product's proxy port: reuse the one persisted in its relay.env; else the first free TCP port
# from 4020 (skip shared ttyd :4021 and ports already claimed by any termtether*mobilebar unit).
pick_listen_port() {
  local p=4020 unit_dir="$HOME/.config/systemd/user" used
  used="$(grep -hoE '^Environment=LISTEN_PORT=[0-9]+' "$unit_dir"/termtether*.service 2>/dev/null | grep -oE '[0-9]+$' || true)"
  while :; do
    if [ "$p" = 4021 ]; then p=$((p + 1)); continue; fi
    if printf '%s\n' "$used" | grep -qx "$p"; then p=$((p + 1)); continue; fi
    if command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -q "[:.]${p}[[:space:]]"; then p=$((p + 1)); continue; fi
    printf '%s' "$p"; return 0
  done
}
LISTEN_PORT=""
[ -f "$RELAY_ENV" ] && LISTEN_PORT="$(sed -n 's/^TT_LISTEN_PORT=//p' "$RELAY_ENV" 2>/dev/null | head -1 || true)"
[ -n "$LISTEN_PORT" ] || LISTEN_PORT="$(pick_listen_port)"

# Seed THIS product's relay.env (only if absent) BEFORE the proxy install.
if [ ! -f "$RELAY_ENV" ]; then
  ( umask 077; { echo "RELAY_BASE_DOMAIN=$BASE"; echo "TT_BROKER_HOST=$BROKER_HOST"; [ -n "${TT_CHANNEL:-}" ] && echo "TT_CHANNEL=$TT_CHANNEL"; echo "TT_LISTEN_PORT=$LISTEN_PORT"; } > "$RELAY_ENV" )
elif ! grep -q '^TT_LISTEN_PORT=' "$RELAY_ENV" 2>/dev/null; then
  ( umask 077; printf 'TT_LISTEN_PORT=%s\n' "$LISTEN_PORT" >> "$RELAY_ENV" )
fi

# Shared ttyd/tmux on :4021 — created by the FIRST product install, reused by every later one.
if ! systemctl --user is-enabled termtether-term.service >/dev/null 2>&1; then
  bash "$SRC/container/setup.sh"
fi
# This product's own CLI (termtether or termtether-dev). Skip enroll here; mobilebar does it.
TT_SKIP_ENROLL=1 TT_PRODUCT="$PRODUCT" TT_CONFIG_DIR="$CONFIG_DIR" TT_INSTALL_ROOT="$INSTALL_ROOT" \
  RELAY_BASE_DOMAIN="$BASE" bash "$SRC/container/install-connector.sh"

# --- migrate a pre-rename (C7RX_*) box in place --- (operates on THIS product's relay.env)
if [ -f "$RELAY_ENV" ] && grep -q '^C7RX_' "$RELAY_ENV" 2>/dev/null; then
  ( umask 077
    TMP="$(mktemp "$CONFIG_DIR/.relay.env.XXXXXX")"
    cp "$RELAY_ENV" "$TMP"
    for K in HANDLE CONNECTOR_CRED BROKER_HOST; do
      if ! grep -q "^TT_$K=" "$TMP" && grep -q "^C7RX_$K=" "$TMP"; then
        printf 'TT_%s=%s\n' "$K" "$(sed -n "s/^C7RX_$K=//p" "$TMP" | head -1)" >> "$TMP"
      fi
    done
    mv "$TMP" "$RELAY_ENV" )
  echo "  ↻ migrated legacy C7RX_* config keys -> TT_* (pre-rename box)"
fi

# This product's mobilebar: own tree + unit, sharing ttyd :4021.
TT_PRODUCT="$PRODUCT" TT_CONFIG_DIR="$CONFIG_DIR" TT_INSTALL_ROOT="$INSTALL_ROOT" \
  TT_LISTEN_PORT="$LISTEN_PORT" TT_SHARE_TTYD=1 RELAY_BASE_DOMAIN="$BASE" \
  ${TT_HANDLE:+TT_HANDLE="$TT_HANDLE"} bash "$SRC/container/install-mobilebar.sh"

# --- optional: lock session sharing to an install-time allowlist (audit hardening) ---
# If TT_ALLOWED is set (comma-separated GitHub logins — INCLUDING yourself, the owner), pin exactly who
# may open this box and enforce it ON THE BOX itself. The relay/registry can still SUSPEND the box, but
# can never ADD someone you didn't list here — so a compromised admin or registry channel can't silently
# graft a new user onto your terminal. To change the list later, re-run the installer with a new TT_ALLOWED.
if [ -n "${TT_ALLOWED:-}" ]; then
  SUBS="$(printf '%s' "$TT_ALLOWED" | tr ',' '\n' \
    | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | tr 'A-Z' 'a-z' \
    | grep -v '^$' | sed 's/^github://; s/^/github:/' | paste -sd, -)"
  ( umask 077
    TMP="$(mktemp "$CONFIG_DIR/.relay.env.XXXXXX")"
    grep -v -E '^TT_(ALLOWED_SUBS|SHARING_LOCKED)=' "$RELAY_ENV" 2>/dev/null > "$TMP" || true
    printf 'TT_ALLOWED_SUBS=%s\n' "$SUBS" >> "$TMP"
    printf 'TT_SHARING_LOCKED=1\n' >> "$TMP"
    mv "$TMP" "$RELAY_ENV" )
  echo "  🔒 session sharing LOCKED to: $SUBS"
  echo "     (the relay can suspend the box but cannot add users; re-run with TT_ALLOWED=... to change)"

  # --- pin the session/cap signing key (audit Finding 1, path #1) ---
  # The lockdown above pins WHO may connect; on its own the box still trusts whatever ES256 keys
  # auth.<base> currently serves in its JWKS. Since the operator controls that endpoint, a compromised
  # control plane could publish a key it holds and mint a session for one of your allowed users — walking
  # past the who-lock. So we also capture the live JWKS NOW and pin it: the box trusts only these keys and
  # stops fetching the live URL. Rotating the broker key then requires re-running this installer (local
  # box access) — the control plane alone can no longer forge a session. TT_PIN_KEYS=0 opts out (keep the
  # who-lock but trust live keys). Unlocked boxes are intentionally NOT pinned: the operator is already
  # trusted there via dynamic grants, so pinning buys nothing.
  if [ "${TT_PIN_KEYS:-1}" != "0" ]; then
    JWKS="$(curl -fsS --max-time 15 "https://auth.$BASE/.well-known/jwks.json" 2>/dev/null || true)"
    # Sanity-check it looks like a JWKS with at least one key (no jq dependency); the box fails CLOSED on a
    # malformed pin, so refuse to write garbage — leave the box on live keys and warn loudly instead.
    if printf '%s' "$JWKS" | tr -d '[:space:]' | grep -q '"keys":\[{.*"kty"'; then
      JWKS_ONE="$(printf '%s' "$JWKS" | tr -d '\n\r')"
      ( umask 077
        TMP="$(mktemp "$CONFIG_DIR/.relay.env.XXXXXX")"
        grep -v -E '^TT_SESSION_JWKS=' "$RELAY_ENV" 2>/dev/null > "$TMP" || true
        printf 'TT_SESSION_JWKS=%s\n' "$JWKS_ONE" >> "$TMP"
        mv "$TMP" "$RELAY_ENV" )
      echo "  🔑 session signing key PINNED at install (control plane can no longer forge a session)"
      echo "     (re-run the installer to pick up a rotated relay key)"
    else
      echo "  ⚠️  could not fetch a valid JWKS from https://auth.$BASE — session key NOT pinned;"
      echo "     the box still trusts the live relay key. Re-run once auth.$BASE is reachable, or set"
      echo "     TT_PIN_KEYS=0 to acknowledge and skip. (The who-lock above is still in effect.)"
    fi
  fi
fi

case ":$PATH:" in *":$HOME/.local/bin:"*) PATH_HINT="" ;; *) PATH_HINT="  (add ~/.local/bin to your PATH first: export PATH=\"\$HOME/.local/bin:\$PATH\")" ;; esac

# Finish the job in ONE command. install.sh is what `termtether update` re-fetches and runs, so doing the
# config self-heal + owner sign-in HERE (rather than telling the user to run a second command) makes
# `termtether update` all an already-enrolled box needs: `doctor` writes the OIDC config and, if the box has
# no email owner yet, runs the device-code login inline (approve in a browser — no token paste). A fresh,
# not-yet-enrolled box still can't know its handle, so it prints the one-time `login <handle>` instruction.
CLI="$HOME/.local/bin/$CLI_NAME"
ENROLLED_HANDLE="$(sed -n 's/^TT_HANDLE=//p' "$RELAY_ENV" 2>/dev/null | head -1)"
if [ -n "$ENROLLED_HANDLE" ] && [ -x "$CLI" ]; then
  echo ""
  echo "  ✓ $CLI_NAME agent installed. Checking config + owner…"
  "$CLI" doctor || true
  echo ""
  echo "  Done. Open:  https://sh.$BASE/$ENROLLED_HANDLE"
else
  cat <<DONE

  ✓ $CLI_NAME agent installed (as $USER — user services, no root daemon).

  Next — enroll this box. It prints a short code; you approve it in a browser
  on ANY device (your phone/laptop) — this box has no browser of its own:
      $CLI_NAME login <your-handle>${PATH_HINT}

  Then open:  https://sh.$BASE/<your-handle>
DONE
fi
