#!/usr/bin/env bash
# dr-drill.sh —— 月度灾备恢复演练
# 目标：RPO ≤ 15min，RTO ≤ 4h
#
# 演练流程：
#   1. 从 S3 列出最新备份，验证时间戳（RPO 检查）
#   2. 下载最新全量备份，age 解密，验证完整性
#   3. 在临时 MySQL 容器中执行完整恢复，验证数据
#   4. 在临时 PostgreSQL 容器中执行恢复，验证数据
#   5. 记录总耗时，与 RTO 4h 对比
#   6. 生成报告，通过 Webhook 推送
#
# 演练使用独立 Docker 容器，不影响生产数据库
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "${SCRIPT_DIR}/lib.sh"

DRILL_ID="drill_$(date +%Y%m%d_%H%M%S)"
DRILL_DIR="/tmp/dr_drill_${DRILL_ID}"
REPORT_FILE="${DRILL_DIR}/report.txt"
RTO_TARGET_SECONDS=$(( 4 * 3600 ))   # 4 小时
DRILL_START="$(timer_start)"

mkdir -p "${DRILL_DIR}"

# age 私钥（演练时必须可用）
AGE_PRIVATE_KEY_FILE="${AGE_PRIVATE_KEY_FILE:-/etc/age-private.txt}"

log_info "========== DR 演练开始 [${DRILL_ID}] =========="

# ---------- 报告工具 ----------
PASS_COUNT=0
FAIL_COUNT=0

check_pass() { PASS_COUNT=$(( PASS_COUNT + 1 )); log_ok "  ✅ $*"; echo "PASS: $*" >> "${REPORT_FILE}"; }
check_fail() { FAIL_COUNT=$(( FAIL_COUNT + 1 )); log_error "  ❌ $*"; echo "FAIL: $*" >> "${REPORT_FILE}"; }
section()    { log_info "--- $* ---"; echo "" >> "${REPORT_FILE}"; echo "=== $* ===" >> "${REPORT_FILE}"; }

{
    echo "Pangolin 灾备演练报告"
    echo "演练 ID: ${DRILL_ID}"
    echo "开始时间: $(date '+%Y-%m-%d %H:%M:%S')"
    echo "目标: RPO ≤ 15min, RTO ≤ 4h"
    echo ""
} > "${REPORT_FILE}"

# ──────────────────────────────────────────────────────
# 检查 1：RPO 验证（最新增量备份时间戳）
# ──────────────────────────────────────────────────────
section "RPO 验证（目标 ≤ 15min）"
check_rpo() {
    log_info "[RPO] 检查 S3 最新增量备份时间"

    # 列出最新的增量备份文件
    local latest_inc
    latest_inc="$(AWS_PROFILE="${AWS_PROFILE}" aws s3 ls \
        "s3://${BACKUP_S3_BUCKET}/${BACKUP_S3_PREFIX}/mysql/inc/" \
        --recursive \
        | sort | tail -1 | awk '{print $1, $2}')"

    if [ -z "${latest_inc}" ]; then
        check_fail "S3 中未找到 MySQL 增量备份"
        return 1
    fi

    log_info "[RPO] 最新增量备份: ${latest_inc}"
    local last_date last_time
    last_date=$(echo "${latest_inc}" | awk '{print $1}')
    last_time=$(echo "${latest_inc}" | awk '{print $2}')
    local last_ts
    last_ts="$(date -d "${last_date} ${last_time}" +%s 2>/dev/null || date -j -f '%Y-%m-%d %H:%M:%S' "${last_date} ${last_time}" +%s)"
    local now
    now="$(date +%s)"
    local gap=$(( now - last_ts ))
    local gap_min=$(( gap / 60 ))

    echo "最新增量备份距今: ${gap_min} 分钟" >> "${REPORT_FILE}"

    if [ "${gap}" -le 900 ]; then   # 900 秒 = 15 分钟
        check_pass "RPO 达标: 最新备份距今 ${gap_min} 分钟 (≤ 15min)"
    else
        check_fail "RPO 未达标: 最新备份距今 ${gap_min} 分钟 (> 15min)"
    fi
}
check_rpo || true

# ──────────────────────────────────────────────────────
# 检查 2：备份文件完整性验证
# ──────────────────────────────────────────────────────
section "备份文件完整性"
check_backup_integrity() {
    if [ ! -f "${AGE_PRIVATE_KEY_FILE}" ]; then
        check_fail "age 私钥不可用，跳过解密验证（设置 AGE_PRIVATE_KEY 环境变量）"
        return 1
    fi

    log_info "[完整性] 下载并解密最新全量备份"

    # 找到最新全量备份的 S3 key
    local latest_full_key
    latest_full_key="$(AWS_PROFILE="${AWS_PROFILE}" aws s3 ls \
        "s3://${BACKUP_S3_BUCKET}/${BACKUP_S3_PREFIX}/mysql/full/" \
        | sort | tail -1 | awk '{print $4}')"

    if [ -z "${latest_full_key}" ]; then
        check_fail "S3 中未找到 MySQL 全量备份"
        return 1
    fi

    local enc_file="${DRILL_DIR}/${latest_full_key}"
    local dec_file="${enc_file%.age}"

    log_info "[完整性] 下载: ${latest_full_key}"
    AWS_PROFILE="${AWS_PROFILE}" aws s3 cp \
        "s3://${BACKUP_S3_BUCKET}/${BACKUP_S3_PREFIX}/mysql/full/${latest_full_key}" \
        "${enc_file}"

    log_info "[完整性] age 解密"
    age --decrypt --identity "${AGE_PRIVATE_KEY_FILE}" -o "${dec_file}" "${enc_file}"
    rm -f "${enc_file}"

    # 验证 tar 归档完整性（不解压）
    if tar -tzf "${dec_file}" > /dev/null 2>&1; then
        check_pass "全量备份 tar.gz 归档完整"
    else
        check_fail "全量备份 tar.gz 归档损坏"
        return 1
    fi

    echo "${dec_file}"
}
FULL_ARCHIVE="$(check_backup_integrity 2>/dev/null)" || FULL_ARCHIVE=""

# ──────────────────────────────────────────────────────
# 检查 3：MySQL 恢复验证（临时容器）
# ──────────────────────────────────────────────────────
section "MySQL 恢复验证"
MYSQL_DRILL_PORT=33306  # 避免与生产 3306 冲突
MYSQL_DRILL_CONTAINER="drill_mysql_${DRILL_ID}"

check_mysql_restore() {
    if [ -z "${FULL_ARCHIVE}" ]; then
        check_fail "全量备份不可用，跳过 MySQL 恢复验证"
        return 1
    fi

    local mysql_restore_dir="${DRILL_DIR}/mysql_restore"
    mkdir -p "${mysql_restore_dir}"

    # 解压备份
    log_info "[MySQL] 解压备份到临时目录"
    tar -xzf "${FULL_ARCHIVE}" -C "${mysql_restore_dir}"

    local backup_subdir
    backup_subdir="$(ls -1 "${mysql_restore_dir}" | head -1)"
    local target_dir="${mysql_restore_dir}/${backup_subdir}"

    # xtrabackup --prepare（无需 MySQL 服务）
    log_info "[MySQL] xtrabackup --prepare 验证"
    if xtrabackup --prepare --target-dir="${target_dir}" 2>&1 | grep -q "completed OK"; then
        check_pass "xtrabackup --prepare 成功（备份一致性验证通过）"
    else
        check_fail "xtrabackup --prepare 失败"
        return 1
    fi

    # 启动临时 MySQL 容器（使用恢复的数据目录）
    log_info "[MySQL] 启动临时 MySQL 容器（端口 ${MYSQL_DRILL_PORT}）"
    docker run -d \
        --name "${MYSQL_DRILL_CONTAINER}" \
        --mount "type=bind,src=${target_dir},dst=/var/lib/mysql" \
        -e MYSQL_ROOT_PASSWORD=drill_tmp_pw \
        -p "127.0.0.1:${MYSQL_DRILL_PORT}:3306" \
        mysql:8.0 \
        --skip-grant-tables 2>/dev/null || {
        check_fail "临时 MySQL 容器启动失败（可能没有 Docker）"
        return 1
    }

    # 等待 MySQL 就绪（最多 60 秒）
    local wait=0
    until mysql --host=127.0.0.1 --port="${MYSQL_DRILL_PORT}" \
            --user=root --password=drill_tmp_pw \
            -e "SELECT 1" > /dev/null 2>&1 || [ "${wait}" -ge 60 ]; do
        sleep 2
        wait=$(( wait + 2 ))
    done

    if mysql --host=127.0.0.1 --port="${MYSQL_DRILL_PORT}" \
            --user=root --password=drill_tmp_pw \
            -e "SHOW DATABASES;" > /dev/null 2>&1; then
        check_pass "MySQL 从备份恢复后成功响应查询"
    else
        check_fail "MySQL 恢复后无法执行查询"
    fi

    # 停止并删除临时容器
    docker stop "${MYSQL_DRILL_CONTAINER}" 2>/dev/null || true
    docker rm -f "${MYSQL_DRILL_CONTAINER}" 2>/dev/null || true
    rm -rf "${mysql_restore_dir}"
}
check_mysql_restore || true

# ──────────────────────────────────────────────────────
# 检查 4：PostgreSQL 恢复验证
# ──────────────────────────────────────────────────────
section "PostgreSQL 恢复验证"
PG_DRILL_PORT=55432
PG_DRILL_CONTAINER="drill_pg_${DRILL_ID}"

check_pg_restore() {
    if [ ! -f "${AGE_PRIVATE_KEY_FILE}" ]; then
        check_fail "age 私钥不可用，跳过 PG 恢复验证"
        return 1
    fi

    local first_db
    IFS=',' read -ra DBS <<< "${PG_DATABASES:-postgres}"
    first_db="${DBS[0]}"
    first_db="$(echo "${first_db}" | tr -d ' ')"

    log_info "[PostgreSQL] 下载最新 ${first_db} 备份"
    local latest_key
    latest_key="$(AWS_PROFILE="${AWS_PROFILE}" aws s3 ls \
        "s3://${BACKUP_S3_BUCKET}/${BACKUP_S3_PREFIX}/postgres/inc/${first_db}/" \
        | sort | tail -1 | awk '{print $4}')"

    if [ -z "${latest_key}" ]; then
        check_fail "S3 中未找到 PostgreSQL 备份: ${first_db}"
        return 1
    fi

    local enc_file="${DRILL_DIR}/pg_${latest_key}"
    local dec_file="${enc_file%.age}"

    AWS_PROFILE="${AWS_PROFILE}" aws s3 cp \
        "s3://${BACKUP_S3_BUCKET}/${BACKUP_S3_PREFIX}/postgres/inc/${first_db}/${latest_key}" \
        "${enc_file}"

    age --decrypt --identity "${AGE_PRIVATE_KEY_FILE}" -o "${dec_file}" "${enc_file}"
    rm -f "${enc_file}"

    # 用 pg_restore --list 验证备份结构（无需启动 PostgreSQL）
    if pg_restore --list "${dec_file}" > /dev/null 2>&1; then
        check_pass "PostgreSQL 备份结构验证通过 (${first_db})"
    else
        check_fail "PostgreSQL 备份文件损坏 (${first_db})"
    fi

    rm -f "${dec_file}"
}
check_pg_restore || true

# ──────────────────────────────────────────────────────
# 汇总报告
# ──────────────────────────────────────────────────────
section "RTO 验证（目标 ≤ 4h）"
ELAPSED=$(timer_elapsed "${DRILL_START}")
DURATION=$(format_duration "${ELAPSED}")

echo "演练总耗时: ${ELAPSED}s (${DURATION})" >> "${REPORT_FILE}"
echo "RTO 目标: ${RTO_TARGET_SECONDS}s (4h)" >> "${REPORT_FILE}"

if [ "${ELAPSED}" -le "${RTO_TARGET_SECONDS}" ]; then
    check_pass "RTO 达标: 演练耗时 ${DURATION} (≤ 4h)"
else
    check_fail "RTO 未达标: 演练耗时 ${DURATION} (> 4h)"
fi

{
    echo ""
    echo "================="
    echo "检查通过: ${PASS_COUNT}"
    echo "检查失败: ${FAIL_COUNT}"
    echo "================="
    echo "结束时间: $(date '+%Y-%m-%d %H:%M:%S')"
    if [ "${FAIL_COUNT}" -eq 0 ]; then
        echo "结论: 演练通过 ✅"
    else
        echo "结论: 演练失败 ❌，请检查上方失败项"
    fi
} >> "${REPORT_FILE}"

# 上传演练报告到 S3
if AWS_PROFILE="${AWS_PROFILE}" aws s3 cp "${REPORT_FILE}" \
    "s3://${BACKUP_S3_BUCKET}/${BACKUP_S3_PREFIX}/dr-reports/${DRILL_ID}.txt" 2>/dev/null; then
    log_ok "演练报告已上传: s3://${BACKUP_S3_BUCKET}/${BACKUP_S3_PREFIX}/dr-reports/${DRILL_ID}.txt"
fi

# 打印报告
echo ""
cat "${REPORT_FILE}"

# 清理
rm -rf "${DRILL_DIR}"

# 通知
if [ "${FAIL_COUNT}" -eq 0 ]; then
    log_ok "========== DR 演练通过 [通过: ${PASS_COUNT}，失败: ${FAIL_COUNT}，耗时: ${DURATION}] =========="
    notify "INFO" "月度 DR 演练通过 | ${DRILL_ID} | 耗时 ${DURATION} | 通过 ${PASS_COUNT} 项"
else
    log_error "========== DR 演练有 ${FAIL_COUNT} 项失败 [耗时: ${DURATION}] =========="
    notify "ERROR" "月度 DR 演练失败 | ${DRILL_ID} | 失败 ${FAIL_COUNT} 项，请检查"
    exit 1
fi
