#!/usr/bin/env bash
# DashCaddy Host-Side Updater
# Triggered by systemd path unit when the container writes trigger.json.
# Reads the trigger, backs up current API + data/, copies new files, rebuilds container.
# Writes result.json so the new container knows the outcome.
#
# This runs on the HOST, outside the container.

set -euo pipefail

readonly UPDATES_DIR="/opt/dashcaddy/updates"
readonly TRIGGER_FILE="${UPDATES_DIR}/trigger.json"
readonly RESULT_FILE="${UPDATES_DIR}/result.json"
readonly BACKUPS_DIR="${UPDATES_DIR}/backups"
readonly CONTAINER_NAME="dashcaddy-api"
readonly MAX_BACKUPS=3
readonly HEALTH_TIMEOUT=60

# Data directory backup — stored alongside code backups so everything rolls back together
readonly DATA_SOURCE_DIR="/opt/dashcaddy/dashcaddy-api/data"
readonly DATA_BACKUP_PREFIX="data-backup"

log() { echo "[dashcaddy-update] $(date '+%Y-%m-%d %H:%M:%S') $*"; }

write_result() {
  local success="$1" version="$2" duration="$3"
  shift 3
  local error="${1:-}"

  if [[ "$success" == "true" ]]; then
    cat > "$RESULT_FILE" <<EOF
{
  "success": true,
  "version": "${version}",
  "duration": ${duration},
  "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
  else
    cat > "$RESULT_FILE" <<EOF
{
  "success": false,
  "version": "${version}",
  "duration": ${duration},
  "error": "${error}",
  "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
  fi
}

cleanup_old_backups() {
  local count
  count=$(find "$BACKUPS_DIR" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | wc -l)
  if (( count > MAX_BACKUPS )); then
    log "Cleaning old backups (${count} > ${MAX_BACKUPS})"
    find "$BACKUPS_DIR" -maxdepth 1 -mindepth 1 -type d -printf '%T+ %p\n' \
      | sort | head -n $(( count - MAX_BACKUPS )) | cut -d' ' -f2- \
      | xargs rm -rf
  fi
}

# ── Data backup (rsync for efficiency + permissions) ──────────────────────────
backup_data_dir() {
  local backup_dir="$1"
  if [[ -d "$DATA_SOURCE_DIR" ]]; then
    log "Backing up data/ to ${backup_dir}/${DATA_BACKUP_PREFIX}/"
    mkdir -p "${backup_dir}/${DATA_BACKUP_PREFIX}"
    rsync -a --delete "$DATA_SOURCE_DIR/" "${backup_dir}/${DATA_BACKUP_PREFIX}/" 2>/dev/null \
      || cp -a "$DATA_SOURCE_DIR" "${backup_dir}/${DATA_BACKUP_PREFIX}"
    log "Data backup complete ($(du -sh "${backup_dir}/${DATA_BACKUP_PREFIX}" 2>/dev/null | cut -f1))"
  else
    log "WARNING: Data source dir $DATA_SOURCE_DIR not found — skipping data backup"
  fi
}

# ── Data restore ──────────────────────────────────────────────────────────────
restore_data_dir() {
  local backup_dir="$1"
  local data_backup="${backup_dir}/${DATA_BACKUP_PREFIX}"
  if [[ -d "$data_backup" ]]; then
    log "Restoring data/ from backup..."
    rsync -a --delete "$data_backup/" "$DATA_SOURCE_DIR/" 2>/dev/null \
      || cp -a "$data_backup" "$DATA_SOURCE_DIR"
    log "Data restored successfully"
  else
    log "WARNING: No data backup found at ${data_backup} — data/ not restored"
  fi
}

wait_for_health() {
  local port="${1:-3001}"
  local timeout="$HEALTH_TIMEOUT"
  local elapsed=0

  log "Waiting for health check (timeout: ${timeout}s)..."
  while (( elapsed < timeout )); do
    if curl -fsSL --max-time 3 "http://localhost:${port}/health" &>/dev/null; then
      log "Health check passed after ${elapsed}s"
      return 0
    fi
    sleep 2
    elapsed=$(( elapsed + 2 ))
  done

  log "Health check FAILED after ${timeout}s"
  return 1
}

# ── Shared rollback: restore code + data ────────────────────────────────────
rollback_restore() {
  local backup_dir="$1"
  log "Rolling back: restoring code files..."
  for item in "$backup_dir"/*.js "$backup_dir"/package.json "$backup_dir"/package-lock.json "$backup_dir"/Dockerfile "$backup_dir"/openapi.yaml "$backup_dir"/VERSION; do
    [[ -f "$item" ]] && cp -f "$item" "$api_source_dir/" 2>/dev/null || true
  done
  if [[ -d "$backup_dir/routes" ]]; then
    rm -rf "$api_source_dir/routes"
    cp -rf "$backup_dir/routes" "$api_source_dir/routes"
  fi
  if [[ -d "$backup_dir/src" ]]; then
    rm -rf "$api_source_dir/src"
    cp -rf "$backup_dir/src" "$api_source_dir/src"
  fi
  restore_data_dir "$backup_dir"
}

# ── Shared container restart (preserves SERVICES_FILE env var) ───────────────
# Uses rm + run so new env vars (e.g. SERVICES_FILE) take effect.
# If docker-compose is not configured, falls back to docker start.
restart_container() {
  local image="$1"
  log "Restarting container (rm + run to pick up env vars)..."
  # Stop and remove existing container so new env var is applied
  docker rm -f "$CONTAINER_NAME" 2>/dev/null || true

  # Re-create with same volumes. CRITICAL: must include CREDENTIALS_FILE +
  # ENCRYPTION_KEY_FILE pointing at /app/data/ so the container reads the TOTP
  # secret from the bind-mounted host data dir (not image-local /app/credentials.json
  # which gets a fresh encryption key on every container recreate = TOTP breaks).
  docker run -d --restart unless-stopped --name "$CONTAINER_NAME" \
    -p 127.0.0.1:3001:3001 \
    -v /opt/dashcaddy/dashcaddy-api/data:/app/data \
    -e SERVICES_FILE=/app/data/services.json \
    -e CREDENTIALS_FILE=/app/d...son \
    -e ENCRYPTION_KEY_FILE=/app/data/.encryption-key \
    "$image"
  log "Container restarted with fresh env"
}

# ── Code-only restore (used after failed build when data hasn't changed yet) ──
code_restore() {
  local backup_dir="$1"
  log "Restoring code files..."
  for item in "$backup_dir"/*.js "$backup_dir"/package.json "$backup_dir"/package-lock.json "$backup_dir"/Dockerfile "$backup_dir"/openapi.yaml "$backup_dir"/VERSION; do
    [[ -f "$item" ]] && cp -f "$item" "$api_source_dir/" 2>/dev/null || true
  done
  if [[ -d "$backup_dir/routes" ]]; then
    rm -rf "$api_source_dir/routes"
    cp -rf "$backup_dir/routes" "$api_source_dir/routes"
  fi
  if [[ -d "$backup_dir/src" ]]; then
    rm -rf "$api_source_dir/src"
    cp -rf "$backup_dir/src" "$api_source_dir/src"
  fi
}

main() {
  local start_time
  start_time=$(date +%s)

  # 1. Read trigger
  if [[ ! -f "$TRIGGER_FILE" ]]; then
    log "No trigger file found — nothing to do"
    exit 0
  fi

  # Parse trigger.json (uses python3 which is available on all supported distros)
  local action version from_version staging_dir api_source_dir commit
  local frontend_staging_dir frontend_target_dir
  action=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}'))['action'])")
  version=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}'))['version'])")
  from_version=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}'))['fromVersion'])")
  staging_dir=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}'))['stagingDir'])")
  api_source_dir=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}'))['apiSourceDir'])")
  commit=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}')).get('commit') or '')")
  frontend_staging_dir=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}')).get('frontendStagingDir') or '')")
  frontend_target_dir=$(python3 -c "import json; print(json.load(open('${TRIGGER_FILE}')).get('frontendTargetDir') or '')")
  # Handle action=rollback (no new version to deploy)
  local to_version="${version}"

  log "=== ${action^^}: v${from_version} -> v${to_version} ==="
  log "Staging: ${staging_dir}"
  log "API source: ${api_source_dir}"

  # Consume the trigger immediately so we don't re-process on failure
  mv "$TRIGGER_FILE" "${TRIGGER_FILE}.processing"

  # ── Handle rollback ────────────────────────────────────────────────────────
  if [[ "$action" == "rollback" ]]; then
    local backup_dir="${BACKUPS_DIR}/${version}"
    if [[ ! -d "$backup_dir" ]]; then
      log "ERROR: No backup found for version ${version}"
      write_result "false" "$version" "$(( $(date +%s) - start_time ))" "No backup found for version ${version}"
      rm -f "${TRIGGER_FILE}.processing"
      exit 1
    fi

    log "Performing rollback to v${version}..."
    rollback_restore "$backup_dir"

    # Rebuild old code
    log "Rebuilding container..."
    cd "$api_source_dir"
    docker build -t dashcaddy-dashcaddy-api:latest . 2>&1 | tail -1 || true

    restart_container "dashcaddy-dashcaddy-api:latest"
    wait_for_health || log "WARNING: Health check failed after rollback"

    write_result "true" "$version" "$(( $(date +%s) - start_time ))"
    rm -f "${TRIGGER_FILE}.processing"
    log "=== Rollback complete ==="
    exit 0
  fi

  # ── Handle update ───────────────────────────────────────────────────────────
  if [[ ! -d "$staging_dir" ]]; then
    log "ERROR: Staging directory not found: ${staging_dir}"
    write_result "false" "$to_version" "$(( $(date +%s) - start_time ))" "Staging directory not found"
    rm -f "${TRIGGER_FILE}.processing"
    exit 1
  fi

  # 2. Backup current API code + data/
  local backup_dir="${BACKUPS_DIR}/${from_version}"
  mkdir -p "$backup_dir"
  log "Backing up current API files to ${backup_dir}"
  for item in "$api_source_dir"/*.js "$api_source_dir"/package.json "$api_source_dir"/package-lock.json "$api_source_dir"/Dockerfile "$api_source_dir"/openapi.yaml "$api_source_dir"/VERSION; do
    [[ -f "$item" ]] && cp -f "$item" "$backup_dir/" 2>/dev/null || true
  done
  [[ -d "$api_source_dir/routes" ]] && cp -rf "$api_source_dir/routes" "$backup_dir/"
  [[ -d "$api_source_dir/src" ]] && cp -rf "$api_source_dir/src" "$backup_dir/"

  # Backup data/ directory (services.json, config.json, credentials, etc.)
  backup_data_dir "$backup_dir"

  cleanup_old_backups

  # 3. Copy new files from staging to API source
  log "Deploying new API files..."
  for item in "$staging_dir"/*.js "$staging_dir"/package.json "$staging_dir"/package-lock.json "$staging_dir"/Dockerfile "$staging_dir"/openapi.yaml "$staging_dir"/VERSION; do
    [[ -f "$item" ]] && cp -f "$item" "$api_source_dir/" 2>/dev/null || true
  done
  if [[ -d "$staging_dir/routes" ]]; then
    rm -rf "$api_source_dir/routes"
    cp -rf "$staging_dir/routes" "$api_source_dir/routes"
  fi
  if [[ -d "$staging_dir/src" ]]; then
    rm -rf "$api_source_dir/src"
    cp -rf "$staging_dir/src" "$api_source_dir/src"
  fi
  if [[ -n "$commit" ]]; then
    echo "$commit" > "$api_source_dir/VERSION"
  fi

  # 3b. Sync frontend
  if [[ -z "$frontend_staging_dir" ]]; then
    parent_staging=$(dirname "$staging_dir")
    [[ -d "$parent_staging/status" ]] && frontend_staging_dir="$parent_staging/status"
  fi
  if [[ -z "$frontend_target_dir" ]]; then
    for candidate in /var/www/dashcaddy-status /etc/dashcaddy/sites/status; do
      [[ -d "$candidate" ]] && frontend_target_dir="$candidate" && break
    done
  fi
  if [[ -n "$frontend_staging_dir" && -n "$frontend_target_dir" && -d "$frontend_staging_dir" ]]; then
    log "Syncing frontend: $frontend_staging_dir -> $frontend_target_dir"
    mkdir -p "$frontend_target_dir"
    [[ -f "$frontend_staging_dir/index.html" ]] && cp -f "$frontend_staging_dir/index.html" "$frontend_target_dir/index.html"
    [[ -f "$frontend_staging_dir/sw.js" ]] && cp -f "$frontend_staging_dir/sw.js" "$frontend_target_dir/sw.js"
    for sub in dist css vendor js; do
      if [[ -d "$frontend_staging_dir/$sub" ]]; then
        mkdir -p "$frontend_target_dir/$sub"
        cp -rf "$frontend_staging_dir/$sub/"* "$frontend_target_dir/$sub/" 2>/dev/null || true
      fi
    done
    if [[ -d "$frontend_staging_dir/assets" ]]; then
      mkdir -p "$frontend_target_dir/assets"
      cp -rf "$frontend_staging_dir/assets/"* "$frontend_target_dir/assets/" 2>/dev/null || true
    fi
  fi

  # 4. Rebuild container
  log "Rebuilding container..."
  cd "$api_source_dir"
  local build_ok=false
  local image_tag="dashcaddy-dashcaddy-api:latest"

  if docker build -t "$image_tag" . 2>&1; then
    build_ok=true
  fi

  if [[ "$build_ok" != "true" ]]; then
    log "ERROR: Docker build failed — rolling back code + data"
    code_restore "$backup_dir"
    docker build -t "$image_tag" . 2>&1 | tail -3 || true
    restart_container "$image_tag"
    wait_for_health || true
    write_result "false" "$to_version" "$(( $(date +%s) - start_time ))" "Docker build failed"
    rm -f "${TRIGGER_FILE}.processing"
    exit 1
  fi

  # 5. Restart container (rm + run so new env vars take effect)
  restart_container "$image_tag"

  # 6. Health check
  if wait_for_health; then
    local duration=$(( $(date +%s) - start_time ))
    log "=== Update successful: v${to_version} in ${duration}s ==="
    write_result "true" "$to_version" "$duration"
  else
    local duration=$(( $(date +%s) - start_time ))
    log "ERROR: Health check failed after update — rolling back code + data"
    rollback_restore "$backup_dir"
    docker build -t "$image_tag" . 2>&1 | tail -3 || true
    restart_container "$image_tag"
    wait_for_health || log "WARNING: Rollback health check also failed"
    write_result "false" "$to_version" "$duration" "Health check failed after update"
  fi

  # 7. Cleanup
  rm -f "${TRIGGER_FILE}.processing"
  rm -rf "${UPDATES_DIR}/staging" 2>/dev/null || true

  log "=== Update process complete ==="
}

main "$@"
