#!/usr/bin/env bash
# =============================================================================
# @analytics — Deploy to vps-0
# =============================================================================
# Build images on apricot, ship via docker save | ssh | docker load, then
# `docker compose up -d --no-build` on the VPS.
#
# Why: vps-0 has 4 GB RAM. Running `docker compose --build` there OOM-kills
# nginx (incident 2026-05-15). Apricot has the headroom and the source.
#
# Strategy:
#   1. bun run build:services      (TS → dist on apricot)
#   2. .vendor-lilith/ staging     (registry @lilith/* deps, VPS can't reach Verdaccio)
#   3. docker compose build        (apricot — produces infrastructure-<svc>:latest)
#   4. docker save | zstd | ssh    (stream images to VPS, decompress, load)
#   5. rsync compose + init.sql    (in case schema/compose changed)
#   6. docker compose up -d --no-build  (VPS — uses already-loaded images)
#   7. Smoke health endpoints
#
# Usage: ./scripts/deploy.sh [svc1 svc2 ...]
#   No args: deploy all build-using services.
#   With args: deploy only the named services (faster iteration).
# =============================================================================
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
REMOTE="quinn-vps"
REMOTE_DIR="~/analytics"
COMPOSE_REL="infrastructure/docker-compose.prod.yaml"
PROJECT="infrastructure"   # docker compose project name (= dir name)

ALL_SERVICES=(collector processor api website-bff realtime)
if [[ $# -gt 0 ]]; then
  SERVICES=("$@")
else
  SERVICES=("${ALL_SERVICES[@]}")
fi

# Validate requested services
for svc in "${SERVICES[@]}"; do
  if ! printf '%s\n' "${ALL_SERVICES[@]}" | grep -qx "${svc}"; then
    echo "ERROR: unknown service '${svc}'. Valid: ${ALL_SERVICES[*]}" >&2
    exit 1
  fi
done

echo "==> Deploying services: ${SERVICES[*]}"

# ---------------------------------------------------------------------------
# [1/6] Compile TS → dist for each service
# ---------------------------------------------------------------------------
echo "==> [1/6] bun run build:services..."
cd "$ROOT_DIR"
bun run build:services

# ---------------------------------------------------------------------------
# [2/6] Stage @lilith registry deps into each service's .vendor-lilith/
# ---------------------------------------------------------------------------
echo "==> [2/6] Staging @lilith registry deps for Docker COPY..."
for svc in "${SERVICES[@]}"; do
  svc_dir="${ROOT_DIR}/services/${svc}/"
  vendor_dir="${svc_dir}.vendor-lilith"
  rm -rf "$vendor_dir"
  mkdir -p "$vendor_dir"
  node -e "
    const fs = require('fs');
    const path = require('path');
    const svcDir = '${svc_dir}';
    const vendorDir = '${vendor_dir}';
    const svcName = '${svc}';
    function stagePackage(name) {
      const dst = path.join(vendorDir, ...name.split('/'));
      if (fs.existsSync(dst)) return;
      const parts = name.split('/');
      const bunKey = parts.join('+');
      let real = null;
      let search = path.resolve(svcDir);
      while (search !== '/') {
        const candidate = path.join(search, 'node_modules', ...parts);
        if (fs.existsSync(candidate)) { real = fs.realpathSync(candidate); break; }
        const bunDir = path.join(search, 'node_modules', '.bun');
        if (fs.existsSync(bunDir)) {
          const match = fs.readdirSync(bunDir).find(d => d.startsWith(bunKey + '@'));
          if (match) {
            const storePkg = path.join(bunDir, match, 'node_modules', ...parts);
            if (fs.existsSync(storePkg)) { real = fs.realpathSync(storePkg); break; }
          }
        }
        search = path.dirname(search);
      }
      if (!real) { console.warn('  WARN: ' + name + ' not found from ' + svcName); return; }
      fs.mkdirSync(path.dirname(dst), { recursive: true });
      fs.cpSync(real, dst, { recursive: true });
      const child = JSON.parse(fs.readFileSync(path.join(real, 'package.json'), 'utf8'));
      for (const [dep] of Object.entries(child.dependencies || {})) {
        if (dep.startsWith('@lilith/')) stagePackage(dep);
      }
    }
    const p = JSON.parse(fs.readFileSync(svcDir + 'package.json', 'utf8'));
    for (const [name, ver] of Object.entries(p.dependencies || {})) {
      if (name.startsWith('@lilith/') && typeof ver === 'string' && !ver.startsWith('workspace:')) {
        stagePackage(name);
      }
    }
  "
done

# ---------------------------------------------------------------------------
# [3/6] Build images on apricot (NOT on the VPS — OOM risk)
# ---------------------------------------------------------------------------
echo "==> [3/6] Building Docker images on apricot..."
# Use a throwaway env file so compose doesn't warn about runtime-only vars.
TMP_ENV="$(mktemp)"
trap 'rm -f "$TMP_ENV"' EXIT
{
  echo "POSTGRES_USER=build"
  echo "POSTGRES_PASSWORD=build"
  echo "POSTGRES_DB=build"
  echo "REDIS_PASSWORD=build"
  echo "CORS_ORIGINS=build"
  echo "COLLECTOR_WRITE_KEY=build"
  echo "API_KEYS=build"
  echo "ADMIN_URL=http://build"
} > "$TMP_ENV"
cd "$ROOT_DIR"
docker compose -f "$COMPOSE_REL" --env-file "$TMP_ENV" -p "$PROJECT" build "${SERVICES[@]}"

# ---------------------------------------------------------------------------
# [4/6] Ship images to vps-0 (compressed save → stream → load)
# ---------------------------------------------------------------------------
echo "==> [4/6] Shipping images to ${REMOTE}..."
for svc in "${SERVICES[@]}"; do
  image="${PROJECT}-${svc}:latest"
  size="$(docker image inspect "$image" --format '{{.Size}}' 2>/dev/null | numfmt --to=iec)"
  echo "  -> ${image} (${size:-?})"
  docker save "$image" \
    | zstd -T0 -q \
    | ssh -o ControlPath=none "$REMOTE" "zstd -d -q | docker load"
done

# ---------------------------------------------------------------------------
# [5/6] Sync compose + init.sql; bring up stack with --no-build
# ---------------------------------------------------------------------------
echo "==> [5/6] Syncing compose config + bringing up stack..."
rsync -avz \
  "$ROOT_DIR/infrastructure/docker-compose.prod.yaml" \
  "$ROOT_DIR/infrastructure/init.sql" \
  "$REMOTE:$REMOTE_DIR/infrastructure/"
ssh -o ControlPath=none "$REMOTE" "cd $REMOTE_DIR && docker compose -f infrastructure/docker-compose.prod.yaml --env-file infrastructure/.env.prod -p $PROJECT up -d --no-build --remove-orphans"

# ---------------------------------------------------------------------------
# [6/6] Health smoke
# ---------------------------------------------------------------------------
echo "==> [6/6] Health smoke (10s settle)..."
sleep 10
declare -A PORTS=( [collector]=4001 [api]=4003 [website-bff]=4005 )
fail=0
for svc in "${SERVICES[@]}"; do
  port="${PORTS[$svc]:-}"
  if [[ -z "$port" ]]; then
    echo "  ${svc}: (no health endpoint to check)"
    continue
  fi
  if ssh -o ControlPath=none "$REMOTE" "curl -sf --max-time 5 http://localhost:${port}/health >/dev/null"; then
    echo "  ${svc} (:${port}): OK"
  else
    echo "  ${svc} (:${port}): NOT READY"
    fail=1
  fi
done

echo ""
if [[ $fail -eq 0 ]]; then
  echo "Deployed at $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
  echo "  Collector: https://data.transquinnftw.com/analytics/track/"
  echo "  API:       https://data.transquinnftw.com/api/"
  echo "  Rollup:    https://data.cocotte.maison/  (basic-auth)"
else
  echo "WARN: one or more services did not respond healthy. Check: ssh $REMOTE 'docker compose -p $PROJECT logs --tail=50'"
  exit 1
fi
