#!/bin/sh

set -eu
umask 077

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

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

require_command() {
  command -v "$1" >/dev/null 2>&1 || fail "$1 is required"
}

require_command curl
require_command mktemp
require_command node

NODE_COMMAND=$(command -v node)
BASE_URL=${VIBECHECK_INSTALL_BASE_URL:-https://vibecheckthis.com}
while [ "${BASE_URL%/}" != "$BASE_URL" ]; do
  BASE_URL=${BASE_URL%/}
done

NORMALIZED_BASE_URL=$("$NODE_COMMAND" -e '
  const url = new URL(process.argv[1])
  const clean = url.username === "" && url.password === "" && url.pathname === "/" && url.search === "" && url.hash === ""
  const secure = url.protocol === "https:"
  const loopback = url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1")
  if (!clean || (!secure && !loopback)) process.exit(1)
  process.stdout.write(url.origin)
' "$BASE_URL" 2>/dev/null) || fail "VIBECHECK_INSTALL_BASE_URL must use HTTPS, except for loopback testing"
BASE_URL=$NORMALIZED_BASE_URL
case "$BASE_URL" in
  https://*) SECURE_DOWNLOADS=1 ;;
  *) SECURE_DOWNLOADS=0 ;;
esac

if [ -n "${VIBECHECK_VERSION:-}" ]; then
  "$NODE_COMMAND" -e '
    if (!/^\d+\.\d+\.\d+$/.test(process.argv[1])) process.exit(1)
  ' "$VIBECHECK_VERSION" || fail "VIBECHECK_VERSION must be a stable semantic version"
  RELEASE_PATH="/downloads/cli/releases/$VIBECHECK_VERSION.json"
else
  RELEASE_PATH=/downloads/cli/stable.json
fi

if [ -z "${VIBECHECK_INSTALL_ROOT:-}" ] && [ -z "${HOME:-}" ]; then
  fail "HOME or VIBECHECK_INSTALL_ROOT must be set"
fi
if [ -z "${VIBECHECK_BIN_DIR:-}" ] && [ -z "${HOME:-}" ]; then
  fail "HOME or VIBECHECK_BIN_DIR must be set"
fi

INSTALL_ROOT=${VIBECHECK_INSTALL_ROOT:-"$HOME/.local/lib/vibecheck"}
BIN_DIR=${VIBECHECK_BIN_DIR:-"$HOME/.local/bin"}
COMMAND_PATH="$BIN_DIR/vibecheck"
CURRENT_LINK="$INSTALL_ROOT/current"

if [ -e "$INSTALL_ROOT" ] && [ ! -d "$INSTALL_ROOT" ]; then
  fail "$INSTALL_ROOT exists and is not a directory"
fi
if [ -e "$BIN_DIR" ] && [ ! -d "$BIN_DIR" ]; then
  fail "$BIN_DIR exists and is not a directory"
fi

mkdir -p "$INSTALL_ROOT" "$BIN_DIR"
chmod 0700 "$INSTALL_ROOT"

TEMP_DIR=$(mktemp -d "$INSTALL_ROOT/.install.XXXXXX")
LINK_DIR=
cleanup() {
  rm -rf "$TEMP_DIR"
  if [ -n "$LINK_DIR" ]; then rm -rf "$LINK_DIR"; fi
}
trap cleanup 0
trap 'exit 1' HUP INT TERM

download() {
  source_url=$1
  destination=$2
  if [ "$SECURE_DOWNLOADS" -eq 1 ]; then
    curl -f --silent --show-error --retry 3 --connect-timeout 15 \
      --max-time 120 --max-filesize 20971520 --proto '=https' --tlsv1.2 \
      -H 'Cache-Control: no-cache' "$source_url" -o "$destination"
  else
    curl -f --silent --show-error --retry 3 --connect-timeout 15 \
      --max-time 120 --max-filesize 20971520 -H 'Cache-Control: no-cache' \
      "$source_url" -o "$destination"
  fi
}

file_sha256() {
  "$NODE_COMMAND" -e '
    const { createHash } = require("node:crypto")
    const { readFileSync } = require("node:fs")
    process.stdout.write(createHash("sha256").update(readFileSync(process.argv[1])).digest("hex"))
  ' "$1"
}

say "Fetching the VibeCheckThis CLI release manifest..."
MANIFEST_FILE="$TEMP_DIR/release.json"
download "$BASE_URL$RELEASE_PATH" "$MANIFEST_FILE"
MANIFEST_BYTES=$("$NODE_COMMAND" -e '
  process.stdout.write(String(require("node:fs").statSync(process.argv[1]).size))
' "$MANIFEST_FILE")
[ "$MANIFEST_BYTES" -le 65536 ] || fail "the release manifest is too large"

RELEASE_FIELDS="$TEMP_DIR/release-fields"
mkdir "$RELEASE_FIELDS"
"$NODE_COMMAND" -e '
  const fs = require("node:fs")
  const release = JSON.parse(fs.readFileSync(process.argv[1], "utf8"))
  const output = process.argv[2]
  const stableVersion = /^\d+\.\d+\.\d+$/
  const digest = /^[0-9a-f]{64}$/
  if (release.schema_version !== 1) throw new Error("Unsupported release manifest schema")
  if (!stableVersion.test(release.version)) throw new Error("Invalid release version")
  if (!stableVersion.test(release.minimum_node_version)) throw new Error("Invalid minimum Node.js version")
  if (!digest.test(release.sha256)) throw new Error("Invalid release checksum")
  if (!Number.isSafeInteger(release.bytes) || release.bytes < 1 || release.bytes > 20971520) throw new Error("Invalid release size")
  const artifact = `/downloads/cli/${release.version}/${release.sha256}/vibecheck.cjs`
  if (release.artifact !== artifact) throw new Error("Invalid release artifact path")
  for (const [name, value] of Object.entries({
    version: release.version,
    minimum_node_version: release.minimum_node_version,
    sha256: release.sha256,
    bytes: String(release.bytes),
    artifact,
  })) fs.writeFileSync(`${output}/${name}`, value, { mode: 0o600 })
' "$MANIFEST_FILE" "$RELEASE_FIELDS" 2>/dev/null || fail "the release manifest is invalid"

RELEASE_VERSION=$(cat "$RELEASE_FIELDS/version")
MINIMUM_NODE_VERSION=$(cat "$RELEASE_FIELDS/minimum_node_version")
EXPECTED_SHA256=$(cat "$RELEASE_FIELDS/sha256")
EXPECTED_BYTES=$(cat "$RELEASE_FIELDS/bytes")
ARTIFACT_PATH=$(cat "$RELEASE_FIELDS/artifact")

if [ -n "${VIBECHECK_VERSION:-}" ] && [ "$RELEASE_VERSION" != "$VIBECHECK_VERSION" ]; then
  fail "requested version $VIBECHECK_VERSION but the release manifest published $RELEASE_VERSION"
fi

"$NODE_COMMAND" -e '
  const actual = process.versions.node.split(".").map(Number)
  const minimum = process.argv[1].split(".").map(Number)
  for (let index = 0; index < 3; index += 1) {
    if (actual[index] > minimum[index]) process.exit(0)
    if (actual[index] < minimum[index]) process.exit(1)
  }
' "$MINIMUM_NODE_VERSION" ||
  fail "Node.js $MINIMUM_NODE_VERSION or newer is required; found $($NODE_COMMAND --version)"

PREVIOUS_CURRENT_TARGET=
if [ -e "$CURRENT_LINK" ] || [ -L "$CURRENT_LINK" ]; then
  [ -L "$CURRENT_LINK" ] || fail "$CURRENT_LINK exists but is not a managed symbolic link"
  PREVIOUS_CURRENT_TARGET=$(readlink "$CURRENT_LINK")
fi

PREVIOUS_COMMAND_TARGET=
if [ -e "$COMMAND_PATH" ] || [ -L "$COMMAND_PATH" ]; then
  [ -L "$COMMAND_PATH" ] || fail "$COMMAND_PATH already exists and is not a managed symbolic link"
  PREVIOUS_COMMAND_TARGET=$(readlink "$COMMAND_PATH")
  case "$PREVIOUS_COMMAND_TARGET" in
    "$INSTALL_ROOT/current" | "$INSTALL_ROOT/vibecheck.cjs") ;;
    *) fail "$COMMAND_PATH is not managed by the VibeCheckThis installer" ;;
  esac
fi

if [ -n "$PREVIOUS_CURRENT_TARGET" ]; then
  CURRENT_VERSION=$("$NODE_COMMAND" -e '
    const path = require("node:path")
    const root = path.resolve(process.argv[1])
    const target = path.resolve(root, process.argv[2])
    const relative = path.relative(path.resolve(root, "versions"), target).split(path.sep)
    if (relative.length !== 2 || !/^\d+\.\d+\.\d+$/.test(relative[0]) || relative[1] !== "vibecheck.cjs") process.exit(1)
    process.stdout.write(relative[0])
  ' "$INSTALL_ROOT" "$PREVIOUS_CURRENT_TARGET") || fail "$CURRENT_LINK points outside the managed versions directory"
  if "$NODE_COMMAND" -e '
    const current = process.argv[1].split(".").map(Number)
    const next = process.argv[2].split(".").map(Number)
    if (current.some((value, index) => value > next[index] && current.slice(0, index).every((part, partIndex) => part === next[partIndex]))) process.exit(0)
    process.exit(1)
  ' "$CURRENT_VERSION" "$RELEASE_VERSION"; then
    [ "${VIBECHECK_ALLOW_DOWNGRADE:-0}" = 1 ] ||
      fail "refusing to downgrade $CURRENT_VERSION to $RELEASE_VERSION without VIBECHECK_ALLOW_DOWNGRADE=1"
  fi
fi

VERSION_DIR="$INSTALL_ROOT/versions/$RELEASE_VERSION"
TARGET="$VERSION_DIR/vibecheck.cjs"
[ ! -L "$INSTALL_ROOT/versions" ] || fail "$INSTALL_ROOT/versions is a symbolic link"
[ ! -L "$VERSION_DIR" ] || fail "$VERSION_DIR is a symbolic link"
mkdir -p "$VERSION_DIR"
chmod 0700 "$INSTALL_ROOT/versions" "$VERSION_DIR"

TARGET_READY=0
if [ -e "$TARGET" ]; then
  [ ! -L "$TARGET" ] || fail "$TARGET exists and is a symbolic link"
  [ -f "$TARGET" ] || fail "$TARGET exists and is not a regular file"
  if [ "$(file_sha256 "$TARGET")" = "$EXPECTED_SHA256" ]; then
    TARGET_READY=1
  fi
fi

if [ "$TARGET_READY" -eq 0 ]; then
  DOWNLOADED_BUNDLE="$TEMP_DIR/vibecheck.cjs"
  say "Downloading VibeCheckThis CLI $RELEASE_VERSION..."
  download "$BASE_URL$ARTIFACT_PATH" "$DOWNLOADED_BUNDLE"

  ACTUAL_SHA256=$(file_sha256 "$DOWNLOADED_BUNDLE")
  [ "$ACTUAL_SHA256" = "$EXPECTED_SHA256" ] || fail "downloaded bundle checksum did not match the release manifest"
  ACTUAL_BYTES=$("$NODE_COMMAND" -e '
    process.stdout.write(String(require("node:fs").statSync(process.argv[1]).size))
  ' "$DOWNLOADED_BUNDLE")
  [ "$ACTUAL_BYTES" = "$EXPECTED_BYTES" ] || fail "downloaded bundle size did not match the release manifest"

  DOWNLOADED_VERSION=$("$NODE_COMMAND" "$DOWNLOADED_BUNDLE" --version)
  [ "$DOWNLOADED_VERSION" = "$RELEASE_VERSION" ] || fail "downloaded bundle reported version $DOWNLOADED_VERSION instead of $RELEASE_VERSION"

  chmod 0755 "$DOWNLOADED_BUNDLE"
  mv -f "$DOWNLOADED_BUNDLE" "$TARGET"
fi

chmod 0755 "$TARGET"
INSTALLED_VERSION=$("$NODE_COMMAND" "$TARGET" --version)
[ "$INSTALLED_VERSION" = "$RELEASE_VERSION" ] || fail "installed bundle failed its version check"
RELEASE_RECORD="$TEMP_DIR/release-record.json"
cp "$MANIFEST_FILE" "$RELEASE_RECORD"
chmod 0600 "$RELEASE_RECORD"
mv -f "$RELEASE_RECORD" "$VERSION_DIR/release.json"

replace_link() {
  link_target=$1
  link_path=$2
  link_parent=$3
  link_name=$4
  LINK_DIR=$(mktemp -d "$link_parent/.vibecheck-link.XXXXXX") || return 1
  if ! ln -s "$link_target" "$LINK_DIR/$link_name"; then
    rm -rf "$LINK_DIR"
    LINK_DIR=
    return 1
  fi
  if ! "$NODE_COMMAND" -e '
    require("node:fs").renameSync(process.argv[1], process.argv[2])
  ' "$LINK_DIR/$link_name" "$link_path"; then
    rm -rf "$LINK_DIR"
    LINK_DIR=
    return 1
  fi
  rm -rf "$LINK_DIR"
  LINK_DIR=
}

restore_previous_activation() {
  if [ -n "$PREVIOUS_CURRENT_TARGET" ]; then
    replace_link "$PREVIOUS_CURRENT_TARGET" "$CURRENT_LINK" "$INSTALL_ROOT" current || true
  else
    rm -f "$CURRENT_LINK"
  fi
  if [ -n "$PREVIOUS_COMMAND_TARGET" ]; then
    replace_link "$PREVIOUS_COMMAND_TARGET" "$COMMAND_PATH" "$BIN_DIR" vibecheck || true
  else
    rm -f "$COMMAND_PATH"
  fi
}

if ! replace_link "$TARGET" "$CURRENT_LINK" "$INSTALL_ROOT" current; then
  restore_previous_activation
  fail "could not activate the verified release"
fi
if ! replace_link "$INSTALL_ROOT/current" "$COMMAND_PATH" "$BIN_DIR" vibecheck; then
  restore_previous_activation
  fail "could not activate the verified command"
fi

if ! ACTIVE_VERSION=$("$COMMAND_PATH" --version); then
  restore_previous_activation
  fail "the installed command could not start; the prior version was restored"
fi
if [ "$ACTIVE_VERSION" != "$RELEASE_VERSION" ]; then
  restore_previous_activation
  fail "the active command failed its version check; the prior version was restored"
fi

say "Installed VibeCheckThis CLI $RELEASE_VERSION at $COMMAND_PATH"
if [ -n "$PREVIOUS_CURRENT_TARGET" ] && [ "$PREVIOUS_CURRENT_TARGET" != "$TARGET" ]; then
  say "The prior local version was kept for rollback."
fi

case ":${PATH:-}:" in
  *:"$BIN_DIR":*) ;;
  *)
    QUOTED_BIN_DIR=$("$NODE_COMMAND" -e '
      const quote = String.fromCharCode(39)
      const escaped = process.argv[1].split(quote).join(quote + "\"" + quote + "\"" + quote)
      process.stdout.write(quote + escaped + quote)
    ' "$BIN_DIR")
    say "Add this directory to PATH, then open a new terminal:"
    say "  export PATH=$QUOTED_BIN_DIR:\$PATH"
    ;;
esac

say "Run: vibecheck --version"
