#!/usr/bin/env bash
# shellcheck shell=bash
# Bash >= 5.0
#
# ==============================================================================
# Name        : image_pipeline.sh
# Version     : 1.0.0
# Author      : OpenAI
# License     : MIT
# Updated     : 2026-03-03
#
# Assumptions
# - 输入镜像文件为纯文本：每行一个完整镜像名；支持空行与以 # 开头的注释行。
# - 默认动作为 download；默认目标架构为 arm64；默认压缩方式为 combined（统一打包）。
# - “重新打包并上传”默认保留原镜像路径：目标镜像为 HARBOR_HOST/<原镜像路径>；
#   若原镜像名不含 "/"（如 alpine:3.19），则上传为 HARBOR_HOST/library/alpine:3.19。
# - Harbor 地址格式为 IP:PORT（不含协议前缀）；用户名/密码/端口可被参数覆盖，脚本内提供默认值。
# - 压缩优先使用 pigz 进行多线程压缩；若系统无 pigz，则回退到 gzip（单线程）。
# - 为满足“可直接落地”，脚本实现为单文件，默认不执行破坏性删除；上传属于显式网络写操作。
#
# Bash 特性说明
# - 使用数组（Bash 特性）安全保存镜像列表与失败列表，避免字符串拼接引发的分词问题。
# - 使用 [[ ]]（Bash 特性）进行更稳健的条件判断。
# - 使用 local、BASH_SOURCE、FUNCNAME、BASH_LINENO（Bash 特性）增强错误可观测性。
#
# Call Graph
#   main
#    ├─ parse_args
#    ├─ init_runtime
#    ├─ validate_args
#    ├─ check_dependencies
#    ├─ load_image_list
#    ├─ case ACTION in
#    │    ├─ download
#    │    │    └─ perform_downloads
#    │    │         └─ download_one_image
#    │    │              ├─ compose_pull_source
#    │    │              ├─ pull_image
#    │    │              ├─ tag_image_if_needed
#    │    │              └─ inspect_and_verify_arch
#    │    ├─ compress
#    │    │    ├─ verify_images_present
#    │    │    └─ perform_compress
#    │    │         ├─ compress_combined
#    │    │         └─ compress_split_parallel
#    │    ├─ push
#    │    │    ├─ verify_images_present
#    │    │    ├─ harbor_login
#    │    │    └─ perform_pushes
#    │    │         ├─ build_target_image_ref
#    │    │         ├─ tag_image_if_needed
#    │    │         └─ push_image
#    │    └─ all
#    │         ├─ perform_downloads
#    │         ├─ verify_images_present
#    │         ├─ perform_compress
#    │         ├─ harbor_login
#    │         └─ perform_pushes
#    ├─ print_summary
#    └─ cleanup
# ==============================================================================

set -Eeuo pipefail
IFS=$' \t\n'

# ==============================================================================
# 全局常量区
# ==============================================================================
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
readonly SCRIPT_VERSION="1.0.0"

readonly EXIT_SUCCESS=0
readonly EXIT_USAGE=2
readonly EXIT_DEPENDENCY=3
readonly EXIT_RUNTIME=4
readonly EXIT_VALIDATION=5
readonly EXIT_INTERRUPT=130

readonly DEFAULT_ACTION="download"
readonly DEFAULT_ARCH="amd64"
readonly DEFAULT_BUNDLE_MODE="combined"
readonly DEFAULT_OUTPUT_DIR="./image-artifacts"
readonly DEFAULT_THREADS="0" # 0 = auto detect
readonly DEFAULT_RETRY="1"
readonly DEFAULT_RETRY_DELAY="2"
readonly DEFAULT_LOG_LEVEL="INFO"

readonly DEFAULT_HARBOR_HOST="192.168.5.41:8033"
readonly DEFAULT_HARBOR_USERNAME="admin"
readonly DEFAULT_HARBOR_PASSWORD="V2ryStr@ngPss"

readonly LOCK_FILE_NAME=".image_pipeline.lock"

# ==============================================================================
# 全局变量区（可配置）
# ==============================================================================
ACTION="${DEFAULT_ACTION}"
IMAGE_FILE=""
ARCH="${DEFAULT_ARCH}"
MIRROR_PREFIX=""
BUNDLE_MODE="${DEFAULT_BUNDLE_MODE}"
OUTPUT_DIR="${DEFAULT_OUTPUT_DIR}"
THREADS="${DEFAULT_THREADS}"
RETRY="${DEFAULT_RETRY}"
RETRY_DELAY="${DEFAULT_RETRY_DELAY}"

HARBOR_HOST="${DEFAULT_HARBOR_HOST}"
HARBOR_USERNAME="${DEFAULT_HARBOR_USERNAME}"
HARBOR_PASSWORD="${DEFAULT_HARBOR_PASSWORD}"

DRY_RUN="false"
FORCE="false"
LOG_LEVEL="${LOG_LEVEL:-${DEFAULT_LOG_LEVEL}}"

TMP_DIR=""
LOCK_FILE=""
COMPRESSOR_CMD="gzip"
COMPRESSOR_ARGS=()

# Bash 数组用于安全保存列表
IMAGE_LIST=()
DOWNLOAD_FAILED=()
DOWNLOAD_USED_LOCAL=()
VERIFY_MISSING=()
VERIFY_ARCH_MISMATCH=()
COMPRESS_FAILED=()
PUSH_FAILED=()
PUSH_SUCCEEDED=()

# ==============================================================================
# usage / help
# ==============================================================================
usage() {
  cat <<'EOF'
Usage:
  image_pipeline.sh --image-file <images.txt> [options]

Required:
  -f, --image-file <file>        镜像清单文件；每行一个完整镜像名

Action:
  -a, --action <name>            动作: download | compress | push | all
                                 默认: download

Download options:
      --arch <arch>              目标架构: arm64 | amd64
                                 默认: arm64
      --mirror-prefix <prefix>   下载镜像加速前缀；如:
                                 registry.local:5000
                                 拉取时会从 <prefix>/<原镜像名> 下载，成功后重新 tag 为原镜像名

Compress options:
      --bundle-mode <mode>       压缩模式: combined | split
                                 combined: 所有镜像统一打包为一个 tar.gz（默认）
                                 split   : 每个镜像单独打包为 tar.gz
      --output-dir <dir>         输出目录，默认: ./image-artifacts
      --threads <n>              压缩线程数；0 表示自动检测 CPU 核数（默认）
      --force                    强制跳过“镜像存在性/架构一致性”检查

Push options:
      --harbor <ip:port>         目标 Harbor 地址，默认: 192.168.5.41:8033
      --harbor-user <user>       Harbor 用户名，默认: admin
      --harbor-password <pass>   Harbor 密码，默认: 脚本内置默认值（可改为环境变量注入）

Common options:
      --retry <n>                外部命令失败重试次数（默认: 1，即不重试）
      --retry-delay <sec>        重试间隔秒数（默认: 2）
      --dry-run                  仅打印将执行的动作，不实际执行
      --verbose                  输出 DEBUG 日志
      --quiet                    仅输出 ERROR 日志
  -h, --help                     显示帮助

Examples:
  1) 正常下载 ARM64 镜像：
     ./image_pipeline.sh -f ./kubernetes-1.30.14-arm.txt --action download --arch arm64

  2) 通过加速前缀下载，并统一打包：
     ./image_pipeline.sh -f ./images.txt --action all \
       --mirror-prefix registry-cache.local:5000 \
       --bundle-mode combined --output-dir ./dist

  3) 仅压缩（每镜像单独 tar.gz），跳过校验：
     ./image_pipeline.sh -f ./images.txt --action compress --bundle-mode split --force

  4) 重新 tag 并上传至 Harbor：
     ./image_pipeline.sh -f ./images.txt --action push \
       --harbor 192.168.5.41:8033 --harbor-user admin --harbor-password '***'

Exit Codes:
  0  成功
  2  参数错误
  3  依赖缺失/环境不满足
  4  运行时错误
  5  输入校验失败（文件不存在、镜像缺失、架构不匹配等）

最小自测方法:
  # 正常路径（dry-run）
  ./image_pipeline.sh -f ./images.txt --action download --dry-run

  # 参数错误
  ./image_pipeline.sh --action download

  # 压缩 dry-run
  ./image_pipeline.sh -f ./images.txt --action compress --bundle-mode split --dry-run
EOF
}

# ==============================================================================
# 日志与错误处理区
# ==============================================================================
### 将日志级别名称转为数值，便于比较
### @param level string 日志级别名称
### @return 0 成功输出数值
### @require 无
### @side_effect 无
level_to_num() {
  local level="${1:-INFO}"
  case "${level^^}" in
    DEBUG) printf '%s\n' "10" ;;
    INFO)  printf '%s\n' "20" ;;
    WARN)  printf '%s\n' "30" ;;
    ERROR) printf '%s\n' "40" ;;
    *)     printf '%s\n' "20" ;;
  esac
}

### 输出统一格式日志
### @param level string 日志级别
### @param message string 日志消息
### @return 0 成功输出
### @require date
### @side_effect 向 stderr 输出日志
log_msg() {
  local level="${1:-INFO}"
  shift || true
  local message="${*:-}"
  local now=""
  local current_level_num=""
  local target_level_num=""

  now="$(date '+%Y-%m-%d %H:%M:%S')"
  current_level_num="$(level_to_num "${LOG_LEVEL}")"
  target_level_num="$(level_to_num "${level}")"

  if [[ "${target_level_num}" -ge "${current_level_num}" ]]; then
    printf '%s [%s] %s\n' "${now}" "${level^^}" "${message}" >&2
  fi
}

### DEBUG 级别日志
### @param message string 日志消息
### @return 0 成功输出
### @require log_msg
### @side_effect 向 stderr 输出日志
log_debug() { log_msg "DEBUG" "$*"; }

### INFO 级别日志
### @param message string 日志消息
### @return 0 成功输出
### @require log_msg
### @side_effect 向 stderr 输出日志
log_info() { log_msg "INFO" "$*"; }

### WARN 级别日志
### @param message string 日志消息
### @return 0 成功输出
### @require log_msg
### @side_effect 向 stderr 输出日志
log_warn() { log_msg "WARN" "$*"; }

### ERROR 级别日志
### @param message string 日志消息
### @return 0 成功输出
### @require log_msg
### @side_effect 向 stderr 输出日志
log_error() { log_msg "ERROR" "$*"; }

### 统一失败出口
### @param message string 错误信息
### @param exit_code int 退出码
### @return 非 0 退出
### @require 无
### @side_effect 终止脚本
die() {
  local message="${1:-Unknown error}"
  local exit_code="${2:-${EXIT_RUNTIME}}"
  log_error "${message}"
  exit "${exit_code}"
}

### 打印调用栈，提升可观测性
### @param none none 无
### @return 0 打印完成
### @require Bash 调用栈变量
### @side_effect 向 stderr 输出栈信息
print_stack_trace() {
  local i=""
  local depth="${#FUNCNAME[@]}"

  if [[ "${depth}" -le 1 ]]; then
    return 0
  fi

  printf 'Stack trace:\n' >&2
  for (( i=1; i<depth; i++ )); do
    printf '  at %s() %s:%s\n' \
      "${FUNCNAME[$i]}" \
      "${BASH_SOURCE[$i]:-unknown}" \
      "${BASH_LINENO[$((i-1))]:-unknown}" >&2
  done
}

### ERR trap 处理器
### @param line_no int 出错行号
### @param cmd string 出错命令
### @param code int 退出码
### @return 非 0 继续退出
### @require trap ERR
### @side_effect 输出错误细节
on_err() {
  local line_no="${1:-unknown}"
  local cmd="${2:-unknown}"
  local code="${3:-1}"

  log_error "命令执行失败: line=${line_no}, exit_code=${code}, cmd=${cmd}"
  print_stack_trace
  exit "${code}"
}

### 信号中断处理器
### @param signal string 信号名
### @return 非 0 中断退出
### @require trap INT/TERM
### @side_effect 输出日志并退出
on_signal() {
  local signal="${1:-INT}"
  log_warn "收到中断信号: ${signal}"
  exit "${EXIT_INTERRUPT}"
}

### 退出清理函数
### @param none none 无
### @return 0 清理完成
### @require mktemp
### @side_effect 删除临时目录/锁文件
cleanup() {
  local code=$?

  # > 只清理本脚本创建的临时资源，避免误删用户文件
  if [[ -n "${LOCK_FILE}" && -f "${LOCK_FILE}" ]]; then
    rm -f "${LOCK_FILE}" || true
  fi

  if [[ -n "${TMP_DIR}" && -d "${TMP_DIR}" ]]; then
    rm -rf "${TMP_DIR}" || true
  fi

  # > EXIT trap 不覆盖原始退出码
  return "${code}"
}

trap 'on_err "${LINENO}" "${BASH_COMMAND}" "$?"' ERR
trap 'on_signal "INT"' INT
trap 'on_signal "TERM"' TERM
trap cleanup EXIT

# ==============================================================================
# 通用工具函数
# ==============================================================================
### 判断命令是否存在
### @param cmd string 命令名
### @return 0 存在 / 1 不存在
### @require command
### @side_effect 无
has_cmd() {
  command -v "${1}" >/dev/null 2>&1
}

### 规范化架构名称
### @param arch string 原始架构名
### @return 0 输出规范化架构名
### @require tr
### @side_effect 无
normalize_arch() {
  local raw="${1:-}"
  local value=""

  value="$(printf '%s' "${raw}" | tr '[:upper:]' '[:lower:]')"
  case "${value}" in
    arm64|aarch64) printf '%s\n' "arm64" ;;
    amd64|x86_64)  printf '%s\n' "amd64" ;;
    *)             printf '%s\n' "${value}" ;;
  esac
}

### 校验是否为正整数
### @param value string 待校验值
### @return 0 合法 / 1 非法
### @require 无
### @side_effect 无
is_positive_integer() {
  local value="${1:-}"
  [[ "${value}" =~ ^[0-9]+$ ]]
}

### 获取 CPU 核数
### @param none none 无
### @return 0 输出核数
### @require nproc 或 getconf
### @side_effect 无
detect_cpu_threads() {
  local cpu_count="1"

  if has_cmd "nproc"; then
    cpu_count="$(nproc)"
  elif has_cmd "getconf"; then
    cpu_count="$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf '%s' "1")"
  fi

  if ! is_positive_integer "${cpu_count}" || [[ "${cpu_count}" -lt 1 ]]; then
    cpu_count="1"
  fi

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

### 通过 run_cmd 统一执行外部命令，支持 dry-run 与重试
### @param ... string 命令及参数
### @return 0 成功 / 非 0 失败
### @require sleep（重试时）
### @side_effect 执行外部命令
run_cmd() {
  local attempt=1
  local max_attempts="${RETRY}"
  local delay="${RETRY_DELAY}"
  local printable_cmd=""
  local arg=""

  if [[ "$#" -eq 0 ]]; then
    die "run_cmd 收到空命令" "${EXIT_RUNTIME}"
  fi

  for arg in "$@"; do
    if [[ -n "${printable_cmd}" ]]; then
      printable_cmd+=" "
    fi
    printable_cmd+="$(printf '%q' "${arg}")"
  done

  if [[ "${DRY_RUN}" == "true" ]]; then
    log_info "[dry-run] ${printable_cmd}"
    return 0
  fi

  while (( attempt <= max_attempts )); do
    log_debug "执行命令（第 ${attempt}/${max_attempts} 次）: ${printable_cmd}"
    if "$@"; then
      return 0
    fi

    if (( attempt < max_attempts )); then
      log_warn "命令失败，${delay}s 后重试: ${printable_cmd}"
      sleep "${delay}"
    fi
    attempt=$((attempt + 1))
  done

  log_error "命令最终失败: ${printable_cmd}"
  return 1
}

# ==============================================================================
# 依赖与环境检查区
# ==============================================================================
### 初始化运行时目录、锁与压缩器
### @param none none 无
### @return 0 初始化完成
### @require mktemp
### @side_effect 创建临时目录/锁文件
init_runtime() {
  TMP_DIR="$(mktemp -d -t image-pipeline.XXXXXX)"
  LOCK_FILE="${TMP_DIR}/${LOCK_FILE_NAME}"

  # > 使用临时目录中的锁文件，避免并发实例相互覆盖中间状态
  : > "${LOCK_FILE}"

  if has_cmd "pigz"; then
    COMPRESSOR_CMD="pigz"
  else
    COMPRESSOR_CMD="gzip"
  fi
}

### 按当前动作检查依赖
### @param none none 无
### @return 0 依赖满足
### @require command
### @side_effect 无
check_dependencies() {
  local missing=()

  has_cmd "docker" || missing+=("docker")
  has_cmd "date" || missing+=("date")
  has_cmd "mktemp" || missing+=("mktemp")

  if [[ "${ACTION}" == "compress" || "${ACTION}" == "all" ]]; then
    if [[ "${COMPRESSOR_CMD}" == "pigz" ]]; then
      :
    else
      has_cmd "gzip" || missing+=("gzip")
    fi
  fi

  if [[ "${#missing[@]}" -gt 0 ]]; then
    die "缺少依赖命令: ${missing[*]}" "${EXIT_DEPENDENCY}"
  fi

  if [[ "${COMPRESSOR_CMD}" == "pigz" ]]; then
    log_info "检测到 pigz：将启用多线程压缩"
  elif [[ "${ACTION}" == "compress" || "${ACTION}" == "all" ]]; then
    log_warn "未检测到 pigz：将回退到 gzip（单线程）"
  fi
}

# ==============================================================================
# 参数解析与校验
# ==============================================================================
### 解析命令行参数
### @param ... string 命令行参数
### @return 0 解析成功
### @require 无
### @side_effect 修改全局配置变量
parse_args() {
  while [[ "$#" -gt 0 ]]; do
    case "$1" in
      -f|--image-file)
        [[ "$#" -ge 2 ]] || die "参数缺失: $1 需要一个文件路径" "${EXIT_USAGE}"
        IMAGE_FILE="$2"
        shift 2
        ;;
      -a|--action)
        [[ "$#" -ge 2 ]] || die "参数缺失: $1 需要一个动作值" "${EXIT_USAGE}"
        ACTION="$2"
        shift 2
        ;;
      --arch)
        [[ "$#" -ge 2 ]] || die "参数缺失: $1 需要一个架构值" "${EXIT_USAGE}"
        ARCH="$2"
        shift 2
        ;;
      --mirror-prefix)
        [[ "$#" -ge 2 ]] || die "参数缺失: $1 需要一个前缀值" "${EXIT_USAGE}"
        MIRROR_PREFIX="$2"
        shift 2
        ;;
      --bundle-mode)
        [[ "$#" -ge 2 ]] || die "参数缺失: $1 需要一个模式值" "${EXIT_USAGE}"
        BUNDLE_MODE="$2"
        shift 2
        ;;
      --output-dir)
        [[ "$#" -ge 2 ]] || die "参数缺失: $1 需要一个目录路径" "${EXIT_USAGE}"
        OUTPUT_DIR="$2"
        shift 2
        ;;
      --threads)
        [[ "$#" -ge 2 ]] || die "参数缺失: $1 需要一个线程数" "${EXIT_USAGE}"
        THREADS="$2"
        shift 2
        ;;
      --harbor)
        [[ "$#" -ge 2 ]] || die "参数缺失: $1 需要一个 Harbor 地址" "${EXIT_USAGE}"
        HARBOR_HOST="$2"
        shift 2
        ;;
      --harbor-user)
        [[ "$#" -ge 2 ]] || die "参数缺失: $1 需要一个用户名" "${EXIT_USAGE}"
        HARBOR_USERNAME="$2"
        shift 2
        ;;
      --harbor-password)
        [[ "$#" -ge 2 ]] || die "参数缺失: $1 需要一个密码" "${EXIT_USAGE}"
        HARBOR_PASSWORD="$2"
        shift 2
        ;;
      --retry)
        [[ "$#" -ge 2 ]] || die "参数缺失: $1 需要一个重试次数" "${EXIT_USAGE}"
        RETRY="$2"
        shift 2
        ;;
      --retry-delay)
        [[ "$#" -ge 2 ]] || die "参数缺失: $1 需要一个秒数" "${EXIT_USAGE}"
        RETRY_DELAY="$2"
        shift 2
        ;;
      --dry-run)
        DRY_RUN="true"
        shift
        ;;
      --force)
        FORCE="true"
        shift
        ;;
      --verbose)
        LOG_LEVEL="DEBUG"
        shift
        ;;
      --quiet)
        LOG_LEVEL="ERROR"
        shift
        ;;
      -h|--help)
        usage
        exit "${EXIT_SUCCESS}"
        ;;
      --)
        shift
        break
        ;;
      -*)
        die "未知参数: $1" "${EXIT_USAGE}"
        ;;
      *)
        die "不支持的位置参数: $1" "${EXIT_USAGE}"
        ;;
    esac
  done
}

### 校验参数与输入
### @param none none 无
### @return 0 校验通过
### @require 无
### @side_effect 可能创建输出目录（dry-run 时仅日志提示）
validate_args() {
  ARCH="$(normalize_arch "${ARCH}")"

  [[ -n "${IMAGE_FILE}" ]] || die "必须提供 --image-file" "${EXIT_USAGE}"

  if [[ -L "${IMAGE_FILE}" ]]; then
    die "出于安全考虑，不接受符号链接作为镜像文件: ${IMAGE_FILE}" "${EXIT_VALIDATION}"
  fi

  [[ -f "${IMAGE_FILE}" ]] || die "镜像文件不存在: ${IMAGE_FILE}" "${EXIT_VALIDATION}"
  [[ -r "${IMAGE_FILE}" ]] || die "镜像文件不可读: ${IMAGE_FILE}" "${EXIT_VALIDATION}"

  case "${ACTION}" in
    download|compress|push|all) ;;
    *)
      die "非法动作: ${ACTION}（允许: download|compress|push|all）" "${EXIT_USAGE}"
      ;;
  esac

  case "${ARCH}" in
    arm64|amd64) ;;
    *)
      die "非法架构: ${ARCH}（允许: arm64|amd64）" "${EXIT_USAGE}"
      ;;
  esac

  case "${BUNDLE_MODE}" in
    combined|split) ;;
    *)
      die "非法压缩模式: ${BUNDLE_MODE}（允许: combined|split）" "${EXIT_USAGE}"
      ;;
  esac

  is_positive_integer "${THREADS}" || die "--threads 必须为非负整数" "${EXIT_USAGE}"
  is_positive_integer "${RETRY}" || die "--retry 必须为非负整数" "${EXIT_USAGE}"
  is_positive_integer "${RETRY_DELAY}" || die "--retry-delay 必须为非负整数" "${EXIT_USAGE}"

  if [[ "${RETRY}" -eq 0 ]]; then
    die "--retry 不能为 0；如不重试，请使用 1" "${EXIT_USAGE}"
  fi

  if [[ "${THREADS}" -eq 0 ]]; then
    THREADS="$(detect_cpu_threads)"
  fi

  if [[ "${ACTION}" == "compress" || "${ACTION}" == "all" ]]; then
    if [[ "${DRY_RUN}" == "true" ]]; then
      log_info "[dry-run] 将创建输出目录: ${OUTPUT_DIR}"
    else
      mkdir -p "${OUTPUT_DIR}"
    fi
  fi

  if [[ "${ACTION}" == "push" || "${ACTION}" == "all" ]]; then
    [[ "${HARBOR_HOST}" =~ ^[A-Za-z0-9._-]+:[0-9]+$ ]] || die \
      "Harbor 地址格式非法（应为 IP:PORT 或 HOST:PORT）: ${HARBOR_HOST}" "${EXIT_USAGE}"
    [[ -n "${HARBOR_USERNAME}" ]] || die "Harbor 用户名不能为空" "${EXIT_USAGE}"
    [[ -n "${HARBOR_PASSWORD}" ]] || die "Harbor 密码不能为空" "${EXIT_USAGE}"
  fi

  log_debug "参数校验通过: action=${ACTION}, arch=${ARCH}, bundle_mode=${BUNDLE_MODE}, threads=${THREADS}"
}

# ==============================================================================
# 业务函数区：镜像清单
# ==============================================================================
### 加载镜像清单到数组
### @param file string 镜像清单文件
### @return 0 加载成功
### @require 文件可读
### @side_effect 修改全局 IMAGE_LIST 数组
load_image_list() {
  local file_path="${1}"
  local line=""
  local trimmed=""

  IMAGE_LIST=()

  while IFS= read -r line || [[ -n "${line}" ]]; do
    # > 去掉行首尾空白后再判断，避免空格行被误识别为镜像名
    trimmed="${line#"${line%%[![:space:]]*}"}"
    trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"

    [[ -z "${trimmed}" ]] && continue
    [[ "${trimmed}" == \#* ]] && continue

    IMAGE_LIST+=("${trimmed}")
  done < "${file_path}"

  [[ "${#IMAGE_LIST[@]}" -gt 0 ]] || die "镜像文件中未读取到任何有效镜像: ${file_path}" "${EXIT_VALIDATION}"
  log_info "已加载镜像数量: ${#IMAGE_LIST[@]}"
}

# ==============================================================================
# 业务函数区：Docker 基础操作
# ==============================================================================
### 生成实际拉取的源镜像名
### @param image string 原始镜像名
### @return 0 输出实际拉取镜像名
### @require 无
### @side_effect 无
compose_pull_source() {
  local image_ref="${1}"
  local prefix="${MIRROR_PREFIX%/}"

  if [[ -n "${prefix}" ]]; then
    printf '%s/%s\n' "${prefix}" "${image_ref}"
  else
    printf '%s\n' "${image_ref}"
  fi
}

### 拉取镜像
### @param source_image string 实际拉取源镜像
### @param arch string 目标架构
### @return 0 拉取成功
### @require docker
### @side_effect 向本地 Docker 写入镜像
pull_image() {
  local source_image="${1}"
  local target_arch="${2}"

  # > 显式指定平台，确保拉取结果符合目标 CPU 架构
  run_cmd docker pull --platform "linux/${target_arch}" "${source_image}"
}

### 条件性重新打 tag
### @param source_image string 源镜像名
### @param target_image string 目标镜像名
### @return 0 成功
### @require docker
### @side_effect 生成新的本地镜像标签
tag_image_if_needed() {
  local source_image="${1}"
  local target_image="${2}"

  if [[ "${source_image}" == "${target_image}" ]]; then
    return 0
  fi

  run_cmd docker tag "${source_image}" "${target_image}"
}

### 获取本地镜像架构
### @param image string 镜像名
### @return 0 输出架构；不存在则返回非 0
### @require docker
### @side_effect 无
get_local_image_arch() {
  local image_ref="${1}"
  local arch_value=""

  arch_value="$(docker image inspect --format '{{.Architecture}}' "${image_ref}" 2>/dev/null)"
  [[ -n "${arch_value}" ]] || return 1
  normalize_arch "${arch_value}"
}

### 检查本地镜像是否存在
### @param image string 镜像名
### @return 0 存在 / 1 不存在
### @require docker
### @side_effect 无
image_exists_locally() {
  local image_ref="${1}"
  docker image inspect "${image_ref}" >/dev/null 2>&1
}

### 检查并输出架构，同时校验是否匹配
### @param image string 镜像名
### @param expected_arch string 期望架构
### @return 0 匹配 / 1 不匹配或不存在
### @require docker
### @side_effect 输出日志
inspect_and_verify_arch() {
  local image_ref="${1}"
  local expected_arch="${2}"
  local actual_arch=""

  if ! actual_arch="$(get_local_image_arch "${image_ref}")"; then
    log_error "镜像不存在或无法读取架构: ${image_ref}"
    return 1
  fi

  log_info "镜像架构检查: ${image_ref} -> ${actual_arch}"

  if [[ "${actual_arch}" != "${expected_arch}" ]]; then
    log_error "镜像架构不匹配: ${image_ref}, expected=${expected_arch}, actual=${actual_arch}"
    return 1
  fi

  return 0
}

# ==============================================================================
# 业务函数区：下载
# ==============================================================================
### 下载单个镜像，并在需要时重新 tag
### @param image string 原始镜像名
### @return 0 成功
### @require docker
### @side_effect 拉取/打 tag 到本地 Docker
download_one_image() {
  local original_image="${1}"
  local source_image=""
  local pull_succeeded="false"

  source_image="$(compose_pull_source "${original_image}")"
  log_info "开始下载镜像: ${original_image}"
  log_debug "实际拉取源: ${source_image}"

  if pull_image "${source_image}" "${ARCH}"; then
    pull_succeeded="true"
    log_info "镜像下载成功: ${source_image}"
  else
    log_warn "镜像下载失败: ${source_image}"
  fi

  if [[ "${pull_succeeded}" == "true" ]]; then
    tag_image_if_needed "${source_image}" "${original_image}"
  else
    # > 若拉取失败但本地已存在同名镜像，则允许复用本地镜像继续流程
    if image_exists_locally "${original_image}"; then
      DOWNLOAD_USED_LOCAL+=("${original_image}")
      log_warn "下载失败但本地已存在镜像，将复用本地镜像: ${original_image}"
    else
      DOWNLOAD_FAILED+=("${original_image}")
      return 1
    fi
  fi

  inspect_and_verify_arch "${original_image}" "${ARCH}"
}

### 批量执行下载
### @param none none 无
### @return 0 成功；存在失败则返回非 0
### @require IMAGE_LIST 已加载
### @side_effect 写入本地 Docker
perform_downloads() {
  local image_ref=""
  local failed_count_before="${#DOWNLOAD_FAILED[@]}"

  for image_ref in "${IMAGE_LIST[@]}"; do
    if ! download_one_image "${image_ref}"; then
      log_error "下载阶段失败: ${image_ref}"
    fi
  done

  if [[ "${#DOWNLOAD_FAILED[@]}" -gt "${failed_count_before}" ]]; then
    return 1
  fi

  return 0
}

# ==============================================================================
# 业务函数区：压缩前校验
# ==============================================================================
### 校验镜像是否全部存在且架构符合预期
### @param none none 无
### @return 0 校验通过
### @require docker
### @side_effect 填充 VERIFY_MISSING / VERIFY_ARCH_MISMATCH
verify_images_present() {
  local image_ref=""
  local actual_arch=""

  VERIFY_MISSING=()
  VERIFY_ARCH_MISMATCH=()

  if [[ "${FORCE}" == "true" ]]; then
    log_warn "已启用 --force：跳过镜像存在性/架构一致性检查"
    return 0
  fi

  for image_ref in "${IMAGE_LIST[@]}"; do
    if ! image_exists_locally "${image_ref}"; then
      VERIFY_MISSING+=("${image_ref}")
      continue
    fi

    actual_arch="$(get_local_image_arch "${image_ref}" || true)"
    if [[ -z "${actual_arch}" ]]; then
      VERIFY_MISSING+=("${image_ref}")
      continue
    fi

    if [[ "${actual_arch}" != "${ARCH}" ]]; then
      VERIFY_ARCH_MISMATCH+=("${image_ref} (actual=${actual_arch})")
    fi
  done

  if [[ "${#VERIFY_MISSING[@]}" -gt 0 || "${#VERIFY_ARCH_MISMATCH[@]}" -gt 0 ]]; then
    if [[ "${#VERIFY_MISSING[@]}" -gt 0 ]]; then
      log_error "缺失镜像:"
      printf '  - %s\n' "${VERIFY_MISSING[@]}" >&2
    fi
    if [[ "${#VERIFY_ARCH_MISMATCH[@]}" -gt 0 ]]; then
      log_error "架构不匹配镜像:"
      printf '  - %s\n' "${VERIFY_ARCH_MISMATCH[@]}" >&2
    fi
    return 1
  fi

  log_info "镜像存在性与架构检查通过"
  return 0
}

# ==============================================================================
# 业务函数区：压缩
# ==============================================================================
### 将镜像名转换为安全文件名
### @param image string 镜像名
### @return 0 输出文件安全名
### @require tr
### @side_effect 无
sanitize_image_to_filename() {
  local image_ref="${1}"
  local safe_name=""

  safe_name="$(printf '%s' "${image_ref}" | sed 's|/|_|g; s|:|__|g; s|@|__|g')"
  printf '%s\n' "${safe_name}"
}

### 构建压缩命令参数
### @param none none 无
### @return 0 设置 COMPRESSOR_ARGS
### @require 无
### @side_effect 修改全局 COMPRESSOR_ARGS 数组
prepare_compressor_args() {
  COMPRESSOR_ARGS=()

  if [[ "${COMPRESSOR_CMD}" == "pigz" ]]; then
    COMPRESSOR_ARGS=(-p "${THREADS}")
  else
    COMPRESSOR_ARGS=()
  fi
}

### 统一打包所有镜像为一个 tar.gz
### @param output_file string 输出文件路径
### @return 0 成功
### @require docker + pigz/gzip
### @side_effect 写入压缩包文件
compress_combined() {
  local output_file="${1}"
  local image_ref=""
  local -a image_args=()

  for image_ref in "${IMAGE_LIST[@]}"; do
    image_args+=("${image_ref}")
  done

  prepare_compressor_args

  log_info "开始统一打包: ${output_file}"
  # > 这里只传“镜像列表”本身，不要把字面量 save 传进去
  run_cmd bash -c '
    set -Eeuo pipefail
    docker save "$@" | '"${COMPRESSOR_CMD}"' '"${COMPRESSOR_ARGS[*]:-}"' > "'"${output_file}"'"
  ' _ "${image_args[@]}" || return 1

  log_info "统一打包完成: ${output_file}"

  if [[ "${DRY_RUN}" != "true" && -f "${output_file}" ]]; then
    log_info "压缩包大小: $(du -h "${output_file}" | awk '{print $1}')"
  fi
}

### 单个镜像压缩为 tar.gz
### @param image string 镜像名
### @param output_dir string 输出目录
### @return 0 成功
### @require docker + pigz/gzip
### @side_effect 写入单镜像压缩包
compress_one_image() {
  local image_ref="${1}"
  local out_dir="${2}"
  local file_name=""
  local output_file=""

  file_name="$(sanitize_image_to_filename "${image_ref}")"
  output_file="${out_dir}/${file_name}.tar.gz"

  prepare_compressor_args

  log_info "开始单镜像打包: ${image_ref} -> ${output_file}"
  run_cmd bash -c '
    set -Eeuo pipefail
    docker save "$1" | '"${COMPRESSOR_CMD}"' '"${COMPRESSOR_ARGS[*]:-}"' > "$2"
  ' _ "${image_ref}" "${output_file}"

  log_info "单镜像打包完成: ${output_file}"
}

### 并发执行每镜像单独压缩
### @param output_dir string 输出目录
### @return 0 全部成功 / 非 0 存在失败
### @require docker + pigz/gzip
### @side_effect 写入多个压缩包
compress_split_parallel() {
  local out_dir="${1}"
  local image_ref=""
  local -a pids=()
  local -a pid_to_image=()
  local index=0
  local active_jobs=0
  local wait_pid=""
  local wait_rc="0"

  COMPRESS_FAILED=()
  pids=()
  pid_to_image=()

  for image_ref in "${IMAGE_LIST[@]}"; do
    (
      compress_one_image "${image_ref}" "${out_dir}"
    ) &
    pids+=("$!")
    pid_to_image+=("${image_ref}")
    active_jobs=$((active_jobs + 1))

    # > 控制最大并发数，避免同时大量 docker save 导致 IO/CPU 失控
    if [[ "${active_jobs}" -ge "${THREADS}" ]]; then
      wait_pid="${pids[0]}"
      if ! wait "${wait_pid}"; then
        wait_rc=$?
        COMPRESS_FAILED+=("${pid_to_image[0]}")
        log_error "单镜像压缩失败: ${pid_to_image[0]} (exit=${wait_rc})"
      fi
      pids=("${pids[@]:1}")
      pid_to_image=("${pid_to_image[@]:1}")
      active_jobs=$((active_jobs - 1))
    fi
    index=$((index + 1))
  done

  for index in "${!pids[@]}"; do
    if ! wait "${pids[$index]}"; then
      wait_rc=$?
      COMPRESS_FAILED+=("${pid_to_image[$index]}")
      log_error "单镜像压缩失败: ${pid_to_image[$index]} (exit=${wait_rc})"
    fi
  done

  [[ "${#COMPRESS_FAILED[@]}" -eq 0 ]]
}

### 执行压缩动作
### @param none none 无
### @return 0 成功
### @require verify_images_present 已通过
### @side_effect 写入压缩文件
perform_compress() {
  local output_file=""
  local image_file_name=""
  local image_file_base=""
  local archive_date=""

  if [[ "${BUNDLE_MODE}" == "combined" ]]; then
    image_file_name="$(basename "${IMAGE_FILE}")"
    image_file_base="${image_file_name%.txt}"
    archive_date="$(date '+%Y%m%d')"

    output_file="${OUTPUT_DIR}/${image_file_base}_${archive_date}_${ARCH}.tar.gz"
    compress_combined "${output_file}"
  else
    compress_split_parallel "${OUTPUT_DIR}"
  fi
}

# ==============================================================================
# 业务函数区：上传 Harbor
# ==============================================================================
### 构建目标 Harbor 镜像名
### @param image string 原始镜像名
### @return 0 输出目标镜像名
build_target_image_ref() {
  local image_ref="${1}"
  local pure_image
  local image_name_tag

  # 1. 剥离域名部分 (处理 docker.io/xxx 或 my.reg.com/xxx)
  # 如果斜杠数量 >= 2，说明带了域名，取最后两部分
  if [[ "${image_ref}" =~ .*/.*/.* ]]; then
    pure_image=$(echo "${image_ref}" | rev | cut -d'/' -f1,2 | rev)
  else
    pure_image="${image_ref}"
  fi

  # 2. 核心转换逻辑
  if [[ "${pure_image}" == rancher/* ]]; then
    # 特例：rancher 开头，直接拼接
    printf '%s/%s\n' "${HARBOR_HOST}" "${pure_image}"

  elif [[ "${pure_image}" == */* ]]; then
    # 包含路径但不是 rancher (如 bitnamilegacy/nginx 或 ossrs/srs)
    # 提取斜杠后的部分 (镜像名:标签)，前缀换成 cmii
    image_name_tag="${pure_image#*/}"
    printf '%s/cmii/%s\n' "${HARBOR_HOST}" "${image_name_tag}"

  else
    # 完全不带路径 (如 nginx:latest)
    # 强制变成 cmii/nginx:latest
    printf '%s/cmii/%s\n' "${HARBOR_HOST}" "${pure_image}"
  fi
}

### 登录 Harbor
### @param none none 无
### @return 0 成功
### @require docker
### @side_effect 向 Docker 写入登录状态
harbor_login() {
  log_info "开始登录 Harbor: ${HARBOR_HOST}"
  # > 使用 --password-stdin 避免密码出现在进程参数列表中
  if [[ "${DRY_RUN}" == "true" ]]; then
    log_info "[dry-run] printf '***' | docker login --username '${HARBOR_USERNAME}' --password-stdin '${HARBOR_HOST}'"
    return 0
  fi

  printf '%s' "${HARBOR_PASSWORD}" | docker login --username "${HARBOR_USERNAME}" --password-stdin "${HARBOR_HOST}"
  log_info "Harbor 登录成功: ${HARBOR_HOST}"
}

### 推送单个镜像到 Harbor
### @param image string 原始镜像名
### @return 0 成功
### @require docker
### @side_effect 向 Harbor 上传镜像
push_one_image() {
  local source_image="${1}"
  local target_image=""

  target_image="$(build_target_image_ref "${source_image}")"
  log_info "开始推送镜像: ${source_image} -> ${target_image}"

  tag_image_if_needed "${source_image}" "${target_image}"
  run_cmd docker push "${target_image}"

  PUSH_SUCCEEDED+=("${target_image}")
}

### 批量执行推送
### @param none none 无
### @return 0 全部成功 / 非 0 存在失败
### @require Harbor 已登录
### @side_effect 向 Harbor 上传多个镜像
perform_pushes() {
  local image_ref=""
  PUSH_FAILED=()
  PUSH_SUCCEEDED=()

  for image_ref in "${IMAGE_LIST[@]}"; do
    if ! push_one_image "${image_ref}"; then
      PUSH_FAILED+=("${image_ref}")
      log_error "上传失败: ${image_ref}"
    fi
  done

  [[ "${#PUSH_FAILED[@]}" -eq 0 ]]
}

# ==============================================================================
# 汇总输出
# ==============================================================================
### 打印流程总结
### @param none none 无
### @return 0 打印完成
### @require 无
### @side_effect 向 stderr 输出汇总
print_summary() {
  log_info "================ 执行总结 ================"

  if [[ "${#DOWNLOAD_FAILED[@]}" -gt 0 ]]; then
    log_warn "下载失败镜像:"
    printf '  - %s\n' "${DOWNLOAD_FAILED[@]}" >&2
  fi

  if [[ "${#DOWNLOAD_USED_LOCAL[@]}" -gt 0 ]]; then
    log_warn "下载失败但已复用本地镜像:"
    printf '  - %s\n' "${DOWNLOAD_USED_LOCAL[@]}" >&2
  fi

  if [[ "${#VERIFY_MISSING[@]}" -gt 0 ]]; then
    log_warn "校验缺失镜像:"
    printf '  - %s\n' "${VERIFY_MISSING[@]}" >&2
  fi

  if [[ "${#VERIFY_ARCH_MISMATCH[@]}" -gt 0 ]]; then
    log_warn "校验架构不匹配镜像:"
    printf '  - %s\n' "${VERIFY_ARCH_MISMATCH[@]}" >&2
  fi

  if [[ "${#COMPRESS_FAILED[@]}" -gt 0 ]]; then
    log_warn "压缩失败镜像:"
    printf '  - %s\n' "${COMPRESS_FAILED[@]}" >&2
  fi

  if [[ "${#PUSH_SUCCEEDED[@]}" -gt 0 ]]; then
    log_info "上传成功镜像数量: ${#PUSH_SUCCEEDED[@]}"
  fi

  if [[ "${#PUSH_FAILED[@]}" -gt 0 ]]; then
    log_warn "上传失败镜像:"
    printf '  - %s\n' "${PUSH_FAILED[@]}" >&2
  fi

  log_info "========================================"
}

# ==============================================================================
# main 与入口区
# ==============================================================================
### 主流程编排
### @param ... string 命令行参数
### @return 0 成功
### @require 各业务函数
### @side_effect 根据动作执行下载/压缩/上传
main() {
  parse_args "$@"
  init_runtime
  validate_args
  check_dependencies
  load_image_list "${IMAGE_FILE}"

  case "${ACTION}" in
    download)
      perform_downloads || die "下载阶段存在失败，请查看上方总结" "${EXIT_RUNTIME}"
      ;;
    compress)
      verify_images_present || die "压缩前校验失败；可使用 --force 跳过" "${EXIT_VALIDATION}"
      perform_compress || die "压缩阶段存在失败" "${EXIT_RUNTIME}"
      ;;
    push)
      verify_images_present || die "上传前校验失败；可使用 --force 跳过" "${EXIT_VALIDATION}"
      harbor_login
      perform_pushes || die "上传阶段存在失败" "${EXIT_RUNTIME}"
      ;;
    all)
      perform_downloads || die "下载阶段存在失败，请先修复后重试" "${EXIT_RUNTIME}"
      verify_images_present || die "下载后校验失败；可使用 --force 跳过" "${EXIT_VALIDATION}"
      perform_compress || die "压缩阶段存在失败" "${EXIT_RUNTIME}"
      harbor_login
      perform_pushes || die "上传阶段存在失败" "${EXIT_RUNTIME}"
      ;;
    *)
      die "未实现的动作: ${ACTION}" "${EXIT_USAGE}"
      ;;
  esac

  print_summary
  log_info "任务完成"
}

main "$@"