#!/usr/bin/env bash
#
# NoteSage — macOS installer.
#
#   curl -fsSL <hosted-url> | bash
#
# Downloads the current release from the NoteSage mirror, verifies it against
# the SHA-512 the release metadata advertises, installs it to /Applications and
# clears the quarantine attribute so Gatekeeper does not block the first launch.
#
# Environment overrides:
#   NOTESAGE_MIRROR    base URL of the release mirror
#   NOTESAGE_VERSION   install this exact version instead of the current one
#   NOTESAGE_DEST      install directory (default /Applications)
#   NOTESAGE_LAUNCH=0  install without launching afterwards
#   NOTESAGE_ALLOW_DOWNGRADE=1  permit installing older than what is installed

set -euo pipefail

MIRROR="${NOTESAGE_MIRROR:-https://notesage-mirror.naaico.com}"
MIRROR="${MIRROR%/}"
DEST="${NOTESAGE_DEST:-/Applications}"
APP_NAME="NoteSage.app"
LAUNCH="${NOTESAGE_LAUNCH:-1}"

# Pre-rename identity. The product was SpeechSage before it was NoteSage, so a machine
# that installed earlier carries a SpeechSage.app bundle and a SpeechSage data directory.
# Both are handled below: the stale bundle is removed so the user is not left with two
# apps, and the data directory is moved so notes, settings and stored keys survive.
LEGACY_APP_NAME="SpeechSage.app"
APP_SUPPORT="${HOME}/Library/Application Support"

# All output goes to stderr: stdout is kept clean in case this is ever piped.
say()  { printf '\033[1;36m==>\033[0m %s\n' "$*" >&2; }
warn() { printf '\033[1;33m warning:\033[0m %s\n' "$*" >&2; }
die()  { printf '\033[1;31m error:\033[0m %s\n' "$*" >&2; exit 1; }

# Move each pre-rename data directory onto its new name, per channel.
#
# Moved, not copied, so there is never a second diverging copy. Skipped when the new
# directory already exists: that means the app has run since the rename and is the
# authoritative copy, and merging the two could overwrite newer data with older. A failure
# here is deliberately non-fatal -- the app performs the same migration itself on launch,
# so the install should still finish.
migrate_legacy_data() {
  local pair legacy_name current_name legacy current
  for pair in "SpeechSage:NoteSage" \
              "SpeechSage-staging:NoteSage-staging" \
              "SpeechSage-development:NoteSage-development"; do
    legacy_name="${pair%%:*}"
    current_name="${pair##*:}"
    legacy="${APP_SUPPORT}/${legacy_name}"
    current="${APP_SUPPORT}/${current_name}"

    [ -d "$legacy" ] || continue
    if [ -d "$current" ]; then
      warn "Both ${current_name} and ${legacy_name} data directories exist; keeping ${current_name} and leaving ${legacy_name} in place."
      continue
    fi

    say "Migrating your data: ${legacy_name} -> ${current_name}"
    mv "$legacy" "$current" \
      || warn "Could not move ${legacy}. NoteSage will retry this on first launch."
  done
}

# ---------------------------------------------------------------- preflight

[ "$(uname -s)" = "Darwin" ] || die "This installer is macOS only (found $(uname -s))."

case "$(uname -m)" in
  arm64)  ARCH=arm64 ;;
  x86_64) ARCH=x64 ;;
  *)      die "Unsupported architecture: $(uname -m)" ;;
esac

for tool in curl ditto openssl xattr; do
  command -v "$tool" >/dev/null 2>&1 || die "Required tool not found: $tool"
done

TMPDIR_INSTALL="$(mktemp -d "${TMPDIR:-/tmp}/notesage-install.XXXXXX")"
cleanup() { rm -rf "$TMPDIR_INSTALL"; }
trap cleanup EXIT

# Highest version wins; returns 0 when $1 is strictly older than $2.
version_lt() {
  [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" = "$1" ]
}

# ------------------------------------------------------- resolve the release
#
# The mirror publishes electron-updater metadata at the bucket root. Its
# top-level `path:` and `sha512:` name the macOS zip for this architecture, so
# resolving through it means the installer never has to guess an artifact
# filename and always gets a checksum to verify against.

if [ -n "${NOTESAGE_VERSION:-}" ]; then
  VERSION="$NOTESAGE_VERSION"
  REMOTE_PATH="releases/${VERSION}/NoteSage-${VERSION}-${ARCH}-mac.zip"
  say "Installing NoteSage ${VERSION} (${ARCH}), pinned by NOTESAGE_VERSION"

  # A pinned install used to skip verification entirely. The release upload
  # publishes a per-version copy of the feed next to the artifact, so there is
  # always a digest to check against — resolve it rather than proceeding blind.
  PINNED_FEED_URL="${MIRROR}/releases/${VERSION}/latest-${ARCH}-mac.yml"
  if PINNED_FEED="$(curl -fsSL --retry 3 --retry-delay 1 "$PINNED_FEED_URL" 2>/dev/null)"; then
    EXPECTED_SHA="$(printf '%s\n' "$PINNED_FEED" | awk '/^sha512: /{print $2; exit}')"
    PINNED_PATH="$(printf '%s\n' "$PINNED_FEED" | awk '/^path: /{print $2; exit}')"
    [ -n "$PINNED_PATH" ] && REMOTE_PATH="$PINNED_PATH"
  else
    EXPECTED_SHA=""
  fi

  [ -n "$EXPECTED_SHA" ] \
    || die "No checksum published for NoteSage ${VERSION} (${ARCH}) at ${PINNED_FEED_URL}. Refusing to install an unverifiable build."
else
  say "Resolving the current release (${ARCH})…"
  FEED_URL="${MIRROR}/latest-${ARCH}-mac.yml"
  if ! FEED="$(curl -fsSL --retry 3 --retry-delay 1 "$FEED_URL")"; then
    if [ "$ARCH" = "x64" ]; then
      die "No Intel (x86_64) release is published yet — NoteSage currently ships for Apple Silicon only. On an Intel Mac, or under Rosetta, there is nothing to install."
    fi
    die "Could not read the release feed at ${FEED_URL}"
  fi

  # Only the top-level keys are unindented; the per-file entries are nested.
  VERSION="$(printf '%s\n' "$FEED"    | awk '/^version: /{print $2; exit}')"
  REMOTE_PATH="$(printf '%s\n' "$FEED" | awk '/^path: /{print $2; exit}')"
  EXPECTED_SHA="$(printf '%s\n' "$FEED" | awk '/^sha512: /{print $2; exit}')"

  [ -n "$VERSION" ] && [ -n "$REMOTE_PATH" ] \
    || die "The release feed at ${FEED_URL} is missing a version or path."
  say "Current release is NoteSage ${VERSION}"
fi

INSTALLED_VERSION=""
if [ -d "${DEST}/${APP_NAME}" ]; then
  INSTALLED_VERSION="$(/usr/bin/defaults read "${DEST}/${APP_NAME}/Contents/Info" \
    CFBundleShortVersionString 2>/dev/null || true)"
  if [ "$INSTALLED_VERSION" = "$VERSION" ]; then
    say "NoteSage ${VERSION} is already installed at ${DEST}/${APP_NAME}. Reinstalling."
  elif [ -n "$INSTALLED_VERSION" ] && version_lt "$VERSION" "$INSTALLED_VERSION"; then
    if [ "${NOTESAGE_ALLOW_DOWNGRADE:-0}" = "1" ]; then
      warn "Downgrading from ${INSTALLED_VERSION} to ${VERSION} (NOTESAGE_ALLOW_DOWNGRADE=1)."
    else
      die "The release feed offers ${VERSION} but ${INSTALLED_VERSION} is already installed. Refusing to downgrade. Set NOTESAGE_ALLOW_DOWNGRADE=1 to override."
    fi
  fi
fi

# ------------------------------------------------------------------ download

ZIP="${TMPDIR_INSTALL}/NoteSage.zip"
say "Downloading ${REMOTE_PATH}…"
curl -fL --retry 3 --retry-delay 2 --progress-bar -o "$ZIP" "${MIRROR}/${REMOTE_PATH}" \
  || die "Download failed: ${MIRROR}/${REMOTE_PATH}"

if [ -n "$EXPECTED_SHA" ]; then
  say "Verifying checksum…"
  # The feed carries the SHA-512 base64-encoded, the way electron-builder writes it.
  ACTUAL_SHA="$(openssl dgst -sha512 -binary "$ZIP" | openssl base64 -A)"
  [ "$ACTUAL_SHA" = "$EXPECTED_SHA" ] \
    || die "Checksum mismatch — the download is corrupt or has been tampered with. Aborting."
else
  die "The release feed published no checksum for this artifact. Refusing to install an unverifiable build."
fi

# ------------------------------------------------------------------- unpack

say "Unpacking…"
# ditto, not unzip: it preserves the code-signature metadata in the bundle.
ditto -x -k "$ZIP" "${TMPDIR_INSTALL}/unpacked" \
  || die "Could not unpack the archive."

STAGED="${TMPDIR_INSTALL}/unpacked/${APP_NAME}"
[ -d "$STAGED" ] || die "The archive did not contain ${APP_NAME}."

# ---------------------------------------------------------------- gatekeeper
#
# Releases are currently ad-hoc signed and not notarized, so Gatekeeper rejects
# the bundle on assessment. Removing com.apple.quarantine before the app is ever
# launched means that assessment never runs, which is the only way a curl
# install can work today. This stays correct once builds are Developer ID signed
# and notarized: it then just suppresses the "downloaded from the internet"
# prompt on first launch.

say "Clearing the quarantine attribute…"
xattr -dr com.apple.quarantine "$STAGED" 2>/dev/null || true

if command -v codesign >/dev/null 2>&1; then
  SIG_INFO="$(codesign -dvv "$STAGED" 2>&1 || true)"
  if printf '%s' "$SIG_INFO" | grep -q "Authority=Developer ID Application"; then
    if codesign --verify --deep --strict "$STAGED" 2>/dev/null; then
      say "Developer ID signature verified."
    else
      warn "The Developer ID signature did not verify. Install only if you trust this source."
    fi
  elif printf '%s' "$SIG_INFO" | grep -q "adhoc"; then
    warn "This build is ad-hoc signed, not Developer ID signed or notarized."
    warn "Its origin cannot be verified by macOS — the SHA-512 check above is the only integrity guarantee."
  else
    warn "This build is unsigned. Install only if you trust this source."
  fi
fi

# ------------------------------------------------------------------ install

SUDO=""
if [ ! -w "$DEST" ]; then
  say "${DEST} needs administrator access; you will be prompted for your password."
  SUDO="sudo"
fi

if [ -d "${DEST}/${APP_NAME}" ]; then
  # Quit a running copy first, or the replace leaves a half-updated bundle.
  osascript -e 'quit app "NoteSage"' >/dev/null 2>&1 || true
  say "Removing the existing installation…"
  $SUDO rm -rf "${DEST}/${APP_NAME}" || die "Could not remove ${DEST}/${APP_NAME}."
fi

# A pre-rename install is a separate bundle under the old name. It has to be quit before
# the data directory moves underneath it, and removed so the user is not left launching a
# stale SpeechSage that now points at a directory this installer just renamed away.
if [ -d "${DEST}/${LEGACY_APP_NAME}" ]; then
  osascript -e 'quit app "SpeechSage"' >/dev/null 2>&1 || true
  say "Removing the previous ${LEGACY_APP_NAME} installation…"
  $SUDO rm -rf "${DEST}/${LEGACY_APP_NAME}" \
    || warn "Could not remove ${DEST}/${LEGACY_APP_NAME}; delete it by hand."
fi

say "Installing to ${DEST}…"
$SUDO ditto "$STAGED" "${DEST}/${APP_NAME}" || die "Could not install to ${DEST}."
$SUDO xattr -dr com.apple.quarantine "${DEST}/${APP_NAME}" 2>/dev/null || true

say "NoteSage ${VERSION} installed at ${DEST}/${APP_NAME}"

migrate_legacy_data

if [ "$LAUNCH" != "0" ]; then
  say "Launching…"
  open -a "${DEST}/${APP_NAME}" || warn "Could not launch automatically; open it from ${DEST}."
fi

say "Done. NoteSage will ask for Microphone and Accessibility permission on first use."
