#!/usr/bin/env bash
set -Eeuo pipefail

# Ubuntu 服务器一键部署脚本。
# 用法：
#   bash deploy/ubuntu-deploy.sh
#   bash deploy/ubuntu-deploy.sh --reset-db

PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="${PROJECT_ROOT}/.env"
ENV_EXAMPLE="${PROJECT_ROOT}/.env.server.example"
BACKEND_DATA_DIR="${PROJECT_ROOT}/apps/backend/data"
BACKEND_CONTAINER_UID=1001
BACKEND_CONTAINER_GID=1001
HEALTH_URL=""
READY_TIMEOUT="${READY_TIMEOUT:-120}"
RESET_DB=false
RESET_DB_CONFIRM="${RESET_DB_CONFIRM:-}"
SKIP_BACKUP="${SKIP_BACKUP:-false}"
RESET_PRE_BACKUP_DONE=false

log() {
  printf '\033[1;34m[deploy]\033[0m %s\n' "$*"
}

warn() {
  printf '\033[1;33m[deploy]\033[0m %s\n' "$*"
}

fail() {
  printf '\033[1;31m[deploy]\033[0m %s\n' "$*" >&2
  exit 1
}

print_usage() {
  cat <<'EOF'
用法：
  bash deploy/ubuntu-deploy.sh
  bash deploy/ubuntu-deploy.sh --reset-db

参数：
  --reset-db    删除 Compose PostgreSQL 数据卷 postgres_data，再按 migrations 目录全量重建（001 基线 + 全部增量）。
  --skip-backup 跳过部署过程中的自动数据库备份（备份失败时确认为可接受后可用它紧急部署）。
  --yes         跳过 --reset-db 的交互确认，也可使用环境变量 RESET_DB_CONFIRM=1。
  -h, --help    显示帮助。

注意：
  --reset-db 会删除所有 PostgreSQL 数据，包括订单、任务、后台用户、审计日志和迁移记录。
  日常结构变更请新增 002_xxx.sql 等渐进迁移，不要改已应用的历史迁移文件。
  它不会删除 apps/backend/data 下的平台 JSON 配置，也不会删除 Caddy 证书卷。

  图片存储使用阿里云 OSS（.env 的 STORAGE_OSS_*），生产编排不含 MinIO。
  每次部署在执行数据库迁移前会自动备份到 OSS（deploy/backup-db.sh）；
  --reset-db 在删除数据卷前也会先抓一份最后备份。备份与恢复见 docs/数据库备份与恢复.md。
EOF
}

parse_args() {
  while (($# > 0)); do
    case "$1" in
      --reset-db)
        RESET_DB=true
        ;;
      --skip-backup)
        SKIP_BACKUP=true
        ;;
      --yes)
        RESET_DB_CONFIRM=1
        ;;
      -h|--help)
        print_usage
        exit 0
        ;;
      *)
        fail "未知参数：$1。执行 bash deploy/ubuntu-deploy.sh --help 查看用法。"
        ;;
    esac
    shift
  done
}

read_env_value() {
  local key="$1"
  awk -F= -v key="${key}" '
    $0 !~ /^[[:space:]]*#/ && $1 == key {
      value = substr($0, index($0, "=") + 1)
      gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
      gsub(/^"|"$/, "", value)
      gsub(/^'\''|'\''$/, "", value)
      print value
      exit
    }
  ' "${ENV_FILE}"
}

ensure_docker() {
  command -v docker >/dev/null 2>&1 || fail "未找到 docker，请先在 Ubuntu 上安装 Docker Engine。"
  docker compose version >/dev/null 2>&1 || fail "未找到 docker compose 插件，请安装 Docker Compose plugin。"
  command -v curl >/dev/null 2>&1 || fail "未找到 curl，请先安装：sudo apt-get update && sudo apt-get install -y curl"
}

ensure_env_file() {
  if [[ -f "${ENV_FILE}" ]]; then
    return
  fi

  cp "${ENV_EXAMPLE}" "${ENV_FILE}"
  fail "已生成 .env，请先编辑真实域名、数据库密码、后台账号和密钥后再重新执行。"
}

validate_env_file() {
  if grep -nE 'change-me|your-domain|example\.com' "${ENV_FILE}" >/tmp/order-site-env-placeholders.txt; then
    cat /tmp/order-site-env-placeholders.txt >&2
    fail ".env 仍包含示例占位值，请替换后再部署。"
  fi

  [[ "${READY_TIMEOUT}" =~ ^[1-9][0-9]*$ ]] || fail "READY_TIMEOUT 必须是正整数秒数，当前值：${READY_TIMEOUT}"

  local site_addr worker_site_addr
  site_addr="$(read_env_value CADDY_SITE_ADDR)"
  [[ -n "${site_addr}" ]] || fail ".env 缺少 CADDY_SITE_ADDR。"
  worker_site_addr="$(read_env_value WORKER_SITE_ADDR)"
  [[ -n "${worker_site_addr}" ]] || fail ".env 缺少 WORKER_SITE_ADDR。"
  HEALTH_URL="${site_addr%/}/health/ready"

  validate_database_url_for_compose
}

validate_database_url_for_compose() {
  local database_url
  database_url="$(read_env_value DATABASE_URL)"

  if [[ -z "${database_url}" ]]; then
    return
  fi

  if [[ "${database_url}" == *"@localhost:"* \
    || "${database_url}" == *"@127.0.0.1:"* \
    || "${database_url}" == *"@[::1]:"* ]]; then
    fail ".env 的 DATABASE_URL 当前指向本机地址。Docker 部署时后端容器内的 localhost 不是 postgres 容器，请改为 postgres 服务名或外部数据库地址。"
  fi
}

validate_reset_database_target() {
  local database_url
  database_url="$(read_env_value DATABASE_URL)"

  if [[ -n "${database_url}" && "${database_url}" != *"@postgres:"* ]]; then
    fail "--reset-db 只支持重置本脚本管理的 Compose PostgreSQL。当前 DATABASE_URL 未指向 postgres 服务，请手动确认外部数据库后再处理。"
  fi
}

confirm_reset_database() {
  if [[ "${RESET_DB}" != "true" ]]; then
    return
  fi

  validate_reset_database_target

  warn "即将重置 PostgreSQL 数据库：会删除订单、任务、后台用户、审计日志和所有迁移记录。"
  warn "不会删除 apps/backend/data 下的平台 JSON 配置，也不会删除 Caddy 证书卷。"

  if [[ "${RESET_DB_CONFIRM}" == "1" ]]; then
    warn "检测到 RESET_DB_CONFIRM=1 或 --yes，跳过交互确认。"
    return
  fi

  if [[ ! -t 0 ]]; then
    fail "当前不是交互式终端。若确认要重置数据库，请执行：RESET_DB_CONFIRM=1 bash deploy/ubuntu-deploy.sh --reset-db"
  fi

  local answer
  printf '请输入 RESET 确认重置数据库：'
  read -r answer
  [[ "${answer}" == "RESET" ]] || fail "未确认重置数据库，已停止。"
}

ensure_data_dir() {
  mkdir -p "${BACKEND_DATA_DIR}"

  if [[ "$(id -u)" == "0" ]]; then
    chown -R "${BACKEND_CONTAINER_UID}:${BACKEND_CONTAINER_GID}" "${BACKEND_DATA_DIR}"
    return
  fi

  if command -v sudo >/dev/null 2>&1; then
    sudo chown -R "${BACKEND_CONTAINER_UID}:${BACKEND_CONTAINER_GID}" "${BACKEND_DATA_DIR}"
    return
  fi

  warn "未找到 sudo，跳过目录属主调整。若后端无法写入日志或配置，请执行："
  warn "sudo chown -R ${BACKEND_CONTAINER_UID}:${BACKEND_CONTAINER_GID} ${BACKEND_DATA_DIR}"
}

check_compose_config() {
  log "检查 Docker Compose 配置"
  docker compose config >/dev/null || fail "Docker Compose 配置校验失败。"
}

# 部署前磁盘空间预检：备份目录所在文件系统至少要有首份备份 + 现有备份量 2 倍的余量。
# 不足时直接失败，避免备份或迁移过程中磁盘写满导致数据库不可用。
ensure_disk_space() {
  local backup_root="${PROJECT_ROOT}/backups"
  local floor_kib=1048576 # 1GiB
  local existing_kib=0 require_kib free_kib

  mkdir -p "${backup_root}" 2>/dev/null || true

  if [[ -d "${backup_root}" ]]; then
    existing_kib="$(du -sk "${backup_root}" 2>/dev/null | awk '{print $1}' || true)"
    existing_kib="${existing_kib:-0}"
  fi

  require_kib=$((existing_kib * 2))
  [[ ${require_kib} -lt ${floor_kib} ]] && require_kib=${floor_kib}

  free_kib="$(df -Pk "${PROJECT_ROOT}" 2>/dev/null | awk 'NR==2 {print $4}' || true)"
  free_kib="${free_kib:-0}"

  log "磁盘空间预检：可用 ${free_kib} KiB，备份目录已占用 ${existing_kib} KiB，要求至少 ${require_kib} KiB"
  if [[ ${free_kib} -lt ${require_kib} ]]; then
    fail "磁盘可用空间不足（可用 ${free_kib} KiB，需 ≥ ${require_kib} KiB）。请清理磁盘或调小 DB_BACKUP_KEEP 后重试。"
  fi
}

pull_base_images() {
  log "拉取基础镜像"
  docker compose pull --ignore-buildable
}

build_images() {
  log "构建后端镜像"
  docker compose build backend || fail "后端镜像构建失败。"

  log "构建 Web 镜像"
  docker compose build web || fail "Web 镜像构建失败。"
}

resolve_compose_project_name() {
  local project_name
  project_name="$(read_env_value COMPOSE_PROJECT_NAME || true)"

  if [[ -z "${project_name}" ]]; then
    project_name="$(basename "${PROJECT_ROOT}")"
  fi

  printf '%s\n' "${project_name}"
}

resolve_postgres_volume_name() {
  local container_id volume_name project_name

  container_id="$(docker compose ps -aq postgres 2>/dev/null | head -n 1 || true)"
  if [[ -n "${container_id}" ]]; then
    volume_name="$(
      docker inspect -f '{{range .Mounts}}{{if eq .Destination "/var/lib/postgresql/data"}}{{if eq .Type "volume"}}{{.Name}}{{end}}{{end}}{{end}}' "${container_id}" 2>/dev/null || true
    )"
    if [[ -n "${volume_name}" ]]; then
      printf '%s\n' "${volume_name}"
      return
    fi
  fi

  volume_name="$(
    docker compose volumes 2>/dev/null \
      | awk '$2 ~ /(^|_)postgres_data$/ { print $2; exit }' || true
  )"
  if [[ -n "${volume_name}" ]]; then
    printf '%s\n' "${volume_name}"
    return
  fi

  project_name="$(resolve_compose_project_name)"
  printf '%s_postgres_data\n' "${project_name}"
}

reset_database() {
  if [[ "${RESET_DB}" != "true" ]]; then
    return
  fi

  local volume_name
  volume_name="$(resolve_postgres_volume_name)"

  warn "开始重置数据库，将删除 Docker volume：${volume_name}"
  docker compose stop web backend postgres >/dev/null 2>&1 || true
  docker compose rm -f -s postgres >/dev/null 2>&1 || true

  if docker volume inspect "${volume_name}" >/dev/null 2>&1; then
    docker volume rm "${volume_name}" >/dev/null || fail "删除 PostgreSQL 数据卷失败：${volume_name}"
    log "已删除 PostgreSQL 数据卷：${volume_name}"
    return
  fi

  warn "未找到 PostgreSQL 数据卷：${volume_name}，将按空库继续部署。"
}

wait_for_service() {
  local service="$1"
  local label="$2"
  local attempt container_id status health_status

  log "等待 ${label} 就绪"

  for ((attempt = 1; attempt <= READY_TIMEOUT; attempt++)); do
    container_id="$(docker compose ps -q "${service}" 2>/dev/null || true)"
    if [[ -n "${container_id}" ]]; then
      status="$(docker inspect -f '{{.State.Status}}' "${container_id}" 2>/dev/null || true)"
      health_status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "${container_id}" 2>/dev/null || true)"

      if [[ -n "${health_status}" && "${health_status}" == "healthy" ]]; then
        log "${label} 已就绪"
        return
      fi

      if [[ -z "${health_status}" && "${status}" == "running" ]]; then
        log "${label} 已启动"
        return
      fi

      if [[ "${status}" == "exited" || "${status}" == "dead" ]]; then
        break
      fi
    fi

    sleep 1
  done

  print_diagnostics
  fail "${label} 启动超时，请根据上面的日志排查。"
}

start_postgres() {
  log "启动 PostgreSQL"
  if docker compose up -d --no-deps postgres; then
    wait_for_service postgres "PostgreSQL"
    return
  fi

  print_diagnostics
  fail "PostgreSQL 启动失败，请根据上面的日志排查。"
}

run_database_migrations() {
  log "执行数据库迁移"
  if docker compose run --rm --no-deps backend node dist/db/migrate.js; then
    log "数据库迁移完成"
    return
  fi

  print_diagnostics
  fail "数据库迁移失败，请根据上面的日志排查。"
}

restart_backend() {
  log "重建并启动后端"
  if docker compose up -d --no-deps --force-recreate backend; then
    wait_for_service backend "后端"
    return
  fi

  print_diagnostics
  fail "后端启动失败，请根据上面的日志排查。"
}

restart_web() {
  log "重建并启动 Web 入口"
  if docker compose up -d --no-deps --force-recreate --remove-orphans web; then
    wait_for_service web "Web"
    return
  fi

  print_diagnostics
  fail "Web 启动失败，请根据上面的日志排查。"
}

run_scheduled_backup() {
  local label="$1"

  if [[ "${SKIP_BACKUP}" == "true" ]]; then
    warn "已指定 --skip-backup，跳过${label}前的数据库备份。"
    return
  fi

  log "${label}前自动备份数据库到 OSS"
  if bash "${PROJECT_ROOT}/deploy/backup-db.sh"; then
    log "${label}前备份完成"
    return
  fi

  fail "${label}前备份失败，已停止部署。确认为可接受的失败后可用 --skip-backup 继续。"
}

backup_before_reset() {
  if [[ "${RESET_DB}" != "true" ]]; then
    return
  fi
  if [[ "${SKIP_BACKUP}" == "true" ]]; then
    warn "已指定 --skip-backup，跳过删除数据卷前的最后一份备份。"
    return
  fi

  local container_id status
  container_id="$(docker compose ps -q postgres 2>/dev/null | head -n 1 || true)"
  status=""
  [[ -n "${container_id}" ]] && status="$(docker inspect -f '{{.State.Status}}' "${container_id}" 2>/dev/null || true)"
  if [[ "${status}" != "running" ]]; then
    local volume_name
    volume_name="$(resolve_postgres_volume_name)"
    if docker volume inspect "${volume_name}" >/dev/null 2>&1; then
      warn "检测到已有 PostgreSQL 数据卷但容器未运行，先启动数据库以完成重置前备份。"
      start_postgres
    else
      warn "未检测到已有 PostgreSQL 数据卷，跳过重置前备份（首次部署属正常现象）。"
      return
    fi
  fi

  run_scheduled_backup "重置数据库"
  RESET_PRE_BACKUP_DONE=true
}

backup_before_migrate() {
  if [[ "${RESET_PRE_BACKUP_DONE}" == "true" ]]; then
    log "已在重置数据库前完成最后备份，跳过重置后的空库备份。"
    return
  fi
  run_scheduled_backup "执行迁移"
}

deploy_stack() {
  check_compose_config
  pull_base_images
  build_images
  backup_before_reset
  reset_database
  start_postgres
  backup_before_migrate
  run_database_migrations
  restart_backend
  restart_web
}

wait_for_health() {
  log "等待服务健康检查：${HEALTH_URL}"

  for _ in $(seq 1 "${READY_TIMEOUT}"); do
    if curl -fsS "${HEALTH_URL}" >/dev/null 2>&1; then
      log "服务已就绪"
      return
    fi
    sleep 1
  done

  print_diagnostics
  fail "部署已启动但健康检查未通过，请根据日志排查。"
}

print_applied_migrations() {
  log "已应用数据库迁移"

  local migrations
  if migrations="$(docker compose exec -T backend node --input-type=module -e '
const { query, closeDb } = await import("./dist/db/client.js");
try {
  const result = await query("SELECT filename FROM schema_migrations ORDER BY filename");
  console.log(result.rows.map((row) => row.filename).join("\n") || "(无迁移记录)");
} finally {
  await closeDb();
}
' 2>&1)"; then
    printf '%s\n' "${migrations}"
    return
  fi

  warn "读取迁移记录失败："
  printf '%s\n' "${migrations}"
}

print_diagnostics() {
  warn "当前容器状态："
  docker compose ps || true

  warn "后端最近日志："
  local backend_logs
  backend_logs="$(docker compose logs --tail=120 backend 2>&1 || true)"
  printf '%s\n' "${backend_logs}"

  if printf '%s\n' "${backend_logs}" | grep -q 'password authentication failed for user "postgres"'; then
    warn "检测到 PostgreSQL 密码认证失败。通常是 postgres_data 已用旧密码初始化，而 .env 改成了新密码。"
    warn "无生产数据时可执行：RESET_DB_CONFIRM=1 bash deploy/ubuntu-deploy.sh --reset-db"
    warn "需要保留数据时，请进入 postgres 容器用当前 .env 的 POSTGRES_PASSWORD 修改数据库用户密码。"
  fi

  warn "后端 ready 接口："
  docker compose exec -T backend sh -lc 'curl -fsS http://127.0.0.1:3000/health/ready || true' || true

  print_applied_migrations || true
}

main() {
  parse_args "$@"

  cd "${PROJECT_ROOT}"

  ensure_docker
  ensure_env_file
  validate_env_file
  confirm_reset_database
  ensure_data_dir
  ensure_disk_space
  deploy_stack
  wait_for_health
  print_applied_migrations

  log "部署完成"
  docker compose ps
}

main "$@"
