#!/usr/bin/env bash

# Because Apple wants to take from the commons but not give back to it,
# we have to limit the features we use in this Bash script to Bash
# version 3.2 (specifically 3.2.57), the archaic version from the 2000s
# that the multi-trillion-dollar corporation includes in its OS.
#
# (Otherwise, we’d have to ask people on Macs to upgrade their Bash
# versions, which is easily done using Brew, but Brew only runs on the
# latest three versions of macOS, thereby locking out people with
# older computers.)
#
# (If you’re on Linux, you don’t have to worry about any of this.)

set -e

# Start timer, except on macOS because trillion-dollar company’s date
# command cannot support sub-second precision ¯\_(ツ)_/¯
os="$(uname | tr '[:upper:]' '[:lower:]')"

if [ "${os}" != 'darwin' ]; then
  T="$(date +%s%N)"
fi

echo '🐱 Installing Kitten…'
echo ''

# Ensure we’re running on a supported operating system.
# For this bash-based installer, that’s Linux and macOS.
if [ "${os}" != 'linux' ] && [ "${os}" != 'darwin' ]; then
  printf '❌ Sorry, Kitten is only supported on Linux and macOS.\n\n   If you want to take on supporting Kitten on an unlisted platform, please maintain a soft fork – thank you and apologies for the inconvenience. Kitten is currently being developed by one person and there are only so many platforms I can test on myself.\n'
  echo "   (Your operating system is detected as ${os}.)"
  exit 1
fi

if [ "${BASH_VERSINFO[0]}" -lt 3 ]; then
  printf '❌ Kitten install requires Bash version >= 3.2, you have: %s\n' "$BASH_VERSION"
  exit 1
fi

architecture=$(uname -m)

# Map architecture to runtime architecture string
mapArchitecture() {
  case "$1" in
    x86_64)  echo 'x64' ;;
    aarch64) echo 'arm64' ;;
    arm64)   echo 'arm64' ;;
    armv7l)  echo 'armv7l' ;;
    *)       echo '' ;;
  esac
}

# Node 24 is the latest LTS as of Oct 2025.
runtimeVersion='v24.14.1' 
runtimePrefix='https://nodejs.org/dist'
runtimeUrl="${runtimePrefix}/${runtimeVersion}/node-${runtimeVersion}-${os}-$(mapArchitecture "$architecture").tar.xz"

# If the API version is specified (as a positional argument, integer),
# then set it so we tack it to the download URL of the package.
apiVersion=''
if [ $# -eq 1 ]; then
  apiVersion="${1}"
fi

LATEST_KITTEN_DISTRIBUTION_DOWNLOAD_URL="https://kittens.small-web.org/download/${apiVersion}"

# Paths

# Binary and configuration path usage adheres to 
# freedesktop.org XDG Base Directory Specification
# https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
BINARY_HOME="${HOME}/.local/bin"
KITTEN_BINARY_PATH="${BINARY_HOME}/kitten"
KITTEN_NPM_BINARY_PATH="${BINARY_HOME}/kitten-npm"
KITTEN_NODE_BINARY_PATH="${BINARY_HOME}/kitten-node"

DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
SMALL_TECH_DATA_HOME="${DATA_HOME}/small-tech.org"

KITTEN_DATA_HOME="${SMALL_TECH_DATA_HOME}/kitten"
KITTEN_DATA_DIRECTORY="${KITTEN_DATA_HOME}/data"
KITTEN_APP_DIRECTORY_PATH="${KITTEN_DATA_HOME}/app"

KITTEN_TEMP_DIRECTORY="$(mktemp -d)"
KITTEN_TEMP_RUNTIME_ARCHIVE_PATH="${KITTEN_TEMP_DIRECTORY}/runtime.tar.xz"
KITTEN_TEMP_LATEST_KITTEN_DISTRIBUTION_PATH="${KITTEN_TEMP_DIRECTORY}/kitten.tar.xz"

KITTEN_RUNTIME_DIRECTORY="${KITTEN_DATA_HOME}/runtime"
KITTEN_RUNTIME_BINARIES_PATH="${KITTEN_RUNTIME_DIRECTORY}/bin"
KITTEN_RUNTIME_NODE_BINARY_PATH="${KITTEN_RUNTIME_BINARIES_PATH}/node"
KITTEN_RUNTIME_NPM_BINARY_PATH="${KITTEN_RUNTIME_BINARIES_PATH}/npm"
KITTEN_RUNTIME_PACKAGE_MANAGER_BINARY_PATH="${KITTEN_RUNTIME_DIRECTORY}/bin/npm"

# Ensure Kitten folder exists that we’re going to install into.
mkdir -p "${KITTEN_APP_DIRECTORY_PATH}"

# Ensure unprivileged binary folder exists (as we will be symlinking
# to the Kitten binary from there.)
mkdir -p "${BINARY_HOME}"

# The download commands should run silently but show errors.
downloadRuntimeCommand=''
downloadLatestKittenDistributionCommand=''
if command -v 'curl' > /dev/null 2>&1; then
  downloadRuntimeCommand="curl --silent --show-error ${runtimeUrl} --output ${KITTEN_TEMP_RUNTIME_ARCHIVE_PATH}"
  downloadLatestKittenDistributionCommand="curl --silent --show-error --location --output ${KITTEN_TEMP_LATEST_KITTEN_DISTRIBUTION_PATH} ${LATEST_KITTEN_DISTRIBUTION_DOWNLOAD_URL}"
elif command -v 'wget' > /dev/null 2>&1; then
  downloadRuntimeCommand="wget --output-document ${KITTEN_TEMP_RUNTIME_ARCHIVE_PATH} ${runtimeUrl} 2>&1 | grep ERROR"
  downloadLatestKittenDistributionCommand="wget --output-document ${KITTEN_TEMP_LATEST_KITTEN_DISTRIBUTION_PATH} ${LATEST_KITTEN_DISTRIBUTION_DOWNLOAD_URL} 2>&1 | grep ERROR"
else
  echo '❌ Neither curl nor wget found on system.'
  echo "   (Please install one or the other and run the installer again.)"
  exit 1
fi

installRuntimeIfNecessary () {
  # Check if runtime exists. If not, install it.
  # (Kitten always runs from its own runtime, even during dev.
  # This way, we avoid version-related gremlins from raising
  # their heads between development and production.)
  if [ ! -f "${KITTEN_RUNTIME_NODE_BINARY_PATH}" ] || [ ! -f "${KITTEN_RUNTIME_NPM_BINARY_PATH}" ]; then
    installRuntime
  else
    # Make doubly sure that the actual runtime binary exists AND
    # that it is the correct version.
    actualRuntimeVersion=$("${KITTEN_RUNTIME_NODE_BINARY_PATH}" --version)
    if [ "${actualRuntimeVersion}" = "${runtimeVersion}" ]; then
      echo "  • Runtime ${actualRuntimeVersion} already exists, not updating."
    else
      echo "  • Runtime (${actualRuntimeVersion}) is not the expected version (${runtimeVersion}), will update."
      installRuntime
    fi
  fi
}

installRuntime () {
  # Install runtime binary (currently Node.js).
  echo '  • Installing runtime binary.'

  # Download the runtime.
  $downloadRuntimeCommand

  # Extract the runtime.
  rm -rf "${KITTEN_RUNTIME_DIRECTORY}"
  mkdir -p "${KITTEN_RUNTIME_DIRECTORY}"
  tar -xf "${KITTEN_TEMP_RUNTIME_ARCHIVE_PATH}" --directory="${KITTEN_RUNTIME_DIRECTORY}" --strip-components=1

  # Also create symlinks to Kitten’s built-in npm and node binaries
  # as kitten-npm and kitten-node.
  rm -f "${KITTEN_NPM_BINARY_PATH}"
  rm -f "${KITTEN_NODE_BINARY_PATH}"
  ln -s "${KITTEN_RUNTIME_NPM_BINARY_PATH}" "${KITTEN_NPM_BINARY_PATH}"
  ln -s "${KITTEN_RUNTIME_NODE_BINARY_PATH}" "${KITTEN_NODE_BINARY_PATH}"

  # Rewrite Kitten package manager launcher to use the Kitten runtime we
  # installed instead of any that might otherwise be installed on the system.
  printf "#!/usr/bin/env %s\nrequire('../lib/cli.js')(process)\n" "${KITTEN_RUNTIME_NODE_BINARY_PATH}" > "${KITTEN_RUNTIME_PACKAGE_MANAGER_BINARY_PATH}"
}

# Detect whether the script is being run interactively
# (i.e., during development) or being piped to bash
# via the online installation instructions. If it’s the latter,
# we’ll skip the build process and deploy as quickly as
# possible using the pre-built distribution directory.
if [ -t 0 ]; then
  ############################################################
  # Development install (local).
  ############################################################
  echo '  • Development install detected.'

  installRuntimeIfNecessary

  # Install npm dependencies if asked to or if node modules
  # have not been installed yet. This is mutually
  if [ "$1" = '--npm' ] || [ ! -d ./node_modules ]; then
    # Ensure npm dependencies are up to date.
    echo '  • Installing dependencies via npm.'
    "${KITTEN_RUNTIME_NPM_BINARY_PATH}" install
  else
    echo '  • Skipping npm install step (pass --npm to NOT skip)'
  fi

  # If a path to a tar.xz package is provided as the first
  # positional argument in a development install, we will
  # attempt to install that Kitten package instead of the
  # current latest source.
  if [ $# -eq 1 ] && [ "$1" != '--npm' ]; then
    kittenPackageFile="${1}"
    echo "  • Installing from local package file at ${kittenPackageFile}"

    # Delete the web folder in case it has been updated
    # (otherwise, deleted files inside it will not be removed).
    rm -rf "${KITTEN_APP_DIRECTORY_PATH}"/web

    # Extract specified Kitten distribution.
    tar -xf "${kittenPackageFile}" --directory="${KITTEN_APP_DIRECTORY_PATH}" --strip-components=1
  else
    # Create distribution build.
    echo '  • Creating distribution build.'
    ./build

    # Copy distribution folder to installation location.
    echo '  • Copying app directory.'
    rm -rf "${KITTEN_APP_DIRECTORY_PATH}"/web
    cp -R dist/. "${KITTEN_APP_DIRECTORY_PATH}"
  fi
else
  ############################################################
  # Web install (we’re being piped into bash).
  ############################################################
  echo '  • Web install detected.'

  # Download latest Kitten distribution (or the latest for the
  # requested API version, if any).
  if [ $# -eq 1 ]; then
    echo "  • Installing latest Kitten distribution for API version ${apiVersion}."
  else
    echo '  • Installing latest Kitten distribution.'
  fi
  $downloadLatestKittenDistributionCommand

  # Delete the web folder in case it has been updated
  # (otherwise, deleted files inside it will not be removed).
  rm -rf "${KITTEN_APP_DIRECTORY_PATH}"/web

  # Extract latest Kitten distribution.
  tar -xf "${KITTEN_TEMP_LATEST_KITTEN_DISTRIBUTION_PATH}" --directory="${KITTEN_APP_DIRECTORY_PATH}" --strip-components=1

  # Install runtime binary (currently Node.js).
  installRuntimeIfNecessary
fi

# Remove old symlink if it exists.
rm -f "${KITTEN_BINARY_PATH}"

# Create symlink to Kitten binary.
ln -s "${KITTEN_APP_DIRECTORY_PATH}/kitten" "${KITTEN_BINARY_PATH}"

# Create Kitten data directory.
# (We do this here so the data migration script can correctly determine if
# this is a new version install or not.)
mkdir -p "${KITTEN_DATA_DIRECTORY}"

# Check if privileged ports are disabled and, if not, disable them.

styleBold='\e[1m'
styleGreen="\e[0;32m"
styleBoldGreen="\e[1;32m"
styleBlueItalic="\e[3;34m"
styleBoldBlue="\e[1;34m"
styleYellowItalic="\e[3;33m"
styleBoldBlack="\e[1;30m"
styleReset="\e[0m"

if [ "${os}" = 'linux' ]; then
  unprivilegedPortStart=$(/sbin/sysctl -n net.ipv4.ip_unprivileged_port_start)
  if [ "$unprivilegedPortStart" -le 80 ]; then
    echo "  • Unprivileged ports start at ${unprivilegedPortStart}, no action necessary."
    printf "    %sWe can bind to ports 80 and 443 without requiring elevated privileges.%s\n" "$styleBlueItalic" "$styleReset"
  else
    printf "  • Unprivileged ports start at %s, %swill attempt to reduce it to 80%s\n" "$unprivilegedPortStart" "$styleYellowItalic" "$styleReset"
    printf "    %sso we can bind to ports 80 and 443 without elevated privileges.%s\n" "$styleYellowItalic" "$styleReset"
    echo ''
    printf "    %sThis may ask you for your system password.%s\n" "$styleBoldBlack" "$styleReset"

    # Reduce unprivileged ports for this session…
    # (Silence warning: suppress all messages; redirect stderr to stdout and stdout to /dev/null)
    # shellcheck disable=SC2069
    sudo /sbin/sysctl -w net.ipv4.ip_unprivileged_port_start=80 2>&1 1>/dev/null

    # …and also persist the change by writing a sysctl configuration file.
    sysctlConfigurationFilePath='/etc/sysctl.d/99-reduce-unprivileged-port-start-to-80.conf'
    # (Silence warning: suppress all messages; redirect stderr to stdout and stdout to /dev/null)
    # shellcheck disable=SC2069
    echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee "${sysctlConfigurationFilePath}" 2>&1 1>/dev/null
    sudo chmod 0777 "${sysctlConfigurationFilePath}"

    echo ''
    printf "    %sDone. Unprivileged ports now start at 80.%s\n" "$styleGreen" "$styleReset"
    printf "    %sChange also persisted in /etc/sysctl.d/99-reduce-unprivileged-port-start-to-80.conf\n" "$styleGreen"
    printf "    %sWe can bind to ports 80 and 443 without requiring elevated privileges.%s\n" "$styleBlueItalic" "$styleReset"
  fi
fi

if [ "${os}" != 'darwin' ]; then
  # Timing code courtesy of https://stackoverflow.com/a/3684051/92548
  T="$(($(date +%s%N)-T))"
  S="$((T/1000000000))"
  M="$((T%1000000000/1000000))"

  printf "\nInstalled in %ds %03dms!\n\n" "$((S%60))" "${M}"
else
  printf "\nInstalled.\n\n"
fi

if [ "${os}" = 'darwin' ]; then
  # Tell the person to add ~/local/bin to their path.
  echo "╭───────────────────────────────╮"
  printf "│ %b │\n" "${styleBoldBlue}macOS Additional Instructions${styleReset}"
  echo "╰───────────────────────────────╯"
  echo ""
  printf "  %b\n" "${styleBold}1. Update your system’s path to add the location of the kitten binary.${styleReset}"
  echo ""
  printf "     %b\n" "${styleBoldBlue}zsh (default shell on macOS):${styleReset}"
  echo ""
  printf "     %b\n" "${styleBoldGreen}echo \"export PATH=\$PATH:~/.local/bin\" >> ~/.zshrc${styleReset}"
  printf "     %b\n" "${styleBoldGreen}source ~/.zshrc${styleReset}"
  echo ""
  printf "     %b\n" "${styleBoldBlue}Fish shell${styleReset}"
  echo ""
  printf "     %b\n" "${styleBoldGreen}echo 'set -gx PATH \"~/.local/bin\" \"\$PATH\"' >> ~/.config/fish/config.fish${styleReset}"
  printf "     %b\n" "${styleBoldGreen}source ~/.config/fish/config.fish${styleReset}"
  echo ""
  printf "  %b\n" "${styleBold}2. Add localhost aliases to your hosts file.${styleReset}"
  echo ""
  printf "     If you want to test peer-to-peer apps locally, edit your %b file\n" "${styleGreen}/etc/hosts${styleReset}"
  printf "     to ensure your %b line resembles the one below:\n" "${styleGreen}localhost${styleReset}"
  echo ""
  printf "     %b\n" "${styleBoldGreen}127.0.0.1  localhost place1.localhost place2.localhost place3.localhost place4.localhost${styleReset}"
  echo ""

  # (Silence warning: suppress all messages; redirect stderr to stdout and stdout to /dev/null)
  # shellcheck disable=SC2069
  export PATH=$PATH:~/.local/bin 2>&1 1>/dev/null
fi

if [ "${os}" = 'linux' ]; then
  if ! echo ":$PATH:" | grep -q ":$HOME/.local/bin:"; then
    # Tell the person to add ~/local/bin to their path.
    echo "╭───────────────────────────────────────────────────────╮"
    echo "│                                                       │"
    printf "│ %b                               │\n" "${styleBoldBlue}Additional instructions${styleReset}"
    echo "│                                                       │"
    echo "│ Before you can use the kitten command, you must       │"
    echo "│ update your system’s path to add the location of      │"
    echo "│ the kitten binary.                                    │"
    echo "│                                                       │"
    echo "│ Please add the following directory to the end of the  │"
    echo "│ PATH environment variable for your shell of choice:   │"
    echo "│                                                       │"
    printf "│ %b                                      │\n" "${styleBoldGreen}\$HOME/.local/bin${styleReset}"
    echo "│                                                       │"
    echo "│ Please note that on some distributions simply         │"
    echo "│ logging out and logging back in will add that folder  │"
    echo "│ to your path automatically.                           │"
    echo "│                                                       │"
    printf "│ %b  │\n" "${styleBlueItalic}For shell-specific instructions and code, please see${styleReset}"
    printf "│ %b │\n" "${styleBlueItalic}https://codeberg.org/aral/gists/src/local-bin-path.md${styleReset}"
    echo "│                                                       │"
    echo "╰───────────────────────────────────────────────────────╯"
    echo ''
  fi
fi

echo 'Run using: kitten [path to serve]'
