#!/usr/bin/env sh
# qorin-agent installer — macOS and Linux.
#
#   curl -fsSL https://dl.qorin.dev/install.sh | sh
#
# B-236-A (ADR-022 decision 1). The agent installs as ONE system service,
# owned by whoever controls the machine. That costs a sudo prompt before the
# user has seen the product work, which the ADR accepts as the price of an
# auditable access model — so this script's whole job is to make that prompt
# feel earned rather than demanded.
#
# Three rules it follows, in order:
#
#   1. SAY WHAT YOU WILL DO BEFORE YOU DO IT. The plan block is printed
#      before the first privileged action, and `QORIN_DRY_RUN=1` prints it
#      and stops. A sysadmin who wants to read before running is the buyer
#      the root-only model is FOR; refusing to show them the plan would be
#      the wrong kind of irony.
#   2. NEVER CLAIM SUCCESS YOU HAVEN'T CHECKED. The service is verified
#      running after install, and if it is not, the log tail is printed. An
#      installer that prints a tick over a dead daemon is worse than one that
#      fails, because it moves the discovery to a worse moment.
#   3. EVERY FAILURE SAYS WHAT TO DO NEXT. Not just what broke.
#
# POSIX sh — runs on the bare /bin/sh of both platforms. Prompts read from
# /dev/tty because when curl-piped, stdin is the script body itself.

set -eu

DOWNLOAD_BASE="${QORIN_DOWNLOAD_BASE:-https://dl.qorin.dev}"
BINARY_NAME="qorin-agent"
# The system binary. Not ~/.local/bin: the daemon runs as root and the path
# has to be readable by root and stable across users, which a home directory
# is not (and on macOS may be on an encrypted volume that is not mounted at
# boot — a real cause of "the service won't start after a reboot").
SYSTEM_BIN_DIR="${QORIN_INSTALL_DIR:-/usr/local/bin}"
DRY_RUN="${QORIN_DRY_RUN:-0}"

# ─── Presentation ─────────────────────────────────────────────────────────
# Colour discipline mirrors the product's own palette rule: colour reports a
# FACT, it does not decorate. Green = done, amber = needs you, red = failed.
# Nothing else is coloured. Bold marks the thing to read or type; dim marks
# provenance you can ignore on a good day.
#
# Honours NO_COLOR (the de-facto standard), a non-TTY stdout (so a piped log
# has no escape codes in it) and TERM=dumb.
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-}" != "dumb" ]; then
  c_b=$(printf '\033[1m');  c_d=$(printf '\033[2m')
  c_r=$(printf '\033[31m'); c_g=$(printf '\033[32m'); c_y=$(printf '\033[33m')
  c_x=$(printf '\033[0m')
else
  c_b=""; c_d=""; c_r=""; c_g=""; c_y=""; c_x=""
fi

# Symbols degrade to ASCII when the locale can't render them. A mojibake
# checkmark in an install log looks like corruption, and the first thing a
# careful person does with corruption is stop.
case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in
  *UTF-8*|*utf8*|*UTF8*) S_OK="✓"; S_NO="✗"; S_WARN="!"; S_DOT="•"; S_ARR="→"; S_RULE="─" ;;
  *)                     S_OK="[ok]"; S_NO="[!!]"; S_WARN="[!]"; S_DOT="*"; S_ARR="->"; S_RULE="-" ;;
esac

rule() { printf '%s' "$c_d"; i=0; while [ $i -lt 64 ]; do printf '%s' "$S_RULE"; i=$((i+1)); done; printf '%s\n' "$c_x"; }
say()  { printf '%s\n' "$1"; }
step() { printf '%s[%s/%s]%s %s\n' "$c_b" "$1" "$TOTAL_STEPS" "$c_x" "$2"; }
ok()   { printf '  %s%s%s %s\n' "$c_g" "$S_OK" "$c_x" "$1"; }
warn() { printf '  %s%s%s %s\n' "$c_y" "$S_WARN" "$c_x" "$1"; }
note() { printf '    %s%s%s\n' "$c_d" "$1" "$c_x"; }
bullet() { printf '  %s %s\n' "$S_DOT" "$1"; }

# have_tty answers "can I actually talk to a human", which is NOT the same as
# "does /dev/tty exist". In a container started without a TTY the device node
# is present but unopenable, so `[ -e /dev/tty ]` said yes and the redirect
# then failed with a raw `cannot open /dev/tty` printed by sh — above our own
# message, which is precisely the "something is corrupt" signal we work to
# avoid everywhere else in this script. Open it and see.
have_tty() { { : < /dev/tty; } 2>/dev/null; }

# die always answers "and now what". A message that only names the failure
# leaves the reader exactly where they were, one disappointment poorer.
die() {
  printf '\n%s%s%s %s\n' "$c_r" "$S_NO" "$c_x" "$1" >&2
  if [ $# -gt 1 ]; then printf '\n%s\n' "$2" >&2; fi
  printf '\nStuck? %shttps://docs.qorin.dev/install%s  ·  support@qorin.dev\n\n' "$c_b" "$c_x" >&2
  exit 1
}

# ─── Detect ───────────────────────────────────────────────────────────────
case "$(uname -s)" in
  Darwin) OS="darwin"; OS_LABEL="macOS";  SVC_LABEL="launchd (LaunchDaemon)"; UNIT_PATH="/Library/LaunchDaemons/dev.qorin.agent.plist" ;;
  Linux)  OS="linux";  OS_LABEL="Linux";  SVC_LABEL="systemd (system unit)";  UNIT_PATH="/etc/systemd/system/qorin-agent.service" ;;
  *) die "Qorin's agent runs on macOS and Linux. This machine reports $(uname -s)." \
        "On Windows use the PowerShell installer instead:" \
        "  irm https://dl.qorin.dev/install.ps1 | iex" \
        "Inside WSL2 this script is the right one — there the agent is a Linux agent." ;;
esac
case "$(uname -m)" in
  x86_64|amd64)  ARCH="amd64" ;;
  arm64|aarch64) ARCH="arm64" ;;
  *) die "Unsupported CPU architecture: $(uname -m)." "We publish amd64 and arm64 builds. If you need another, tell us at support@qorin.dev." ;;
esac
PLATFORM="${OS}-${ARCH}"
BINARY_URL="${DOWNLOAD_BASE}/${BINARY_NAME}-${PLATFORM}"
INSTALL_PATH="${SYSTEM_BIN_DIR}/${BINARY_NAME}"
LOG_PATH="/var/log/qorin-agent.log"

# Existing installs — worth knowing BEFORE we start, because they change what
# the run means: an upgrade, or a migration off the old per-user mode.
HAVE_SYSTEM=0; [ -e "$UNIT_PATH" ] && HAVE_SYSTEM=1
HAVE_USERMODE=0
INVOKER="${SUDO_USER:-}"
if [ -n "$INVOKER" ] && [ "$INVOKER" != "root" ]; then
  INVOKER_HOME=$(eval echo "~$INVOKER" 2>/dev/null || echo "")
  if [ -n "$INVOKER_HOME" ]; then
    [ -e "$INVOKER_HOME/Library/LaunchAgents/dev.qorin.agent.plist" ] && HAVE_USERMODE=1
    [ -e "$INVOKER_HOME/.config/systemd/user/qorin-agent.service" ] && HAVE_USERMODE=1
  fi
fi

# ─── The plan ─────────────────────────────────────────────────────────────
printf '\n'
printf '  %sQorin agent%s %sinstaller%s\n' "$c_b" "$c_x" "$c_d" "$c_x"
rule
if [ "$HAVE_SYSTEM" = "1" ]; then
  say "  Upgrading the Qorin agent on this ${OS_LABEL} machine."
else
  say "  This will connect this ${OS_LABEL} machine to Qorin as an agent."
fi
printf '\n'
bullet "Binary        ${c_b}${INSTALL_PATH}${c_x}"
bullet "Service       ${SVC_LABEL}"
note   "$UNIT_PATH"
bullet "Logs          ${LOG_PATH}"
bullet "Runs as       ${c_b}root${c_x}, and drops to each person's own UNIX user for their work"
if [ "$HAVE_USERMODE" = "1" ]; then
  printf '\n'
  warn "Found an older per-user agent for ${c_b}${INVOKER}${c_x} — it will be removed."
  note "One machine runs one agent. Two sharing an identity evict each other."
fi
printf '\n'
say "  ${c_d}Root is required and there is no lesser mode: the agent is one${c_x}"
say "  ${c_d}presence per machine, owned by whoever controls the machine.${c_x}"
rule
printf '\n'

if [ "$DRY_RUN" = "1" ]; then
  say "${c_b}Dry run${c_x} — nothing was changed. Re-run without QORIN_DRY_RUN to install."
  printf '\n'
  exit 0
fi

# ─── Privilege ────────────────────────────────────────────────────────────
# Not root? Do not degrade — that is the whole decision. But do not just
# refuse either: if there is a real terminal and sudo exists, re-run
# ourselves under it, so the password prompt arrives AFTER the user has read
# what it is for. Piping to `sudo sh` works too; this just means they did not
# have to know that in advance.
if [ "$(id -u)" != "0" ]; then
  if command -v sudo >/dev/null 2>&1 && have_tty; then
    say "${c_y}${S_WARN}${c_x} Installing a system service needs root."
    say "  Re-running this installer under ${c_b}sudo${c_x}; your password is asked by sudo, not by us."
    printf '\n'
    # Re-exec THIS FILE, not a fresh download: what runs with privilege has
    # to be the same bytes the user just read. When curl-piped there is no
    # file — $0 is "sh" or a pipe — and re-downloading to get one would break
    # that guarantee, so in that case we ask them to re-run it themselves.
    if [ -f "$0" ] && [ -r "$0" ]; then
      exec sudo -p "  [sudo] password for %p: " \
        QORIN_DOWNLOAD_BASE="$DOWNLOAD_BASE" sh "$0" < /dev/tty
    fi
    die "This installer was piped from curl, so it has no file to re-run with sudo." \
        "Run it with sudo instead:

  ${c_b}curl -fsSL ${DOWNLOAD_BASE}/install.sh | sudo sh${c_x}

Or download it first if you would rather read it before it runs:

  ${c_b}curl -fsSL ${DOWNLOAD_BASE}/install.sh -o qorin-install.sh${c_x}
  ${c_b}less qorin-install.sh && sudo sh qorin-install.sh${c_x}"
  fi
  die "Installing a system service needs root." \
      "Run:

  ${c_b}curl -fsSL ${DOWNLOAD_BASE}/install.sh | sudo sh${c_x}

Qorin will not install a half-privileged agent instead: a per-user agent is a
Qorin presence the machine's owner never authorised, so that mode no longer
exists."
fi

TOTAL_STEPS=4

# ─── 1. Download ──────────────────────────────────────────────────────────
step 1 "Downloading the ${PLATFORM} build"
mkdir -p "$SYSTEM_BIN_DIR"
tmp_bin="${INSTALL_PATH}.new.$$"
if ! curl -fL --connect-timeout 30 --max-time 300 "$BINARY_URL" -o "$tmp_bin" 2>/dev/null; then
  rm -f "$tmp_bin"
  die "Could not download ${BINARY_URL}." \
      "Check the machine can reach dl.qorin.dev:

  ${c_b}curl -I ${DOWNLOAD_BASE}/install.sh${c_x}

Behind a proxy? Set https_proxy before re-running."
fi
ok "Downloaded $(du -h "$tmp_bin" 2>/dev/null | awk '{print $1}')"

# ─── 2. Verify ────────────────────────────────────────────────────────────
# Checksum first, then does-it-run. In that order: a binary that fails its
# checksum must never be executed, not even to ask its version.
step 2 "Verifying"
sums=$(mktemp)
if curl -fsSL --connect-timeout 15 --max-time 60 "${DOWNLOAD_BASE}/SHA256SUMS" -o "$sums" 2>/dev/null; then
  expected=$(grep " ${BINARY_NAME}-${PLATFORM}$" "$sums" 2>/dev/null | awk '{print $1}')
  rm -f "$sums"
  if [ -n "$expected" ]; then
    if command -v sha256sum >/dev/null 2>&1; then actual=$(sha256sum "$tmp_bin" | awk '{print $1}')
    elif command -v shasum >/dev/null 2>&1;   then actual=$(shasum -a 256 "$tmp_bin" | awk '{print $1}')
    else actual=""; fi
    if [ -z "$actual" ]; then
      warn "No sha256sum or shasum on this machine — checksum not verified."
    elif [ "$expected" != "$actual" ]; then
      rm -f "$tmp_bin"
      die "Checksum mismatch — refusing to install." \
          "Expected ${expected}
Got      ${actual}

This means the file that arrived is not the file we published. Could be a
proxy rewriting traffic or a corrupted CDN cache. Please retry, and if it
persists tell us at support@qorin.dev — we want to know."
    else
      ok "Checksum matches the published build"
    fi
  else
    warn "Manifest has no entry for ${BINARY_NAME}-${PLATFORM} — checksum not verified."
  fi
else
  rm -f "$sums"
  warn "Could not fetch SHA256SUMS — checksum not verified."
fi

chmod 0755 "$tmp_bin"
if ! "$tmp_bin" --version >/dev/null 2>&1; then
  rm -f "$tmp_bin"
  die "The binary downloaded but will not run on this machine." \
      "On macOS this is usually Gatekeeper quarantine:

  ${c_b}sudo xattr -dr com.apple.quarantine ${INSTALL_PATH}${c_x}

On Linux, check the architecture matches: this build is ${PLATFORM}."
fi
mv -f "$tmp_bin" "$INSTALL_PATH"
VERSION=$("$INSTALL_PATH" --version 2>/dev/null | head -1)
ok "Installed ${c_b}${VERSION}${c_x}"

# ─── 3. Dependencies ──────────────────────────────────────────────────────
step 3 "Checking what the agent needs"
missing=""
command -v git >/dev/null 2>&1 || missing="git"
if [ -n "$missing" ]; then
  warn "${c_b}git${c_x} is not installed. Registering a project will fail without it."
  if   command -v brew    >/dev/null 2>&1; then note "brew install git"
  elif command -v apt-get >/dev/null 2>&1; then note "sudo apt-get install -y git"
  elif command -v dnf     >/dev/null 2>&1; then note "sudo dnf install -y git"
  elif command -v pacman  >/dev/null 2>&1; then note "sudo pacman -S git"
  else note "Install git with this machine's package manager."; fi
else
  ok "git $(git --version 2>/dev/null | awk '{print $3}')"
fi

# macOS + a root LaunchDaemon means TCC. This is the one new friction that
# root-only introduces on Mac, and ADR-022 makes B-197 mandatory because of
# it: a daemon that cannot read the user's folders is worse than the
# LaunchAgent it replaced. We do not pretend to detect it — TCC probes are
# unreliable and a wrong answer here is worse than no answer — we say when it
# will bite and exactly where to fix it.
if [ "$OS" = "darwin" ]; then
  warn "macOS privacy protection (TCC) may hide some folders from the agent."
  note "If Desktop / Documents / Downloads look empty in the folder picker:"
  note "System Settings → Privacy & Security → Full Disk Access → add ${INSTALL_PATH}"
fi

# ─── 4. Service ───────────────────────────────────────────────────────────
# `qorin-agent install` = log in (device flow) + install the system service.
# On a headless box the browser cannot open, and waiting for one that never
# appears is a classic dead end — so detect and hand over the URL instead.
step 4 "Connecting this machine to your Qorin account"
BROWSER_FLAG=""
if [ -n "${SSH_CONNECTION:-}" ] || { [ "$OS" = "linux" ] && [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; }; then
  BROWSER_FLAG="--no-browser"
  note "No desktop session detected — you'll get a link to open elsewhere."
fi
printf '\n'
if have_tty; then
  # shellcheck disable=SC2086
  "$INSTALL_PATH" install $BROWSER_FLAG < /dev/tty || die \
    "Sign-in or service install did not finish." \
    "Nothing is broken — the binary is installed. Finish when ready:

  ${c_b}sudo ${INSTALL_PATH} install${c_x}"
else
  die "No terminal available to complete sign-in." \
      "The binary is installed. Finish it from a real terminal:

  ${c_b}sudo ${INSTALL_PATH} install${c_x}"
fi

# ─── Verify it is actually up ─────────────────────────────────────────────
# Rule 2. `install` already printed its own ticks; this asks the operating
# system whether the thing is running, which is a different question and the
# only one that matters tomorrow morning.
printf '\n'
running=0
if [ "$OS" = "darwin" ]; then
  launchctl print system/dev.qorin.agent >/dev/null 2>&1 && running=1
else
  systemctl is-active --quiet qorin-agent 2>/dev/null && running=1
fi

rule
if [ "$running" = "1" ]; then
  printf '  %s%s%s %sThe agent is running and will restart with this machine.%s\n' "$c_g" "$S_OK" "$c_x" "$c_b" "$c_x"
  printf '\n'
  say "  ${c_b}You${c_x} are already set up: this machine registered you as ${c_b}$(id -un 2>/dev/null || echo "$USER")${c_x},"
  say "  which is the UNIX user your commands will run as. Open the dashboard,"
  say "  the machine is under ${c_b}Agents${c_x}, and you can spawn a workspace now."
  printf '\n'
  # B-273d — la frase che mancava, ed e' quella che decide se un secondo
  # utente ci arriva da solo o scrive a chi ha installato. Sta qui perche'
  # questo e' il momento in cui qualcuno HA appena usato sudo: e' l'unico
  # istante in cui "il primo login registra la macchina" e' evidente invece
  # che una regola da ricordare.
  say "  ${c_b}Everyone else on this machine${c_x} needs no sudo and no token — they run:"
  printf '\n'
  say "      ${c_b}qorin-agent login${c_x}"
  printf '\n'
  say "  as their own UNIX user, and they can work here immediately — they"
  say "  already have a shell on this machine, so Qorin isn't giving them"
  say "  access, only a comfortable way to use it. You'll see them in the"
  say "  dashboard under the agent's ${c_b}People${c_x}, and you can suspend anyone from there."
  printf '\n'
  say "  ${c_d}Logs${c_x}    ${INSTALL_PATH} service logs --tail"
  say "  ${c_d}Health${c_x}  ${INSTALL_PATH} doctor"
  say "  ${c_d}Remove${c_x}  sudo ${INSTALL_PATH} uninstall"
else
  printf '  %s%s%s %sInstalled, but the service is not running.%s\n' "$c_y" "$S_WARN" "$c_x" "$c_b" "$c_x"
  printf '\n'
  say "  The last lines of ${LOG_PATH}:"
  printf '\n'
  tail -n 15 "$LOG_PATH" 2>/dev/null | sed 's/^/    /' || note "(no log file yet)"
  printf '\n'
  say "  ${c_b}${INSTALL_PATH} doctor${c_x} checks the usual causes."
fi
rule
printf '\n'
