#!/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"
BACKUP_ROOT="${BACKUP_ROOT:-${ROOT_DIR}/backups}"
HELPER_IMAGE="${BACKUP_HELPER_IMAGE:-redis:7.4-alpine}"
if [[ "${BACKUP_ROOT}" != /* ]]; then
  BACKUP_ROOT="${ROOT_DIR}/${BACKUP_ROOT}"
fi

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

log() {
  printf "${BLUE}[backup]${NC} %s\n" "$*" >&2
}

log_success() {
  printf "${GREEN}[backup]${NC} %s\n" "$*" >&2
}

log_warn() {
  printf "${YELLOW}[backup]${NC} %s\n" "$*" >&2
}

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

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

absolute_dir() {
  local dir="$1"
  (
    cd "${dir}"
    pwd -P
  )
}

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

命令:
  backup                  创建完整生产备份
  restore <备份目录>       从备份目录恢复生产数据（需要 --yes）
  list                    列出本机备份目录

选项:
  --yes                   确认执行破坏性恢复，只用于 restore
  --no-stop               backup 时不主动停止 backend/caddy，适合只读低风险窗口（不推荐）
  -h, --help              显示帮助

环境变量:
  BACKUP_ROOT=/path       备份根目录，默认 ./backups
  BACKUP_HELPER_IMAGE=img 用于打包/还原 Docker volume 的镜像，默认 redis:7.4-alpine

备份内容:
  - MySQL 逻辑备份 mysql.sql
  - MinIO volume: minio_data.tgz
  - Redis volume: redis_data.tgz
  - Caddy 证书/配置 volume: caddy_data.tgz, caddy_config.tgz
  - backend/.env、deploy 配置、backend/logs
  - 当前 Git commit、Docker Compose 状态、goose 迁移状态

示例:
  ./scripts/backup-prod.sh backup
  ./scripts/backup-prod.sh list
  ./scripts/backup-prod.sh restore backups/20260618_153000 --yes
EOF
}

compose() {
  local app_env caddy_domain caddy_email admin_base_path
  app_env="$(env_value APP_ENV)"
  caddy_domain="${CADDY_DOMAIN:-$(env_value CADDY_DOMAIN)}"
  caddy_email="${CADDY_EMAIL:-$(env_value CADDY_EMAIL)}"
  admin_base_path="${VITE_ADMIN_BASE_PATH:-$(env_value VITE_ADMIN_BASE_PATH)}"

  if [[ "${app_env}" == "production" && (-z "${caddy_domain}" || -z "${caddy_email}") ]]; then
    log_error "APP_ENV=production 时必须配置 CADDY_DOMAIN 和 CADDY_EMAIL"
    exit 1
  fi

  CADDY_DOMAIN="${caddy_domain:-backup.localhost}" \
    CADDY_EMAIL="${caddy_email:-backup@example.com}" \
    VITE_ADMIN_BASE_PATH="${admin_base_path:-/manage-7x9k2p}" \
    docker compose --env-file "${BACKEND_ENV}" -f "${COMPOSE_FILE}" "$@"
}

require_env_file() {
  if [[ ! -f "${BACKEND_ENV}" ]]; then
    log_error "未找到 backend/.env，无法读取生产环境变量"
    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}"
}

container_id() {
  local service="$1"
  local cid
  cid="$(compose ps -a -q "${service}" 2>/dev/null || true)"
  if [[ -z "${cid}" ]]; then
    log "创建 ${service} 容器以挂载 volume..."
    compose create "${service}" >/dev/null
    cid="$(compose ps -a -q "${service}" 2>/dev/null || true)"
  fi
  if [[ -z "${cid}" ]]; then
    log_error "无法找到 ${service} 容器"
    exit 1
  fi
  printf "%s" "${cid}"
}

service_was_running() {
  local service="$1"
  local cid status
  cid="$(compose ps -q "${service}" 2>/dev/null || true)"
  if [[ -z "${cid}" ]]; then
    return 1
  fi
  status="$(docker inspect -f '{{.State.Status}}' "${cid}" 2>/dev/null || true)"
  [[ "${status}" == "running" ]]
}

start_services() {
  if [[ "$#" -eq 0 ]]; then
    return
  fi
  log "启动服务：$*"
  compose up -d "$@"
}

stop_services() {
  if [[ "$#" -eq 0 ]]; then
    return
  fi
  log "停止服务：$*"
  compose stop "$@"
}

start_recorded_services() {
  if [[ "$#" -eq 0 ]]; then
    return
  fi
  start_services "$@"
}

wait_service_ready() {
  local service="$1"
  local label="$2"
  local timeout="${3:-120}"
  local attempt cid status

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

  log_error "${label} 启动超时"
  compose logs --tail=80 "${service}" || true
  exit 1
}

remember_running() {
  local service="$1"
  local result_array_name="$2"
  if service_was_running "${service}"; then
    eval "${result_array_name}+=(\"${service}\")"
  fi
}

tar_from_service_volume() {
  local service="$1"
  local source_dir="$2"
  local backup_dir="$3"
  local output_name="$4"
  local cid
  cid="$(container_id "${service}")"
  log "备份 ${service}:${source_dir} -> ${output_name}"
  docker run --rm \
    --volumes-from "${cid}:ro" \
    -v "${backup_dir}:/backup" \
    "${HELPER_IMAGE}" \
    sh -c "cd '${source_dir}' && tar czf '/backup/${output_name}' ."
}

restore_service_volume() {
  local service="$1"
  local target_dir="$2"
  local backup_dir="$3"
  local input_name="$4"
  local cid
  if [[ ! -f "${backup_dir}/${input_name}" ]]; then
    log_warn "跳过 ${service}:${target_dir}，备份文件不存在：${input_name}"
    return
  fi
  cid="$(container_id "${service}")"
  log "还原 ${input_name} -> ${service}:${target_dir}"
  docker run --rm \
    --volumes-from "${cid}" \
    -v "${backup_dir}:/backup:ro" \
    "${HELPER_IMAGE}" \
    sh -c "rm -rf '${target_dir}'/* '${target_dir}'/.[!.]* '${target_dir}'/..?* 2>/dev/null || true; tar xzf '/backup/${input_name}' -C '${target_dir}'"
}

copy_if_exists() {
  local source="$1"
  local target="$2"
  if [[ -e "${source}" ]]; then
    cp -a "${source}" "${target}"
  fi
}

write_manifest() {
  local backup_dir="$1"
  {
    printf "created_at=%s\n" "$(date -Iseconds)"
    printf "host=%s\n" "$(hostname)"
    printf "git_commit=%s\n" "$(git -C "${ROOT_DIR}" rev-parse HEAD 2>/dev/null || true)"
    printf "git_branch=%s\n" "$(git -C "${ROOT_DIR}" branch --show-current 2>/dev/null || true)"
    printf "mysql_database=%s\n" "$(require_env MYSQL_DATABASE)"
    printf "storage_bucket=%s\n" "$(require_env STORAGE_BUCKET)"
  } > "${backup_dir}/manifest.env"

  git -C "${ROOT_DIR}" log --oneline -5 > "${backup_dir}/git-log.txt" 2>/dev/null || true
  compose ps > "${backup_dir}/docker-compose-ps.txt" 2>/dev/null || true
  compose config > "${backup_dir}/docker-compose.config.rendered.yml" 2>/dev/null || true
}

backup_mysql() {
  local backup_dir="$1"
  log "备份 MySQL -> mysql.sql"
  compose exec -T mysql sh -c \
    'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysqldump -uroot --single-transaction --routines --triggers --events --hex-blob --default-character-set=utf8mb4 "$MYSQL_DATABASE"' \
    > "${backup_dir}/mysql.sql"
  compose exec -T mysql sh -c 'mysql --version' > "${backup_dir}/mysql-version.txt" 2>/dev/null || true
}

backup_goose_status() {
  local backup_dir="$1"
  log "记录 goose 迁移状态"
  compose run --rm --no-deps backend /app/goose -dir /app/migrations mysql "$(goose_dsn "$(require_env MYSQL_DSN)")" status \
    > "${backup_dir}/goose-status.txt" 2>&1 || true
}

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}"
}

backup_files() {
  local backup_dir="$1"
  log "备份配置文件"
  mkdir -p "${backup_dir}/config"
  copy_if_exists "${BACKEND_ENV}" "${backup_dir}/config/backend.env"
  copy_if_exists "${COMPOSE_FILE}" "${backup_dir}/config/docker-compose.prod.yml"
  copy_if_exists "${ROOT_DIR}/deploy/caddy/Caddyfile" "${backup_dir}/config/Caddyfile"
  copy_if_exists "${ROOT_DIR}/deploy/mysql/my.cnf" "${backup_dir}/config/mysql.my.cnf"

  if [[ -d "${ROOT_DIR}/backend/logs" ]]; then
    log "备份后端日志 -> backend_logs.tgz"
    tar czf "${backup_dir}/backend_logs.tgz" -C "${ROOT_DIR}/backend" logs
  fi
}

restore_files() {
  local backup_dir="$1"
  local safety_dir="${BACKUP_ROOT}/pre-restore-files-$(date +%Y%m%d_%H%M%S)"
  mkdir -p "${safety_dir}"

  log "恢复配置文件（当前文件会先备份到 ${safety_dir}）"
  copy_if_exists "${BACKEND_ENV}" "${safety_dir}/backend.env"
  copy_if_exists "${COMPOSE_FILE}" "${safety_dir}/docker-compose.prod.yml"
  copy_if_exists "${ROOT_DIR}/deploy/caddy/Caddyfile" "${safety_dir}/Caddyfile"
  copy_if_exists "${ROOT_DIR}/deploy/mysql/my.cnf" "${safety_dir}/mysql.my.cnf"

  copy_if_exists "${backup_dir}/config/backend.env" "${BACKEND_ENV}"
  copy_if_exists "${backup_dir}/config/docker-compose.prod.yml" "${COMPOSE_FILE}"
  copy_if_exists "${backup_dir}/config/Caddyfile" "${ROOT_DIR}/deploy/caddy/Caddyfile"
  copy_if_exists "${backup_dir}/config/mysql.my.cnf" "${ROOT_DIR}/deploy/mysql/my.cnf"

  if [[ -f "${backup_dir}/backend_logs.tgz" ]]; then
    log "恢复后端日志"
    mkdir -p "${ROOT_DIR}/backend"
    rm -rf "${ROOT_DIR}/backend/logs"
    tar xzf "${backup_dir}/backend_logs.tgz" -C "${ROOT_DIR}/backend"
  fi
}

prepare_restore_env() {
  local backup_dir="$1"
  if [[ ! -f "${BACKEND_ENV}" ]]; then
    if [[ ! -f "${backup_dir}/config/backend.env" ]]; then
      log_error "当前缺少 backend/.env，备份中也没有 config/backend.env，无法恢复"
      exit 1
    fi
    log_warn "当前缺少 backend/.env，先从备份恢复环境变量文件"
    mkdir -p "$(dirname "${BACKEND_ENV}")"
    cp -a "${backup_dir}/config/backend.env" "${BACKEND_ENV}"
  fi
  if [[ ! -f "${COMPOSE_FILE}" && -f "${backup_dir}/config/docker-compose.prod.yml" ]]; then
    log_warn "当前缺少 deploy/docker-compose.prod.yml，先从备份恢复 Compose 文件"
    mkdir -p "$(dirname "${COMPOSE_FILE}")"
    cp -a "${backup_dir}/config/docker-compose.prod.yml" "${COMPOSE_FILE}"
  fi
  require_env_file
}

restore_mysql() {
  local backup_dir="$1"
  local database
  database="$(require_env MYSQL_DATABASE)"
  if [[ ! "${database}" =~ ^[A-Za-z0-9_]+$ ]]; then
    log_error "MYSQL_DATABASE 只能包含字母、数字和下划线，当前值：${database}"
    exit 1
  fi
  if [[ ! -f "${backup_dir}/mysql.sql" ]]; then
    log_error "缺少 MySQL 备份文件：${backup_dir}/mysql.sql"
    exit 1
  fi

  start_services mysql
  wait_service_ready mysql "MySQL"
  log "重建数据库 ${database}"
  compose exec -T mysql sh -c \
    'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot -e "DROP DATABASE IF EXISTS \`$MYSQL_DATABASE\`; CREATE DATABASE \`$MYSQL_DATABASE\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"'
  log "导入 MySQL 备份"
  compose exec -T mysql sh -c 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot "$MYSQL_DATABASE"' < "${backup_dir}/mysql.sql"
}

backup_all() {
  require_env_file
  need_cmd docker
  local no_stop="$1"
  local backup_dir="${BACKUP_ROOT}/$(date +%Y%m%d_%H%M%S)"
  local restart_services=()
  local backup_ok=0

  backup_failure_cleanup() {
    local exit_code=$?
    trap - ERR
    if [[ "${no_stop}" == "0" && "${backup_ok}" != "1" ]]; then
      log_warn "备份失败，尝试恢复备份前运行的服务"
      start_recorded_services "${restart_services[@]+"${restart_services[@]}"}" || true
    fi
    exit "${exit_code}"
  }
  trap backup_failure_cleanup ERR

  umask 077
  mkdir -p "${backup_dir}"
  chmod 700 "${backup_dir}"
  log "备份目录：${backup_dir}"

  if [[ "${no_stop}" == "0" ]]; then
    remember_running caddy restart_services
    remember_running backend restart_services
    stop_services caddy backend
  else
    log_warn "已开启 --no-stop，备份期间仍可能有写入，建议只在低风险窗口使用"
  fi

  start_services mysql
  wait_service_ready mysql "MySQL"
  backup_mysql "${backup_dir}"
  backup_goose_status "${backup_dir}"

  if [[ "${no_stop}" == "0" ]]; then
    remember_running minio restart_services
    remember_running redis restart_services
    stop_services minio redis caddy
  fi

  tar_from_service_volume minio /data "${backup_dir}" minio_data.tgz
  tar_from_service_volume redis /data "${backup_dir}" redis_data.tgz
  tar_from_service_volume caddy /data "${backup_dir}" caddy_data.tgz
  tar_from_service_volume caddy /config "${backup_dir}" caddy_config.tgz
  backup_files "${backup_dir}"
  write_manifest "${backup_dir}"

  if [[ "${no_stop}" == "0" ]]; then
    start_recorded_services "${restart_services[@]+"${restart_services[@]}"}"
  fi

  backup_ok=1
  trap - ERR
  log_success "备份完成：${backup_dir}"
  log_warn "备份包含生产密钥，请离线加密保存，不要提交到 Git。"
}

restore_all() {
  need_cmd docker
  local backup_dir="$1"
  local yes="$2"
  local restore_ok=0

  if [[ -z "${backup_dir}" || ! -d "${backup_dir}" ]]; then
    log_error "备份目录不存在：${backup_dir}"
    exit 1
  fi
  if [[ "${yes}" != "1" ]]; then
    log_error "restore 会覆盖当前生产数据库、MinIO、Redis、Caddy volume 和配置文件。确认执行请追加 --yes"
    exit 1
  fi
  backup_dir="$(absolute_dir "${backup_dir}")"

  restore_failure_cleanup() {
    local exit_code=$?
    trap - ERR
    if [[ "${restore_ok}" != "1" ]]; then
      log_warn "恢复失败，尝试启动当前配置下的服务，便于继续排查"
      compose up -d || true
    fi
    exit "${exit_code}"
  }
  trap restore_failure_cleanup ERR

  prepare_restore_env "${backup_dir}"
  log_warn "即将从 ${backup_dir} 恢复，当前生产数据会被覆盖。"
  stop_services backend caddy minio redis
  restore_files "${backup_dir}"
  restore_mysql "${backup_dir}"

  restore_service_volume minio /data "${backup_dir}" minio_data.tgz
  restore_service_volume redis /data "${backup_dir}" redis_data.tgz
  restore_service_volume caddy /data "${backup_dir}" caddy_data.tgz
  restore_service_volume caddy /config "${backup_dir}" caddy_config.tgz

  log "启动生产服务"
  compose up -d
  restore_ok=1
  trap - ERR
  log_success "恢复完成。建议立刻执行健康检查并抽查图片/二维码/订单数据。"
}

list_backups() {
  if [[ ! -d "${BACKUP_ROOT}" ]]; then
    log_warn "备份目录不存在：${BACKUP_ROOT}"
    return
  fi
  find "${BACKUP_ROOT}" -mindepth 2 -maxdepth 2 -type f -name manifest.env -exec dirname {} \; | sort
}

main() {
  local command="${1:-}"
  local no_stop=0
  local yes=0
  local restore_dir=""

  if [[ -z "${command}" || "${command}" == "-h" || "${command}" == "--help" ]]; then
    show_help
    exit 0
  fi
  shift || true

  case "${command}" in
    backup)
      while [[ $# -gt 0 ]]; do
        case "$1" in
          --no-stop) no_stop=1; shift ;;
          -h|--help) show_help; exit 0 ;;
          *) log_error "未知选项：$1"; show_help; exit 1 ;;
        esac
      done
      backup_all "${no_stop}"
      ;;
    restore)
      restore_dir="${1:-}"
      if [[ $# -gt 0 ]]; then
        shift
      fi
      while [[ $# -gt 0 ]]; do
        case "$1" in
          --yes) yes=1; shift ;;
          -h|--help) show_help; exit 0 ;;
          *) log_error "未知选项：$1"; show_help; exit 1 ;;
        esac
      done
      restore_all "${restore_dir}" "${yes}"
      ;;
    list)
      list_backups
      ;;
    *)
      log_error "未知命令：${command}"
      show_help
      exit 1
      ;;
  esac
}

main "$@"
