#!/bin/sh
# Usage Radar Connector installer
#
#   curl -fsSL https://autopayradar.com/install.sh | sh
#
# What this does, in full:
#   1. works out which published build matches this machine (uname)
#   2. downloads it, and checks its SHA-256 against the published manifest
#   3. unpacks the connector and its usage engine into ~/.usage-radar-connector
#   4. registers autostart and waits until it answers on 127.0.0.1:41414
#
# There is no prerequisite. The connector is a single compiled executable that
# carries its own runtime, and the usage engine sits beside it — no Node, no
# npm, no dependency resolution, nothing to go wrong at install time.
#
# It never asks for a password, never uses sudo, never writes outside the
# connector directory and your own autostart directory, never reads your
# prompts or source, and binds loopback only — 127.0.0.1, not your LAN.
#
# Read the whole script before piping it to a shell. You should do that with
# every install script, including this one.

set -eu

BASE="${USAGE_RADAR_BASE:-https://autopayradar.com}"
DIR="${USAGE_RADAR_CONNECTOR_DIR:-$HOME/.usage-radar-connector}"
MANIFEST_URL="$BASE/connector-version.json"
# The website's own fetches and its CSP connect-src are both compiled against
# 127.0.0.1:41414, so a page served from $BASE genuinely cannot chase another
# port. Overriding this is supported for the connector's own dashboard only,
# and the script says so rather than letting the website appear to be broken.
PORT="${USAGE_RADAR_HELPER_PORT:-41414}"
READY_TIMEOUT=30

red()  { printf '\033[31m%s\033[0m\n' "$1"; }
dim()  { printf '\033[2m%s\033[0m\n' "$1"; }
bold() { printf '\033[1m%s\033[0m\n' "$1"; }

bold "Usage Radar Connector"
if [ "$PORT" != 41414 ]; then
  dim "USAGE_RADAR_HELPER_PORT=$PORT: the connector will listen there, and its own"
  dim "dashboard will work, but $BASE only ever looks at 127.0.0.1:41414."
fi
dim  "local only · installs to $DIR"
echo

# --- 1. Which build is this machine? -----------------------------------------
OS="$(uname -s)"
ARCH="$(uname -m)"
case "$OS" in
  Darwin)
    ARTIFACT="usage-radar-connector-macos.zip"
    SHA_KEY="macos"
    ;;
  Linux)
    case "$ARCH" in
      x86_64|amd64)        ARTIFACT="usage-radar-connector-linux-x64.tar.gz";   SHA_KEY="linuxX64" ;;
      aarch64|arm64)       ARTIFACT="usage-radar-connector-linux-arm64.tar.gz"; SHA_KEY="linuxArm64" ;;
      *)
        red "No published Linux build for $ARCH."
        echo "Published builds: x86_64 and aarch64. See $BASE/downloads/"
        exit 1
        ;;
    esac
    ;;
  *)
    red "This installer supports macOS and Linux."
    echo "On Windows, download the zip and run \"Install Usage Radar\": $BASE/downloads/"
    echo "Detected: $OS $ARCH"
    exit 1
    ;;
esac

# --- 2. The two tools this script cannot do without --------------------------
if command -v curl >/dev/null 2>&1; then
  DOWNLOAD=curl
elif command -v wget >/dev/null 2>&1; then
  DOWNLOAD=wget
else
  red "Neither curl nor wget is available, so nothing can be downloaded."
  echo "Install either one, or download the build by hand: $BASE/downloads/"
  exit 1
fi

# A piped-to-shell installer pulling a ~50MB executable has to verify what it
# got. If we cannot hash the file, we do not run it.
if command -v shasum >/dev/null 2>&1; then
  SHA_TOOL=shasum
elif command -v sha256sum >/dev/null 2>&1; then
  SHA_TOOL=sha256sum
else
  red "Neither shasum nor sha256sum is available, so the download cannot be verified."
  echo "This script will not install an unverified binary. Install either tool,"
  echo "or download and check the build by hand: $BASE/downloads/"
  exit 1
fi

fetch() { # url dest
  if [ "$DOWNLOAD" = curl ]; then
    curl -fsSL "$1" -o "$2"
  else
    wget -q -O "$2" "$1"
  fi
}

probe() { # url — true when it answers 2xx, used for the readiness poll
  if [ "$DOWNLOAD" = curl ]; then
    curl -fsS --max-time 2 "$1" >/dev/null 2>&1
  else
    wget -q -T 2 -O /dev/null "$1" >/dev/null 2>&1
  fi
}

sha256_of() { # path
  if [ "$SHA_TOOL" = shasum ]; then
    shasum -a 256 "$1" | awk '{print $1}'
  else
    sha256sum "$1" | awk '{print $1}'
  fi
}

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT INT TERM

# --- 3. The manifest, then the build it names --------------------------------
dim "· fetching the release manifest"
if ! fetch "$MANIFEST_URL" "$TMP/connector-version.json"; then
  red "Could not fetch $MANIFEST_URL"
  echo "Check your network or proxy, then run this again."
  exit 1
fi

# Pulling two fields out of a small, known JSON file rather than requiring jq.
# The digest match insists on exactly 64 hex characters, which is also what
# keeps it from picking up the same key name under "sizeBytes" (bare numbers).
EXPECTED="$(sed -n 's/.*"'"$SHA_KEY"'"[[:space:]]*:[[:space:]]*"\([0-9a-fA-F]\{64\}\)".*/\1/p' "$TMP/connector-version.json" | head -n 1)"
VERSION="$(sed -n 's/.*"latest"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$TMP/connector-version.json" | head -n 1)"

if [ -z "$EXPECTED" ]; then
  red "The release manifest lists no SHA-256 for this platform ($SHA_KEY)."
  echo "Nothing was installed. Please report this: $BASE/downloads/"
  exit 1
fi

dim "· downloading $ARTIFACT${VERSION:+ (v$VERSION)} — about 40-50MB"
if ! fetch "$BASE/downloads/$ARTIFACT" "$TMP/$ARTIFACT"; then
  red "Could not download $BASE/downloads/$ARTIFACT"
  exit 1
fi

ACTUAL="$(sha256_of "$TMP/$ARTIFACT")"
if [ "$ACTUAL" != "$EXPECTED" ]; then
  red "SHA-256 mismatch — the download does not match the published manifest."
  echo "  expected  $EXPECTED"
  echo "  got       $ACTUAL"
  echo
  echo "Nothing was installed. This is either a corrupted download or a"
  echo "tampered one; re-run to retry, and report it if it happens twice."
  exit 1
fi
dim "OK sha256 verified"

# --- 4. Stop and replace whatever is there now -------------------------------
# The service name is derived from the install directory, not hardcoded. launchd
# resolves a job by the Label INSIDE the plist, not by the file's path, so a
# second install into a different directory used to overwrite the first one's
# agent and the documented uninstall then tore down whichever connector was
# actually running. A default install keeps the historical name so an upgrade
# still replaces itself; any other directory gets its own.
SERVICE="com.usageradar.helper"
UNIT_NAME="usage-radar-helper"
if [ "$DIR" != "$HOME/.usage-radar-connector" ]; then
  SUFFIX=$(printf '%s' "$DIR" | (shasum -a 256 2>/dev/null || sha256sum) | cut -c1-8)
  SERVICE="com.usageradar.helper.$SUFFIX"
  UNIT_NAME="usage-radar-helper-$SUFFIX"
fi
PLIST="$HOME/Library/LaunchAgents/$SERVICE.plist"

if [ "$OS" = Darwin ]; then
  launchctl unload -w "$PLIST" >/dev/null 2>&1 || true
else
  systemctl --user disable --now "$UNIT_NAME.service" >/dev/null 2>&1 || true
fi
# Scoped to this install path: a bare name match would also kill a connector
# the user runs from somewhere else.
pkill -f "$DIR/usage-radar-connector" >/dev/null 2>&1 || true

mkdir -p "$DIR/engine"

if [ "$OS" = Darwin ]; then
  # The macOS artifact is the .app bundle, carrying both CPU slices.
  if command -v ditto >/dev/null 2>&1; then
    ditto -x -k "$TMP/$ARTIFACT" "$TMP/unpacked"
  elif command -v unzip >/dev/null 2>&1; then
    unzip -q "$TMP/$ARTIFACT" -d "$TMP/unpacked"
  else
    red "Neither ditto nor unzip is available to unpack $ARTIFACT."
    exit 1
  fi
  RES="$TMP/unpacked/Usage Radar Connector.app/Contents/Resources"
  case "$ARCH" in
    arm64) SLICE=arm64 ;;
    *)     SLICE=x64 ;;
  esac
  if [ ! -f "$RES/usage-radar-connector-$SLICE" ]; then
    red "The downloaded bundle has no $SLICE build inside it."
    exit 1
  fi
  cp "$RES/usage-radar-connector-$SLICE" "$DIR/usage-radar-connector"
  cp "$RES/engine/ccusage-$SLICE" "$DIR/engine/ccusage"
else
  tar xzf "$TMP/$ARTIFACT" -C "$TMP"
  cp "$TMP/usage-radar-connector" "$DIR/usage-radar-connector"
  cp "$TMP/engine/ccusage" "$DIR/engine/ccusage"
  # The completion message points at this path, so the uninstaller has to live
  # here — not in whatever directory the tarball was unpacked into.
  if [ -f "$TMP/uninstall.sh" ]; then
    cp "$TMP/uninstall.sh" "$DIR/uninstall.sh"
  fi
fi

chmod +x "$DIR/usage-radar-connector" "$DIR/engine/ccusage"
# Spelled out rather than `[ -f x ] && chmod`: under `set -eu` a trailing
# AND-list that tests false is itself a failing command, and would abort here.
if [ -f "$DIR/uninstall.sh" ]; then
  chmod +x "$DIR/uninstall.sh"
fi
dim "OK unpacked to $DIR"

if [ "$OS" = Darwin ]; then
  # Downloaded files carry a quarantine flag; clearing it on the copies we own
  # stops Gatekeeper prompting when the LaunchAgent starts them.
  xattr -dr com.apple.quarantine "$DIR" >/dev/null 2>&1 || true

  # Quarantine alone was NOT enough. macOS SIGKILLs an unsigned executable that
  # arrived from the internet: launchd reported exit -9 and running it by hand
  # printed nothing at all. An ad-hoc signature is what makes it runnable. It is
  # not notarization — `spctl -a` still says "rejected", and a real Developer ID
  # signature is still the right fix — but this is enough for it to execute.
  if command -v codesign >/dev/null 2>&1; then
    if codesign -s - -f "$DIR/usage-radar-connector" >/dev/null 2>&1 \
       && codesign -s - -f "$DIR/engine/ccusage" >/dev/null 2>&1; then
      dim "OK ad-hoc signed"
    else
      # Not fatal: on some machines it is already signed, or codesign refuses
      # for reasons that do not stop the binary running. The readiness check
      # below is what actually decides whether this install worked.
      dim "NOTE could not ad-hoc sign; continuing"
    fi
  fi
fi

# --- 5. Autostart ------------------------------------------------------------
if [ "$OS" = Darwin ]; then
  mkdir -p "$HOME/Library/LaunchAgents"
  # The environment has to travel with the job: a LaunchAgent does not inherit
  # the shell that wrote it, so without these the service would bind the default
  # port while this script waited on the one it was told to use.
  cat > "$PLIST" <<PLISTEOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>$SERVICE</string>
  <key>ProgramArguments</key><array><string>$DIR/usage-radar-connector</string></array>
  <key>EnvironmentVariables</key><dict>
    <key>USAGE_RADAR_HELPER_PORT</key><string>$PORT</string>
    <key>USAGE_RADAR_CONNECTOR_DIR</key><string>$DIR</string>
  </dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
</dict></plist>
PLISTEOF
  # `launchctl load` exits 0 even when it registered nothing — most often when
  # the label is already taken — so the exit code is not evidence. Ask launchd
  # what it actually holds before claiming anything.
  launchctl load -w "$PLIST" >/dev/null 2>&1 || true
  if launchctl print "gui/$(id -u)/$SERVICE" 2>/dev/null | grep -qF "$DIR/usage-radar-connector"; then
    dim "OK registered as a LaunchAgent ($SERVICE)"
  else
    # launchctl can refuse in odd session contexts (ssh without a GUI session).
    # Start it now so the install still works; it loads at next login.
    nohup "$DIR/usage-radar-connector" >/dev/null 2>&1 &
    dim "NOTE launchd did not take the agent — started now; it should load at next login."
  fi
elif command -v systemctl >/dev/null 2>&1; then
  UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
  mkdir -p "$UNIT_DIR"
  cat > "$UNIT_DIR/$UNIT_NAME.service" <<UNITEOF
[Unit]
Description=Usage Radar connector (local only, 127.0.0.1:$PORT)
After=default.target

[Service]
Type=simple
Environment=USAGE_RADAR_HELPER_PORT=$PORT
Environment=USAGE_RADAR_CONNECTOR_DIR=$DIR
ExecStart=$DIR/usage-radar-connector
Restart=on-failure

[Install]
WantedBy=default.target
UNITEOF
  systemctl --user daemon-reload >/dev/null 2>&1 || true
  systemctl --user enable --now "$UNIT_NAME.service" >/dev/null 2>&1 || true
  if systemctl --user is-active --quiet "$UNIT_NAME.service"; then
    dim "OK registered as a systemd user service ($UNIT_NAME)"
  else
    nohup "$DIR/usage-radar-connector" >/dev/null 2>&1 &
    dim "NOTE systemd did not start the unit — started now; check: systemctl --user status $UNIT_NAME"
  fi
else
  # No systemd (containers, some minimal distros): start detached so the
  # install still succeeds, and say plainly that it will not survive a reboot.
  nohup "$DIR/usage-radar-connector" >/dev/null 2>&1 &
  dim "NOTE systemd not found — started now, but it will not restart at login."
fi

# --- 6. Only claim success once it answers -----------------------------------
# /health, not /usage: the first usage scan reads every transcript on the disk
# and can take ~13 seconds, while health answers as soon as the socket is up.
dim "· waiting for it to answer on 127.0.0.1:$PORT"
i=0
while [ "$i" -lt "$READY_TIMEOUT" ]; do
  if probe "http://127.0.0.1:$PORT/health"; then
    echo
    bold "Done. Your numbers are ready."
    echo
    echo "  Dashboard:  http://127.0.0.1:$PORT"
    # The website only ever looks at the default port, so promising it would
    # find a connector on any other one is a claim this install cannot keep.
    if [ "$PORT" = 41414 ]; then
      echo "  Website:    $BASE  (your waiting tab connects automatically)"
    else
      echo "  Website:    not reachable on port $PORT — use the local dashboard above,"
      echo "              or reinstall without USAGE_RADAR_HELPER_PORT"
    fi
    echo
    dim "  status:    curl -s http://127.0.0.1:$PORT/health"
    if [ -f "$DIR/uninstall.sh" ]; then
      dim "  uninstall: $DIR/uninstall.sh"
    else
      dim "  uninstall: launchctl unload -w $PLIST"
      dim "             rm -rf $DIR $PLIST"
    fi
    exit 0
  fi
  i=$((i + 1))
  sleep 1
done

# --- 7. Say what actually went wrong, not just that something did ------------
# Two very different failures look identical from the outside, so run the
# binary here and read what it does: a Gatekeeper-killed executable dies on
# signal 9 without printing anything, while a port conflict stays up and talks.
echo
red "Installed, but nothing answered on http://127.0.0.1:$PORT/health within ${READY_TIMEOUT}s."
echo
"$DIR/usage-radar-connector" >"$TMP/run.log" 2>&1 &
RUN_PID=$!
sleep 3
if kill -0 "$RUN_PID" 2>/dev/null; then
  kill "$RUN_PID" >/dev/null 2>&1 || true
  echo "The connector starts and stays up, so the port is the suspect."
  echo "Something else may be holding $PORT, or a firewall is blocking loopback."
  echo "Check with:  lsof -nP -iTCP:$PORT"
else
  RUN_CODE=0
  wait "$RUN_PID" 2>/dev/null || RUN_CODE=$?
  if [ "$RUN_CODE" -eq 137 ]; then
    # 128 + SIGKILL. This is the Gatekeeper kill, the exact failure the ad-hoc
    # signature above exists to prevent — so say the command that fixes it.
    echo "The connector was killed by the system on startup (signal 9)."
    echo "On macOS that means the binary is not accepted as signed. Try:"
    echo "  codesign -s - -f $DIR/usage-radar-connector"
    echo "  codesign -s - -f $DIR/engine/ccusage"
  else
    echo "The connector exited immediately (code $RUN_CODE). It said:"
    sed -n '1,10p' "$TMP/run.log" | sed 's/^/  /'
  fi
fi
echo
echo "Run it in the foreground to see the whole story:  $DIR/usage-radar-connector"
exit 1
