#!/usr/bin/env bash
# ============================================================================
#  mysql_backup.sh — MySQL / MariaDB 自动备份工具
#
#  功能：
#    * 支持单库、多库、全库自动发现备份
#    * 支持按库拆分备份 / 全库合并备份，gzip 压缩
#    * 可配置留存天数（自动删除超过 N 天的旧备份）
#    * 可移植的并发锁（不依赖 flock，Windows git-bash 也能用）
#    * 可选：通过 rclone 将备份同步到异地 / 云端
#    * 用法：
#         ./mysql_backup.sh            -> 立即执行一次备份
#         ./mysql_backup.sh --init     -> 生成 / 查看 crontab 定时任务行
#         ./mysql_backup.sh --list     -> 列出已有备份
#         ./mysql_backup.sh --test     -> 检查依赖与数据库连接
#         ./mysql_backup.sh --help     -> 显示本说明
#
#  配置写在同目录的 mysql_backup.conf 中，其中每一项都可被同名环境变量覆盖。
# ============================================================================

# 未定义变量视为错误（注意：不使用 -e，失败由我们自己处理）
set -u

# --- 定位脚本所在目录并加载配置 --------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/mysql_backup.conf"

if [[ -f "$CONFIG_FILE" ]]; then
    # 先快照：记录哪些变量已被显式导出到环境变量，
    # 以便让环境变量的值能够覆盖配置文件里设定的值。
    declare -A _env_val=()
    for _v in MYSQL_HOST MYSQL_PORT MYSQL_USER MYSQL_PASSWORD MYSQL_DATABASES \
              INCLUDE_SYSTEM_DBS MYSQLDUMP_OPTIONS BACKUP_DIR RETENTION_DAYS \
              COMPRESS LOG_DIR VERBOSE ENABLE_RCLONE RCLONE_REMOTE; do
        [[ -n "${!_v+x}" ]] && _env_val[$_v]="${!_v}"
    done
    # shellcheck disable=SC1090
    source "$CONFIG_FILE"
    # 还原显式传入的环境变量覆盖
    for _v in "${!_env_val[@]}"; do
        declare -g "$_v=${_env_val[$_v]}"
    done
    unset _v _env_val
else
    echo "ERROR: 找不到配置文件: $CONFIG_FILE" >&2
    exit 1
fi

# 配置文件和环境变量都未设置时使用以下默认值
MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
MYSQL_PORT="${MYSQL_PORT:-3306}"
MYSQL_USER="${MYSQL_USER:-root}"
MYSQL_PASSWORD="${MYSQL_PASSWORD:-}"
MYSQL_DATABASES="${MYSQL_DATABASES:-}"
INCLUDE_SYSTEM_DBS="${INCLUDE_SYSTEM_DBS:-no}"
MYSQLDUMP_OPTIONS="${MYSQLDUMP_OPTIONS:---single-transaction --routines --triggers --events --hex-blob --skip-lock-tables}"
BACKUP_DIR="${BACKUP_DIR:-$SCRIPT_DIR/backups}"
RETENTION_DAYS="${RETENTION_DAYS:-7}"
COMPRESS="${COMPRESS:-yes}"
LOG_DIR="${LOG_DIR:-$SCRIPT_DIR/logs}"
VERBOSE="${VERBOSE:-no}"
ENABLE_RCLONE="${ENABLE_RCLONE:-no}"
RCLONE_REMOTE="${RCLONE_REMOTE:-}"

# --- 日志函数 ----------------------------------------------------------------
mkdir -p "$LOG_DIR" "$BACKUP_DIR"
LOG_FILE="$LOG_DIR/backup_$(date +%Y%m%d).log"

# 记录一条普通日志（VERBOSE=yes 时同时输出到终端）
log() {
    local ts
    ts="$(date '+%Y-%m-%d %H:%M:%S')"
    echo "[$ts] $*" >> "$LOG_FILE"
    if [[ "$VERBOSE" == "yes" ]]; then
        echo "[$ts] $*"
    fi
}
# 记录一条错误日志（始终同时输出到终端）
err() {
    local ts
    ts="$(date '+%Y-%m-%d %H:%M:%S')"
    echo "[$ts] ERROR: $*" | tee -a "$LOG_FILE" >&2
}

# --- 依赖检查 ----------------------------------------------------------------
check_deps() {
    local missing=0
    # mysqldump 与 mysql 是必须的
    for bin in mysqldump mysql; do
        if ! command -v "$bin" >/dev/null 2>&1; then
            err "缺少必需的命令: $bin"
            missing=1
        fi
    done
    # 开启压缩时必须有 gzip
    if [[ "$COMPRESS" == "yes" ]] && ! command -v gzip >/dev/null 2>&1; then
        err "已开启 COMPRESS=yes，但未安装 'gzip'"
        missing=1
    fi
    # 开启 rclone 异地备份时必须有 rclone
    if [[ "$ENABLE_RCLONE" == "yes" ]]; then
        if ! command -v rclone >/dev/null 2>&1; then
            err "已开启 ENABLE_RCLONE=yes，但未安装 'rclone'"
            missing=1
        fi
    fi
    return $missing
}

# 使用临时凭据文件测试连接（避免密码出现在 `ps` 进程列表中）
test_connection() {
    local cnf rc
    cnf="$(mktemp)"
    chmod 600 "$cnf"
    cat > "$cnf" <<EOF
[client]
host=$MYSQL_HOST
port=$MYSQL_PORT
user=$MYSQL_USER
password=$MYSQL_PASSWORD
EOF
    mysql --defaults-extra-file="$cnf" -e "SELECT VERSION();" >/dev/null 2>&1
    rc=$?
    rm -f "$cnf"
    return $rc
}

# --- 获取待备份的数据库列表 -------------------------------------------------
get_databases() {
    if [[ -n "$MYSQL_DATABASES" ]]; then
        # 按空格拆分为多个数据库，每个占一行（便于 while read 逐库处理）
        # shellcheck disable=SC2086
        printf '%s\n' $MYSQL_DATABASES
        return
    fi
    # 未指定具体库时，自动发现所有库
    local cnf out
    cnf="$(mktemp)"; chmod 600 "$cnf"
    cat > "$cnf" <<EOF
[client]
host=$MYSQL_HOST
port=$MYSQL_PORT
user=$MYSQL_USER
password=$MYSQL_PASSWORD
EOF
    out="$(mysql --defaults-extra-file="$cnf" -N -e "SHOW DATABASES;" 2>/dev/null)"
    rm -f "$cnf"
    # 默认排除 MySQL 系统库
    if [[ "$INCLUDE_SYSTEM_DBS" != "yes" ]]; then
        echo "$out" | grep -viE '^(information_schema|performance_schema|mysql|sys)$'
    else
        echo "$out"
    fi
}

# --- 执行备份 ----------------------------------------------------------------
run_backup() {
    log "==== 开始备份 (host=$MYSQL_HOST:$MYSQL_PORT user=$MYSQL_USER) ===="

    local databases db ok=0 fail=0 ts cnf dump_opts out_file
    databases="$(get_databases)"
    if [[ -z "$databases" ]]; then
        err "没有发现可备份的数据库"
        return 1
    fi
    log "待备份数据库: $(echo "$databases" | tr '\n' ' ')"

    # 生成一份凭据文件，供本次所有 dump 复用
    cnf="$(mktemp)"; chmod 600 "$cnf"
    cat > "$cnf" <<EOF
[client]
host=$MYSQL_HOST
port=$MYSQL_PORT
user=$MYSQL_USER
password=$MYSQL_PASSWORD
EOF
    dump_opts="$MYSQLDUMP_OPTIONS"
    ts="$(date +%Y%m%d_%H%M%S)"

    # 逐库导出
    while IFS= read -r db; do
        [[ -z "$db" ]] && continue
        out_file="$BACKUP_DIR/${db}_${ts}.sql"
        log "正在导出数据库: $db"
        if mysqldump --defaults-extra-file="$cnf" $dump_opts "$db" > "$out_file" 2>>"$LOG_FILE"; then
            # 需要压缩则 gzip，并刷新输出文件名
            if [[ "$COMPRESS" == "yes" ]]; then
                gzip -f "$out_file" && out_file="$out_file.gz"
            fi
            log "成功 $db -> $(basename "$out_file") ($(du -h "$out_file" | cut -f1))"
            ok=$((ok+1))
        else
            err "导出失败: $db"
            rm -f "$out_file"
            fail=$((fail+1))
        fi
    done <<< "$databases"
    rm -f "$cnf"

    # 留存清理：删除超过 RETENTION_DAYS 天的旧备份
    if [[ "${RETENTION_DAYS:-0}" -gt 0 ]]; then
        log "留存清理：删除早于 $RETENTION_DAYS 天的备份"
        find "$BACKUP_DIR" -type f \( -name '*.sql' -o -name '*.sql.gz' \) \
             -mtime "+$RETENTION_DAYS" -print -delete >> "$LOG_FILE" 2>&1
    fi

    # 可选的异地拷贝
    if [[ "$ENABLE_RCLONE" == "yes" && -n "$RCLONE_REMOTE" ]]; then
        log "rclone 同步 -> $RCLONE_REMOTE"
        rclone copy "$BACKUP_DIR" "$RCLONE_REMOTE" >> "$LOG_FILE" 2>&1 \
            && log "rclone 同步完成" || err "rclone 同步失败"
    fi

    log "==== 备份结束：成功 $ok 个，失败 $fail 个 ===="
    # 有任何失败则返回非 0，供调用方 / 定时任务判断
    [[ "$fail" -eq 0 ]]
}

# --- 列出已有备份 ------------------------------------------------------------
list_backups() {
    echo "备份目录 $BACKUP_DIR 下的文件："
    if [[ -d "$BACKUP_DIR" ]] && compgen -G "$BACKUP_DIR"/*.sql* >/dev/null; then
        ls -lh "$BACKUP_DIR"/*.sql* | awk '{print $5, $6, $7, $9}'
    else
        echo "  （暂无备份）"
    fi
}

# --- 生成 crontab 定时任务行 ------------------------------------------------
emit_cron() {
    local minute hour
    minute=$((RANDOM % 60)); hour=3   # 默认每天 03:xx（随机分钟，错峰避免整点打满）
    echo "# 请将下面这一行加入你的 crontab（执行 crontab -e 编辑）"
    echo "# 备份会在每天 $hour:$(printf '%02d' "$minute") 执行一次。"
    echo "$minute $hour * * *  '$SCRIPT_DIR/mysql_backup.sh' >> '$LOG_DIR/cron.out' 2>&1"
}

# --- 可移植的单实例锁（无需 flock） ------------------------------------------
LOCK_DIR="$BACKUP_DIR/.lock"
acquire_lock() {
    # 目录创建具有原子性：已存在说明另有备份正在运行
    if ! mkdir "$LOCK_DIR" 2>/dev/null; then
        err "已有备份任务正在运行（锁目录已存在: $LOCK_DIR）"
        exit 1
    fi
    # 脚本退出（含异常）时自动清理锁目录
    trap 'rm -rf "$LOCK_DIR"' EXIT INT TERM
}

# --- 命令行入口 --------------------------------------------------------------
case "${1:-}" in
    --help|-h)
        # 从文件顶部注释块抽取帮助信息（从标题到 set -u 之前）
        awk '/^#  mysql_backup\.sh/{f=1} f&&/^set -u/{exit} f{print}' "${BASH_SOURCE[0]}" \
            | sed 's/^# \{0,1\}//'
        echo
        echo "配置文件位置: $CONFIG_FILE"
        exit 0
        ;;
    --init)
        emit_cron
        exit 0
        ;;
    --list|-l)
        list_backups
        exit 0
        ;;
    --test)
        check_deps || exit 1
        if test_connection; then
            echo "OK: 已找到 mysqldump/mysql，且连接 $MYSQL_HOST 成功。"
            exit 0
        else
            err "连接测试失败（请检查配置文件中的 host/port/user/password）。"
            exit 1
        fi
        ;;
    "")
        check_deps || exit 1
        acquire_lock
        run_backup
        exit $?
        ;;
    *)
        err "未知参数: $1  （用 --help 查看用法）"
        exit 2
        ;;
esac
