#!/bin/sh

set -eu
umask 077

PRODUCT_NAME="PAUIOps Manager"

# These three values are stamped by the website build from the published
# release manifest. They are defaults, not destinations: unless a version is
# pinned explicitly, the installer re-resolves the requested channel from live
# release metadata, so a release becomes installable the moment ./deploy.sh
# publishes it — no download URL is ever hardcoded.
DEFAULT_VERSION='1.1.4'
DEFAULT_RELEASE_BASE_URL='https://pauiops.kpa.ph/releases'
DEFAULT_CHANNEL='stable'

RELEASE_BASE_URL="${PAUIOPS_RELEASE_BASE_URL:-$DEFAULT_RELEASE_BASE_URL}"
CHANNEL="${PAUIOPS_CHANNEL:-$DEFAULT_CHANNEL}"
VERSION="${PAUIOPS_VERSION:-}"
VERSION_SOURCE=""
MANIFEST_LOOKUP="${PAUIOPS_MANIFEST_LOOKUP:-1}"
RELEASE_MANIFEST=""
ASSET_SHA256=""
MODE="auto"
DRY_RUN="0"
PRINT_ASSET="0"
TMP_DIR=""
MAC_STAGED_BINARY=""
MAC_STAGED_PLIST=""
INSTALL_PATH_WAS_SET="0"
ROOT_PATH_WAS_SET="0"
PORT_WAS_SET="0"
[ "${PAUIOPS_MANAGER_INSTALL_PATH+x}" = "x" ] && INSTALL_PATH_WAS_SET="1"
[ "${PAUIOPS_ROOT_PATH+x}" = "x" ] && ROOT_PATH_WAS_SET="1"
[ "${PORT+x}" = "x" ] && PORT_WAS_SET="1"

log() {
  printf '%s\n' "$*"
}

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

usage() {
  cat <<'EOF'
PAUIOps Manager installer and updater

Usage:
  curl -fsSL https://pauiops.kpa.ph/install.sh | sh
  curl -fsSL https://pauiops.kpa.ph/install.sh | sh -s -- --update

Options:
  --install            Force installation/reinstallation.
  --update             Require an existing managed installation and update it.
  --channel <name>     Follow a release channel: stable (default), beta or dev.
  --version <version>  Install one specific published version instead.
  --dry-run            Show the resolved release and planned paths, change nothing.
  --help               Show this help.

Release resolution:
  With no --version, the installer downloads <release-base>/<channel>.json and
  installs the version that manifest publishes, verifying the SHA-256 recorded
  there. Nothing about the download is hardcoded in this script.

Optional environment variables:
  PAUIOPS_CHANNEL                       Release channel (same as --channel).
  PAUIOPS_VERSION                       Install a specific published version.
  PAUIOPS_RELEASE_BASE_URL              Alternate trusted release base URL.
  PAUIOPS_MANIFEST_LOOKUP               Set to 0 to skip channel resolution and
                                        use the version this website was built
                                        with (useful on an isolated network).
  PAUIOPS_ALLOW_INSECURE_RELEASES       Set to 1 only for local release testing.
  PAUIOPS_MANAGER_INSTALL_PATH          Custom binary installation path.
  PAUIOPS_ROOT_PATH                     Directory containing managed projects.
  PAUIOPS_BOOTSTRAP_ADMIN_USERNAME      First-install admin username.
  PAUIOPS_BOOTSTRAP_ADMIN_PASSWORD      First-install admin password.
  PAUIOPS_BOOTSTRAP_ADMIN_EMAIL         First-install admin email.
  PORT                                  Manager HTTP port (default: 51800).

The same command is idempotent: it installs when PAUIOps is absent and performs
a verified update when it is already managed. Existing configuration, projects,
and databases are preserved.
EOF
}

cleanup() {
  if [ -n "$MAC_STAGED_BINARY" ]; then
    rm -f "$MAC_STAGED_BINARY"
  fi
  if [ -n "$MAC_STAGED_PLIST" ]; then
    rm -f "$MAC_STAGED_PLIST"
  fi
  if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
    rm -rf "$TMP_DIR"
  fi
}

trap cleanup EXIT HUP INT TERM

while [ "$#" -gt 0 ]; do
  case "$1" in
    --install)
      MODE="install"
      ;;
    --update)
      MODE="update"
      ;;
    --channel)
      shift
      [ "$#" -gt 0 ] || fail "--channel requires a value."
      CHANNEL="$1"
      ;;
    --channel=*)
      CHANNEL="${1#--channel=}"
      ;;
    --version)
      shift
      [ "$#" -gt 0 ] || fail "--version requires a value."
      VERSION="$1"
      ;;
    --version=*)
      VERSION="${1#--version=}"
      ;;
    --dry-run)
      DRY_RUN="1"
      ;;
    --print-asset)
      PRINT_ASSET="1"
      ;;
    --help | -h)
      usage
      exit 0
      ;;
    *)
      fail "unknown option: $1"
      ;;
  esac
  shift
done

case "$CHANNEL" in
  stable | beta | dev)
    ;;
  *)
    fail "unsupported release channel: $CHANNEL. Supported: stable, beta, dev."
    ;;
esac
case "$RELEASE_BASE_URL" in
  https://*)
    ;;
  *)
    [ "${PAUIOPS_ALLOW_INSECURE_RELEASES:-0}" = "1" ] ||
      fail "the Manager release base must use HTTPS."
    ;;
esac
RELEASE_BASE_URL="${RELEASE_BASE_URL%/}"

have_downloader() {
  command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1
}

download_file() {
  download_url="$1"
  download_destination="$2"
  # A file:// base is the documented local-release-testing path. Copying it
  # directly keeps that path working on a host with no HTTP client at all, and
  # wget cannot fetch file:// URLs in any case.
  case "$download_url" in
    file://*)
      cp "${download_url#file://}" "$download_destination"
      return
      ;;
  esac
  if command -v curl >/dev/null 2>&1; then
    case "$download_url" in
      https://*)
        curl -fsSL --proto '=https' --tlsv1.2 --retry 3 --connect-timeout 15 \
          --output "$download_destination" "$download_url"
        ;;
      *)
        curl -fsSL --retry 3 --connect-timeout 15 \
          --output "$download_destination" "$download_url"
        ;;
    esac
    return
  fi
  if command -v wget >/dev/null 2>&1; then
    wget -q --tries=3 --timeout=15 --output-document="$download_destination" "$download_url"
    return
  fi
  fail "curl or wget is required."
}

TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/pauiops-install.XXXXXX")"

# --------------------------------------------------------------------------
# Release resolution
#
# Release metadata, not this script, decides what gets installed. The channel
# manifest published by the deployment pipeline names the current version and
# carries the SHA-256 of every artifact in it, so the installer never has to
# guess a URL or trust an unverified download.
#
# Resolution order:
#   1. an explicitly requested version (--version / PAUIOPS_VERSION)
#   2. <release-base>/<channel>.json — the live channel manifest
#   3. the version this website was built with (offline fallback)
# --------------------------------------------------------------------------

# json_field prints a top-level "key": "value" string from a manifest. The
# manifests are generated with one field per line precisely so that this stays
# a two-line awk program on hosts that have no JSON tooling.
json_field() {
  awk -v key="$2" '
    $0 ~ "^[[:space:]]*\"" key "\"[[:space:]]*:" {
      line = $0
      sub(/^[^:]*:[[:space:]]*/, "", line)
      sub(/,[[:space:]]*$/, "", line)
      gsub(/^"|"$/, "", line)
      if (line != "" && line != "null") { print line; found = 1 }
      exit
    }
    END { if (!found) exit 1 }
  ' "$1"
}

# json_artifact prints "<filename> <sha256>" for one target from the artifacts
# array of a release manifest.
json_artifact() {
  awk -v want="$2" '
    function value(line) {
      sub(/^[^:]*:[[:space:]]*/, "", line)
      sub(/,[[:space:]]*$/, "", line)
      gsub(/^"|"$/, "", line)
      return line
    }
    /^[[:space:]]*\{/ { filename = ""; sha = ""; selected = 0 }
    /^[[:space:]]*"target"[[:space:]]*:/ { if (value($0) == want) selected = 1 }
    /^[[:space:]]*"filename"[[:space:]]*:/ { filename = value($0) }
    /^[[:space:]]*"sha256"[[:space:]]*:/ { sha = value($0) }
    /^[[:space:]]*\}/ {
      if (selected && filename != "") { print filename " " sha; found = 1; exit }
    }
    END { if (!found) exit 1 }
  ' "$1"
}

# fetch_manifest is best-effort: an unreachable manifest is a normal condition
# that falls back to the pinned version, so it must never abort the installer.
fetch_manifest() {
  manifest_url="$1"
  manifest_path="$2"
  case "$manifest_url" in
    file://*)
      ;;
    *)
      # download_file treats a missing HTTP client as fatal, and stderr is
      # redirected below, so that message would be lost and the installer would
      # exit silently. Skip resolution instead and let the real download report
      # the missing tool properly.
      have_downloader || return 1
      ;;
  esac
  download_file "$manifest_url" "$manifest_path" 2>/dev/null || return 1
  [ -s "$manifest_path" ] || return 1
}

resolve_version() {
  if [ -n "$VERSION" ]; then
    VERSION_SOURCE="requested explicitly"
    return 0
  fi

  if [ "$MANIFEST_LOOKUP" != "0" ] && [ "$PRINT_ASSET" != "1" ]; then
    channel_manifest="$TMP_DIR/$CHANNEL.json"
    if fetch_manifest "$RELEASE_BASE_URL/$CHANNEL.json" "$channel_manifest"; then
      resolved="$(json_field "$channel_manifest" version || printf '')"
      if [ -n "$resolved" ]; then
        VERSION="$resolved"
        VERSION_SOURCE="$CHANNEL channel manifest"
        RELEASE_MANIFEST="$channel_manifest"
        return 0
      fi
      printf 'Warning: %s/%s.json did not publish a version.\n' \
        "$RELEASE_BASE_URL" "$CHANNEL" >&2
    fi
  fi

  VERSION="$DEFAULT_VERSION"
  VERSION_SOURCE="website build"
}

resolve_version

[ "$VERSION" != "unpublished" ] ||
  fail "no Manager release is published on this website yet."
case "$VERSION" in
  "" | *[!A-Za-z0-9._+-]*)
    fail "the resolved release version contains unsupported characters: $VERSION"
    ;;
esac

detect_platform() {
  case "$(uname -s)" in
    Linux)
      PLATFORM_OS="linux"
      ;;
    Darwin)
      PLATFORM_OS="darwin"
      ;;
    *)
      fail "unsupported operating system: $(uname -s). Supported: Linux and macOS."
      ;;
  esac

  case "$(uname -m)" in
    x86_64 | amd64)
      PLATFORM_ARCH="amd64"
      ;;
    arm64 | aarch64)
      PLATFORM_ARCH="arm64"
      ;;
    armv6l | armv7l | arm)
      [ "$PLATFORM_OS" = "linux" ] ||
        fail "32-bit ARM is supported on Linux only."
      PLATFORM_ARCH="arm"
      ;;
    *)
      fail "unsupported CPU architecture: $(uname -m)."
      ;;
  esac

  PLATFORM_TARGET="${PLATFORM_OS}-${PLATFORM_ARCH}"
  # The release contract; the manifest below overrides it whenever it can be
  # read, so a future change to artifact naming needs no installer change.
  ASSET_NAME="pauiops-${VERSION}-${PLATFORM_TARGET}"
}

detect_platform

if [ "$PRINT_ASSET" = "1" ]; then
  printf '%s\n' "$ASSET_NAME"
  exit 0
fi

# resolve_asset asks the release manifest for the exact filename and digest
# published for this platform. Whatever it finds is authoritative; when the
# manifest is unavailable the installer falls back to the naming contract plus
# the artifact's own .sha256 companion, which is how earlier releases were
# published.
resolve_asset() {
  if [ "$MANIFEST_LOOKUP" = "0" ]; then
    return 0
  fi

  if [ -z "$RELEASE_MANIFEST" ] || [ "$(json_field "$RELEASE_MANIFEST" version || printf '')" != "$VERSION" ]; then
    RELEASE_MANIFEST="$TMP_DIR/release-$VERSION.json"
    fetch_manifest "$RELEASE_BASE_URL/$VERSION/release.json" "$RELEASE_MANIFEST" || {
      RELEASE_MANIFEST=""
      return 0
    }
  fi

  entry="$(json_artifact "$RELEASE_MANIFEST" "$PLATFORM_TARGET" || printf '')"
  if [ -z "$entry" ]; then
    fail "release $VERSION does not publish an artifact for $PLATFORM_TARGET."
  fi

  ASSET_NAME="${entry%% *}"
  ASSET_SHA256="${entry#* }"
  case "$ASSET_NAME" in
    "" | */* | *[!A-Za-z0-9._+-]*)
      fail "release $VERSION published an unsafe artifact name: $ASSET_NAME"
      ;;
  esac
}

resolve_asset

PORT="${PORT:-51800}"

validate_port() {
  case "$PORT" in
    "" | *[!0-9]*)
      fail "PORT must be numeric."
      ;;
  esac
  if [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then
    fail "PORT must be between 1 and 65535."
  fi
}

validate_port

if [ "$PLATFORM_OS" = "linux" ]; then
  LINUX_ENV_FILE="${PAUIOPS_SYSTEMD_ENV_PATH:-/etc/pauiops/pauiops.env}"
  LINUX_UNIT_FILE="${PAUIOPS_SYSTEMD_UNIT_PATH:-/etc/systemd/system/pauiops.service}"
  INSTALL_PATH="${PAUIOPS_MANAGER_INSTALL_PATH:-/usr/local/bin/pauiops}"
  ROOT_PATH="${PAUIOPS_ROOT_PATH:-/srv/pauiops/repos}"
  PLAN_SERVICE="systemd system service"
  PLAN_CONFIG="$LINUX_ENV_FILE"
else
  [ -n "${HOME:-}" ] || fail "HOME is not set."
  MAC_STATE_DIR="${PAUIOPS_MAC_STATE_DIR:-$HOME/Library/Application Support/PAUIOps}"
  MAC_LABEL="${PAUIOPS_MAC_LAUNCHD_LABEL:-ph.kpa.pauiops}"
  MAC_PLIST="${PAUIOPS_MAC_PLIST_PATH:-$HOME/Library/LaunchAgents/$MAC_LABEL.plist}"
  MAC_ENV_FILE="${PAUIOPS_MAC_ENV_PATH:-$MAC_STATE_DIR/.env}"
  INSTALL_PATH="${PAUIOPS_MANAGER_INSTALL_PATH:-$HOME/.local/bin/pauiops}"
  ROOT_PATH="${PAUIOPS_ROOT_PATH:-$HOME/PAUIOps/projects}"
  PLAN_SERVICE="per-user launchd agent"
  PLAN_CONFIG="$MAC_ENV_FILE"
fi

if [ "$DRY_RUN" = "1" ]; then
  log "$PRODUCT_NAME installation plan"
  log "  platform: $PLATFORM_OS/$PLATFORM_ARCH"
  log "  channel:  $CHANNEL"
  log "  version:  $VERSION ($VERSION_SOURCE)"
  log "  asset:    $RELEASE_BASE_URL/$VERSION/$ASSET_NAME"
  if [ -n "$ASSET_SHA256" ]; then
    log "  sha256:   $ASSET_SHA256 (from release metadata)"
  else
    log "  sha256:   $RELEASE_BASE_URL/$VERSION/$ASSET_NAME.sha256"
  fi
  log "  binary:   $INSTALL_PATH"
  log "  projects: $ROOT_PATH"
  log "  config:   $PLAN_CONFIG"
  log "  service:  $PLAN_SERVICE"
  log "  mode:     $MODE"
  exit 0
fi

sha256_file() {
  checksum_path="$1"
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$checksum_path" | awk '{print $1}'
    return
  fi
  if command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$checksum_path" | awk '{print $1}'
    return
  fi
  if command -v openssl >/dev/null 2>&1; then
    openssl dgst -sha256 "$checksum_path" | awk '{print $NF}'
    return
  fi
  fail "SHA-256 verification requires sha256sum, shasum, or openssl."
}

DOWNLOADED_BINARY="$TMP_DIR/$ASSET_NAME"
CHECKSUM_FILE="$TMP_DIR/$ASSET_NAME.sha256"

log "Downloading $PRODUCT_NAME $VERSION for $PLATFORM_OS/$PLATFORM_ARCH..."
ASSET_URL="$RELEASE_BASE_URL/$VERSION/$ASSET_NAME"
download_file "$ASSET_URL" "$DOWNLOADED_BINARY"

RELEASE_VERSION="$VERSION"
if [ -n "$ASSET_SHA256" ]; then
  # The digest travelled with the release manifest that named this artifact.
  EXPECTED_SHA256="$ASSET_SHA256"
else
  download_file "$ASSET_URL.sha256" "$CHECKSUM_FILE"
  EXPECTED_SHA256="$(awk 'NR == 1 {print $1; exit}' "$CHECKSUM_FILE")"
fi
case "$EXPECTED_SHA256" in
  "" | *[!A-Fa-f0-9]*)
    fail "published checksum has an invalid format."
    ;;
esac
[ "${#EXPECTED_SHA256}" -eq 64 ] ||
  fail "published checksum must contain exactly 64 hexadecimal characters."
ACTUAL_SHA256="$(sha256_file "$DOWNLOADED_BINARY")"
EXPECTED_SHA256="$(printf '%s' "$EXPECTED_SHA256" | tr 'A-F' 'a-f')"
ACTUAL_SHA256="$(printf '%s' "$ACTUAL_SHA256" | tr 'A-F' 'a-f')"
[ "$ACTUAL_SHA256" = "$EXPECTED_SHA256" ] ||
  fail "SHA-256 verification failed for $ASSET_NAME."
chmod 0755 "$DOWNLOADED_BINARY"
log "Verified SHA-256: $ACTUAL_SHA256"

generate_password() {
  if command -v openssl >/dev/null 2>&1; then
    openssl rand -hex 18
    return
  fi
  od -An -N18 -tx1 /dev/urandom | tr -d ' \n'
}

validate_config_value() {
  config_name="$1"
  config_value="$2"
  case "$config_value" in
    *'
'*)
      fail "$config_name must not contain a newline."
      ;;
  esac
}

ADMIN_USERNAME="${PAUIOPS_BOOTSTRAP_ADMIN_USERNAME:-admin}"
ADMIN_PASSWORD="${PAUIOPS_BOOTSTRAP_ADMIN_PASSWORD:-}"
ADMIN_EMAIL="${PAUIOPS_BOOTSTRAP_ADMIN_EMAIL:-admin@localhost}"
if [ -z "$ADMIN_PASSWORD" ]; then
  ADMIN_PASSWORD="$(generate_password)"
fi
validate_config_value "PAUIOPS_BOOTSTRAP_ADMIN_USERNAME" "$ADMIN_USERNAME"
validate_config_value "PAUIOPS_BOOTSTRAP_ADMIN_PASSWORD" "$ADMIN_PASSWORD"
validate_config_value "PAUIOPS_BOOTSTRAP_ADMIN_EMAIL" "$ADMIN_EMAIL"
validate_config_value "PAUIOPS_ROOT_PATH" "$ROOT_PATH"

write_initial_config() {
  config_destination="$1"
  database_destination="$2"
  cat >"$config_destination" <<EOF
# Created by the PAUIOps Website /install.sh endpoint
PAUIOPS_APP_NAME=PAUIOps Manager
PAUIOPS_ROOT_PATH=$ROOT_PATH
PAUIOPS_MANAGER_INSTALL_PATH=$INSTALL_PATH
PAUIOPS_UPGRADE_HEALTH_URL=http://127.0.0.1:$PORT/api/ready
PAUIOPS_DB_DRIVER=sqlite
PAUIOPS_DB_DSN=$database_destination
PAUIOPS_BOOTSTRAP_ADMIN_USERNAME=$ADMIN_USERNAME
PAUIOPS_BOOTSTRAP_ADMIN_PASSWORD=$ADMIN_PASSWORD
PAUIOPS_BOOTSTRAP_ADMIN_EMAIL=$ADMIN_EMAIL
PORT=$PORT
EOF
}

wait_until_ready() {
  ready_url="$1"
  attempts=30
  while [ "$attempts" -gt 0 ]; do
    if command -v curl >/dev/null 2>&1; then
      if curl -fsS --max-time 5 "$ready_url" >/dev/null 2>&1; then
        return 0
      fi
    elif wget -q --timeout=5 --spider "$ready_url" >/dev/null 2>&1; then
      return 0
    fi
    attempts=$((attempts - 1))
    sleep 1
  done
  return 1
}

remove_bootstrap_password() {
  config_path="$1"
  filtered_path="$TMP_DIR/config-without-bootstrap-password"
  awk '!/^[[:space:]]*(export[[:space:]]+)?PAUIOPS_BOOTSTRAP_ADMIN_PASSWORD=/' \
    "$config_path" >"$filtered_path"
  chmod 0600 "$filtered_path"
  mv "$filtered_path" "$config_path"
}

remove_bootstrap_password_as_root() {
  config_path="$1"
  filtered_path="$TMP_DIR/config-without-bootstrap-password-root"
  run_as_root awk \
    '!/^[[:space:]]*(export[[:space:]]+)?PAUIOPS_BOOTSTRAP_ADMIN_PASSWORD=/' \
    "$config_path" >"$filtered_path"
  chmod 0600 "$filtered_path"
  run_as_root sh -c 'cat "$1" >"$2"' sh "$filtered_path" "$config_path"
}

run_as_root() {
  if [ "$(id -u)" -eq 0 ]; then
    "$@"
    return
  fi
  command -v sudo >/dev/null 2>&1 ||
    fail "sudo is required for the Linux system service installation."
  sudo "$@"
}

config_value() {
  config_path="$1"
  config_key="$2"
  awk -F= -v key="$config_key" '
    $1 == key {
      value = substr($0, index($0, "=") + 1)
      gsub(/^[[:space:]"]+|[[:space:]"]+$/, "", value)
      print value
      exit
    }
  ' "$config_path"
}

linux_config_value() {
  config_key="$1"
  if ! run_as_root test -f "$LINUX_ENV_FILE"; then
    return
  fi
  run_as_root awk -F= -v key="$config_key" '
    $1 == key {
      value = substr($0, index($0, "=") + 1)
      gsub(/^[[:space:]"]+|[[:space:]"]+$/, "", value)
      print value
      exit
    }
  ' "$LINUX_ENV_FILE"
}

install_linux() {
  SYSTEMD_RUNTIME_DIR="${PAUIOPS_SYSTEMD_RUNTIME_DIR:-/run/systemd/system}"
  [ -d "$SYSTEMD_RUNTIME_DIR" ] ||
    fail "this Linux host is not running systemd."
  command -v systemctl >/dev/null 2>&1 ||
    fail "systemctl is required on Linux."

  if run_as_root test -f "$LINUX_ENV_FILE"; then
    configured_install_path="$(linux_config_value PAUIOPS_MANAGER_INSTALL_PATH || true)"
    configured_root_path="$(linux_config_value PAUIOPS_ROOT_PATH || true)"
    configured_port="$(linux_config_value PORT || true)"
    if [ "$INSTALL_PATH_WAS_SET" = "0" ] && [ -n "$configured_install_path" ]; then
      INSTALL_PATH="$configured_install_path"
    fi
    if [ "$ROOT_PATH_WAS_SET" = "0" ] && [ -n "$configured_root_path" ]; then
      ROOT_PATH="$configured_root_path"
    fi
    if [ "$PORT_WAS_SET" = "0" ] && [ -n "$configured_port" ]; then
      PORT="$configured_port"
    fi
    validate_port
  fi

  installed="0"
  if run_as_root test -x "$INSTALL_PATH" && run_as_root test -f "$LINUX_UNIT_FILE"; then
    installed="1"
  fi
  if [ "$MODE" = "update" ] && [ "$installed" != "1" ]; then
    fail "no managed Linux installation was found at $INSTALL_PATH."
  fi

  fresh_config="0"
  if ! run_as_root test -f "$LINUX_ENV_FILE"; then
    initial_config="$TMP_DIR/pauiops.env"
    write_initial_config "$initial_config" "/var/lib/pauiops/pauiops.db"
    run_as_root install -d -m 0750 "$(dirname "$LINUX_ENV_FILE")"
    run_as_root install -m 0600 "$initial_config" "$LINUX_ENV_FILE"
    fresh_config="1"
  fi

  READY_URL="http://127.0.0.1:$PORT/api/ready"
  if [ "$installed" = "1" ] && [ "$MODE" != "install" ]; then
    log "Updating managed Linux service..."
    run_as_root env \
      "PAUIOPS_MANAGER_INSTALL_PATH=$INSTALL_PATH" \
      "PAUIOPS_SYSTEMD_ENV_PATH=$LINUX_ENV_FILE" \
      "PAUIOPS_SYSTEMD_UNIT_PATH=$LINUX_UNIT_FILE" \
      "PAUIOPS_UPGRADE_HEALTH_URL=$READY_URL" \
      "$INSTALL_PATH" "-upgrade=$DOWNLOADED_BINARY"
  else
    log "Installing managed Linux service..."
    run_as_root env \
      "PAUIOPS_MANAGER_INSTALL_PATH=$INSTALL_PATH" \
      "PAUIOPS_SYSTEMD_ENV_PATH=$LINUX_ENV_FILE" \
      "PAUIOPS_SYSTEMD_UNIT_PATH=$LINUX_UNIT_FILE" \
      "PAUIOPS_UPGRADE_HEALTH_URL=$READY_URL" \
      "$DOWNLOADED_BINARY" -service=enable
  fi

  wait_until_ready "$READY_URL" ||
    fail "the Linux service did not become ready at $READY_URL."

  if [ "$fresh_config" = "1" ]; then
    remove_bootstrap_password_as_root "$LINUX_ENV_FILE"
  fi

  log ""
  log "$PRODUCT_NAME $RELEASE_VERSION is ready."
  log "  API:    http://127.0.0.1:$PORT"
  log "  Binary: $INSTALL_PATH"
  log "  Config: $LINUX_ENV_FILE"
  if [ "$fresh_config" = "1" ]; then
    log "  Admin:  $ADMIN_USERNAME"
    log "  Email:  $ADMIN_EMAIL"
    log "  Password: $ADMIN_PASSWORD"
    log "Save this password now; it was removed from the on-disk bootstrap config."
  fi
}

xml_escape() {
  printf '%s' "$1" |
    sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g' \
      -e 's/"/\&quot;/g' -e "s/'/\&apos;/g"
}

write_launchd_plist() {
  plist_destination="$1"
  escaped_label="$(xml_escape "$MAC_LABEL")"
  escaped_binary="$(xml_escape "$INSTALL_PATH")"
  escaped_state="$(xml_escape "$MAC_STATE_DIR")"
  escaped_stdout="$(xml_escape "$MAC_STATE_DIR/pauiops.log")"
  escaped_stderr="$(xml_escape "$MAC_STATE_DIR/pauiops-error.log")"

  if ! cat >"$plist_destination" <<EOF
<?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>$escaped_label</string>
  <key>ProgramArguments</key>
  <array>
    <string>$escaped_binary</string>
  </array>
  <key>WorkingDirectory</key>
  <string>$escaped_state</string>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>ProcessType</key>
  <string>Interactive</string>
  <key>StandardOutPath</key>
  <string>$escaped_stdout</string>
  <key>StandardErrorPath</key>
  <string>$escaped_stderr</string>
</dict>
</plist>
EOF
  then
    return 1
  fi
  chmod 0600 "$plist_destination" || return 1
  if command -v plutil >/dev/null 2>&1; then
    plutil -lint "$plist_destination" >/dev/null || return 1
  fi
}

stop_launchd_agent() {
  launch_uid="$(id -u)"
  launchctl bootout "gui/$launch_uid/$MAC_LABEL" >/dev/null 2>&1 ||
    launchctl bootout "gui/$launch_uid" "$MAC_PLIST" >/dev/null 2>&1 ||
    launchctl bootout "user/$launch_uid/$MAC_LABEL" >/dev/null 2>&1 ||
    launchctl unload "$MAC_PLIST" >/dev/null 2>&1 ||
    true
}

start_launchd_agent() {
  launch_uid="$(id -u)"
  if launchctl bootstrap "gui/$launch_uid" "$MAC_PLIST" >/dev/null 2>&1; then
    launchctl kickstart -k "gui/$launch_uid/$MAC_LABEL" >/dev/null 2>&1 || true
    return 0
  fi
  if launchctl bootstrap "user/$launch_uid" "$MAC_PLIST" >/dev/null 2>&1; then
    launchctl kickstart -k "user/$launch_uid/$MAC_LABEL" >/dev/null 2>&1 || true
    return 0
  fi
  launchctl load -w "$MAC_PLIST" >/dev/null 2>&1
}

rollback_macos_install() {
  backup_path="$1"
  previous_plist="$2"
  was_installed="$3"

  stop_launchd_agent
  if [ -n "$backup_path" ] && [ -f "$backup_path" ]; then
    mv "$backup_path" "$INSTALL_PATH"
  elif [ "$was_installed" != "1" ]; then
    rm -f "$INSTALL_PATH"
  fi

  if [ -n "$previous_plist" ] && [ -f "$previous_plist" ]; then
    cp -p "$previous_plist" "$MAC_PLIST"
  elif [ "$was_installed" != "1" ]; then
    rm -f "$MAC_PLIST"
  fi

  if [ "$was_installed" = "1" ]; then
    start_launchd_agent || true
  fi
}

install_macos() {
  command -v launchctl >/dev/null 2>&1 ||
    fail "launchctl is required on macOS."

  if [ -f "$MAC_ENV_FILE" ]; then
    configured_install_path="$(config_value "$MAC_ENV_FILE" PAUIOPS_MANAGER_INSTALL_PATH || true)"
    configured_root_path="$(config_value "$MAC_ENV_FILE" PAUIOPS_ROOT_PATH || true)"
    configured_port="$(config_value "$MAC_ENV_FILE" PORT || true)"
    if [ "$INSTALL_PATH_WAS_SET" = "0" ] && [ -n "$configured_install_path" ]; then
      INSTALL_PATH="$configured_install_path"
    fi
    if [ "$ROOT_PATH_WAS_SET" = "0" ] && [ -n "$configured_root_path" ]; then
      ROOT_PATH="$configured_root_path"
    fi
    if [ "$PORT_WAS_SET" = "0" ] && [ -n "$configured_port" ]; then
      PORT="$configured_port"
    fi
    validate_port
  fi

  installed="0"
  if [ -x "$INSTALL_PATH" ] && [ -f "$MAC_PLIST" ]; then
    installed="1"
  fi
  if [ "$MODE" = "update" ] && [ "$installed" != "1" ]; then
    fail "no managed macOS installation was found at $INSTALL_PATH."
  fi

  mkdir -p "$(dirname "$INSTALL_PATH")" "$MAC_STATE_DIR" "$ROOT_PATH" "$(dirname "$MAC_PLIST")"
  chmod 0700 "$MAC_STATE_DIR"

  fresh_config="0"
  if [ ! -f "$MAC_ENV_FILE" ]; then
    write_initial_config "$MAC_ENV_FILE" "$MAC_STATE_DIR/pauiops.db"
    chmod 0600 "$MAC_ENV_FILE"
    fresh_config="1"
  fi

  backup_path="$INSTALL_PATH.backup"
  if [ -f "$INSTALL_PATH" ]; then
    cp -p "$INSTALL_PATH" "$backup_path"
  else
    backup_path=""
  fi

  previous_plist=""
  if [ -f "$MAC_PLIST" ]; then
    previous_plist="$TMP_DIR/previous-launchd.plist"
    cp -p "$MAC_PLIST" "$previous_plist"
  fi

  MAC_STAGED_BINARY="$INSTALL_PATH.new.$$"
  cp "$DOWNLOADED_BINARY" "$MAC_STAGED_BINARY"
  chmod 0755 "$MAC_STAGED_BINARY"
  MAC_STAGED_PLIST="$MAC_PLIST.new.$$"
  if ! write_launchd_plist "$MAC_STAGED_PLIST"; then
    fail "could not create a valid launchd agent at $MAC_PLIST."
  fi

  stop_launchd_agent
  if ! mv "$MAC_STAGED_BINARY" "$INSTALL_PATH"; then
    rollback_macos_install "$backup_path" "$previous_plist" "$installed"
    fail "could not replace the macOS binary at $INSTALL_PATH."
  fi
  MAC_STAGED_BINARY=""
  if ! mv "$MAC_STAGED_PLIST" "$MAC_PLIST"; then
    rollback_macos_install "$backup_path" "$previous_plist" "$installed"
    fail "could not replace the macOS launchd agent at $MAC_PLIST."
  fi
  MAC_STAGED_PLIST=""

  log "$([ "$installed" = "1" ] && printf 'Updating' || printf 'Installing') macOS launchd agent..."
  if ! start_launchd_agent; then
    rollback_macos_install "$backup_path" "$previous_plist" "$installed"
    fail "launchd could not load $MAC_PLIST; the previous installation was restored."
  fi

  READY_URL="http://127.0.0.1:$PORT/api/ready"
  if ! wait_until_ready "$READY_URL"; then
    log "New binary did not become ready; rolling back." >&2
    rollback_macos_install "$backup_path" "$previous_plist" "$installed"
    if [ "$installed" = "1" ]; then
      wait_until_ready "$READY_URL" || true
    fi
    fail "the macOS agent did not become ready at $READY_URL."
  fi

  if [ "$fresh_config" = "1" ]; then
    remove_bootstrap_password "$MAC_ENV_FILE"
  fi

  log ""
  log "$PRODUCT_NAME $RELEASE_VERSION is ready."
  log "  API:    http://127.0.0.1:$PORT"
  log "  Binary: $INSTALL_PATH"
  log "  Config: $MAC_ENV_FILE"
  log "  Agent:  $MAC_PLIST"
  if [ "$fresh_config" = "1" ]; then
    log "  Admin:  $ADMIN_USERNAME"
    log "  Email:  $ADMIN_EMAIL"
    log "  Password: $ADMIN_PASSWORD"
    log "Save this password now; it was removed from the on-disk bootstrap config."
  fi
}

case "$PLATFORM_OS" in
  linux)
    install_linux
    ;;
  darwin)
    install_macos
    ;;
esac
