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

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
COMPOSE_FILE="${ROOT_DIR}/deploy/docker-compose.prod.yml"
BACKEND_ENV="${ROOT_DIR}/backend/.env"
BACKEND_LOG_DIR="${ROOT_DIR}/backend/logs"
HEALTH_URL="${PROD_HEALTH_URL:-}"
PUBLIC_URL="${PROD_PUBLIC_URL:-}"
READY_TIMEOUT="${PROD_READY_TIMEOUT:-120}"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[1;34m'
NC='\033[0m'

log() {
  printf "${BLUE}[deploy]${NC} %s\n" "$*"
}

log_success() {
  printf "${GREEN}[deploy]${NC} %s\n" "$*"
}

log_warn() {
  printf "${YELLOW}[deploy]${NC} %s\n" "$*"
}

log_error() {
  printf "${RED}[deploy]${NC} %s\n" "$*" >&2
}

need_cmd() {
  if ! command -v "$1" >/dev/null 2>&1; then
    log_error "缺少命令：$1"
    exit 1
  fi
}

env_value() {
  local key="$1"
  awk -F= -v key="${key}" '$1 == key { sub(/^[^=]*=/, ""); print; exit }' "${BACKEND_ENV}"
}

require_env() {
  local key="$1"
  local value
  value="$(env_value "${key}")"
  if [[ -z "${value}" ]]; then
    log_error "backend/.env 缺少或未填写 ${key}"
    exit 1
  fi
  printf "%s" "${value}"
}

show_help() {
  cat << EOF
用法: ./scripts/deploy-prod.sh [选项]

选项:
  --no-build      不重新构建镜像，仅启动已有镜像
  --no-migrate    不执行数据库迁移
  --reset-db      重置数据库（删除并重建）
  --logs          部署完成后跟随查看后端日志
  --skip-check    跳过部署前代码门禁（不推荐）
  --no-clean      跳过部署后的 Docker 垃圾清理
  -h, --help      显示帮助信息

环境变量:
  PROD_HEALTH_URL=https://你的域名/api/health
                  部署后健康检查地址，默认使用 CADDY_DOMAIN
  PROD_PUBLIC_URL=https://你的域名
                  输出给用户看的访问地址
  PROD_READY_TIMEOUT=120
                  等待服务就绪秒数

示例:
  ./scripts/deploy-prod.sh
  ./scripts/deploy-prod.sh --no-build
  ./scripts/deploy-prod.sh --reset-db
  ./scripts/deploy-prod.sh --logs
EOF
}

NO_BUILD=0
NO_MIGRATE=0
RESET_DB=0
FOLLOW_LOGS=0
SKIP_CHECK=0
NO_CLEAN=0

while [[ $# -gt 0 ]]; do
  case "$1" in
    --no-build)
      NO_BUILD=1
      shift
      ;;
    --no-migrate)
      NO_MIGRATE=1
      shift
      ;;
    --reset-db)
      RESET_DB=1
      shift
      ;;
    --logs)
      FOLLOW_LOGS=1
      shift
      ;;
    --skip-check)
      SKIP_CHECK=1
      shift
      ;;
    --no-clean)
      NO_CLEAN=1
      shift
      ;;
    -h|--help)
      show_help
      exit 0
      ;;
    *)
      log_error "未知选项：$1"
      show_help
      exit 1
      ;;
  esac
done

if [[ "${RESET_DB}" == "1" && "${NO_MIGRATE}" == "1" ]]; then
  log_error "--reset-db 会清空数据库，不能和 --no-migrate 同时使用"
  exit 1
fi

validate_env() {
  if [[ ! -f "${BACKEND_ENV}" ]]; then
    log_error "未找到 backend/.env，请先执行：cp backend/.env.prod.example backend/.env 并填写生产配置"
    exit 1
  fi

  local app_env caddy_domain caddy_email mysql_dsn redis_addr storage_endpoint
  local minio_root_user minio_root_password storage_access_key_id storage_secret_access_key
  app_env="$(require_env APP_ENV)"
  caddy_domain="$(require_env CADDY_DOMAIN)"
  caddy_email="$(require_env CADDY_EMAIL)"
  mysql_dsn="$(require_env MYSQL_DSN)"
  redis_addr="$(require_env REDIS_ADDR)"
  storage_endpoint="$(require_env STORAGE_ENDPOINT)"
  require_env MYSQL_DATABASE >/dev/null
  require_env MYSQL_ROOT_PASSWORD >/dev/null
  require_env MYSQL_USER >/dev/null
  require_env MYSQL_PASSWORD >/dev/null
  require_env JWT_SECRET >/dev/null
  minio_root_user="$(require_env MINIO_ROOT_USER)"
  minio_root_password="$(require_env MINIO_ROOT_PASSWORD)"
  storage_access_key_id="$(require_env STORAGE_ACCESS_KEY_ID)"
  storage_secret_access_key="$(require_env STORAGE_SECRET_ACCESS_KEY)"

  if [[ "${app_env}" != "production" ]]; then
    log_warn "APP_ENV 当前是 ${app_env}，生产部署建议改为 production"
  fi

  if [[ "${caddy_domain}" == http://* || "${caddy_domain}" == https://* || "${caddy_domain}" == */* ]]; then
    log_error "CADDY_DOMAIN 只填写域名本身，不要包含 http(s):// 或路径，当前值：${caddy_domain}"
    exit 1
  fi
  if [[ "${caddy_domain}" == *"127.0.0.1"* || "${caddy_domain}" == *"localhost"* || "${caddy_domain}" != *"."* ]]; then
    log_error "CADDY_DOMAIN 必须填写可公网解析到本机的正式域名，当前值：${caddy_domain}"
    exit 1
  fi
  if [[ "${caddy_email}" == "admin@example.com" || "${caddy_email}" != *"@"* ]]; then
    log_error "CADDY_EMAIL 需要填写真实邮箱，用于 ACME 证书通知"
    exit 1
  fi

  if [[ "${mysql_dsn}" == *"127.0.0.1"* || "${mysql_dsn}" == *"localhost"* ]]; then
    log_error "MYSQL_DSN 仍指向本机，请改为 tcp(mysql:3306)"
    exit 1
  fi
  if [[ "${redis_addr}" == "127.0.0.1:"* || "${redis_addr}" == "localhost:"* ]]; then
    log_error "REDIS_ADDR 仍指向本机，请改为 redis:6379"
    exit 1
  fi
  if [[ "${storage_endpoint}" == *"127.0.0.1"* || "${storage_endpoint}" == *"localhost"* ]]; then
    log_error "STORAGE_ENDPOINT 仍指向本机，请改为 http://minio:9000"
    exit 1
  fi
  if [[ "${storage_endpoint}" == "http://minio:9000" || "${storage_endpoint}" == "minio:9000" ]]; then
    if [[ "${storage_access_key_id}" != "${minio_root_user}" || "${storage_secret_access_key}" != "${minio_root_password}" ]]; then
      log_error "使用内置 MinIO 时，STORAGE_ACCESS_KEY_ID/SECRET_ACCESS_KEY 必须和 MINIO_ROOT_USER/ROOT_PASSWORD 保持一致"
      exit 1
    fi
  fi
  if awk -F= '/^[A-Z0-9_]+=/{ if ($0 ~ /=(change-|保持你的|你的)/) found=1 } END { exit found ? 0 : 1 }' "${BACKEND_ENV}"; then
    log_error "backend/.env 里还有占位值，请先替换后再部署"
    exit 1
  fi
}

compose() {
  docker compose --env-file "${BACKEND_ENV}" -f "${COMPOSE_FILE}" "$@"
}

compose_user_cmd() {
  printf "docker compose --env-file backend/.env -f deploy/docker-compose.prod.yml"
}

export_caddy_env() {
  export CADDY_DOMAIN
  export CADDY_EMAIL
  CADDY_DOMAIN="$(require_env CADDY_DOMAIN)"
  CADDY_EMAIL="$(require_env CADDY_EMAIL)"
}

public_url() {
  if [[ -n "${PUBLIC_URL}" ]]; then
    printf "%s" "${PUBLIC_URL}"
    return
  fi
  printf "https://%s" "$(require_env CADDY_DOMAIN)"
}

health_url() {
  if [[ -n "${HEALTH_URL}" ]]; then
    printf "%s" "${HEALTH_URL}"
    return
  fi
  printf "%s/api/health" "$(public_url)"
}

prepare_log_dir() {
  mkdir -p "${BACKEND_LOG_DIR}"
}

wait_service_healthy() {
  local service="$1"
  local label="$2"
  local attempt

  log "等待 ${label} 就绪..."
  for ((attempt = 1; attempt <= READY_TIMEOUT; attempt++)); do
    local container_id status
    container_id="$(compose ps -q "${service}" 2>/dev/null || true)"
    if [[ -n "${container_id}" ]]; then
      status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${container_id}" 2>/dev/null || true)"
      if [[ "${status}" == "healthy" || "${status}" == "running" ]]; then
        log_success "${label} 已就绪"
        return 0
      fi
    fi
    sleep 1
  done

  log_error "${label} 启动超时"
  log_warn "最近 ${service} 日志："
  compose logs --tail=120 "${service}" || true
  log_error "可继续执行：$(compose_user_cmd) logs ${service}"
  exit 1
}

mysql_root_exec() {
  compose exec -T mysql sh -c 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot "$@"' sh "$@"
}

goose_dsn() {
  local dsn="$1"
  if [[ "${dsn}" != *"multiStatements="* ]]; then
    if [[ "${dsn}" == *"?"* ]]; then
      dsn="${dsn}&multiStatements=true"
    else
      dsn="${dsn}?multiStatements=true"
    fi
  fi
  printf "%s" "${dsn}"
}

goose_cmd() {
  compose run --rm --no-deps backend /app/goose -dir /app/migrations mysql "$(goose_dsn "$(require_env MYSQL_DSN)")" "$@"
}

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

  local database
  database="$(require_env MYSQL_DATABASE)"
  if [[ ! "${database}" =~ ^[A-Za-z0-9_]+$ ]]; then
    log_error "MYSQL_DATABASE 只能包含字母、数字和下划线，当前值：${database}"
    exit 1
  fi

  log_warn "重置数据库 ${database}..."
  mysql_root_exec -e \
    "DROP DATABASE IF EXISTS \`${database}\`; CREATE DATABASE \`${database}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
  log_success "数据库已重建"
}

run_migrations() {
  if [[ "${NO_MIGRATE}" == "1" ]]; then
    log_warn "已跳过数据库迁移"
    return 0
  fi

  log "使用 goose 执行数据库迁移..."
  goose_cmd up
  log_success "数据库迁移完成"
}

wait_http_ready() {
  local attempt
  local url
  url="$(health_url)"
  log "等待接口健康检查通过..."
  for ((attempt = 1; attempt <= READY_TIMEOUT; attempt++)); do
    if curl -fsS --max-time 5 "${url}" >/dev/null 2>&1; then
      log_success "健康检查通过：${url}"
      return 0
    fi
    sleep 1
  done

  log_error "健康检查超时：${url}"
  exit 1
}

# 清理部署过程中产生的 Docker 垃圾（旧镜像、构建缓存）
# 每次 docker compose build 会生成新镜像，旧镜像变为 <none>:<none>
# 构建缓存（BuildKit）也会持续增长，不清理会轻松堆积几十 GB。
# 关键：用 --filter until=168h 只清超过 7 天的旧缓存，保留近期缓存（基础镜像层、
# Go module / go-build 缓存），让下次构建复用，避免每次重新下载 node 镜像、重拉依赖。
docker_cleanup() {
  log "清理 Docker 垃圾..."
  local before after

  before=$(docker system df --format '{{.TotalCount}}' 2>/dev/null || echo "?")

  docker image prune -f 2>/dev/null || true
  docker builder prune -f --filter until=168h 2>/dev/null || true

  after=$(docker system df --format '{{.TotalCount}}' 2>/dev/null || echo "?")

  log_success "Docker 清理完成（对象数: ${before} -> ${after}）"
}

main() {
  cd "${ROOT_DIR}"
  need_cmd docker
  need_cmd curl
  validate_env
  export_caddy_env
  prepare_log_dir

  log "检查 Docker Compose 配置..."
  compose config >/dev/null

  # 部署前代码门禁：本地有 go + node 时跑全量检查，挡住未通过的提交进镜像。
  # 部署机无 go/node 或 --skip-check 时跳过并告警，不阻断（避免环境耦合）。
  if [[ "${NO_BUILD}" == "0" && "${SKIP_CHECK}" == "0" ]]; then
    if command -v go >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then
      log "执行部署前代码门禁 (scripts/check.sh)..."
      if ! bash "${ROOT_DIR}/scripts/check.sh"; then
        log_error "代码门禁未通过，已中止部署。确认修复后重试，或用 --skip-check 强制跳过。"
        exit 1
      fi
    else
      log_warn "本地缺少 go 或 npm，跳过代码门禁。建议在开发机执行 ./scripts/check.sh 后再部署。"
    fi
  fi

  if [[ "${NO_BUILD}" == "1" ]]; then
    log "启动生产服务..."
    compose up -d
  else
    log "清理旧镜像以释放空间..."
    docker image prune -f 2>/dev/null || true

    log "构建后端镜像..."
    compose build backend

    log "构建 Caddy 镜像（含前端静态资源）..."
    compose build caddy

    log "启动生产服务..."
    compose up -d --no-build --remove-orphans
  fi

  wait_service_healthy mysql "MySQL"
  wait_service_healthy redis "Redis"
  wait_service_healthy minio "MinIO"
  wait_service_healthy caddy "Caddy"
  reset_database
  run_migrations

  log "重启后端以加载最新数据库状态..."
  compose up -d --no-deps --force-recreate backend
  wait_service_healthy backend "后端"
  compose up -d --no-deps --force-recreate caddy
  wait_service_healthy caddy "Caddy"
  wait_http_ready

  if [[ "${NO_CLEAN}" != "1" ]]; then
    docker_cleanup
  fi

  log_success "部署完成：$(public_url)"
  log "常用命令：$(compose_user_cmd) logs -f caddy backend"

  if [[ "${FOLLOW_LOGS}" == "1" ]]; then
    compose logs -f backend
  fi
}

main "$@"
