#!/usr/bin/env bash
# =============================================================================
# Script: restic_backup.sh
# Description: 文件增量加密备份脚本 (V3.1 终极版 - 安全解耦与智能防呆 + 自动运维)
# Version: 3.1.2
# Compatibility: Bash 5.0+
#
# 【定时任务配置指南】(建议以 root 运行: sudo crontab -e)
#   0 3 * * * /usr/bin/env bash /opt/scripts/restic_backup.sh
#
# ▶ 查看所有快照: restic snapshots
# ▶ 查看最新快照: restic snapshots latest
# ▶ 查看快照内容: restic ls <snapshot-ID>
# ▶ 查看最新快照内容: restic ls latest
#
# ▶ 恢复最新快照: restic restore latest --target /tmp/restore_folder
# ▶ 恢复指定快照: restic restore abc12345 --target /tmp/restore_folder
# ▶ 仅恢复部分目录: restic restore latest --target /tmp/partial --include "/etc/nginx"
#
# ▶ 检查仓库健康: restic check
# ▶ 清理旧快照: restic forget --keep-last 10 --keep-daily 7 --prune
# =============================================================================
set -euo pipefail
shopt -s inherit_errexit
IFS=$'\n\t'

# ─────────────────────────────────────────────────────────────────────────────
# 【用户自定义区域】
# ─────────────────────────────────────────────────────────────────────────────

# --- 1. 需要备份的目录 (绝对路径) ---
declare -ar BACKUP_PATHS=(
    "/opt/pod"
    # "/var/www/html"
)

# --- 2. 需要排除的规则 (支持 Restic 通配符 **) ---
declare -ar EXCLUDE_PATTERNS=(
    "/opt/pod/**/logs"
    "/opt/pod/**/cache"
    "/opt/pod/**/node_modules"
    "*.tmp"
    "*.pid"
    "*.sock"
)

# --- 3. 云端快照保留策略 ---
readonly KEEP_LAST=3
readonly KEEP_WEEKLY=4
readonly KEEP_MONTHLY=2
readonly BACKUP_TAG="system-data"

# ─────────────────────────────────────────────────────────────────────────────
# 【系统常量与环境初始化】
# ─────────────────────────────────────────────────────────────────────────────

# SC2155 fix: 分离声明与赋值，防止命令替换失败时返回值被 readonly/local 掩盖
TIMESTAMP="$(date '+%Y-%m-%d_%H-%M-%S')"
readonly TIMESTAMP

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR

readonly LOG_DIR="${SCRIPT_DIR}/logs"
readonly LOG_FILE="${LOG_DIR}/restic_${TIMESTAMP}.log"
readonly ENV_FILE="${SCRIPT_DIR}/.restic_env"

mkdir -p "${LOG_DIR}"

utils::cleanup() {
    rm -f /tmp/restic_*_linux_*.bz2 /tmp/restic_*_SHA256SUMS /tmp/restic_download.zst 2>/dev/null || true
}
trap utils::cleanup EXIT

utils::log() {
    local level="$1"
    local msg="$2"
    # SC2155 fix: 分离声明与赋值
    local log_content
    log_content="[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] ${msg}"
    echo "${log_content}" >&2
    echo "${log_content}" >>"${LOG_FILE}"
}

log::info() { utils::log "INFO" "$1"; }
log::warn() { utils::log "WARN" "$1"; }
log::error() { utils::log "ERROR" "$1"; }
log::die() {
    utils::log "FATAL" "$1"
    exit 1
}

rotate_logs() {
    local logs=()
    readarray -t logs < <(find "${LOG_DIR}" -maxdepth 1 -name 'restic_*.log' -type f | sort)
    local count="${#logs[@]}"
    if ((count > 30)); then
        local to_delete=$((count - 30))
        for ((i = 0; i < to_delete; i++)); do
            rm -f "${logs[i]}"
        done
    fi
}

# -----------------------------------------------------------------------------
# 阶段 1: 安全凭证加载与前置检查
# -----------------------------------------------------------------------------
pre_flight_checks() {
    log::info "============================================================"
    log::info "阶段 1/5：加载凭证与环境扫描"
    log::info "============================================================"

    # 1. 强制从独立文件加载环境变量
    if [[ ! -f "${ENV_FILE}" ]]; then
        log::die "找不到安全配置文件！请在 ${SCRIPT_DIR} 下创建 .restic_env 文件。"
    fi

    local env_perms
    env_perms=$(stat -c "%a" "${ENV_FILE}" || true)
    if [[ "${env_perms}" != "600" ]]; then
        log::warn "安全警告: ${ENV_FILE} 的权限为 ${env_perms}，建议执行 'chmod 600 ${ENV_FILE}' 以防密钥泄露！"
    fi

    # SC1090 fix: 路径已由前置逻辑保证存在，使用指令注释告知 ShellCheck 忽略此处
    # shellcheck source=/dev/null
    source "${ENV_FILE}"
    if [[ -z "${RESTIC_PASSWORD:-}" || -z "${RESTIC_REPOSITORY:-}" ]]; then
        log::die ".restic_env 文件中缺失必要的 RESTIC_PASSWORD 或 RESTIC_REPOSITORY！"
    fi

    # --- 新增：空路径保护 ---
    if ((${#BACKUP_PATHS[@]} == 0)); then
        log::die "致命错误：没有配置任何需要备份的路径 (BACKUP_PATHS 数组为空)！"
    fi

    # 2. 验证路径与数据库风险扫描 (带智能排除逻辑)
    local path
    for path in "${BACKUP_PATHS[@]}"; do
        if [[ ! -d "${path}" ]]; then
            log::die "致命错误：要备份的路径不存在 -> ${path}"
        fi

        # 动态将 EXCLUDE_PATTERNS 转换为 find 命令的 -prune 参数
        local prune_args=()
        local pattern
        for pattern in "${EXCLUDE_PATTERNS[@]}"; do
            local p="${pattern//\*\*/\*}" # 将 Restic 的 ** 转为 find 的 *
            p="${p%/}"                    # 去除末尾斜杠

            ((${#prune_args[@]} > 0)) && prune_args+=("-o")

            if [[ "${p}" == /* ]]; then
                prune_args+=("-path" "${p}")
            else
                prune_args+=("-name" "${p}")
            fi
        done

        # 组装 find 命令
        local find_cmd=("find" "${path}")
        if ((${#prune_args[@]} > 0)); then
            find_cmd+=("(" "${prune_args[@]}" ")" "-prune" "-o")
        fi
        find_cmd+=("-type" "f" "(" "-name" "*.db" "-o" "-name" "*.sqlite" "-o" "-name" "*.sqlite3" "-o" "-name" "*.mdf" ")" "-print")

        # 执行扫描（忽略权限报错）
        local db_files=()
        readarray -t db_files < <("${find_cmd[@]}" 2>/dev/null | head -n 3 || true)

        if ((${#db_files[@]} > 0)); then
            log::warn "⚠️ 风险提示：在 [${path}] 中检测到未排除的数据库文件 (例如: ${db_files[0]})。"
            log::warn "⚠️ 直接物理备份运行中的数据库极易导致数据损坏。请确保容器已暂停，或通过专门脚本进行逻辑备份！"
        fi
    done
    log::info "前置检查通过 ✓"
}

# -----------------------------------------------------------------------------
# 阶段 2: 自动检测与安装 Restic
# -----------------------------------------------------------------------------
auto_install_restic() {
    if command -v restic &>/dev/null; then
        return 0 # 已经安装了，直接跳过
    fi

    log::info "============================================================"
    log::info "阶段 2/5：检测到 restic 未安装，准备自动下载安装"
    log::info "============================================================"

    local download_url="https://oss.bitfennec.com/mgrsc-top/restic_0.18.1_linux_amd64.zst"
    local tmp_file="/tmp/restic_download.zst"
    local target_bin="/usr/local/bin/restic"

    # 1. 检查下载工具
    local dl_cmd=""
    if command -v curl &>/dev/null; then
        dl_cmd="curl -fSL -o ${tmp_file}"
    elif command -v wget &>/dev/null; then
        dl_cmd="wget -qO ${tmp_file}"
    else
        log::die "安装失败：系统中未找到 curl 或 wget，无法下载！"
    fi

    # 2. 检查解压工具 (zstd)
    if ! command -v zstd &>/dev/null; then
        log::die "安装失败：系统中未找到 zstd 工具无法解压 .zst 文件。请先执行: apt install zstd 或 yum install zstd。"
    fi

    # 3. 开始下载
    log::info "正在从指定的镜像源下载 restic 0.18.1..."
    if ! ${dl_cmd} "${download_url}"; then
        rm -f "${tmp_file}"
        log::die "下载失败，请检查网络或下载链接是否有效！"
    fi

    # 4. 解压并安装
    log::info "下载完成，正在解压并安装到 ${target_bin}..."
    if ! zstd -d "${tmp_file}" -o "${target_bin}" -f &>/dev/null; then
        rm -f "${tmp_file}"
        log::die "解压失败！文件可能损坏。"
    fi

    # 5. 赋权并清理
    chmod +x "${target_bin}"
    rm -f "${tmp_file}"

    # 6. 最终验证
    if ! command -v restic &>/dev/null; then
        log::die "安装完毕，但系统仍无法识别 restic，请检查 /usr/local/bin 是否在 PATH 环境变量中。"
    fi

    local version_info
    version_info=$(restic version)
    log::info "✓ 自动安装成功：${version_info}"
}

# -----------------------------------------------------------------------------
# 阶段 3: 核心备份
# -----------------------------------------------------------------------------
run_restic_backup() {
    log::info "============================================================"
    log::info "阶段 3/5：Restic 增量加密备份"
    log::info "============================================================"

    if ! restic snapshots &>/dev/null; then
        log::info "仓库不存在，初始化（repository version 2）..."
        restic init 2>&1 | tee -a "${LOG_FILE}" || log::die "仓库初始化失败"
    fi

    # --- 新增：自动清理死锁 (安全操作，无锁时自动忽略) ---
    log::info "正在清理可能存在的残留锁 (stale locks)..."
    restic unlock 2>&1 | tee -a "${LOG_FILE}" || true

    local exclude_args=()
    local pattern
    for pattern in "${EXCLUDE_PATTERNS[@]}"; do
        exclude_args+=("--exclude=${pattern}")
    done

    # --- 新增：网络重试逻辑 (最多尝试 3 次) ---
    local max_retries=3
    local attempt=1
    local backup_success=false

    log::info "开始上传数据至云端 (最大重试 ${max_retries} 次)..."

    while ((attempt <= max_retries)); do
        if restic backup \
            --compression max \
            --tag "${BACKUP_TAG}" \
            --tag "ts-${TIMESTAMP}" \
            --one-file-system \
            "${exclude_args[@]}" \
            "${BACKUP_PATHS[@]}" 2>&1 | tee -a "${LOG_FILE}"; then

            backup_success=true
            break # 成功则跳出循环
        else
            log::warn "❌ 第 ${attempt} 次备份尝试失败！"
            ((attempt++))

            if ((attempt <= max_retries)); then
                log::info "等待 10 秒后进行第 ${attempt} 次重试..."
                sleep 10
            fi
        fi
    done

    if [[ "${backup_success}" == "true" ]]; then
        log::info "✓ 备份上传成功"
    else
        log::die "已达到最大重试次数 (${max_retries})，备份彻底失败，请检查网络或配置。"
    fi
}

# -----------------------------------------------------------------------------
# 阶段 4: 云端快照清理
# -----------------------------------------------------------------------------
prune_old_snapshots() {
    log::info "============================================================"
    log::info "阶段 4/5：云端快照清理（forget --prune）"
    log::info "============================================================"

    restic forget \
        --tag "${BACKUP_TAG}" \
        --keep-last "${KEEP_LAST}" \
        --keep-weekly "${KEEP_WEEKLY}" \
        --keep-monthly "${KEEP_MONTHLY}" \
        --prune \
        2>&1 | tee -a "${LOG_FILE}" || log::warn "快照清理可能未完全成功"
}

# -----------------------------------------------------------------------------
# 阶段 5: 备份健康校验 (10% 抽查)
# -----------------------------------------------------------------------------
verify_backup() {
    log::info "============================================================"
    log::info "阶段 5/5：随机抽取 10% 数据进行完整性验证"
    log::info "============================================================"

    if restic check --read-data-subset=10% 2>&1 | tee -a "${LOG_FILE}"; then
        log::info "✓ 备份健康校验通过 (10%)"
    else
        log::error "❌ 备份校验失败！存储库可能存在数据损坏，请立刻进行全量排查 (restic check --read-data)！"
    fi
}

# -----------------------------------------------------------------------------
# 主函数
# -----------------------------------------------------------------------------
main() {
    [[ "${EUID}" -ne 0 ]] && log::die "请以 root 运行：sudo $0"

    rotate_logs

    log::info "████████████████████████████████████████████████████████████"
    log::info "  备份任务开始 — ${TIMESTAMP}"
    log::info "████████████████████████████████████████████████████████████"

    pre_flight_checks
    auto_install_restic
    run_restic_backup
    prune_old_snapshots
    verify_backup

    log::info "████████████████████████████████████████████████████████████"
    log::info "  全部流程执行完毕 ✓"
    log::info "████████████████████████████████████████████████████████████"
}

main "$@"
