#!/usr/bin/env bash
# Passkeys for FreeScout - installer / updater
#   curl -fsSL https://store.ittle.co/install.sh -o passkeys-install.sh
#   sudo bash passkeys-install.sh PK-XXXXX-XXXXX-XXXXX-XXXXX
#
# Finds FreeScout (Docker container or regular install), activates your licence
# for its domain, downloads the module, installs it into Modules/Passkeys,
# enables it and activates the licence inside FreeScout. Re-run to update.
#
# Options:  --path /var/www/freescout    use this FreeScout folder
#           --container NAME             use this Docker container
#           --yes                        don't ask for confirmation
set -euo pipefail

STORE="${PASSKEYS_STORE:-https://store.ittle.co}"
ALIAS="passkeys"
FOLDER="Passkeys"

KEY=""; FS_PATH=""; CONTAINER=""; ASSUME_YES=0
while [ $# -gt 0 ]; do
  case "$1" in
    --path) FS_PATH="$2"; shift 2 ;;
    --container) CONTAINER="$2"; shift 2 ;;
    --yes|-y) ASSUME_YES=1; shift ;;
    -h|--help) sed -n '2,14p' "$0"; exit 0 ;;
    *) KEY="$1"; shift ;;
  esac
done

c_red=$'\e[31m'; c_grn=$'\e[32m'; c_ylw=$'\e[33m'; c_b=$'\e[1m'; c_0=$'\e[0m'
say()  { printf '%s==>%s %s\n' "$c_b" "$c_0" "$*"; }
ok()   { printf '  %s✔%s %s\n' "$c_grn" "$c_0" "$*"; }
warn() { printf '  %s!%s %s\n' "$c_ylw" "$c_0" "$*"; }
has_tty() { { : </dev/tty; } 2>/dev/null; }
die()  { printf '%sError:%s %s\n' "$c_red" "$c_0" "$*" >&2; exit 1; }

KEY=$(printf '%s' "$KEY" | tr '[:lower:]' '[:upper:]' | tr -d ' ')
[[ "$KEY" =~ ^PK-[A-Z0-9]{5}(-[A-Z0-9]{5}){3}$ ]] || die "Usage: sudo bash $0 PK-XXXXX-XXXXX-XXXXX-XXXXX"
command -v curl >/dev/null || die "curl is required."
command -v unzip >/dev/null || command -v python3 >/dev/null || die "unzip (or python3) is required."

# --------------------------------------------------------------------- locate FreeScout
MODE=""      # docker | local
ROOT=""      # FreeScout root (inside the container for docker)

is_fs_root_local() { [ -f "$1/artisan" ] && [ -d "$1/Modules" ] && grep -qi freescout "$1/composer.json" 2>/dev/null; }

dexec() { docker exec -i "$CONTAINER" sh -c "$1"; }

find_root_in_container() {
  for p in /www/html /var/www/html /var/www/freescout /app /var/www; do
    if dexec "[ -f $p/artisan ] && [ -d $p/Modules ] && grep -qi freescout $p/composer.json" 2>/dev/null; then
      echo "$p"; return 0
    fi
  done
  return 1
}

if [ -n "$CONTAINER" ]; then
  MODE=docker
  ROOT=$(find_root_in_container) || die "FreeScout not found inside container $CONTAINER."
elif [ -n "$FS_PATH" ]; then
  is_fs_root_local "$FS_PATH" || die "$FS_PATH is not a FreeScout folder (artisan/Modules not found)."
  MODE=local; ROOT=$(cd "$FS_PATH" && pwd)
else
  candidates=()
  if command -v docker >/dev/null && docker info >/dev/null 2>&1; then
    while read -r name image; do
      if [[ "$name $image" =~ [Ff]ree[Ss]cout ]]; then candidates+=("docker:$name"); fi
    done < <(docker ps --format '{{.Names}} {{.Image}}')
  fi
  for p in /var/www/html /var/www/freescout /var/www/*/ /srv/*/ /opt/*/ /home/*/public_html /home/*/www /usr/share/nginx/html; do
    p=${p%/}
    [ -d "$p" ] && is_fs_root_local "$p" && candidates+=("local:$p")
  done
  # de-duplicate
  mapfile -t candidates < <(printf '%s\n' "${candidates[@]}" | awk 'NF && !seen[$0]++')
  # drop docker candidates that aren't actually FreeScout app containers (e.g. *-db)
  filtered=()
  for c in "${candidates[@]}"; do
    if [[ $c == docker:* ]]; then
      CONTAINER=${c#docker:}
      find_root_in_container >/dev/null 2>&1 && filtered+=("$c")
    else
      filtered+=("$c")
    fi
  done
  CONTAINER=""
  candidates=("${filtered[@]}")

  if [ ${#candidates[@]} -eq 0 ]; then
    die "Couldn't find FreeScout automatically. Re-run with --path /path/to/freescout or --container NAME."
  elif [ ${#candidates[@]} -gt 1 ]; then
    echo "Found more than one FreeScout:"
    i=1; for c in "${candidates[@]}"; do echo "  $i) $c"; i=$((i+1)); done
    has_tty || die "More than one FreeScout found. Re-run with --container NAME or --path /path/to/freescout."
    read -r -p "Which one? [1-${#candidates[@]}] " n </dev/tty
    [[ "$n" =~ ^[0-9]+$ ]] && [ "$n" -ge 1 ] && [ "$n" -le ${#candidates[@]} ] || die "Cancelled."
    choice=${candidates[$((n-1))]}
  else
    choice=${candidates[0]}
  fi
  if [[ $choice == docker:* ]]; then
    MODE=docker; CONTAINER=${choice#docker:}; ROOT=$(find_root_in_container)
  else
    MODE=local; ROOT=${choice#local:}
  fi
fi

# --------------------------------------------------------------------- helpers per mode
run_in() {  # run a shell command in the FreeScout root
  if [ "$MODE" = docker ]; then dexec "cd $ROOT && $1"; else (cd "$ROOT" && sh -c "$1"); fi
}

PHP=""
for cand in php php85 php84 php83 php82 php81 php8 php7; do
  if run_in "command -v $cand >/dev/null 2>&1"; then PHP=$cand; break; fi
done
[ -n "$PHP" ] || die "PHP not found for FreeScout."

# Run PHP code inside a fully booted FreeScout. Code comes from stdin.
fs_php() {
  local boot='<?php
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE & ~E_WARNING);
require "vendor/autoload.php";
$app = require "bootstrap/app.php";
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
'
  { printf '%s\n' "$boot"; cat; } | run_in "$PHP" 2>/dev/null
}

# Run as the owner of the Modules folder so files get the right ownership.
fs_owner=$(run_in "stat -c '%u:%g' \$(readlink -f Modules)")
MODULES_DIR=$(run_in "readlink -f Modules")

APP_URL=$(printf 'echo config("app.url");' | fs_php | tail -n1)
DOMAIN=$(printf '%s' "$APP_URL" | sed -E 's#^[a-zA-Z]+://##; s#[/:].*$##' | tr '[:upper:]' '[:lower:]')
[ -n "$DOMAIN" ] || die "Could not read APP_URL from FreeScout."
CURRENT=$(printf '$m = \Module::findByAlias("%s"); echo $m ? $m->get("version") : "";' "$ALIAS" | fs_php | tail -n1 || true)

say "Passkeys for FreeScout installer"
echo "  FreeScout:  $( [ "$MODE" = docker ] && echo "Docker container $CONTAINER, $ROOT" || echo "$ROOT" )"
echo "  Site:       $APP_URL"
echo "  Modules:    $MODULES_DIR (owner $fs_owner)"
echo "  Installed:  ${CURRENT:-not installed}"
echo "  Licence:    ${KEY:0:9}…${KEY: -5}  (will be bound to $DOMAIN)"
if [ $ASSUME_YES -eq 0 ]; then
  has_tty || die "No terminal to confirm. Re-run with --yes."
  read -r -p "Continue? [Y/n] " a </dev/tty
  [[ -z "$a" || "$a" =~ ^[Yy] ]] || die "Cancelled."
fi

# --------------------------------------------------------------------- licence + download
say "Activating licence for $DOMAIN"
resp=$(curl -fsS -X POST "$STORE/api/v1/license/activate" -H 'Content-Type: application/json' \
  -d "{\"key\":\"$KEY\",\"domain\":\"$DOMAIN\",\"version\":\"installer\"}") || die "Could not reach $STORE."
case "$resp" in
  *'"status": "active"'*) ok "Licence active for $DOMAIN" ;;
  *) die "$(printf '%s' "$resp" | sed -E 's/.*"message": "([^"]*)".*/\1/')" ;;
esac

TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
say "Downloading module"
code=$(curl -sS -o "$TMP/p.zip" -w '%{http_code}' -X POST "$STORE/api/v1/license/download" \
  -H 'Content-Type: application/json' -d "{\"key\":\"$KEY\",\"domain\":\"$DOMAIN\"}")
[ "$code" = 200 ] || die "Download failed (HTTP $code): $(head -c 300 "$TMP/p.zip")"
mkdir "$TMP/x"
if command -v unzip >/dev/null; then unzip -q "$TMP/p.zip" -d "$TMP/x"; else python3 -m zipfile -e "$TMP/p.zip" "$TMP/x"; fi
[ -f "$TMP/x/$FOLDER/module.json" ] || die "Downloaded archive looks wrong."
NEW=$(sed -nE 's/.*"version": *"([^"]+)".*/\1/p' "$TMP/x/$FOLDER/module.json" | head -1)
ok "Version $NEW"

# --------------------------------------------------------------------- install files
say "Installing into $MODULES_DIR/$FOLDER"
stamp=$(date +%Y%m%d-%H%M%S)
if [ "$MODE" = docker ]; then
  dexec "[ -d '$MODULES_DIR/$FOLDER' ] && mv '$MODULES_DIR/$FOLDER' '/tmp/$FOLDER.bak-$stamp' || true"
  docker cp "$TMP/x/$FOLDER" "$CONTAINER:$MODULES_DIR/$FOLDER"
  dexec "chown -R $fs_owner '$MODULES_DIR/$FOLDER'"
  backup="(container) /tmp/$FOLDER.bak-$stamp"
else
  [ -w "$MODULES_DIR" ] || die "No write access to $MODULES_DIR. Run with sudo."
  if [ -d "$MODULES_DIR/$FOLDER" ]; then mv "$MODULES_DIR/$FOLDER" "/tmp/$FOLDER.bak-$stamp"; fi
  cp -R "$TMP/x/$FOLDER" "$MODULES_DIR/$FOLDER"
  chown -R "$fs_owner" "$MODULES_DIR/$FOLDER" 2>/dev/null || warn "Couldn't chown; make sure the web server can read $MODULES_DIR/$FOLDER"
  backup="/tmp/$FOLDER.bak-$stamp"
fi
[ -n "$CURRENT" ] && ok "Previous version saved to $backup"
ok "Files installed"

# --------------------------------------------------------------------- enable in FreeScout
say "Enabling module in FreeScout"
as_owner=""
if [ "$MODE" = docker ]; then
  as_owner="--user $fs_owner"
fi
printf '\Module::clearCache(); \App\Module::setActive("%s", true); echo "ok";' "$ALIAS" | fs_php | tail -n1 | grep -q ok || die "Couldn't enable the module."
if [ "$MODE" = docker ]; then
  docker exec $as_owner -i "$CONTAINER" sh -c "cd $ROOT && $PHP artisan freescout:module-install $ALIAS" >/dev/null 2>&1 || true
else
  (cd "$ROOT" && sudo -u "#${fs_owner%%:*}" "$PHP" artisan freescout:module-install "$ALIAS" >/dev/null 2>&1) \
    || (cd "$ROOT" && "$PHP" artisan freescout:module-install "$ALIAS" >/dev/null 2>&1) || true
fi
ok "Module enabled (database updated)"

say "Activating licence inside FreeScout"
res=$(printf 'list($ok, $m) = \Modules\Passkeys\Misc\License::activate("%s"); echo ($ok ? "OK " : "FAIL ").$m;' "$KEY" | fs_php | tail -n1)
case "$res" in OK*) ok "${res#OK }" ;; *) die "${res#FAIL }" ;; esac

# cache files written as root must stay writable by the web server
run_in "chown -R $fs_owner \$(readlink -f storage) \$(readlink -f bootstrap/cache) 2>/dev/null || true" || true

echo
printf '%s✔ Passkeys %s installed on %s%s\n' "$c_grn" "$NEW" "$APP_URL" "$c_0"
echo "  Next: open your FreeScout profile → Passkeys → Add passkey."
echo "  Then sign out and use \"Sign in with a passkey\" on the login page."
