#!/usr/bin/env bash
set -euo pipefail
shopt -s inherit_errexit
IFS=$'\n\t'

# =============================================================================
# Script: mysql_backup.sh
# Description: MySQL 多版本智能热备份（全量/增量自动调度，外部环境变量驱动）
# Version: 4.0.0
# Compatibility: Bash 5.0+ / Ubuntu / CentOS / RHEL / Debian
#
# 【工作流程】:
# 1. 权限与依赖：检查 Root 权限，自动安装缺失的系统依赖 (wget, tar, libaio 等)。
# 2. 实例解析：读取 $MYSQL_INSTANCES 获取分组，若为空则执行 default。
# 3. 版本路由：基于 $MYSQL_ID_VERSION (如 5.7, 8.0, 8.4) 智能下载并隔离运行对应版本的 XtraBackup。
# 4. 备份调度：根据历史 Checkpoint 和周几自动执行全量或增量备份。
# 5. 自愈清理：增量失败自动降级为全量备份，并自动清理 7 天前的历史数据。
#
# 【环境变量配置示例】:
# export MYSQL_INSTANCES="db1 db2"
#
# # db1 实例 (MySQL 8.4)
# export MYSQL_db1_VERSION="8.4"
# export MYSQL_db1_USER="backup_user"
# export MYSQL_db1_PASS="secret"
# export MYSQL_db1_BACKUP_BASE="/backup/db1"
#
# # db2 实例 (MySQL 5.7)
# export MYSQL_db2_VERSION="5.7"
# export MYSQL_db2_USER="backup_user"
# export MYSQL_db2_PASS="secret"
# export MYSQL_db2_BACKUP_BASE="/backup/db2"
# =============================================================================

readonly SCRIPT_VERSION="4.0.0"
readonly SCRIPT_NAME="mysql_backup"

TIMESTAMP="$(date '+%Y-%m-%d_%H-%M-%S')"
readonly TIMESTAMP

readonly GLOBAL_LOG_DIR="${BACKUP_LOG_DIR:-/var/log/${SCRIPT_NAME}}"
readonly LOG_FILE="${GLOBAL_LOG_DIR}/mysql_${TIMESTAMP}.log"

mkdir -p "${GLOBAL_LOG_DIR}" 2>/dev/null || true

# 捕获中断信号
trap 'utils::log "ERROR" "⚠️ 接收到中断信号，正在退出..."; exit 130' INT TERM

# ----------------------------------------------------------------------------
# 基础/公共函数
# ----------------------------------------------------------------------------
utils::log() {
    local level="$1"
    local msg="$2"
    local content # 先声明

    # 后赋值
    content="[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] ${msg}"

    echo "${content}" >&2
    echo "${content}" >>"${LOG_FILE}" 2>/dev/null || true
}

check_root_privileges() {
    if [[ $EUID -ne 0 ]]; then
        utils::log "FATAL" "权限不足: 此脚本需要 Root 权限执行。"
        exit 1
    fi
}

detect_glibc_version() {
    if command -v ldd &>/dev/null; then
        ldd --version 2>&1 | head -n1 | awk '{print $NF}' | cut -d. -f1,2
    else
        echo "0.0"
    fi
}

# ----------------------------------------------------------------------------
# 依赖与环境初始化
# ----------------------------------------------------------------------------
install_dependencies() {
    local cmd_deps=("wget" "tar" "gzip" "zstd" "awk")
    local missing=()

    # 检查命令级依赖
    for dep in "${cmd_deps[@]}"; do
        if ! command -v "$dep" &>/dev/null; then
            missing+=("$dep")
        fi
    done

    # 动态检测 libaio (XtraBackup 必备)
    if ! ldconfig -p | grep -q "libaio"; then
        missing+=("libaio")
    fi

    if [[ ${#missing[@]} -eq 0 ]]; then
        utils::log "INFO" "✓ 系统基础依赖检查通过"
        return 0
    fi

    utils::log "INFO" "正在自动安装缺失的依赖: ${missing[*]} ..."

    if command -v apt-get &>/dev/null; then
        export DEBIAN_FRONTEND=noninteractive
        apt-get update -y -q >/dev/null 2>&1
        local apt_pkg=("${missing[@]}")
        # Ubuntu/Debian 中 libaio 包名为 libaio1
        apt_pkg=("${apt_pkg[@]/libaio/libaio1}")
        apt-get install -y -q "${apt_pkg[@]}" >/dev/null 2>&1
    elif command -v yum &>/dev/null; then
        yum install -y -q "${missing[@]}" >/dev/null 2>&1
    elif command -v dnf &>/dev/null; then
        dnf install -y -q "${missing[@]}" >/dev/null 2>&1
    else
        utils::log "FATAL" "不支持的包管理器，请手动安装: ${missing[*]}"
        exit 1
    fi
    utils::log "INFO" "✓ 依赖安装完成"
}

check_disk_space() {
    local target_dir="$1"
    local min_mb="${2:-2048}"

    if ! mkdir -p "${target_dir}" 2>>"${LOG_FILE}"; then
        utils::log "ERROR" "无法创建或访问备份目标目录 [${target_dir}]"
        return 1
    fi

    local available_mb
    available_mb=$(df -Pm "${target_dir}" 2>>"${LOG_FILE}" | awk 'NR==2 {print $4}')

    if [[ ! "${available_mb}" =~ ^[0-9]+$ ]]; then
        utils::log "ERROR" "无法准确获取目录 [${target_dir}] 的磁盘空间信息。"
        return 1
    fi

    if [[ "${available_mb}" -lt "${min_mb}" ]]; then
        utils::log "ERROR" "磁盘空间不足！目录 [${target_dir}] 仅剩 ${available_mb}MB，要求预留 ${min_mb}MB。"
        return 1
    fi
    return 0
}

# ----------------------------------------------------------------------------
# 核心: 多版本 XtraBackup 智能路由与下载
# 注意: 该函数通过 stdout 返回 XtraBackup 可执行文件的绝对路径
# ----------------------------------------------------------------------------
get_xtrabackup_bin() {
    local mysql_ver="$1"
    local xb_major="" xb_full="" tarball="" base_url=""

    # 1. 版本匹配路由
    if [[ "${mysql_ver}" == 5.6* || "${mysql_ver}" == 5.7* ]]; then
        xb_major="2.4"
        xb_full="2.4.29"
        tarball="percona-xtrabackup-2.4.29-Linux-x86_64.glibc2.12-minimal.tar.gz"
        base_url="https://downloads.percona.com/downloads/Percona-XtraBackup-2.4/Percona-XtraBackup-${xb_full}/binary/tarball/${tarball}"
    elif [[ "${mysql_ver}" == 8.0* ]]; then
        xb_major="8.0"
        xb_full="8.0.35-30"
        tarball="percona-xtrabackup-8.0.35-30-Linux-x86_64.glibc2.17-minimal.tar.gz"
        base_url="https://downloads.percona.com/downloads/Percona-XtraBackup-8.0/Percona-XtraBackup-${xb_full}/binary/tarball/${tarball}"
    elif [[ "${mysql_ver}" == 8.4* ]]; then
        xb_major="8.4"
        xb_full="8.4.0-5"
        local glibc_ver
        glibc_ver=$(detect_glibc_version)
        if [[ "$(printf '%s\n' "$glibc_ver" "2.39" | sort -V | tail -n1)" == "$glibc_ver" ]]; then
            tarball="percona-xtrabackup-${xb_full}-Linux-x86_64.glibc2.39-minimal.tar.gz"
        elif [[ "$(printf '%s\n' "$glibc_ver" "2.34" | sort -V | tail -n1)" == "$glibc_ver" ]]; then
            tarball="percona-xtrabackup-${xb_full}-Linux-x86_64.glibc2.34-minimal.tar.gz"
        else
            tarball="percona-xtrabackup-${xb_full}-Linux-x86_64.glibc2.28-minimal.tar.gz"
        fi
        base_url="https://downloads.percona.com/downloads/Percona-XtraBackup-8.4/Percona-XtraBackup-${xb_full}/binary/tarball/${tarball}"
    else
        utils::log "FATAL" "不支持的 MySQL 版本号 [${mysql_ver}]。合法前缀为: 5.6, 5.7, 8.0, 8.4"
        return 1
    fi

    # aarch64 架构转换
    [[ "$(uname -m)" == "aarch64" ]] && base_url="${base_url/x86_64/aarch64}"

    local install_dir="/opt/percona-xtrabackup-${xb_full}"
    local bin_path="${install_dir}/bin/xtrabackup"

    # 2. 检查本地是否已有该版本
    if [[ -x "${bin_path}" ]]; then
        echo "${bin_path}"
        return 0
    fi

    # 3. 下载与安装过程 (日志输出至 stderr 以防干扰 stdout 路径返回)
    utils::log "INFO" "检测到 MySQL ${mysql_ver}，正在智能下载适配的 XtraBackup ${xb_major} ..."
    local tmp_dir="/tmp"

    if ! wget -q --show-progress -O "${tmp_dir}/${tarball}" "${base_url}" >&2; then
        utils::log "FATAL" "下载 XtraBackup [${tarball}] 失败，请检查网络。"
        return 1
    fi

    mkdir -p "${install_dir}"
    if ! tar -xzf "${tmp_dir}/${tarball}" -C "${install_dir}" --strip-components=1 >&2; then
        utils::log "FATAL" "解压失败: ${tmp_dir}/${tarball}"
        return 1
    fi

    if [[ ! -x "${bin_path}" ]]; then
        utils::log "FATAL" "XtraBackup 安装后二进制不可执行: ${bin_path}"
        return 1
    fi

    utils::log "INFO" "✓ Percona XtraBackup ${xb_full} 准备就绪: ${bin_path}"

    # 核心：将二进制路径通过 stdout 返回给调用者
    echo "${bin_path}"
    return 0
}

# ----------------------------------------------------------------------------
# 核心备份逻辑
# ----------------------------------------------------------------------------
backup_one_instance() {
    local instance_id="${1:-}"
    local display_name="${instance_id:-default}"

    local mysql_ver="" mysql_user="" mysql_pass="" backup_base="" host_val="" port_val="" socket_val=""

    if [[ -z "${instance_id}" ]]; then
        mysql_ver="${MYSQL_VERSION:-8.0}"
        mysql_user="${MYSQL_USER:-}"
        mysql_pass="${MYSQL_PASS:-}"
        backup_base="${BACKUP_BASE:-/backup/databases/mysql_default}"
        host_val="${MYSQL_HOST:-}"
        port_val="${MYSQL_PORT:-}"
        socket_val="${MYSQL_SOCKET:-}"
    else
        local ver_var="MYSQL_${instance_id}_VERSION"
        local user_var="MYSQL_${instance_id}_USER"
        local pass_var="MYSQL_${instance_id}_PASS"
        local base_var="MYSQL_${instance_id}_BACKUP_BASE"
        local host_var="MYSQL_${instance_id}_HOST"
        local port_var="MYSQL_${instance_id}_PORT"
        local socket_var="MYSQL_${instance_id}_SOCKET"

        mysql_ver="${!ver_var:-${MYSQL_VERSION:-8.0}}"
        mysql_user="${!user_var:-}"
        mysql_pass="${!pass_var:-}"
        backup_base="${!base_var:-/backup/databases/mysql_${instance_id}}"
        host_val="${!host_var:-}"
        port_val="${!port_var:-}"
        socket_val="${!socket_var:-}"
    fi

    if [[ -z "${mysql_user}" || -z "${mysql_pass}" ]]; then
        utils::log "WARN" "实例 [${display_name}] 缺失账号或密码，已跳过！"
        return 1
    fi

    utils::log "INFO" "开始处理实例 [${display_name}] (MySQL ${mysql_ver}) → 目标目录: ${backup_base}"

    # 1. 动态获取/安装对应版本的 XtraBackup
    local xtrabackup_cmd
    if ! xtrabackup_cmd=$(get_xtrabackup_bin "${mysql_ver}"); then
        utils::log "ERROR" "无法加载适用于实例 [${display_name}] 的 XtraBackup！"
        return 1
    fi

    # 2. 磁盘预检
    if ! check_disk_space "${backup_base}" 2048; then
        return 1
    fi

    local backup_dir="${backup_base}/${TIMESTAMP}"
    local extra_args=()
    [[ -n "${host_val}" ]] && extra_args+=("--host=${host_val}")
    [[ -n "${port_val}" ]] && extra_args+=("--port=${port_val}")
    [[ -n "${socket_val}" ]] && extra_args+=("--socket=${socket_val}")

    # 特殊处理：XtraBackup 2.4 不支持 zstd
    if [[ "${mysql_ver}" == 8.0* || "${mysql_ver}" == 8.4* ]]; then
        extra_args+=("--compress=zstd")
    fi

    local force_full=0
    local latest_base=""

    # 寻找最近的一次有效全量或增量备份
    latest_base=$(find "${backup_base}" -maxdepth 2 -name 'xtrabackup_checkpoints' -exec dirname {} \; 2>/dev/null | sort | tail -n 1 || true)

    if [[ -z "${latest_base}" ]]; then
        utils::log "INFO" "💡 [智能判定]: 未发现有效的历史备份记录，强制执行全量(full)备份。"
        force_full=1
    elif [[ "$(date +%u)" -eq 7 ]]; then
        utils::log "INFO" "🗓️ [策略调度]: 周日例行全量(full)备份。"
        force_full=1
    else
        utils::log "INFO" "🗓️ [策略调度]: 日常增量(incr)备份。基准点: ${latest_base}"
    fi

    if ! mkdir -p "${backup_dir}" 2>>"${LOG_FILE}"; then
        utils::log "ERROR" "无法创建本次备份目录 [${backup_dir}]"
        return 1
    fi

    # 3. 执行备份
    if [[ ${force_full} -eq 1 ]]; then
        if ! "${xtrabackup_cmd}" --backup --target-dir="${backup_dir}" --user="${mysql_user}" --password="${mysql_pass}" "${extra_args[@]}" --parallel=4 2>&1 | tee -a "${LOG_FILE}"; then
            utils::log "ERROR" "❌ 全量备份执行失败！"
            return 1
        fi
    else
        if ! "${xtrabackup_cmd}" --backup --target-dir="${backup_dir}" --incremental-basedir="${latest_base}" --user="${mysql_user}" --password="${mysql_pass}" "${extra_args[@]}" --parallel=4 2>&1 | tee -a "${LOG_FILE}"; then
            utils::log "WARN" "⚠️ 增量备份失败！历史基准可能损坏或已失效。"
            utils::log "INFO" "🔄 正在触发自愈机制：强制退回并重新发起全新的全量备份..."

            rm -rf "${backup_dir}" 2>/dev/null || true
            local fallback_dir="${backup_dir}_fallback_full"
            mkdir -p "${fallback_dir}"

            if ! "${xtrabackup_cmd}" --backup --target-dir="${fallback_dir}" --user="${mysql_user}" --password="${mysql_pass}" "${extra_args[@]}" --parallel=4 2>&1 | tee -a "${LOG_FILE}"; then
                utils::log "ERROR" "❌ 自愈失败！全量降级备份亦无法完成！"
                return 1
            fi
            utils::log "INFO" "✅ 自愈成功！已重新完成打底全量备份。"
        fi
    fi

    # 4. 清理过期备份
    find "${backup_base}" -maxdepth 1 -type d -name '20*' -mtime +7 -exec rm -rf {} + 2>/dev/null || true
    utils::log "INFO" "✓ 实例 [${display_name}] 完毕"
    return 0
}

# ----------------------------------------------------------------------------
# 主入口
# ----------------------------------------------------------------------------
main() {
    utils::log "INFO" "══════════════════════════════════════════════════════"
    utils::log "INFO" "MySQL 智能备份任务 (v${SCRIPT_VERSION}) — ${TIMESTAMP}"
    utils::log "INFO" "══════════════════════════════════════════════════════"

    check_root_privileges

    # 文件锁机制
    exec 9>"/var/run/${SCRIPT_NAME}.lock"
    if ! flock -n 9; then
        utils::log "FATAL" "检测到另一个备份进程正在运行，为避免踩踏，本次任务退出。"
        exit 1
    fi

    # 自动检查并安装系统级依赖包
    install_dependencies

    local has_error=0

    if [[ -n "${MYSQL_INSTANCES:-}" ]]; then
        local instances
        IFS=' ' read -ra instances <<<"${MYSQL_INSTANCES//,/ }"

        for instance in "${instances[@]}"; do
            [[ -z "${instance}" ]] && continue

            utils::log "INFO" "------------------------------------------------------"
            backup_one_instance "${instance}" || {
                utils::log "ERROR" "❌ 实例 [${instance}] 任务链中断，继续处理下一实例..."
                has_error=1
            }
        done
    else
        utils::log "INFO" "------------------------------------------------------"
        backup_one_instance "" || has_error=1
    fi

    utils::log "INFO" "══════════════════════════════════════════════════════"
    if [[ ${has_error} -eq 1 ]]; then
        utils::log "ERROR" "⚠️ 备份任务结束，部分实例处理失败，请查阅日志！"
        exit 1
    else
        utils::log "INFO" "🎉 所有 MySQL 实例均已成功完成备份！"
    fi
}

main "$@"
