#!/usr/bin/env bash
# ==============================================================================
# Metadata
# ==============================================================================
# Author      : ChatGPT
# Version     : 1.1.0
# License     : MIT
# Last Update : 2026-03-02
# Bash        : >= 5.0
# Description : 生产级 Docker 在线安装脚本（CentOS 7 / CentOS 8 / openEuler 20.03 / 22.03）
#
# Assumptions
# 1) 默认使用 YUM 作为包管理工具（按你的要求），即使系统存在 dnf，也优先走 yum。
# 2) openEuler 使用 openEuler Everything 临时仓补齐依赖；若仍不足，再回退到 CentOS 8 Vault 兼容仓。
# 3) Docker 仓库按 el7 / el8 路径适配；openEuler 20.03 / 22.03 映射为 el8。
# 4) GPG 支持三种模式：
#    - strict（默认）：启用 gpgcheck，并优先使用镜像站 gpgkey（或官方 gpgkey）
#    - mirror：显式使用镜像站 gpgkey
#    - skip：跳过 repo 包签名校验（gpgcheck=0），适合受限网络或 key 拉取失败场景
# 5) 为安全起见，默认不覆盖已有 daemon.json；如需覆盖，请加 --force。
#
# ==============================================================================
# Call Graph (ASCII)
# ==============================================================================
# main
#  |
#  +--> init_runtime
#  |      +--> init_traps
#  |      +--> detect_os
#  |      +--> validate_arch
#  |
#  +--> parse_args
#  +--> validate_args
#  +--> check_requirements
#  +--> prepare_proxy_env
#  +--> cleanup_conflicting_packages
#  +--> configure_docker_repo
#  +--> handle_gpg_key
#  +--> install_prerequisites
#  |      +--> add_openeuler_everything_repo
#  |      +--> add_el8_compat_repo
#  |
#  +--> resolve_docker_version
#  +--> install_docker_packages
#  +--> configure_docker_daemon
#  +--> enable_and_start_docker
#  +--> verify_installation
#  +--> print_summary
#
# ==============================================================================
# Strict Mode / Safety
# ==============================================================================
set -Eeuo pipefail
set -o errtrace
IFS=$' \t\n'

# ==============================================================================
# Constants
# ==============================================================================
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_VERSION="1.1.0"

readonly EC_OK=0
readonly EC_GENERAL=1
readonly EC_USAGE=2
readonly EC_UNSUPPORTED_OS=10
readonly EC_REQUIREMENT=11
readonly EC_PERMISSION=12
readonly EC_NETWORK=20
readonly EC_REPO=21
readonly EC_INSTALL=22
readonly EC_SERVICE=23
readonly EC_VERIFY=24

readonly LOG_DEBUG=10
readonly LOG_INFO=20
readonly LOG_WARN=30
readonly LOG_ERROR=40

readonly DEFAULT_LOG_LEVEL="INFO"
readonly DEFAULT_MIRROR_PROVIDER="aliyun"
readonly DEFAULT_DOCKER_VERSION="20.10"
readonly DEFAULT_GPG_MODE="strict"

readonly TMP_ROOT_PREFIX="/tmp/docker-online-install"
readonly DOCKER_REPO_FILE="/etc/yum.repos.d/docker-ce.repo"
readonly DOCKER_DAEMON_CFG="/etc/docker/daemon.json"
readonly OE_PREREQ_REPO_FILE="/etc/yum.repos.d/openeuler-prereq-tmp.repo"
readonly EL8_COMPAT_REPO_FILE="/etc/yum.repos.d/el8-prereq-compat-tmp.repo"

readonly MIRROR_URL_ALIYUN="https://mirrors.aliyun.com/docker-ce/linux/centos"
readonly MIRROR_URL_TENCENT="https://mirrors.tencent.com/docker-ce/linux/centos"
readonly MIRROR_URL_HUAWEI="https://repo.huaweicloud.com/docker-ce/linux/centos"
readonly MIRROR_URL_OFFICIAL="https://download.docker.com/linux/centos"
readonly OFFICIAL_DOCKER_GPG_KEY_URL="https://download.docker.com/linux/centos/gpg"

readonly OE_REPO_BASE="https://repo.openeuler.org"

# ==============================================================================
# Globals
# ==============================================================================
LOG_LEVEL="${LOG_LEVEL:-$DEFAULT_LOG_LEVEL}"
DRY_RUN=0
FORCE=0
QUIET=0
CONFIRM=0

MIRROR_PROVIDER="$DEFAULT_MIRROR_PROVIDER"
DOCKER_VERSION="${DOCKER_VERSION:-$DEFAULT_DOCKER_VERSION}"
GPG_MODE="${GPG_MODE:-$DEFAULT_GPG_MODE}"
PROXY_URL="${PROXY_URL:-}"

SKIP_START=0
SKIP_ENABLE=0
CONFIGURE_DAEMON=1
REMOVE_CONFLICTS=1

OS_ID_RAW=""
OS_ID=""
OS_VERSION_ID=""
OS_EL_VER=""
OS_PRETTY_NAME=""
OS_ARCH=""
PKG_MGR="yum"

MIRROR_BASE_URL=""
DOCKER_GPG_KEY_URL=""
TMP_DIR=""

INSTALL_FLAGS=()
PKG_GLOBAL_OPTS=()
PKG_MGR_STABLE_OPTS=()

# Bash 数组用于安全传参，避免 eval。
PREREQ_PKGS=(
  "container-selinux"
  "fuse-overlayfs"
  "slirp4netns"
)

# ==============================================================================
# Logging
# ==============================================================================
### 将日志级别转换为数值
### @param level string 日志级别
### @return 0 输出对应数值
### @require none
log_level_to_num() {
  case "${1:-INFO}" in
    DEBUG) printf '%s\n' "$LOG_DEBUG" ;;
    INFO)  printf '%s\n' "$LOG_INFO" ;;
    WARN)  printf '%s\n' "$LOG_WARN" ;;
    ERROR) printf '%s\n' "$LOG_ERROR" ;;
    *)     printf '%s\n' "$LOG_INFO" ;;
  esac
}

### 统一日志输出
### @param level string 日志级别
### @param message string 消息
### @return 0 成功
### @require date
### @side_effect 向 stderr 输出
log_msg() {
  local level="${1:?level required}"
  shift
  local message="${*:-}"
  local current_num target_num timestamp

  current_num="$(log_level_to_num "$LOG_LEVEL")"
  target_num="$(log_level_to_num "$level")"

  if [[ "$target_num" -lt "$current_num" ]]; then
    return 0
  fi

  if [[ "$QUIET" -eq 1 && "$level" != "ERROR" ]]; then
    return 0
  fi

  timestamp="$(date '+%F %T')"
  printf '%s [%s] %s: %s\n' "$timestamp" "$level" "$SCRIPT_NAME" "$message" >&2
}

### DEBUG 日志
### @param message string 日志消息
### @return 0 成功
### @require none
log_debug() { log_msg "DEBUG" "$@"; }

### INFO 日志
### @param message string 日志消息
### @return 0 成功
### @require none
log_info() { log_msg "INFO" "$@"; }

### WARN 日志
### @param message string 日志消息
### @return 0 成功
### @require none
log_warn() { log_msg "WARN" "$@"; }

### ERROR 日志
### @param message string 日志消息
### @return 0 成功
### @require none
log_error() { log_msg "ERROR" "$@"; }

# ==============================================================================
# Error / Trap / Cleanup
# ==============================================================================
### 统一失败退出
### @param message string 错误消息
### @param exit_code int 退出码
### @return <exit_code> 失败退出
### @require none
die() {
  local message="${1:-unknown error}"
  local exit_code="${2:-$EC_GENERAL}"
  log_error "$message"
  exit "$exit_code"
}

### 输出函数调用栈
### @param none
### @return 0 成功
### @require none
print_call_stack() {
  local i
  for ((i=1; i<${#FUNCNAME[@]}; i++)); do
    printf '  at %s() %s:%s\n' \
      "${FUNCNAME[$i]}" \
      "${BASH_SOURCE[$i]:-unknown}" \
      "${BASH_LINENO[$((i-1))]:-unknown}" >&2
  done
}

### ERR trap 处理器
### @param none
### @return 0 由 trap 调用
### @require none
on_err() {
  local exit_code="$?"
  local cmd="${BASH_COMMAND:-unknown}"
  local line_no="${BASH_LINENO[0]:-unknown}"
  local src="${BASH_SOURCE[1]:-${BASH_SOURCE[0]:-unknown}}"

  log_error "命令执行失败：${cmd}"
  log_error "失败位置：${src}:${line_no}"
  log_error "退出码：${exit_code}"
  print_call_stack
  exit "$exit_code"
}

### 信号处理器
### @param signal string 信号名
### @return 130 中断退出
### @require none
on_signal() {
  local signal_name="${1:-SIGNAL}"
  log_warn "收到信号：${signal_name}，开始退出。"
  exit 130
}

### EXIT trap 处理器
### @param none
### @return 0 成功
### @require none
### @side_effect 清理临时目录
on_exit() {
  local exit_code="$?"
  cleanup || true
  if [[ "$exit_code" -eq 0 ]]; then
    log_info "脚本执行完成。"
  else
    log_warn "脚本退出，状态码：${exit_code}"
  fi
}

### 清理临时文件
### @param none
### @return 0 成功
### @require rm
### @side_effect 删除临时目录、临时 repo
cleanup() {
#   if [[ -f "$OE_PREREQ_REPO_FILE" ]]; then
#     rm -f -- "$OE_PREREQ_REPO_FILE"
#   fi
#   if [[ -f "$EL8_COMPAT_REPO_FILE" ]]; then
#     rm -f -- "$EL8_COMPAT_REPO_FILE"
#   fi
  if [[ -n "${TMP_DIR:-}" && -d "${TMP_DIR:-}" ]]; then
    rm -rf -- "$TMP_DIR"
  fi
  return 0
}

### 初始化 trap
### @param none
### @return 0 成功
### @require none
init_traps() {
  trap 'on_err' ERR
  trap 'on_signal INT' INT
  trap 'on_signal TERM' TERM
  trap 'on_exit' EXIT
}

# ==============================================================================
# Helpers
# ==============================================================================
### 输出帮助信息
### @param none
### @return 0 成功
### @require cat
usage() {
  cat <<EOF
用法：
  bash ${SCRIPT_NAME} [选项]

选项：
  -h, --help                    显示帮助
  --dry-run                     仅打印将执行的动作，不实际执行
  --force                       允许覆盖 docker repo / daemon.json
  --verbose                     输出 DEBUG 日志
  --quiet                       仅输出 ERROR 日志
  --log-level <LEVEL>           DEBUG|INFO|WARN|ERROR
  --mirror <PROVIDER>           aliyun|tencent|huawei|official（默认：aliyun）
  --proxy <URL>                 代理地址，例如：http://192.168.1.10:3128
  --docker-version <VERSION>    Docker 版本前缀（默认：20.10）
  --gpg-mode <MODE>             strict|mirror|skip（默认：strict）
  --skip-start                  安装后不启动 Docker
  --skip-enable                 安装后不设置开机自启
  --skip-daemon-config          不写入 /etc/docker/daemon.json
  --keep-conflicts              不移除旧 Docker 冲突包
  --yes                         非交互确认

说明：
  1) 支持：CentOS 7 / CentOS 8 / openEuler 20.03 / openEuler 22.03
  2) openEuler 会优先启用临时 Everything 仓补齐依赖，再回退 CentOS 8 Vault 仓
  3) 包管理工具固定使用 yum
  4) --gpg-mode skip 可跳过 repo 包签名校验（适合 GPG key 获取困难场景）

示例：
  sudo bash ${SCRIPT_NAME} --mirror aliyun
  sudo bash ${SCRIPT_NAME} --mirror tencent --proxy http://192.168.1.10:3128
  sudo bash ${SCRIPT_NAME} --gpg-mode skip --docker-version 24.0
  sudo bash ${SCRIPT_NAME} --dry-run --mirror huawei
EOF
}

### 安全执行外部命令（支持 dry-run，并输出真实失败命令）
### @param ... command 命令及参数
### @return 0 成功；非 0 失败
### @require none
run_cmd() {
  local -a cmd=( "$@" )
  local rc=0

  [[ "${#cmd[@]}" -gt 0 ]] || die "run_cmd 收到空命令。" "$EC_GENERAL"
  log_debug "执行命令：${cmd[*]}"

  if [[ "$DRY_RUN" -eq 1 ]]; then
    printf '[DRY-RUN] %s\n' "${cmd[*]}" >&2
    return 0
  fi

  # > 在函数内接管错误输出，避免 ERR trap 只能拿到 "${cmd[@]}" 这种包装表达式
  set +e
  "${cmd[@]}"
  rc=$?
  set -e

  if [[ "$rc" -ne 0 ]]; then
    log_error "外部命令执行失败：${cmd[*]}"
    log_error "命令退出码：${rc}"
    return "$rc"
  fi

  return 0
}

### 以 root 权限执行命令
### @param ... command 命令及参数
### @return 0 成功；非 0 失败
### @require sudo（非 root 时）
run_as_root() {
  local -a cmd=( "$@" )
  if [[ "$(id -u)" -eq 0 ]]; then
    run_cmd "${cmd[@]}"
  else
    command -v sudo >/dev/null 2>&1 || die "当前非 root，且缺少 sudo。" "$EC_PERMISSION"
    run_cmd sudo "${cmd[@]}"
  fi
}

### 校验命令是否存在
### @param cmd string 命令名
### @return 0 存在；非 0 不存在
### @require none
require_cmd() {
  local cmd="${1:?command required}"
  command -v "$cmd" >/dev/null 2>&1
}

### 安全写文件（原子替换）
### @param target string 目标路径
### @param content string 内容
### @return 0 成功；非 0 失败
### @require mktemp install dirname
### @side_effect 写入文件
write_file_atomic() {
  local target="${1:?target required}"
  local content="${2:-}"
  local tmp_file=""

  tmp_file="$(mktemp "${TMP_DIR}/write.XXXXXX")"
  printf '%s' "$content" > "$tmp_file"

  run_as_root mkdir -p "$(dirname "$target")"
  run_as_root install -m 0644 "$tmp_file" "$target"
  rm -f -- "$tmp_file"
}

### 读取确认输入
### @param prompt string 提示信息
### @return 0 同意；1 拒绝
### @require read
confirm_action() {
  local prompt="${1:-确认继续？}"
  local answer=""

  if [[ "$CONFIRM" -eq 1 ]]; then
    return 0
  fi

  printf '%s [y/N]: ' "$prompt" >&2
  read -r answer
  case "$answer" in
    y|Y|yes|YES) return 0 ;;
    *) return 1 ;;
  esac
}

# ==============================================================================
# Arg Parsing
# ==============================================================================
### 解析命令行参数
### @param ... args 参数列表
### @return 0 成功；非 0 失败
### @require none
parse_args() {
  while [[ "$#" -gt 0 ]]; do
    case "$1" in
      -h|--help)
        usage
        exit 0
        ;;
      --dry-run)
        DRY_RUN=1
        ;;
      --force)
        FORCE=1
        ;;
      --verbose)
        LOG_LEVEL="DEBUG"
        ;;
      --quiet)
        QUIET=1
        LOG_LEVEL="ERROR"
        ;;
      --log-level)
        shift
        [[ "$#" -gt 0 ]] || die "--log-level 缺少参数。" "$EC_USAGE"
        LOG_LEVEL="$1"
        ;;
      --mirror)
        shift
        [[ "$#" -gt 0 ]] || die "--mirror 缺少参数。" "$EC_USAGE"
        MIRROR_PROVIDER="$1"
        ;;
      --proxy)
        shift
        [[ "$#" -gt 0 ]] || die "--proxy 缺少参数。" "$EC_USAGE"
        PROXY_URL="$1"
        ;;
      --docker-version)
        shift
        [[ "$#" -gt 0 ]] || die "--docker-version 缺少参数。" "$EC_USAGE"
        DOCKER_VERSION="$1"
        ;;
      --gpg-mode)
        shift
        [[ "$#" -gt 0 ]] || die "--gpg-mode 缺少参数。" "$EC_USAGE"
        GPG_MODE="$1"
        ;;
      --skip-start)
        SKIP_START=1
        ;;
      --skip-enable)
        SKIP_ENABLE=1
        ;;
      --skip-daemon-config)
        CONFIGURE_DAEMON=0
        ;;
      --keep-conflicts)
        REMOVE_CONFLICTS=0
        ;;
      --yes)
        CONFIRM=1
        ;;
      -*)
        die "未知参数：$1" "$EC_USAGE"
        ;;
      *)
        die "不支持的位置参数：$1" "$EC_USAGE"
        ;;
    esac
    shift
  done
}

### 校验参数合法性
### @param none
### @return 0 成功；非 0 失败
### @require none
validate_args() {
  case "$LOG_LEVEL" in
    DEBUG|INFO|WARN|ERROR) ;;
    *) die "无效的 --log-level：${LOG_LEVEL}" "$EC_USAGE" ;;
  esac

  case "$MIRROR_PROVIDER" in
    aliyun|tencent|huawei|official) ;;
    *) die "无效的 --mirror：${MIRROR_PROVIDER}" "$EC_USAGE" ;;
  esac

  case "$GPG_MODE" in
    strict|mirror|skip) ;;
    *) die "无效的 --gpg-mode：${GPG_MODE}" "$EC_USAGE" ;;
  esac

  if [[ -n "$PROXY_URL" && ! "$PROXY_URL" =~ ^https?://[^[:space:]]+$ ]]; then
    die "无效的 --proxy：${PROXY_URL}" "$EC_USAGE"
  fi
}

# ==============================================================================
# OS / Environment
# ==============================================================================
### 检测操作系统
### @param none
### @return 0 成功；非 0 失败
### @require /etc/os-release
detect_os() {
  [[ -r "/etc/os-release" ]] || die "缺少 /etc/os-release。" "$EC_UNSUPPORTED_OS"

  # shellcheck disable=SC1091
  . /etc/os-release

  OS_ID_RAW="${ID:-}"
  OS_ID="$(printf '%s' "$OS_ID_RAW" | tr '[:upper:]' '[:lower:]')"
  OS_VERSION_ID="${VERSION_ID:-}"
  OS_PRETTY_NAME="${PRETTY_NAME:-${NAME:-unknown}}"
  OS_ARCH="$(uname -m)"

  log_info "系统识别：ID=${OS_ID_RAW}（规范化=${OS_ID}）, VERSION_ID=${OS_VERSION_ID}, ARCH=${OS_ARCH}"

  case "$OS_ID" in
    centos)
      case "${OS_VERSION_ID%%.*}" in
        7) OS_EL_VER="7" ;;
        8) OS_EL_VER="8" ;;
        *) die "不支持的 CentOS 版本：${OS_VERSION_ID}" "$EC_UNSUPPORTED_OS" ;;
      esac
      ;;
    openeuler)
      # > 修复 openEuler 大小写问题：ID="openEuler" 会先转小写再判断
      case "${OS_VERSION_ID%%.*}" in
        20|22) OS_EL_VER="8" ;;
        *) die "不支持的 openEuler 版本：${OS_VERSION_ID}" "$EC_UNSUPPORTED_OS" ;;
      esac
      log_info "openEuler 已映射到 Docker el8 仓路径。"
      ;;
    *)
      die "不支持的系统：${OS_PRETTY_NAME}" "$EC_UNSUPPORTED_OS"
      ;;
  esac
}

### 校验架构
### @param none
### @return 0 成功；非 0 失败
### @require none
validate_arch() {
  case "$OS_ARCH" in
    x86_64|aarch64) ;;
    *) die "不支持的架构：${OS_ARCH}" "$EC_UNSUPPORTED_OS" ;;
  esac
}

### 初始化运行时
### @param none
### @return 0 成功
### @require mktemp
init_runtime() {
  init_traps
  TMP_DIR="$(mktemp -d "${TMP_ROOT_PREFIX}.XXXXXX")"
  detect_os
  validate_arch
}

# ==============================================================================
# Requirements / Proxy
# ==============================================================================
### 检查依赖
### @param none
### @return 0 成功；非 0 失败
### @require 基础命令
check_requirements() {
  local required_cmds=( curl sed awk grep tee uname rpm systemctl yum )
  local cmd=""

  for cmd in "${required_cmds[@]}"; do
    require_cmd "$cmd" || die "缺少依赖命令：${cmd}" "$EC_REQUIREMENT"
  done

  if [[ "$(id -u)" -ne 0 ]] && ! require_cmd sudo; then
    die "当前非 root，且缺少 sudo。" "$EC_PERMISSION"
  fi
}

### 准备代理环境
### @param none
### @return 0 成功
### @require none
prepare_proxy_env() {
  PKG_GLOBAL_OPTS=()
  PKG_MGR_STABLE_OPTS=(
    "--setopt=timeout=60"
    "--setopt=retries=10"
    "--setopt=minrate=1k"
  )

  if [[ -n "$PROXY_URL" ]]; then
    export http_proxy="$PROXY_URL"
    export https_proxy="$PROXY_URL"
    export HTTP_PROXY="$PROXY_URL"
    export HTTPS_PROXY="$PROXY_URL"
    export no_proxy="${no_proxy:-localhost,127.0.0.1}"
    export NO_PROXY="${NO_PROXY:-localhost,127.0.0.1}"

    PKG_GLOBAL_OPTS+=( "--setopt=proxy=${PROXY_URL}" )
    log_info "已启用代理：${PROXY_URL}"
  else
    log_info "未启用代理。"
  fi
}

### 获取镜像基地址
### @param provider string 镜像提供商
### @return 0 成功输出 URL；非 0 失败
### @require none
get_mirror_base_url() {
  local provider="${1:?provider required}"
  case "$provider" in
    aliyun)   printf '%s\n' "$MIRROR_URL_ALIYUN" ;;
    tencent)  printf '%s\n' "$MIRROR_URL_TENCENT" ;;
    huawei)   printf '%s\n' "$MIRROR_URL_HUAWEI" ;;
    official) printf '%s\n' "$MIRROR_URL_OFFICIAL" ;;
    *) return 1 ;;
  esac
}

### 获取 gpgkey URL
### @param provider string 镜像提供商
### @return 0 成功输出 URL；非 0 失败
### @require none
get_gpg_key_url() {
  local provider="${1:?provider required}"

  case "$provider" in
    aliyun)   printf '%s\n' "${MIRROR_URL_ALIYUN}/gpg" ;;
    tencent)  printf '%s\n' "${MIRROR_URL_TENCENT}/gpg" ;;
    huawei)   printf '%s\n' "${MIRROR_URL_HUAWEI}/gpg" ;;
    official) printf '%s\n' "${OFFICIAL_DOCKER_GPG_KEY_URL}" ;;
    *) return 1 ;;
  esac
}

# ==============================================================================
# YUM Wrappers
# ==============================================================================
### 执行 yum 命令
### @param ... args yum 参数
### @return 0 成功；非 0 失败
### @require yum
pkg_mgr() {
  local -a cmd=( "$PKG_MGR" "${PKG_GLOBAL_OPTS[@]}" "$@" )
  run_as_root "${cmd[@]}"
}

### 安装软件包
### @param ... pkgs 包名列表
### @return 0 成功；非 0 失败
### @require yum
install_pkgs() {
  local -a packages=( "$@" )
  [[ "${#packages[@]}" -gt 0 ]] || die "install_pkgs 收到空包列表。" "$EC_GENERAL"

  pkg_mgr install -y "${PKG_MGR_STABLE_OPTS[@]}" "${packages[@]}"
}

### 卸载软件包
### @param ... pkgs 包名列表
### @return 0 成功；非 0 失败
### @require yum
remove_pkgs() {
  local -a packages=( "$@" )
  [[ "${#packages[@]}" -gt 0 ]] || die "remove_pkgs 收到空包列表。" "$EC_GENERAL"

  pkg_mgr remove -y "${packages[@]}"
}

### 刷新缓存（Docker 镜像失败时自动回退官方源）
### @param none
### @return 0 成功；非 0 失败
### @require yum
make_cache() {
  log_info "刷新系统软件仓缓存..."
  if ! pkg_mgr makecache -y "${PKG_MGR_STABLE_OPTS[@]}"; then
    log_warn "全量 makecache 失败，尝试仅刷新 Docker 仓..."
  fi

  log_info "刷新 docker-ce-stable 元数据..."
  if pkg_mgr makecache -y "${PKG_MGR_STABLE_OPTS[@]}" --disablerepo='*' --enablerepo='docker-ce-stable'; then
    log_info "docker-ce-stable 元数据刷新成功。"
    return 0
  fi

  log_warn "当前 Docker 镜像源不可用：${MIRROR_PROVIDER} (${MIRROR_BASE_URL})"

  # > 国内镜像失败时，自动回退到官方源，避免直接中断
  if [[ "$MIRROR_PROVIDER" != "official" ]]; then
    log_warn "开始自动回退到 Docker 官方源..."
    MIRROR_PROVIDER="official"

    configure_docker_repo
    handle_gpg_key

    if pkg_mgr makecache -y "${PKG_MGR_STABLE_OPTS[@]}" --disablerepo='*' --enablerepo='docker-ce-stable'; then
      log_info "已成功回退到 Docker 官方源。"
      return 0
    fi
  fi

  die "docker-ce-stable 元数据刷新失败：国内镜像与官方源均不可用，请检查网络/代理/仓库地址。" "$EC_REPO"
}

# ==============================================================================
# Repo / GPG
# ==============================================================================
### 移除旧冲突包
### @param none
### @return 0 成功；非 0 失败
### @require yum
cleanup_conflicting_packages() {
  local -a conflicts=(
    docker
    docker-common
    docker-client
    docker-client-latest
    docker-latest
    docker-latest-logrotate
    docker-logrotate
    docker-engine
    docker-ce
    docker-ce-cli
    containerd.io
    docker-buildx-plugin
    docker-compose-plugin
  )

  if [[ "$REMOVE_CONFLICTS" -ne 1 ]]; then
    log_info "已按参数要求跳过冲突包清理。"
    return 0
  fi

  remove_pkgs "${conflicts[@]}" || log_warn "冲突包清理未完全成功，继续尝试。"
}

### 配置 Docker repo
### @param none
### @return 0 成功；非 0 失败
### @require none
### @side_effect 写入 /etc/yum.repos.d/docker-ce.repo
configure_docker_repo() {
  local gpgcheck_value="1"
  local repo_content=""

  MIRROR_BASE_URL="$(get_mirror_base_url "$MIRROR_PROVIDER")" \
    || die "获取镜像 URL 失败。" "$EC_REPO"

  if [[ "$GPG_MODE" == "skip" ]]; then
    gpgcheck_value="0"
    DOCKER_GPG_KEY_URL=""
  else
    if [[ "$GPG_MODE" == "mirror" ]]; then
      DOCKER_GPG_KEY_URL="$(get_gpg_key_url "$MIRROR_PROVIDER")" || die "获取镜像 GPG Key URL 失败。" "$EC_REPO"
    else
      # strict 默认也优先用镜像站 key，失败时导入阶段再回退官方
      DOCKER_GPG_KEY_URL="$(get_gpg_key_url "$MIRROR_PROVIDER")" || die "获取 GPG Key URL 失败。" "$EC_REPO"
    fi
  fi

  if [[ -f "$DOCKER_REPO_FILE" && "$FORCE" -ne 1 ]]; then
    log_warn "检测到已有 ${DOCKER_REPO_FILE}，未使用 --force，跳过覆盖。"
    return 0
  fi

  if [[ -f "$DOCKER_REPO_FILE" && "$FORCE" -eq 1 ]]; then
    confirm_action "检测到已有 ${DOCKER_REPO_FILE}，是否覆盖？" || die "用户取消覆盖 repo 文件。" "$EC_USAGE"
  fi

  repo_content="[docker-ce-stable]
name=Docker CE Stable - \$basearch
baseurl=${MIRROR_BASE_URL}/${OS_EL_VER}/\$basearch/stable
enabled=1
gpgcheck=${gpgcheck_value}
"

  if [[ "$gpgcheck_value" == "1" ]]; then
    repo_content="${repo_content}gpgkey=${DOCKER_GPG_KEY_URL}
"
  fi

  repo_content="${repo_content}retries=10
timeout=60
minrate=1k
skip_if_unavailable=0

[docker-ce-test]
name=Docker CE Test - \$basearch
baseurl=${MIRROR_BASE_URL}/${OS_EL_VER}/\$basearch/test
enabled=0
gpgcheck=${gpgcheck_value}
"

  if [[ "$gpgcheck_value" == "1" ]]; then
    repo_content="${repo_content}gpgkey=${DOCKER_GPG_KEY_URL}
"
  fi

  write_file_atomic "$DOCKER_REPO_FILE" "$repo_content"
  log_info "Docker repo 已写入：${DOCKER_REPO_FILE}"
}

### 处理 GPG key 导入
### @param none
### @return 0 成功；非 0 失败
### @require curl rpm
handle_gpg_key() {
  local gpg_tmp=""
  local try_url=""

  if [[ "$GPG_MODE" == "skip" ]]; then
    log_warn "已启用 --gpg-mode skip：跳过 GPG key 导入与 repo 包签名校验。"
    return 0
  fi

  if [[ "$DRY_RUN" -eq 1 ]]; then
    log_info "DRY-RUN：跳过实际导入 GPG key。"
    return 0
  fi

  gpg_tmp="$(mktemp "${TMP_DIR}/docker-gpg.XXXXXX")"

  # > 优先使用镜像站 gpgkey；strict 模式下若失败，再回退官方 key
  if curl -fsSL --connect-timeout 15 --retry 3 "$DOCKER_GPG_KEY_URL" -o "$gpg_tmp"; then
    try_url="$DOCKER_GPG_KEY_URL"
  elif [[ "$GPG_MODE" == "strict" ]] && curl -fsSL --connect-timeout 15 --retry 3 "$OFFICIAL_DOCKER_GPG_KEY_URL" -o "$gpg_tmp"; then
    try_url="$OFFICIAL_DOCKER_GPG_KEY_URL"
    log_warn "镜像站 GPG key 获取失败，已回退官方 key。"
  else
    rm -f -- "$gpg_tmp"
    die "GPG key 下载失败；可改用 --gpg-mode skip 跳过校验。" "$EC_REPO"
  fi

  rpm --import "$gpg_tmp" || {
    rm -f -- "$gpg_tmp"
    die "导入 GPG key 失败（来源：${try_url}）。可使用 --gpg-mode skip。" "$EC_REPO"
  }

  rm -f -- "$gpg_tmp"
  log_info "GPG key 导入成功：${try_url}"
}

# ==============================================================================
# Prerequisites
# ==============================================================================
### 添加 openEuler 临时仓（优先 OS，其次 Everything）
### @param none
### @return 0 成功；非 0 失败
### @require none
### @side_effect 写入临时 repo 文件
add_openeuler_everything_repo() {
  local os_baseurl=""
  local everything_baseurl=""

  [[ "$OS_ID" == "openeuler" ]] || return 1
  [[ ! -f "$OE_PREREQ_REPO_FILE" ]] || return 0

  case "${OS_VERSION_ID%%.*}" in
    20)
      os_baseurl="${OE_REPO_BASE}/openEuler-20.03-LTS-SP3/OS/\$basearch/"
      everything_baseurl="${OE_REPO_BASE}/openEuler-20.03-LTS-SP3/everything/\$basearch/"
      ;;
    22)
      os_baseurl="${OE_REPO_BASE}/openEuler-22.03-LTS-SP3/OS/\$basearch/"
      everything_baseurl="${OE_REPO_BASE}/openEuler-22.03-LTS-SP3/everything/\$basearch/"
      ;;
    *)
      return 1
      ;;
  esac

  write_file_atomic "$OE_PREREQ_REPO_FILE" "[openeuler-os-tmp]
name=openEuler OS (temporary)
baseurl=${os_baseurl}
enabled=0
gpgcheck=0
skip_if_unavailable=1

[openeuler-everything-tmp]
name=openEuler Everything (temporary)
baseurl=${everything_baseurl}
enabled=0
gpgcheck=0
skip_if_unavailable=1
"
  log_info "已添加 openEuler 临时仓：${OE_PREREQ_REPO_FILE}（优先 OS，其次 Everything）"
}

### 添加 EL8 兼容仓（仅用于 openEuler 依赖兜底，优先国内镜像，默认禁用）
### @param none
### @return 0 成功；非 0 失败
### @require none
### @side_effect 写入临时 repo 文件
add_el8_compat_repo() {
  local base_root=""

  # > 该兼容仓是给 openEuler 补 Docker 依赖的兜底方案，不用于普通 CentOS 主流程
  [[ "$OS_ID" == "openeuler" ]] || return 1
  [[ "$OS_EL_VER" == "8" ]] || return 1
  [[ ! -f "$EL8_COMPAT_REPO_FILE" ]] || return 0

  case "$MIRROR_PROVIDER" in
    aliyun)
      base_root="https://mirrors.aliyun.com/centos-vault/centos/8.5.2111"
      ;;
    tencent)
      base_root="https://mirrors.tencent.com/centos-vault/8.5.2111"
      ;;
    huawei)
      base_root="https://repo.huaweicloud.com/centos-vault/8.5.2111"
      ;;
    official|*)
      base_root="https://vault.centos.org/8.5.2111"
      ;;
  esac

  write_file_atomic "$EL8_COMPAT_REPO_FILE" "[el8-compat-baseos-tmp]
name=EL8 Compatibility BaseOS (temporary for openEuler)
baseurl=${base_root}/BaseOS/\$basearch/os/
enabled=0
gpgcheck=0
skip_if_unavailable=1

[el8-compat-appstream-tmp]
name=EL8 Compatibility AppStream (temporary for openEuler)
baseurl=${base_root}/AppStream/\$basearch/os/
enabled=0
gpgcheck=0
skip_if_unavailable=1

[el8-compat-powertools-tmp]
name=EL8 Compatibility PowerTools (temporary for openEuler)
baseurl=${base_root}/PowerTools/\$basearch/os/
enabled=0
gpgcheck=0
skip_if_unavailable=1

[el8-compat-extras-tmp]
name=EL8 Compatibility Extras (temporary for openEuler)
baseurl=${base_root}/extras/\$basearch/os/
enabled=0
gpgcheck=0
skip_if_unavailable=1
"
  log_info "已添加 openEuler 专用 EL8 兼容临时仓：${EL8_COMPAT_REPO_FILE}（${base_root}）"
}

### 尝试安装单个依赖包（container-selinux 安装后再校验版本约束）
### @param pkg string 包名
### @param ... extra_flags string 额外 yum 参数
### @return 0 成功；1 失败
### @require yum rpm
try_install_pkg() {
  local pkg="${1:?pkg required}"
  shift
  local -a extra_flags=( "$@" )

  if ! pkg_mgr install -y "${PKG_MGR_STABLE_OPTS[@]}" "${extra_flags[@]}" "$pkg" >/dev/null 2>&1; then
    return 1
  fi

  if [[ "$pkg" == "container-selinux" ]]; then
    is_container_selinux_satisfied
    return $?
  fi

  rpm -q "$pkg" >/dev/null 2>&1
}

### 判断 container-selinux 是否满足 Docker 所需依赖（直接使用 RPM 依赖语义）
### @param none
### @return 0 满足；1 不满足
### @require rpm
is_container_selinux_satisfied() {
  # > 不再手写 epoch/version 比较，直接让 rpm 按依赖表达式判断
  if rpm -q --whatprovides 'container-selinux >= 2:2.74' >/dev/null 2>&1; then
    return 0
  fi
  return 1
}

### 安装前置依赖（openEuler 优先 OS 仓安装 container-selinux）
### @param none
### @return 0 成功；非 0 失败
### @require yum rpm
install_prerequisites() {
  local -a missing=()
  local -a still_missing=()
  local -a unresolved=()
  local pkg=""

  log_info "检查前置依赖：${PREREQ_PKGS[*]}"

  for pkg in "${PREREQ_PKGS[@]}"; do
    pkg="$(printf '%s' "$pkg" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"

    if [[ "$pkg" == "container-selinux" ]]; then
      if is_container_selinux_satisfied; then
        log_info "依赖已满足：container-selinux >= 2:2.74"
      else
        missing+=( "container-selinux" )
        log_warn "container-selinux 未安装或版本不足（需要 >= 2:2.74）"
      fi
      continue
    fi

    if rpm -q "$pkg" >/dev/null 2>&1; then
      log_info "依赖已存在：${pkg}"
    else
      missing+=( "$pkg" )
      log_warn "缺少依赖：${pkg}"
    fi
  done

  if [[ "${#missing[@]}" -eq 0 ]]; then
    log_info "前置依赖已满足。"
    return 0
  fi

  # 1) 当前系统仓
  for pkg in "${missing[@]}"; do
    if try_install_pkg "$pkg"; then
      log_info "${pkg}：已从当前系统仓安装/升级。"
    else
      still_missing+=( "$pkg" )
    fi
  done

  if [[ "${#still_missing[@]}" -eq 0 ]]; then
    return 0
  fi

  # 2) openEuler：优先临时仓
  if [[ "$OS_ID" == "openeuler" ]]; then
    add_openeuler_everything_repo || log_warn "添加 openEuler 临时仓失败。"
    unresolved=()

    for pkg in "${still_missing[@]}"; do
      if [[ "$pkg" == "container-selinux" ]]; then
        # > container-selinux 优先从 OS 仓拿，OS 仓已明确存在满足版本的包
        if try_install_pkg "$pkg" "--enablerepo=openeuler-os-tmp"; then
          log_info "container-selinux：已从 openEuler OS 仓安装/升级。"
        elif try_install_pkg "$pkg" "--enablerepo=openeuler-everything-tmp"; then
          log_info "container-selinux：已从 openEuler Everything 仓安装/升级。"
        else
          unresolved+=( "container-selinux" )
        fi
      else
        if try_install_pkg "$pkg" "--enablerepo=openeuler-everything-tmp"; then
          log_info "${pkg}：已从 openEuler Everything 仓安装。"
        else
          unresolved+=( "$pkg" )
        fi
      fi
    done

    still_missing=( "${unresolved[@]}" )
  fi

  if [[ "${#still_missing[@]}" -eq 0 ]]; then
    return 0
  fi

  # 3) openEuler：再用 EL8 兼容仓兜底
  if [[ "$OS_ID" == "openeuler" && "$OS_EL_VER" == "8" ]]; then
    add_el8_compat_repo || log_warn "添加 EL8 兼容仓失败。"
    unresolved=()

    for pkg in "${still_missing[@]}"; do
      if try_install_pkg "$pkg" \
        "--enablerepo=el8-compat-baseos-tmp" \
        "--enablerepo=el8-compat-appstream-tmp" \
        "--enablerepo=el8-compat-powertools-tmp" \
        "--enablerepo=el8-compat-extras-tmp"; then
        log_info "${pkg}：已从 EL8 兼容仓安装。"
      else
        unresolved+=( "$pkg" )
      fi
    done

    still_missing=( "${unresolved[@]}" )
  fi

  if [[ "${#still_missing[@]}" -gt 0 ]]; then
    die "前置依赖仍缺失或版本不足：${still_missing[*]}（尤其 container-selinux 需满足 'container-selinux >= 2:2.74'）" "$EC_INSTALL"
  fi
}

# ==============================================================================
# Docker Version
# ==============================================================================
### 解析 Docker 可用版本
### @param none
### @return 0 成功
### @require yum awk sed grep sort tail
resolve_docker_version() {
  local available=""

  log_info "刷新 docker-ce-stable 元数据..."
  pkg_mgr makecache -y "${PKG_MGR_STABLE_OPTS[@]}" --disablerepo='*' --enablerepo='docker-ce-stable' || log_warn "仅 docker-ce-stable 刷新失败，继续。"

  available="$(
    yum "${PKG_GLOBAL_OPTS[@]}" list available docker-ce --showduplicates 2>/dev/null \
      | awk '/^docker-ce\./ {print $2}' \
      | sed 's/^[0-9]*://' \
      | grep "^${DOCKER_VERSION}" \
      | sort -t'.' -k1,1n -k2,2n -k3,3n \
      | tail -1
  )" || true

  if [[ -z "$available" ]]; then
    log_warn "未解析到精确版本，将使用通配安装：${DOCKER_VERSION}*"
    DOCKER_VERSION=""
  else
    DOCKER_VERSION="$available"
    log_info "已解析 Docker 版本：${DOCKER_VERSION}"
  fi
}

### 将 Docker repo 的 gpgcheck / repo_gpgcheck 临时关闭
### @param none
### @return 0 成功；非 0 失败
### @require sed grep
### @side_effect 修改 /etc/yum.repos.d/docker-ce.repo
disable_docker_repo_gpgcheck() {
  [[ -f "$DOCKER_REPO_FILE" ]] || die "Docker repo 文件不存在：${DOCKER_REPO_FILE}" "$EC_REPO"

  if [[ "$DRY_RUN" -eq 1 ]]; then
    printf '[DRY-RUN] disable gpg checks in %s\n' "$DOCKER_REPO_FILE" >&2
    return 0
  fi

  run_as_root sed -i \
    -e 's/^gpgcheck=.*/gpgcheck=0/g' \
    -e 's/^repo_gpgcheck=.*/repo_gpgcheck=0/g' \
    "$DOCKER_REPO_FILE"

  if ! grep -q '^repo_gpgcheck=' "$DOCKER_REPO_FILE"; then
    run_as_root bash -c "printf '\nrepo_gpgcheck=0\n' >> '$DOCKER_REPO_FILE'"
  fi

  log_warn "已关闭 Docker repo 的 gpgcheck / repo_gpgcheck：${DOCKER_REPO_FILE}"

  make_cache
}

### 执行 yum 安装并记录日志，避免管道触发 ERR trap
### @param log_file string 日志文件路径
### @param ... args string yum install 后续参数
### @return 0 成功；非 0 失败（返回 yum 的退出码）
### @require yum tee
### @side_effect 写入日志文件
run_yum_install_with_log() {
  local log_file="${1:?log_file required}"
  shift
  local rc=0

  # > 用 if 包裹失败管道，避免 set -E/ERR trap 把失败点记录成 tee
  if yum "${PKG_GLOBAL_OPTS[@]}" install -y \
    "${PKG_MGR_STABLE_OPTS[@]}" \
    "${INSTALL_FLAGS[@]}" \
    "$@" 2>&1 | tee "$log_file"; then
    return 0
  else
    rc=${PIPESTATUS[0]:-1}
    return "$rc"
  fi
}

### 安装 Docker 软件包（处理未签名包 / GPG 校验失败，并避免 tee 管道触发 ERR trap）
### @param none
### @return 0 成功；非 0 失败
### @require yum grep mktemp
install_docker_packages() {
  local pkg_ce=""
  local pkg_cli=""
  local install_log=""
  local rc=0

  if [[ -n "$DOCKER_VERSION" && "$DOCKER_VERSION" =~ - ]]; then
    pkg_ce="docker-ce-${DOCKER_VERSION}"
    pkg_cli="docker-ce-cli-${DOCKER_VERSION}"
  elif [[ -n "$DOCKER_VERSION" ]]; then
    pkg_ce="docker-ce-${DOCKER_VERSION}*"
    pkg_cli="docker-ce-cli-${DOCKER_VERSION}*"
  else
    pkg_ce="docker-ce"
    pkg_cli="docker-ce-cli"
  fi

  log_info "开始安装 Docker：${pkg_ce} ${pkg_cli}"

  if [[ "$DRY_RUN" -eq 1 ]]; then
    pkg_mgr install -y \
      "${PKG_MGR_STABLE_OPTS[@]}" \
      "${INSTALL_FLAGS[@]}" \
      "$pkg_ce" \
      "$pkg_cli" \
      containerd.io \
      docker-compose-plugin
    return 0
  fi

  install_log="$(mktemp "${TMP_DIR}/yum-install.XXXXXX")"

  if run_yum_install_with_log \
    "$install_log" \
    "$pkg_ce" \
    "$pkg_cli" \
    containerd.io \
    docker-compose-plugin; then
    rm -f -- "$install_log"
    log_info "Docker 软件包安装成功。"
    return 0
  fi

  rc=$?

  # > 两类都要处理：
  # > 1) 包本身未签名：is not signed
  # > 2) 包有签名但校验失败：GPG check FAILED
  if grep -Eiq 'is not signed|Package .* is not signed|GPG check FAILED|Public key for .* is not installed' "$install_log"; then
    log_warn "检测到 Docker 仓包签名校验失败，准备临时关闭 Docker 仓 gpgcheck 后重试。"

    disable_docker_repo_gpgcheck

    if run_yum_install_with_log \
      "$install_log" \
      "$pkg_ce" \
      "$pkg_cli" \
      containerd.io \
      docker-compose-plugin; then
      rm -f -- "$install_log"
      log_warn "已通过关闭 Docker 仓 gpgcheck 成功完成安装。"
      return 0
    fi

    rc=$?
    rm -f -- "$install_log"
    die "关闭 gpgcheck 后重试仍失败，退出码：${rc}" "$EC_INSTALL"
  fi

  rm -f -- "$install_log"
  die "Docker 安装失败，且并非签名校验类错误，退出码：${rc}" "$EC_INSTALL"
}


# ==============================================================================
# Docker Config / Service
# ==============================================================================
### 配置 Docker daemon.json
### @param none
### @return 0 成功；非 0 失败
### @require none
### @side_effect 写入 /etc/docker/daemon.json
configure_docker_daemon() {
  local content=""

  if [[ "$CONFIGURE_DAEMON" -ne 1 ]]; then
    log_info "已按参数要求跳过 daemon.json 配置。"
    return 0
  fi

  content='{
  "registry-mirrors": [
     "https://docker.xuanyuan.me",
     "https://docker.1ms.run",
     "https://docker.m.daocloud.io"
  ],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "300m",
    "max-file": "3"
  },
  "storage-driver": "overlay2",
  "live-restore": true
}
'

  if [[ -f "$DOCKER_DAEMON_CFG" && "$FORCE" -ne 1 ]]; then
    log_warn "检测到已有 ${DOCKER_DAEMON_CFG}，未使用 --force，跳过覆盖。"
    return 0
  fi

  if [[ -f "$DOCKER_DAEMON_CFG" && "$FORCE" -eq 1 ]]; then
    confirm_action "检测到已有 ${DOCKER_DAEMON_CFG}，是否覆盖？" || die "用户取消覆盖 daemon.json。" "$EC_USAGE"
  fi

  write_file_atomic "$DOCKER_DAEMON_CFG" "$content"
  log_info "已写入 Docker daemon 配置：${DOCKER_DAEMON_CFG}"
}

### 启用并启动 Docker
### @param none
### @return 0 成功；非 0 失败
### @require systemctl
enable_and_start_docker() {
  run_as_root systemctl daemon-reload

  if [[ "$SKIP_ENABLE" -ne 1 ]]; then
    run_as_root systemctl enable docker || die "设置 Docker 开机自启失败。" "$EC_SERVICE"
  else
    log_info "已跳过开机自启。"
  fi

  if [[ "$SKIP_START" -ne 1 ]]; then
    run_as_root systemctl start docker || die "启动 Docker 失败。" "$EC_SERVICE"
  else
    log_info "已跳过启动 Docker。"
  fi
}

### 验证安装结果
### @param none
### @return 0 成功；非 0 失败
### @require docker
verify_installation() {
  if [[ "$SKIP_START" -eq 1 ]]; then
    log_warn "已跳过启动，因此不做运行态校验。"
    return 0
  fi

  require_cmd docker || die "未检测到 docker 命令。" "$EC_VERIFY"
  run_cmd docker --version || die "docker --version 校验失败。" "$EC_VERIFY"
  run_cmd docker info >/dev/null 2>&1 || die "docker info 校验失败。" "$EC_VERIFY"

  if run_cmd docker compose version >/dev/null 2>&1; then
    log_info "docker compose 插件可用。"
  else
    log_warn "docker compose 插件校验失败，但主安装已完成。"
  fi
}

### 打印摘要
### @param none
### @return 0 成功
### @require none
print_summary() {
  log_info "安装摘要："
  log_info "  系统              : ${OS_PRETTY_NAME}"
  log_info "  OS ID             : ${OS_ID_RAW} -> ${OS_ID}"
  log_info "  架构              : ${OS_ARCH}"
  log_info "  EL 映射版本       : el${OS_EL_VER}"
  log_info "  包管理器          : ${PKG_MGR}"
  log_info "  镜像提供方        : ${MIRROR_PROVIDER}"
  log_info "  Docker 仓地址     : ${MIRROR_BASE_URL}"
  log_info "  GPG 模式          : ${GPG_MODE}"
  log_info "  代理              : ${PROXY_URL:-<none>}"
  log_info "  container-selinux : $(rpm -q container-selinux 2>/dev/null || echo not-installed)"
  log_info "  fuse-overlayfs    : $(rpm -q fuse-overlayfs 2>/dev/null || echo not-installed)"
  log_info "  slirp4netns       : $(rpm -q slirp4netns 2>/dev/null || echo not-installed)"
}

# ==============================================================================
# Main
# ==============================================================================
### 主流程
### @param ... args 参数列表
### @return 0 成功；非 0 失败
### @require none
main() {
  parse_args "$@"
  validate_args
  init_runtime
  check_requirements
  prepare_proxy_env

  log_info "开始执行 Docker 在线安装脚本 v${SCRIPT_VERSION}"

  cleanup_conflicting_packages
  configure_docker_repo
  handle_gpg_key
  make_cache
  install_prerequisites
  resolve_docker_version
  install_docker_packages
  configure_docker_daemon
  enable_and_start_docker
  verify_installation
  print_summary

  exit "$EC_OK"
}

main "$@"

# ==============================================================================
# Minimal Self-Test
# ==============================================================================
# 1) 正常路径：
#    sudo bash docker-online-install.sh --mirror aliyun --yes
#
# 2) 参数错误：
#    bash docker-online-install.sh --gpg-mode invalid
#
# 3) dry-run：
#    sudo bash docker-online-install.sh --mirror huawei --proxy http://192.168.1.10:3128 --dry-run
# ==============================================================================