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

# =============================================================================
# Script: sqlite_backup.sh
# Description: SQLite 一致性热备份（完全依赖外部环境变量驱动）
# Version: 1.1.0
# Compatibility: Bash 5.0+ / sqlite3
#
# 【工作流程】:
# 1. 环境初始化：定义日志路径与捕获异常中断信号。
# 2. 进程互斥锁：使用 flock 防止多个 Cron 任务重叠执行。
# 3. 实例循环处理：通过 SQLITE_INSTANCES 读取目标库路径与备份基准目录。
# 4. 磁盘预留检查：检查目标目录是否具备基础的写入空间。
# 5. 一致性热备：调用 SQLite 原生 `.backup` 指令，保证备份期间无锁/不损坏。
# 6. 文件压缩：检测系统环境，优先使用 zstd，降级使用 gzip 极致压缩备份。
# 7. 滚动清理：自动删除超过 7 天的历史备份文件，释放空间。
# =============================================================================

readonly SCRIPT_VERSION="1.1.0"
readonly SCRIPT_NAME="sqlite_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}/sqlite_${TIMESTAMP}.log"

mkdir -p "${GLOBAL_LOG_DIR}"

# 捕获中断信号，确保不会留下半残的压缩文件
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}"
}

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:-512}" # SQLite 默认最小预留 512 MB
    
    mkdir -p "${target_dir}"
    local available_mb
    available_mb=$(df -m "${target_dir}" | awk 'NR==2 {print $4}')
    
    if [[ -n "${available_mb}" && "${available_mb}" -lt "${min_mb}" ]]; then
        utils::log "ERROR" "磁盘空间不足！目录 [${target_dir}] 剩余 ${available_mb}MB，要求下限 ${min_mb}MB。"
        return 1
    fi
    return 0
}

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

    if [[ -z "${instance_id}" ]]; then
        db_path="${SQLITE_DB:-}"
        backup_base="${SQLITE_BACKUP_BASE:-/backup/databases/sqlite_default}"
    else
        local db_var="SQLITE_${instance_id}_DB"
        local base_var="SQLITE_${instance_id}_BACKUP_BASE"
        db_path="${!db_var:-}"
        backup_base="${!base_var:-/backup/databases/sqlite_${instance_id}}"
    fi

    if [[ -z "${db_path}" || ! -f "${db_path}" ]]; then
        utils::log "WARN" "实例 [${display_name}] 数据库文件无效或未设置 (${db_path})，已跳过！"
        return 1
    fi

    local backup_dir="${backup_base}/${TIMESTAMP}"
    if ! check_disk_space "${backup_base}" 512; then
        return 1
    fi
    
    mkdir -p "${backup_dir}"

    local db_name="${db_path##*/}"
    db_name="${db_name%.sqlite3}"
    db_name="${db_name%.db}"
    db_name="${db_name%.sqlite}"
    local backup_file="${backup_dir}/${db_name}_${TIMESTAMP}.db"

    utils::log "INFO" "开始处理实例 [${display_name}] → ${backup_file}"

    # SQLite 一致性热备份
    if sqlite3 "${db_path}" ".backup '${backup_file}'" 2>&1 | tee -a "${LOG_FILE}"; then
        
        # 可选压缩（优先 zstd，其次 gzip）
        if command -v zstd &>/dev/null; then
            zstd -T0 -19 --rm "${backup_file}" -o "${backup_file}.zst" 2>&1 | tee -a "${LOG_FILE}"
            backup_file="${backup_file}.zst"
        elif command -v gzip &>/dev/null; then
            gzip -9 "${backup_file}" 2>&1 | tee -a "${LOG_FILE}"
            backup_file="${backup_file}.gz"
        fi

        # 清理7天前的备份目录
        find "${backup_base}" -maxdepth 1 -type d -mtime +7 -exec rm -rf {} + 2>/dev/null || true

        utils::log "INFO" "✓ 实例 [${display_name}] 备份完成: ${backup_file}"
    else
        utils::log "ERROR" "❌ 实例 [${display_name}] 备份失败"
        return 1
    fi
}

# ----------------------------------------------------------------------------
# 主入口
# ----------------------------------------------------------------------------
main() {
    utils::log "INFO" "══════════════════════════════════════════════════════"
    utils::log "INFO" "SQLite 备份任务 (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

    if ! command -v sqlite3 &>/dev/null; then
        utils::log "FATAL" "未检测到 sqlite3 命令行工具，请先安装。"
        exit 1
    fi

    local has_error=0

    if [[ -n "${SQLITE_INSTANCES:-}" ]]; then
        local instances
        IFS=$' \t' read -r -a instances <<< "${SQLITE_INSTANCES}"
        for instance in "${instances[@]}"; do
            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" "🎉 所有 SQLite 备份成功完成！"
    fi
}

main "$@"
