#!/bin/sh
# Installs Porticol on a Mac or a Linux machine the way a developer expects it: one
# command, no password, no Gatekeeper prompt.
#
#   curl -fsSL https://porticol.app/install.sh | sh
#   curl -fsSL https://porticol.app/install.sh | sh -s -- --version VERSION
#
# It takes the version from installer.json on the update host, or from the signed
# update manifest beside it, unless --version names one, and checks every download
# against the SHA256SUMS published beside it.
#
# On a Mac it downloads VERSION/Porticol.app.tar.gz and copies the app into
# /Applications, or into ~/Applications when /Applications is not writable, then opens
# it. On Linux it downloads the x64 AppImage into ~/.local/share/porticol, adds it to
# the applications menu and puts a porticol launcher in ~/.local/bin. There is no Arm
# Linux build yet.
#
# The command line and MCP server, porticol-cli, goes into the first of
# /usr/local/bin and /opt/homebrew/bin that can be written on a Mac, and into
# ~/.local/bin otherwise. A release without it is an error rather than a quiet
# half-install, since the page that sent the person here promises it.
#
# The checksum catches a truncated download or a file from another release. It is
# not a signature: a file and its checksum come from one server, so a server that
# means to lie can make them agree. What the app trusts is its own update key, which
# the copy put here is unaffected by.
#
# Options:
#   --version VERSION  install this release rather than the newest
#   --dir DIR          install the app into DIR
#   --url URL          read the release from URL rather than the update host
#   --no-open          install without opening the app
#   --no-cli           install the app alone, without the command line
#   -h, --help         print this and do nothing
#
# Environment: PORTICOL_UPDATE_URL, PORTICOL_VERSION and PORTICOL_DIR do the same
# as the three options above.
set -eu

base=${PORTICOL_UPDATE_URL:-https://updates.porticol.app}
base=${base%/}
version=${PORTICOL_VERSION:-}
dir=${PORTICOL_DIR:-}
launch=1
cli=1

usage() {
  cat <<'USAGE'
Install Porticol on a Mac or on Linux.

  curl -fsSL https://porticol.app/install.sh | sh
  curl -fsSL https://porticol.app/install.sh | sh -s -- --version VERSION

Options:
  --version VERSION  install this release rather than the newest
  --dir DIR          install the app into DIR rather than /Applications on a Mac
                     or ~/.local/share/porticol on Linux
  --url URL          read the release from URL rather than the update host
  --no-open          install without opening the app
  --no-cli           install the app alone, without the command line
  -h, --help         print this and do nothing

Environment: PORTICOL_UPDATE_URL, PORTICOL_VERSION and PORTICOL_DIR do the same
as the three options above.

On Windows, in PowerShell: irm https://porticol.app/install.ps1 | iex
USAGE
}

die() {
  printf 'install.sh: %s\n' "$1" >&2
  exit 1
}

note() {
  printf '  %s\n' "$1"
}

while [ "$#" -gt 0 ]; do
  case "$1" in
    --version) [ "$#" -ge 2 ] || die "--version needs a value"; version=$2; shift 2 ;;
    --version=*) version=${1#*=}; shift ;;
    --dir) [ "$#" -ge 2 ] || die "--dir needs a value"; dir=$2; shift 2 ;;
    --dir=*) dir=${1#*=}; shift ;;
    --url) [ "$#" -ge 2 ] || die "--url needs a value"; base=${2%/}; shift 2 ;;
    --url=*) base=${1#*=}; base=${base%/}; shift ;;
    --no-open) launch=; shift ;;
    --no-cli) cli=; shift ;;
    -h|--help) usage; exit 0 ;;
    *) die "unknown option $1; try --help" ;;
  esac
done

system=$(uname -s)
case "$system" in
  Darwin | Linux) ;;
  *) die "this script installs on macOS and Linux. On Windows, in PowerShell: irm https://porticol.app/install.ps1 | iex. Every file is on https://porticol.app/download" ;;
esac

command -v curl >/dev/null 2>&1 || die "curl is missing, and this script downloads with it"
command -v tar >/dev/null 2>&1 || die "tar is missing"

tmp=$(mktemp -d "${TMPDIR:-/tmp}/porticol-install.XXXXXX")
trap 'rm -rf "$tmp"' EXIT HUP INT TERM

fetch() {
  curl -fsSL --proto '=https' --tlsv1.2 --retry 3 --retry-delay 1 "$1"
}

fetch_to() {
  if curl -fsSL --proto '=https' --tlsv1.2 --retry 3 --retry-delay 1 -o "$2" "$1" 2>"$tmp/curl.log"; then
    return 0
  fi
  # curl repeats itself once per retry; the last line says what went wrong.
  if [ -s "$tmp/curl.log" ]; then
    printf '  %s\n' "$(tail -n 1 "$tmp/curl.log")" >&2
  fi
  return 1
}

digest() {
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | awk '{ print $1 }'
  else
    shasum -a 256 "$1" | awk '{ print $1 }'
  fi
}

# The newest published version, from the file the publisher writes for this
# script and, when a signed release exists, from the update manifest beside it.
published() {
  for name in installer.json stable.json; do
    if fetch_to "$base/$name" "$tmp/$name" 2>/dev/null; then
      if command -v plutil >/dev/null 2>&1 &&
        plutil -extract version raw -o - "$tmp/$name" 2>/dev/null; then
        return 0
      fi
      sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$tmp/$name" | head -n 1
      return 0
    fi
  done
  return 1
}

if [ -z "$version" ]; then
  version=$(published) || die "nothing is published at $base yet; see https://porticol.app/download, or pass --version"
  [ -n "$version" ] || die "the release index at $base has no version"
fi

# The version names a folder on the update host, so it may hold nothing that
# could climb out of it.
case "$version" in
  *[!0-9A-Za-z.-]*) die "\"$version\" is not a version" ;;
esac

printf 'Porticol %s\n' "$version"
sums=$(fetch "$base/$version/SHA256SUMS") || die "could not read $base/$version/SHA256SUMS, so nothing was installed"

# Downloads one file of the release into $tmp and checks it against SHA256SUMS.
download() {
  file=$1
  note "Downloading $base/$version/$file"
  fetch_to "$base/$version/$file" "$tmp/$file" || return 1
  expected=$(printf '%s\n' "$sums" | awk -v file="$file" '$2 == file { print $1 }')
  [ -n "$expected" ] || die "$file is not in this release's SHA256SUMS, so it was not installed"
  actual=$(digest "$tmp/$file")
  [ "$expected" = "$actual" ] || die "$file does not match its published checksum: expected $expected, got $actual"
  note "Checksum matches $expected"
}

install_mac() {
  app_name=Porticol.app
  archive=Porticol.app.tar.gz
  if pgrep -x porticol >/dev/null 2>&1; then
    note "Porticol is running; asking it to quit"
    osascript -e 'tell application "Porticol" to quit' >/dev/null 2>&1 || true
    waited=0
    while pgrep -x porticol >/dev/null 2>&1 && [ "$waited" -lt 40 ]; do
      sleep 0.25
      waited=$((waited + 1))
    done
    if pgrep -x porticol >/dev/null 2>&1; then
      die "Porticol is still running; quit it and run this again"
    fi
  fi

  download "$archive" || die "no $archive for $version at $base"
  mkdir "$tmp/unpack"
  tar -xzf "$tmp/$archive" -C "$tmp/unpack" || die "$archive did not unpack"
  app=$(find "$tmp/unpack" -maxdepth 2 -type d -name "$app_name" | head -n 1)
  [ -n "$app" ] || die "$archive does not hold $app_name"

  # A tarball that arrived through a browser carries the quarantine flag and would
  # be refused on first launch. Taken through curl it does not, so this only
  # matters for an archive downloaded, unzipped and passed to --dir by hand.
  xattr -dr com.apple.quarantine "$app" 2>/dev/null || true

  if [ -n "$dir" ]; then
    target=$dir
  elif [ -w /Applications ]; then
    target=/Applications
  else
    target=$HOME/Applications
  fi
  case "$target" in
    /|"") die "refusing to install into \"$target\"" ;;
  esac
  mkdir -p "$target"
  rm -rf "${target:?}/$app_name"
  ditto "$app" "$target/$app_name" || die "could not write to $target"
  printf '\nInstalled Porticol %s in %s/%s\n' "$version" "$target" "$app_name"
  opener="open -a Porticol"
  opened() { open "$target/$app_name" 2>/dev/null; }
}

install_linux() {
  appimage=Porticol_${version}_amd64.AppImage
  download "$appimage" || die "no $appimage for $version at $base"
  target=${dir:-${XDG_DATA_HOME:-$HOME/.local/share}/porticol}
  case "$target" in
    /|"") die "refusing to install into \"$target\"" ;;
  esac
  mkdir -p "$target"
  # Moved into place rather than written over, so a copy that is running keeps its
  # own file until it quits.
  install -m 755 "$tmp/$appimage" "$target/.Porticol.AppImage.new" || die "could not write to $target"
  mv -f "$target/.Porticol.AppImage.new" "$target/Porticol.AppImage"
  app=$target/Porticol.AppImage

  launcher_dir=$HOME/.local/bin
  mkdir -p "$launcher_dir"
  ln -sf "$app" "$launcher_dir/porticol"

  applications=${XDG_DATA_HOME:-$HOME/.local/share}/applications
  mkdir -p "$applications"
  fetch_to https://porticol.app/logo.png "$target/porticol.png" 2>/dev/null || true
  cat >"$applications/porticol.desktop" <<DESKTOP
[Desktop Entry]
Type=Application
Name=Porticol
Comment=API client
Exec="$app" %U
Icon=$target/porticol.png
Terminal=false
Categories=Development;
DESKTOP
  command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$applications" >/dev/null 2>&1 || true

  printf '\nInstalled Porticol %s in %s\n' "$version" "$app"
  fuse=1
  if ! { ldconfig -p 2>/dev/null || true; } | grep -q 'libfuse\.so\.2'; then
    fuse=
    note "The AppImage needs FUSE 2, which Ubuntu 22.04 and later leave out: sudo apt install libfuse2"
  fi
  case ":$PATH:" in
    *":$launcher_dir:"*) opener=porticol ;;
    *) opener=$app ;;
  esac
  opened() {
    [ -n "$fuse" ] && [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ] || return 1
    nohup "$app" >/dev/null 2>&1 &
  }
}

machine=$(uname -m)
if [ "$system" = Darwin ]; then
  install_mac
  cli_archive=porticol-cli-$version-macos-universal.tar.gz
else
  case "$machine" in
    x86_64 | amd64)
      install_linux
      cli_archive=porticol-cli-$version-linux-x64.tar.gz
      ;;
    *) die "there is no Porticol build for $machine; see https://porticol.app/download" ;;
  esac
fi

if [ -n "$cli" ]; then
  download "$cli_archive" || die "no $cli_archive for $version at $base"
  tar -xzf "$tmp/$cli_archive" -C "$tmp" porticol-cli || die "$cli_archive did not unpack"
  bin=$HOME/.local/bin
  if [ "$system" = Darwin ]; then
    for candidate in /usr/local/bin /opt/homebrew/bin; do
      if [ -d "$candidate" ] && [ -w "$candidate" ]; then bin=$candidate; break; fi
    done
  fi
  mkdir -p "$bin"
  install -m 755 "$tmp/porticol-cli" "$bin/porticol-cli" || die "could not write to $bin"
  printf 'Installed porticol-cli in %s\n' "$bin"
  case ":$PATH:" in
    *":$bin:"*)
      note "Connect Claude Code: claude mcp add porticol -- porticol-cli mcp"
      note "Or as a plugin: claude plugin marketplace add pavanmanishd/porticol-cli && claude plugin install porticol@porticol-cli"
      ;;
    *)
      rc=~/.profile
      case "${SHELL:-}" in
        */zsh) rc=~/.zshrc ;;
        */bash) rc=~/.bashrc ;;
      esac
      note "$bin is not on your PATH. Add it, then open a new terminal:"
      note "  echo 'export PATH=\"$bin:\$PATH\"' >> $rc"
      note "Connect Claude Code: claude mcp add porticol -- $bin/porticol-cli mcp"
      note "Or as a plugin, once porticol-cli is on your PATH: claude plugin marketplace add pavanmanishd/porticol-cli && claude plugin install porticol@porticol-cli"
      ;;
  esac
fi

if [ -n "$launch" ] && opened; then
  note "Opening it now"
elif [ -n "${opener:-}" ]; then
  note "Open it with: $opener"
fi
