#!/bin/bash
#===============================================================================
# 脚本名称: mysql_backup.sh
# 功能描述: 基于 Percona XtraBackup 的 MySQL 物理热备份/恢复脚本
# 支持模式: 全量备份(--full) | 增量备份(--inc) | 恢复(--restore) | 清理(--cleanup) | 状态(--status)
# 适用版本: XtraBackup 8.0.x (MySQL 8.0) / XtraBackup 2.4.x (MySQL 5.7)
# 作者: DBA Toolkit
# 日期: 2026-07-30
#===============================================================================

set -euo pipefail

#-------------------------------------------------------------------------------
# 【用户配置区】请根据实际环境修改
#-------------------------------------------------------------------------------
BACKUP_BASE_DIR="/data/backups/mysql"
MYSQL_USER="backup"
MYSQL_PASSWORD="Zyf@123.."
MYSQL_SOCKET="/data/mysql/tmp/mysql.sock"
MYSQL_HOST="localhost"
MYSQL_PORT="3306"

# MySQL 数据目录（恢复时使用）
MYSQL_DATADIR="/data/mysql/data"
    
# 保留策略（天）
RETENTION_FULL_DAYS=14
RETENTION_INC_DAYS=7
RETENTION_LOG_DAYS=30
    
# 日志目录与按日期滚动的日志文件
LOG_DIR="${BACKUP_BASE_DIR}/logs"
LOG_FILE="${LOG_DIR}/mysql_backup_$(date +%Y%m%d).log"

# 状态与锁文件
STATE_FILE="${BACKUP_BASE_DIR}/.backup_state"
LOCK_FILE="/var/run/mysql_backup.lock"

# XtraBackup 命令路径
XTRABACKUP_CMD="/opt/xtrabackup/bin/xtrabackup"
    
# 并行线程数（根据 CPU 和磁盘 IO 调整）
PARALLEL=4

# 压缩选项（根据版本选择）:
#   2.4:  --compress
#   8.0:  --compress=zstd  或  --compress=lz4
#   不压缩: 留空
COMPRESS="--compress"

# 恢复数据使用内存大小
MEMORY=3G

# 加密选项（需提前配置 keyring）
# 留空表示不加密
ENCRYPT=""

# 备份成功通知脚本/命令（留空表示不通知）
# 示例: NOTIFY_CMD="curl -s https://oapi.dingtalk.com/robot/send?access_token=xxx ..."
NOTIFY_CMD=""

#-------------------------------------------------------------------------------
# 常量与工具函数
#-------------------------------------------------------------------------------
DATE_FMT=$(date +%Y%m%d_%H%M%S)

log_msg() {
    local level="$1"
    local msg="$2"
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    # 控制台只输出脚本自定义的日志；全量日志写入按日期滚动的日志文件
    echo "[${timestamp}] [${level}] ${msg}" | tee -a "${LOG_FILE}"
}

# 错误退出：提示日志路径
fatal_exit() {
    local msg="${1:-脚本异常退出}"
    log_msg "ERROR" "${msg}"
    log_msg "ERROR" "详细错误日志: ${LOG_FILE}"
    exit 1
}

# 发送通知
send_notify() {
    local status="$1"
    local msg="$2"
    if [[ -n "${NOTIFY_CMD}" ]]; then
        bash -c "${NOTIFY_CMD} '${status}: ${msg}'" &>/dev/null || true
    fi
}

# 检查 root 权限（恢复需要）
check_root() {
    if [[ $EUID -ne 0 ]]; then
        log_msg "ERROR" "恢复操作需要 root 权限执行。"
        fatal_exit
    fi
}

# 检查环境与依赖
check_env() {
    local mode="$1"  # backup 或 restore

    if ! command -v "${XTRABACKUP_CMD}" &>/dev/null; then
        log_msg "ERROR" "未找到 ${XTRABACKUP_CMD}，请确认 Percona XtraBackup 已安装。"
        fatal_exit
    fi

    if [[ "${mode}" == "backup" ]]; then
        if ! mysql -u"${MYSQL_USER}" -p"${MYSQL_PASSWORD}" -S"${MYSQL_SOCKET}" \
                  -e "SELECT 1;" &>/dev/null; then
            log_msg "ERROR" "无法连接 MySQL，请检查账号、密码、Socket 配置。"
            fatal_exit
        fi
        log_msg "INFO" "MySQL 连接检测通过。"
    fi

    mkdir -p "${BACKUP_BASE_DIR}" "${LOG_DIR}"
}

# 检查磁盘空间（GB）
check_disk_space() {
    local need_gb="$1"
    local avail_kb
    avail_kb=$(df -k "${BACKUP_BASE_DIR}" | awk 'NR==2 {print $4}')
    local avail_gb=$((avail_kb / 1024 / 1024))
    if [[ ${avail_gb} -lt ${need_gb} ]]; then
        log_msg "ERROR" "磁盘空间不足: 需要 ${need_gb}GB, 可用 ${avail_gb}GB"
        fatal_exit
    fi
}

# 安全读写状态文件
write_state() {
    local key="$1"
    local value="$2"
    local tmpf="${STATE_FILE}.tmp.$$"
    if [[ -f "${STATE_FILE}" ]]; then
        grep -v "^${key}=" "${STATE_FILE}" > "${tmpf}" 2>/dev/null || true
    fi
    echo "${key}=${value}" >> "${tmpf}"
    mv -f "${tmpf}" "${STATE_FILE}"
}

read_state() {
    local key="$1"
    if [[ -f "${STATE_FILE}" ]]; then
        grep "^${key}=" "${STATE_FILE}" | cut -d'=' -f2- | tail -1
    fi
}

# 获取目录大小
get_size() {
    du -sh "$1" 2>/dev/null | awk '{print $1}'
}

#-------------------------------------------------------------------------------
# 自动检测压缩格式并解压（兼容 2.4 和 8.0）
#-------------------------------------------------------------------------------
decompress_if_needed() {
    local target_dir="$1"
    local compress_type=""

    # 检测压缩类型
    if find "${target_dir}" -name "*.qp" -print -quit | grep -q .; then
        compress_type="qpress"
    elif find "${target_dir}" -name "*.zst" -print -quit | grep -q .; then
        compress_type="zstd"
    elif find "${target_dir}" -name "*.lz4" -print -quit | grep -q .; then
        compress_type="lz4"
    fi

    if [[ -z "${compress_type}" ]]; then
        log_msg "INFO" "备份未压缩，跳过解压步骤。"
        return 0
    fi

    log_msg "INFO" "检测到 ${compress_type} 压缩备份，开始解压: ${target_dir}"

    # 检查对应依赖
    case "${compress_type}" in
        qpress)
            if ! command -v qpress &>/dev/null; then
                log_msg "ERROR" "备份为 qpress 压缩格式，但系统中未找到 qpress 工具。"
                log_msg "INFO" "请安装: yum install qpress 或 wget http://www.quicklz.com/qpress-11-linux-x64.tar"
                fatal_exit
            fi
            ;;
        zstd)
            if ! command -v zstd &>/dev/null; then
                log_msg "ERROR" "备份为 zstd 压缩格式，但系统中未找到 zstd 工具。"
                log_msg "INFO" "请安装: yum install zstd 或 apt-get install zstd"
                fatal_exit
            fi
            ;;
        lz4)
            if ! command -v lz4 &>/dev/null; then
                log_msg "ERROR" "备份为 lz4 压缩格式，但系统中未找到 lz4 工具。"
                log_msg "INFO" "请安装: yum install lz4 或 apt-get install liblz4-tool"
                fatal_exit
            fi
            ;;
    esac

    # 执行解压：详细输出只写入日志文件，不输出到控制台
    if ! ${XTRABACKUP_CMD} --decompress \
        --target-dir="${target_dir}" \
        --parallel=${PARALLEL} >> "${LOG_FILE}" 2>&1; then
        fatal_exit "解压失败: ${target_dir}"
    fi

    # 清理压缩文件（节省空间）
    log_msg "INFO" "解压完成，正在清理压缩文件..."
    case "${compress_type}" in
        qpress) find "${target_dir}" -name "*.qp" -delete ;;
        zstd)   find "${target_dir}" -name "*.zst" -delete ;;
        lz4)    find "${target_dir}" -name "*.lz4" -delete ;;
    esac

    log_msg "INFO" "解压并清理完成。"
}

#-------------------------------------------------------------------------------
# 全量备份
#-------------------------------------------------------------------------------
do_full_backup() {
    log_msg "INFO" "========== 开始全量备份 =========="

    check_env "backup"

    # 估算所需空间（数据目录的 1.2 倍 + 5GB 余量）
    local data_kb
    data_kb=$(du -sk "${MYSQL_DATADIR}" 2>/dev/null | awk '{print $1}')
    local need_gb=$((data_kb * 12 / 10 / 1024 / 1024 + 5))
    check_disk_space "${need_gb}"

    local backup_dir="${BACKUP_BASE_DIR}/full_${DATE_FMT}"
    mkdir -p "${backup_dir}"

    log_msg "INFO" "备份目录: ${backup_dir}"

    local start_ts end_ts duration
    start_ts=$(date +%s)

    # 执行备份：详细输出只写入日志文件，不输出到控制台
    if ! ${XTRABACKUP_CMD} --backup \
        --target-dir="${backup_dir}" \
        --socket="${MYSQL_SOCKET}" \
        --user="${MYSQL_USER}" \
        --password="${MYSQL_PASSWORD}" \
        --host="${MYSQL_HOST}" \
        --port="${MYSQL_PORT}" \
        --parallel=${PARALLEL} \
        ${COMPRESS:+${COMPRESS}} \
        ${ENCRYPT:+${ENCRYPT}} \
        >> "${LOG_FILE}" 2>&1; then
        log_msg "ERROR" "全量备份执行失败，删除不完整目录。"
        rm -rf "${backup_dir}"
        send_notify "FAILED" "全量备份失败"
        fatal_exit
    fi

    end_ts=$(date +%s)
    duration=$((end_ts - start_ts))

    # 更新状态
    write_state "LAST_FULL" "${backup_dir}"
    write_state "LAST_FULL_NAME" "$(basename "${backup_dir}")"
    write_state "LAST_INC" ""

    log_msg "INFO" "全量备份完成。耗时: ${duration}s, 大小: $(get_size "${backup_dir}")"
    send_notify "SUCCESS" "全量备份完成，耗时 ${duration}s"
    log_msg "INFO" "========== 全量备份结束 =========="
}

#-------------------------------------------------------------------------------
# 增量备份
# 参数: $1 = 指定的基础备份目录（可选，由 --inc-base 传入）
#-------------------------------------------------------------------------------
do_inc_backup() {
    local specified_base="${1:-}"

    log_msg "INFO" "========== 开始增量备份 =========="

    check_env "backup"

    local base_dir=""
    local last_full last_inc

    # 如果指定了 --inc-base，优先使用
    if [[ -n "${specified_base}" ]]; then
        local base_path="${BACKUP_BASE_DIR}/${specified_base}"
        if [[ ! -d "${base_path}" ]]; then
            fatal_exit "指定的基础备份不存在: ${specified_base}"
        fi
        base_dir="${base_path}"
        log_msg "INFO" "基于指定备份 (--inc-base): ${base_dir}"
    else
        # 未指定时，优先基于上一次增量；否则基于最近一次全量
        last_full=$(read_state "LAST_FULL")
        last_inc=$(read_state "LAST_INC")

        if [[ -n "${last_inc}" && -d "${last_inc}" ]]; then
            base_dir="${last_inc}"
            log_msg "INFO" "基于上一次增量: ${base_dir}"
        elif [[ -n "${last_full}" && -d "${last_full}" ]]; then
            base_dir="${last_full}"
            log_msg "INFO" "基于全量备份: ${base_dir}"
        else
            log_msg "WARN" "未找到有效基础备份，自动降级为全量备份。"
            do_full_backup
            return
        fi
    fi

    local backup_dir="${BACKUP_BASE_DIR}/inc_${DATE_FMT}"
    mkdir -p "${backup_dir}"

    log_msg "INFO" "增量目录: ${backup_dir}"

    local start_ts end_ts duration
    start_ts=$(date +%s)

    # 执行备份：详细输出只写入日志文件，不输出到控制台
    if ! ${XTRABACKUP_CMD} --backup \
        --target-dir="${backup_dir}" \
        --incremental-basedir="${base_dir}" \
        --socket="${MYSQL_SOCKET}" \
        --user="${MYSQL_USER}" \
        --password="${MYSQL_PASSWORD}" \
        --host="${MYSQL_HOST}" \
        --port="${MYSQL_PORT}" \
        --parallel=${PARALLEL} \
        ${COMPRESS:+${COMPRESS}} \
        ${ENCRYPT:+${ENCRYPT}} \
        >> "${LOG_FILE}" 2>&1; then
        log_msg "ERROR" "增量备份执行失败，删除不完整目录。"
        rm -rf "${backup_dir}"
        send_notify "FAILED" "增量备份失败"
        fatal_exit
    fi

    end_ts=$(date +%s)
    duration=$((end_ts - start_ts))

    # 记录该增量归属的全量（恢复时用于关联）
    local full_base
    full_base=$(read_state "LAST_FULL_NAME")
    write_state "LAST_INC" "${backup_dir}"
    write_state "BASE_FOR_$(basename "${backup_dir}")" "${full_base}"

    log_msg "INFO" "增量备份完成。耗时: ${duration}s, 大小: $(get_size "${backup_dir}")"
    send_notify "SUCCESS" "增量备份完成，耗时 ${duration}s"
    log_msg "INFO" "========== 增量备份结束 =========="
}

#-------------------------------------------------------------------------------
# 准备备份（apply-log / 合并增量）
#-------------------------------------------------------------------------------
do_prepare() {
    local target_dir="$1"
    local apply_only="${2:-yes}"

    log_msg "INFO" "准备备份: ${target_dir} (apply-log-only=${apply_only})"

    # 执行 prepare：详细输出只写入日志文件，不输出到控制台
    local apply_opt=""
    if [[ "${apply_only}" == "yes" ]]; then
        apply_opt="--apply-log-only"
    fi

    if ! ${XTRABACKUP_CMD} --prepare ${apply_opt} --use-memory=${MEMORY} --target-dir="${target_dir}" >> "${LOG_FILE}" 2>&1; then
        fatal_exit "备份准备失败: ${target_dir}"
    fi
}

#-------------------------------------------------------------------------------
# 恢复备份
# 参数: $1 = --restore-to 指定的目标备份名称（可选）
#       如果指向 full_*: 只恢复到该全量，不合并任何增量
#       如果指向 inc_* : 恢复到该增量点（合并到该增量为止）
#       如果未指定    : 恢复到最近一次全量及其所有关联增量
#-------------------------------------------------------------------------------
do_restore() {
    local restore_to="${1:-}"

    log_msg "INFO" "========== 开始恢复流程 =========="
    check_root

    # 1. 检查 MySQL 已停止
    if pgrep -x mysqld &>/dev/null; then
        fatal_exit "MySQL 仍在运行，请先停止: systemctl stop mysqld"
    fi

    # 2. 解析 --restore-to 参数
    local target_full=""      # 最终要恢复的全量备份路径
    local target_inc_name=""  # 如果 restore_to 是增量，记录增量名
    local merge_all_inc=1     # 默认合并该全量后的所有增量

    if [[ -n "${restore_to}" ]]; then
        local restore_path="${BACKUP_BASE_DIR}/${restore_to}"
        if [[ ! -d "${restore_path}" ]]; then
            fatal_exit "指定的恢复目标不存在: ${restore_to}"
        fi

        if [[ "${restore_to}" == full_* ]]; then
            # 指定的是全量：只恢复该全量，不合并任何增量
            target_full="${restore_path}"
            merge_all_inc=0
            log_msg "INFO" "恢复目标为全量: ${restore_to}（不合并增量）"
        elif [[ "${restore_to}" == inc_* ]]; then
            # 指定的是增量：恢复到该增量点
            target_inc_name="${restore_to}"
            local inc_base
            inc_base=$(read_state "BASE_FOR_${restore_to}")
            if [[ -z "${inc_base}" || ! -d "${BACKUP_BASE_DIR}/${inc_base}" ]]; then
                fatal_exit "无法找到增量 ${restore_to} 所属的全量备份"
            fi
            target_full="${BACKUP_BASE_DIR}/${inc_base}"
            log_msg "INFO" "恢复目标为增量: ${restore_to}，所属全量: ${inc_base}"
        else
            fatal_exit "恢复目标名称格式错误，必须以 full_ 或 inc_ 开头: ${restore_to}"
        fi
    else
        # 未指定 --restore-to，使用最近一次全量
        target_full=$(read_state "LAST_FULL")
        if [[ -z "${target_full}" || ! -d "${target_full}" ]]; then
            log_msg "ERROR" "未找到有效的全量备份目录。"
            log_msg "INFO" "可用全量备份列表:"
            ls -1td "${BACKUP_BASE_DIR}"/full_* 2>/dev/null | head -5 | while read -r d; do
                log_msg "INFO" "  - $(basename "$d")"
            done
            fatal_exit "未指定有效的全量备份"
        fi
        log_msg "INFO" "未指定 --restore-to，使用最近一次全量: $(basename "${target_full}")"
    fi

    # 3. 创建临时工作目录，复制全量备份（避免破坏原始备份）
    local work_dir="${BACKUP_BASE_DIR}/.restore_work_$(date +%s)"
    local work_full="${work_dir}/full"
    mkdir -p "${work_dir}"

    log_msg "INFO" "复制全量备份到工作目录: ${work_full}"
    if ! cp -a "${target_full}" "${work_full}"; then
        rm -rf "${work_dir}"
        fatal_exit "复制全量备份到工作目录失败"
    fi

    # 4. 在工作目录的全量上解压（如果需要）
    decompress_if_needed "${work_full}"

    # 5. 确认/清空数据目录
    if [[ -d "${MYSQL_DATADIR}" ]]; then
        if [[ -n "$(ls -A "${MYSQL_DATADIR}" 2>/dev/null)" ]]; then
            log_msg "WARN" "数据目录 ${MYSQL_DATADIR} 非空！"
            read -r -p ">>> 确认清空数据目录并继续恢复? [y/N]: " confirm
            if [[ "${confirm}" != "y" && "${confirm}" != "Y" ]]; then
                rm -rf "${work_dir}"
                log_msg "INFO" "恢复已取消。"
                exit 0
            fi
            rm -rf "${MYSQL_DATADIR:?}"/*
        fi
    else
        mkdir -p "${MYSQL_DATADIR}"
    fi

    # 6. 查找该全量对应的所有增量备份
    local full_name inc_all=() inc_list=()
    full_name=$(basename "${target_full}")

    while IFS= read -r inc_dir; do
        [[ -z "${inc_dir}" ]] && continue
        local inc_base
        inc_base=$(read_state "BASE_FOR_$(basename "${inc_dir}")")
        if [[ "${inc_base}" == "${full_name}" ]]; then
            inc_all+=("${inc_dir}")
        fi
    done < <(find "${BACKUP_BASE_DIR}" -maxdepth 1 -type d -name "inc_*" | sort)

    # 7. 根据 --restore-to 参数决定合并哪些增量
    if [[ "${merge_all_inc}" -eq 0 ]]; then
        # 指定了全量，不合并任何增量
        inc_list=()
        log_msg "INFO" "只恢复全量，跳过增量合并。"
    elif [[ -n "${target_inc_name}" ]]; then
        # 恢复到指定增量点：只合并到该增量为止
        local found=0
        for inc_dir in "${inc_all[@]}"; do
            if [[ "${found}" -eq 1 || "$(basename "${inc_dir}")" == "${target_inc_name}" ]]; then
                inc_list+=("${inc_dir}")
                found=1
                if [[ "$(basename "${inc_dir}")" == "${target_inc_name}" ]]; then
                    break
                fi
            fi
        done
        if [[ ${#inc_list[@]} -eq 0 ]]; then
            rm -rf "${work_dir}"
            fatal_exit "未找到指定的增量备份: ${target_inc_name}（或不属于全量 ${full_name}）"
        fi
        log_msg "INFO" "恢复到增量点: ${target_inc_name}，将合并 ${#inc_list[@]} 个增量"
    else
        # 恢复到全量：合并该全量后的所有增量
        inc_list=("${inc_all[@]}")
        log_msg "INFO" "找到 ${#inc_list[@]} 个增量备份待合并。"
    fi

    # 8. 依次解压增量（在工作目录创建增量副本）
    local work_inc_list=()
    for inc_dir in "${inc_list[@]}"; do
        local work_inc="${work_dir}/$(basename "${inc_dir}")"
        log_msg "INFO" "复制增量到工作目录: $(basename "${inc_dir}")"
        cp -a "${inc_dir}" "${work_inc}"
        decompress_if_needed "${work_inc}"
        work_inc_list+=("${work_inc}")
    done

    # 9. 在工作目录的全量上 prepare
    if [[ ${#work_inc_list[@]} -gt 0 ]]; then
        do_prepare "${work_full}" "yes"
    else
        do_prepare "${work_full}" "no"
    fi

    # 10. 依次合并增量到工作目录的全量
    # 修复：((i++)) 在 set -e 下当 i=0 时返回 1 会导致脚本异常退出
    # 改为 i=$((i + 1)) 避免此陷阱
    local total_inc=${#work_inc_list[@]}
    local i=0
    for work_inc in "${work_inc_list[@]}"; do
        i=$((i + 1))
        log_msg "INFO" "合并增量 [${i}/${total_inc}]: $(basename "${work_inc}")"
        local apply_opt=""
        if [[ ${i} -lt ${total_inc} ]]; then
            apply_opt="--apply-log-only"
        fi

        # 执行增量合并：详细输出只写入日志文件，不输出到控制台
        if ! ${XTRABACKUP_CMD} --prepare ${apply_opt} --use-memory=${MEMORY} \
            --target-dir="${work_full}" \
            --incremental-dir="${work_inc}" >> "${LOG_FILE}" 2>&1; then
            rm -rf "${work_dir}"
            fatal_exit "合并增量失败: $(basename "${work_inc}")"
        fi
    done

    # 11. 从工作目录复制回数据目录
    log_msg "INFO" "执行 --copy-back 到 ${MYSQL_DATADIR}"
    if ! ${XTRABACKUP_CMD} --copy-back \
        --target-dir="${work_full}" \
        --datadir="${MYSQL_DATADIR}" >> "${LOG_FILE}" 2>&1; then
        rm -rf "${work_dir}"
        fatal_exit "--copy-back 失败"
    fi

    # 12. 修正权限
    chown -R mysql:mysql "${MYSQL_DATADIR}"
    chmod 750 "${MYSQL_DATADIR}"

    # 13. 清理临时工作目录
    rm -rf "${work_dir}"

    log_msg "INFO" "恢复完成。请确认权限后启动 MySQL: systemctl start mysqld"
    log_msg "INFO" "========== 恢复流程结束 =========="
}

#-------------------------------------------------------------------------------
# 清理旧备份
#-------------------------------------------------------------------------------
do_cleanup() {
    log_msg "INFO" "========== 开始清理旧备份 =========="

    local deleted=0

    # 清理过期全量
    # 修复：((deleted++)) 在 set -e 下当 deleted=0 时返回 1 会导致脚本异常退出
    # 改为 deleted=$((deleted + 1)) 避免此陷阱
    while IFS= read -r dir; do
        [[ -z "${dir}" ]] && continue
        log_msg "INFO" "删除过期全量: $(basename "${dir}")"
        rm -rf "${dir}"
        deleted=$((deleted + 1))
    done < <(find "${BACKUP_BASE_DIR}" -maxdepth 1 -type d -name "full_*" -mtime +${RETENTION_FULL_DAYS})

    # 清理过期增量
    while IFS= read -r dir; do
        [[ -z "${dir}" ]] && continue
        log_msg "INFO" "删除过期增量: $(basename "${dir}")"
        rm -rf "${dir}"
        deleted=$((deleted + 1))
    done < <(find "${BACKUP_BASE_DIR}" -maxdepth 1 -type d -name "inc_*" -mtime +${RETENTION_INC_DAYS})

    # 清理孤儿增量（对应全量已不存在）
    for inc_dir in "${BACKUP_BASE_DIR}"/inc_*; do
        [[ -e "${inc_dir}" ]] || continue
        local inc_base
        inc_base=$(read_state "BASE_FOR_$(basename "${inc_dir}")")
        if [[ -n "${inc_base}" && ! -d "${BACKUP_BASE_DIR}/${inc_base}" ]]; then
            log_msg "INFO" "删除孤儿增量: $(basename "${inc_dir}")"
            rm -rf "${inc_dir}"
            deleted=$((deleted + 1))
        fi
    done

    # 清理过期日志（按日期滚动）
    local log_deleted=0
    while IFS= read -r logf; do
        [[ -z "${logf}" ]] && continue
        log_msg "INFO" "删除过期日志: $(basename "${logf}")"
        rm -f "${logf}"
        log_deleted=$((log_deleted + 1))
    done < <(find "${LOG_DIR}" -maxdepth 1 -type f -name "mysql_backup_*.log" -mtime +${RETENTION_LOG_DAYS})

    # 清理残留的临时恢复目录（以防之前恢复异常退出未清理）
    local work_deleted=0
    while IFS= read -r workd; do
        [[ -z "${workd}" ]] && continue
        log_msg "INFO" "删除残留临时恢复目录: $(basename "${workd}")"
        rm -rf "${workd}"
        work_deleted=$((work_deleted + 1))
    done < <(find "${BACKUP_BASE_DIR}" -maxdepth 1 -type d -name ".restore_work_*" -mtime +1)

    log_msg "INFO" "清理完成，共删除 ${deleted} 个备份，${log_deleted} 个日志文件，${work_deleted} 个临时目录。"
    log_msg "INFO" "========== 清理结束 =========="
}

#-------------------------------------------------------------------------------
# 查看状态
#-------------------------------------------------------------------------------
do_status() {
    echo ""
    echo "╔════════════════════════════════════════════════════════════╗"
    echo "║                   MySQL 备份状态面板                        ║"
    echo "╚════════════════════════════════════════════════════════════╝"
    echo "备份根目录: ${BACKUP_BASE_DIR}"
    echo "日志目录:   ${LOG_DIR}"
    echo ""

    echo "--- 全量备份 ---"
    if ls -1d "${BACKUP_BASE_DIR}"/full_* &>/dev/null; then
        ls -1td "${BACKUP_BASE_DIR}"/full_* | while read -r d; do
            printf "  %-28s  %6s\n" "$(basename "$d")" "$(get_size "$d")"
        done
    else
        echo "  (无)"
    fi

    echo ""
    echo "--- 增量备份 ---"
    if ls -1d "${BACKUP_BASE_DIR}"/inc_* &>/dev/null; then
        ls -1td "${BACKUP_BASE_DIR}"/inc_* | while read -r d; do
            local base
            base=$(read_state "BASE_FOR_$(basename "$d")")
            printf "  %-28s  %6s  (基于: %s)\n" "$(basename "$d")" "$(get_size "$d")" "${base:-未知}"
        done
    else
        echo "  (无)"
    fi

    echo ""
    echo "--- 日志文件 ---"
    if ls -1 "${LOG_DIR}"/mysql_backup_*.log &>/dev/null; then
        ls -1t "${LOG_DIR}"/mysql_backup_*.log | head -5 | while read -r f; do
            printf "  %-28s  %6s\n" "$(basename "$f")" "$(get_size "$f")"
        done
    else
        echo "  (无)"
    fi

    echo ""
    echo "--- 当前状态 ---"
    if [[ -f "${STATE_FILE}" ]]; then
        cat "${STATE_FILE}" | sed 's/^/  /'
    else
        echo "  (无状态记录)"
    fi
    echo ""
}

#-------------------------------------------------------------------------------
# 参数解析
#-------------------------------------------------------------------------------
parse_args() {
    ACTION=""
    INC_BASE=""
    RESTORE_TO=""

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --full)
                ACTION="full"
                shift
                ;;
            --inc)
                ACTION="inc"
                shift
                ;;
            --inc-base)
                if [[ -n "${2:-}" && "${2:0:1}" != "-" ]]; then
                    INC_BASE="$2"
                    shift 2
                else
                    echo "ERROR: --inc-base 需要指定一个备份名称" >&2
                    exit 1
                fi
                ;;
            --restore)
                ACTION="restore"
                shift
                ;;
            --restore-to)
                if [[ -n "${2:-}" && "${2:0:1}" != "-" ]]; then
                    RESTORE_TO="$2"
                    shift 2
                else
                    echo "ERROR: --restore-to 需要指定一个备份名称" >&2
                    exit 1
                fi
                ;;
            --cleanup)
                ACTION="cleanup"
                shift
                ;;
            --status)
                ACTION="status"
                shift
                ;;
            -h|--help)
                usage
                exit 0
                ;;
            *)
                echo "ERROR: 未知参数: $1" >&2
                usage
                exit 1
                ;;
        esac
    done

    if [[ -z "${ACTION}" ]]; then
        echo "ERROR: 必须指定一个操作模式" >&2
        usage
        exit 1
    fi
}

#-------------------------------------------------------------------------------
# 使用帮助
#-------------------------------------------------------------------------------
usage() {
    cat << EOF
用法: $0 {--full|--inc [--inc-base <name>]|--restore [--restore-to <name>]|--cleanup|--status}

命令说明:
  --full                  执行全量备份
  --inc                   执行增量备份（基于最近一次全量或增量）
  --inc-base <name>       指定基于哪个备份做增量（全量或增量均可）
  --restore               执行恢复备份
  --restore-to <name>     指定恢复到哪个备份点：
                            - 全量名(full_xxx): 恢复到该全量并合并其后的所有增量
                            - 增量名(inc_xxx) : 恢复到该增量点（合并到该增量为止）
                            - 不指定          : 恢复到最近一次全量及其关联增量
  --cleanup               按保留策略清理过期备份与日志
  --status                查看备份状态与存储情况
  -h, --help              显示此帮助信息

示例:
  # 执行全量备份
  ./backup.sh --full

  # 执行增量备份（自动基于最近一次）
  ./backup.sh --inc

  # 基于指定全量做增量
  ./backup.sh --inc --inc-base full_20260813_150813

  # 基于指定增量做增量（链式增量）
  ./backup.sh --inc --inc-base inc_20260813_152745

  # 恢复到最近一次全量（自动合并所有增量）
  ./backup.sh --restore

  # 恢复到指定全量
  ./backup.sh --restore --restore-to full_20260813_150813

  # 恢复到指定增量点
  ./backup.sh --restore --restore-to inc_20260813_152745

  # 清理过期备份与日志
  ./backup.sh --cleanup

  # 查看备份状态
  ./backup.sh --status
EOF
}

#-------------------------------------------------------------------------------
# 主入口
#-------------------------------------------------------------------------------
main() {
    # 防并发锁（flock）
    exec 200>"${LOCK_FILE}"
    if ! flock -n 200; then
        log_msg "ERROR" "另一个备份进程正在运行，退出。"
        fatal_exit
    fi

    # 解析参数
    parse_args "$@"

    mkdir -p "${BACKUP_BASE_DIR}" "${LOG_DIR}"

    case "${ACTION}" in
        full)
            do_full_backup
            do_cleanup
            ;;
        inc)
            do_inc_backup "${INC_BASE}"
            do_cleanup
            ;;
        restore)
            do_restore "${RESTORE_TO}"
            ;;
        cleanup)
            do_cleanup
            ;;
        status)
            do_status
            ;;
        *)
            usage
            exit 1
            ;;
    esac
}

main "$@"