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

# =============================================================================
# Script: postgres_backup.sh
# Description: PostgreSQL 热备份（基于 pgBackRest，完全依赖环境变量驱动）
# Version: 1.1.0
# Compatibility: Bash 5.0+ / pgBackRest 2.40+
#
# 【工作流程】:
# 1. 环境初始化：严格的安全模式，定义日志与基础函数。
# 2. 进程互斥锁：使用 flock 确保同一时间只有一个备份进程运行。
# 3. 权限与依赖检查：确保以 Root 权限执行，智能检测并安装 pgBackRest。
# 4. 实例循环处理：通过 PG_INSTANCES 提取环境变量。
# 5. 容量预警：检查备份目标磁盘可用空间是否满足最低阈值 (>1GB)。
# 6. Stanza 自动化：检查并自动创建/初始化 pgBackRest stanza。
# 7. 智能备份：根据日期执行 全量 (周日) 或 增量 (周一至周六) 备份。
# 8. 自动清理：利用 pgBackRest 内置 expire 机制保留指定周期的历史备份。
# =============================================================================

readonly SCRIPT_VERSION="2.0.0"
readonly SCRIPT_NAME="postgres_backup"

# 修复 SC2155，分步声明
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}/postgres_${TIMESTAMP}.log"

# 增加目录创建容错
mkdir -p "${GLOBAL_LOG_DIR}" 2>/dev/null || true

# 捕获中断信号，确保安全退出
trap 'utils::log "ERROR" "⚠️ 接收到中断信号 (SIGINT/SIGTERM)，正在安全退出..."; 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
}

check_disk_space() {
    local target_dir="$1"
    local min_mb="${2:-1024}" # 默认最小预留 1024 MB (1GB)

    # 修复：增加对 mkdir 失败的捕获
    if ! mkdir -p "${target_dir}" 2>>"${LOG_FILE}"; then
        utils::log "ERROR" "无法创建或访问备份目标目录 [${target_dir}]"
        return 1
    fi

    local available_mb
    # 修复：防止 df 异常导致 awk 报错
    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
}

# ----------------------------------------------------------------------------
# 环境依赖安装 (pgBackRest + zstd)
# ----------------------------------------------------------------------------
ensure_pgbackrest() {
    # 修复：除了 pgbackrest，还必须检测 zstd，避免 --compress-type=zstd 失败
    if command -v pgbackrest &>/dev/null && command -v zstd &>/dev/null; then
        local ver; ver=$(pgbackrest --version 2>/dev/null | awk '{print $2}')
        utils::log "INFO" "✓ pgbackrest (v${ver:-未知}) 与 zstd 均已就绪"
        return 0
    fi

    utils::log "INFO" "未检测到 pgbackrest 或 zstd，开始智能安装 (输出将重定向至日志)..."

    export DEBIAN_FRONTEND=noninteractive
    # 修复：去掉 -qq，用重定向+容错 (|| true) 替代，以便排查且防止 set -e 中断
    if command -v apt-get &>/dev/null; then
        apt-get update >>"${LOG_FILE}" 2>&1 || true
        apt-get install -y pgbackrest zstd >>"${LOG_FILE}" 2>&1 || true
    elif command -v dnf &>/dev/null; then
        dnf install -y pgbackrest zstd >>"${LOG_FILE}" 2>&1 || true
    elif command -v yum &>/dev/null; then
        yum install -y pgbackrest zstd >>"${LOG_FILE}" 2>&1 || true
    else
        utils::log "FATAL" "无法识别的包管理器，请手动安装 pgbackrest 和 zstd。"
        exit 1
    fi

    if ! command -v pgbackrest &>/dev/null || ! command -v zstd &>/dev/null; then
        utils::log "FATAL" "自动安装失败，详情请查看日志文件: ${LOG_FILE}"
        exit 1
    fi

    local ver; ver=$(pgbackrest --version 2>/dev/null | awk '{print $2}')
    utils::log "INFO" "✓ 依赖安装成功 (pgbackrest v${ver:-未知})"
}

# ----------------------------------------------------------------------------
# 核心备份逻辑 (单实例)
# ----------------------------------------------------------------------------
backup_one_instance() {
    local instance_id="${1:-}"
    local display_name="${instance_id:-default}"
    local stanza="" pgdata="" repo_path=""

    if [[ -z "${instance_id}" ]]; then
        stanza="${PG_STANZA:-main}"
        pgdata="${PG_PGDATA:-/var/lib/postgresql/16/main}"
        repo_path="${PG_BACKUP_REPO:-/backup/postgres/default}"
    else
        local stanza_var="PG_${instance_id}_STANZA"
        local pgdata_var="PG_${instance_id}_PGDATA"
        local repo_var="PG_${instance_id}_BACKUP_REPO"

        stanza="${!stanza_var:-${instance_id}}"
        pgdata="${!pgdata_var:-}"
        repo_path="${!repo_var:-/backup/postgres/${instance_id}}"
    fi

    if [[ -z "${pgdata}" || ! -d "${pgdata}" ]]; then
        utils::log "WARN" "实例 [${display_name}] 的 PGDATA [${pgdata:-空}] 不存在或未设置，已跳过！"
        return 1
    fi

    if ! check_disk_space "${repo_path}" 1024; then
        return 1
    fi

    utils::log "INFO" "开始处理实例 [${display_name}] → Stanza: ${stanza} | Repo: ${repo_path}"

    local stanza_args=("--stanza=${stanza}" "--pg1-path=${pgdata}" "--repo1-path=${repo_path}")

    # 检测并初始化 Stanza
    if ! pgbackrest "${stanza_args[@]}" info &>/dev/null; then
        utils::log "INFO" "Stanza [${stanza}] 需初始化，正在创建..."
        # 修复：防止初始化失败拖垮脚本
        if ! pgbackrest "${stanza_args[@]}" stanza-create --force 2>&1 | tee -a "${LOG_FILE}"; then
            utils::log "ERROR" "Stanza 创建失败！"
            return 1
        fi
    fi

    # ------------------------------------------------------------------------
    # 智能备份策略判定
    # ------------------------------------------------------------------------
    local backup_type="incr"
    local force_full=0

    # 修复：判定历史备份。如果 grep 不到 type: full 或 incr 证明没备份过
    if ! pgbackrest "${stanza_args[@]}" info 2>/dev/null | grep -qE 'type: (full|incr|diff)'; then
        utils::log "INFO" "💡 [智能判定]: 当前 Stanza 无有效备份记录，首次强制使用全量(full)备份。"
        force_full=1
    elif [[ "$(date +%u)" -eq 7 ]]; then
        utils::log "INFO" "🗓️ [策略调度]: 周日例行全量(full)备份。"
        force_full=1
    else
        utils::log "INFO" "🗓️ [策略调度]: 日常增量(incr)备份。"
    fi

    [[ ${force_full} -eq 1 ]] && backup_type="full"

    # 执行备份（附带自愈回退机制）
    # 修复：使用 if ! ... then return 1 替换原来的静默拖垮机制
    if ! pgbackrest "${stanza_args[@]}" --log-level-console=detail --compress-type=zstd --process-max=4 backup --type="${backup_type}" 2>&1 | tee -a "${LOG_FILE}"; then

        # 自愈逻辑：如果增量失败，很可能是因为前序归档/全量受损，立刻尝试补做一次全量
        if [[ "${backup_type}" != "full" ]]; then
            utils::log "WARN" "⚠️ 增量备份失败！可能是由于历史全量备份缺失或损坏。正在启动降级自愈机制..."
            utils::log "INFO" "🔄 正在尝试强制重新发起全量(full)备份..."

            if ! pgbackrest "${stanza_args[@]}" --log-level-console=detail --compress-type=zstd --process-max=4 backup --type="full" 2>&1 | tee -a "${LOG_FILE}"; then
                utils::log "ERROR" "❌ 自愈失败，全量备份亦无法完成，请排查实例状况！"
                return 1
            fi
            utils::log "INFO" "✅ 自愈成功！已完成全新的全量备份底座打底。"
        else
            utils::log "ERROR" "❌ 备份执行彻底失败！"
            return 1
        fi
    fi

    # 过期清理 (报错则忽略，不影响主流程)
    pgbackrest "${stanza_args[@]}" expire --repo1-retention-full=2 --repo1-retention-archive=7 >>"${LOG_FILE}" 2>&1 || true

    utils::log "INFO" "✓ 实例 [${display_name}] 备份处理圆满完成"
    return 0
}

# ----------------------------------------------------------------------------
# 主入口
# ----------------------------------------------------------------------------
main() {
    utils::log "INFO" "══════════════════════════════════════════════════════"
    utils::log "INFO" "PostgreSQL 智能备份任务 (v${SCRIPT_VERSION}) — ${TIMESTAMP}"
    utils::log "INFO" "日志输出: ${LOG_FILE}"
    utils::log "INFO" "══════════════════════════════════════════════════════"

    check_root_privileges

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

    ensure_pgbackrest

    local has_error=0

    if [[ -n "${PG_INSTANCES:-}" ]]; then
        local instances
        IFS=' ' read -ra instances <<< "${PG_INSTANCES//,/ }"
        for instance in "${instances[@]}"; do
            # 修复：防止输入 "db1,,db2" 导致的脏数据解析出空实例
            [[ -z "${instance}" ]] && continue

            utils::log "INFO" "------------------------------------------------------"
            # 单一实例失败仅记录 has_error，不终止其他实例
            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" "🎉 所有 PostgreSQL 实例均已成功完成备份！"
    fi
}

main "$@"
