#!/usr/bin/env bash
#
# single-db-backup-enterprise.sh (v3 - Formatted)
#
# 功能: 将 MongoDB 单个数据库备份为 .archive.gz 压缩归档。
# 特点: 提供中英双语、企业级风格的日志输出，包含详细的元数据分析和结果验证。
#
# Function: Backup a single MongoDB database to a compressed .archive.gz.
# Features: Bilingual (EN/CN), enterprise-grade SSH-style logging with detailed metadata analysis and verification.
#
# 用法 / Usage:
#   ./single-db-backup-enterprise.sh <USER> <PASS> <DB_NAME> <ABS_DEST_DIR>
#

set -euo pipefail

# --- Configuration ---
HOST="127.0.0.1"
PORT="27017"
AUTH_DB="admin"

# --- Style & Colors ---
CLR_RESET="\x1b[0m"; BOLD="\x1b[1m";
CLR_GRY="\x1b[90m"; CLR_SKY="\x1b[96m";
CLR_GRN="\x1b[32m"; CLR_YLW="\x1b[33m";
CLR_RED="\x1b[31m";

# --- Logging & Formatting ---
HOSTNAME_SHORT="$(hostname -s || echo 'localhost')"
ts() { date +"%Y-%m-%d %H:%M:%S%z"; }

# 基础日志行 / Base log line
log_line() {
    local level="$1"; shift; local color="$1"; shift
    printf "%b%s [%s] [%s] %s%b\n" "${color}" "$(ts)" "${HOSTNAME_SHORT}" "${level}" "$*" "${CLR_RESET}"
}

info() { log_line "INFO"  "${CLR_SKY}" "$*"; }
ok()   { log_line "OK"    "${CLR_GRN}" "$*"; }
warn() { log_line "WARN"  "${CLR_YLW}" "$*"; }
err()  { log_line "ERROR" "${CLR_RED}" "$*"; }

# 格式化输出函数 / Formatting functions
print_header() {
    printf "\n%b╭──────────────────────────────────────────────────────────────────────────────╮%b\n" "${CLR_GRY}" "${CLR_RESET}"
    printf "%b│ %b%-75s%b │%b\n" "${CLR_GRY}" "${BOLD}${CLR_SKY}" "$1" "${CLR_GRY}" "${CLR_RESET}"
    printf "%b╰──────────────────────────────────────────────────────────────────────────────╯%b\n" "${CLR_GRY}" "${CLR_RESET}"
}

print_kv() {
    printf "  %b%-25s %b»%b %s\n" "${CLR_SKY}" "$1" "${CLR_GRY}" "${CLR_RESET}" "$2"
}

format_bytes() {
    local bytes=$1
    if (( bytes < 1024 )); then
        echo "${bytes} B"
    elif (( bytes < 1024 * 1024 )); then
        printf "%.2f KB\n" "$(bc <<< "scale=2; ${bytes}/1024")"
    elif (( bytes < 1024 * 1024 * 1024 )); then
        printf "%.2f MB\n" "$(bc <<< "scale=2; ${bytes}/1024/1024")"
    else
        printf "%.2f GB\n" "$(bc <<< "scale=2; ${bytes}/1024/1024/1024")"
    fi
}

spinner() {
    local pid=$!
    local delay=0.1
    local spinstr='|/-\'
    while ps a | awk '{print $1}' | grep -q "$pid"; do
        local temp=${spinstr#?}
        printf "  [%c] " "$spinstr"
        local spinstr=$temp${spinstr%"$temp"}
        sleep $delay
        printf "\r"
    done
    printf "    \r"
}

# --- Argument Validation ---
if [[ $# -ne 4 ]]; then
    err "参数错误 / Invalid arguments. 用法 / Usage: $0 <USER> <PASS> <DB_NAME> <ABS_DEST_DIR>"
    exit 1
fi
USER="$1"; PASS="$2"; DB="$3"; DEST="$4"

# ==============================================================================
#                              START SCRIPT
# ==============================================================================
print_header "环境检查 / PRE-FLIGHT CHECKS"

info "校验脚本参数... / Validating arguments..."
if [[ -z "$USER" || -z "$PASS" || -z "$DB" || -z "$DEST" ]]; then
    err "所有参数均不能为空 / No argument can be empty."
    exit 1
fi
if [[ "${DEST}" != /* ]]; then
    err "目标目录必须为绝对路径 / Destination directory must be an absolute path, e.g. /var/backups/mongo"
    exit 1
fi
ok "参数校验通过 / Arguments OK."

info "检查所需工具... / Checking for required tools..."
need_tool() {
    local bin="$1" pkg="$2"
    if ! command -v "$bin" >/dev/null 2>&1; then
        warn "缺少 $bin, 尝试自动安装 $pkg (需要 root 权限) / Missing $bin, attempting to install $pkg (root required)."
        if [[ $EUID -ne 0 ]]; then
            err "非 root 用户无法安装, 请手动执行: apt install $pkg / Not root, please install manually: apt install $pkg"
            exit 2
        fi
        apt-get update && apt-get install -y "$pkg"
    fi
}
need_tool "mongodump" "mongodb-database-tools"
need_tool "mongosh"   "mongodb-mongosh"
need_tool "sha256sum" "coreutils"
need_tool "bc"        "bc"
ok "所有工具均已安装 / All tools are present."

info "检查目标目录... / Checking destination directory: ${DEST}"
mkdir -p "${DEST}"
if [[ ! -w "${DEST}" ]]; then
    err "目录无法写入 / Directory is not writable."
    exit 1
fi
ok "目标目录可写 / Destination is writable."

info "测试数据库连接... / Testing database connectivity..."
if ! mongosh --quiet --host "${HOST}" --port "${PORT}" \
    --username "${USER}" --password "${PASS}" --authenticationDatabase "${AUTH_DB}" \
    --eval 'db.adminCommand({ ping: 1 })' >/dev/null 2>&1; then
    err "无法连接或认证失败 / Connection or authentication failed."
    exit 3
fi
ok "数据库连接成功 / Database connection successful."

# --- Metadata Probe ---
JS_META=$(cat <<'JS'
(function(){
    const dbName = "@@DB@@";
    const d = db.getSiblingDB(dbName);
    let totalC=0,totalDocs=0,totalIdx=0,dataBytes=0,storageBytes=0;
    let views=0,timeseries=0,capped=0;
    let collections = [];

    let infos=[];
    try { infos = d.getCollectionInfos({ name: { $not: /system\./ } }); } catch(e) {
        try {
            const names = d.getCollectionNames();
            infos = names.filter(n => !n.startsWith('system.')).map(n => ({name:n, type:"collection"}));
        } catch(e2) { infos = []; }
    }

    for (const ci of infos) {
        if (ci.type === "view") { views++; continue; }
        if (ci.options && ci.options.timeSeries) { timeseries++; }
        if (ci.options && ci.options.capped) { capped++; }

        if (ci.type !== "collection") continue;
        totalC++;
        const name = ci.name;

        let cnt=0, idx=0, cs={ok:0};
        try { cnt = d.getCollection(name).estimatedDocumentCount(); } catch(e) {}
        try { idx = d.getCollection(name).getIndexes().length; } catch(e) {}
        try { cs = d.runCommand({collStats: name, scale:1}); } catch(e) {}
        
        if (typeof cnt === "number") totalDocs += cnt;
        if (typeof idx === "number") totalIdx += idx;
        if (cs && cs.ok===1) {
            dataBytes += (cs.size||0);
            storageBytes += (cs.storageSize||0);
        }
        collections.push({
            name: name,
            count: cnt,
            indexes: idx,
            size: (cs && cs.ok === 1 ? (cs.size || 0) : 0),
            storageSize: (cs && cs.ok === 1 ? (cs.storageSize || 0) : 0)
        });
    }
    
    collections.sort((a,b) => b.storageSize - a.storageSize);

    print("SUMMARY collections="+totalC+" documents="+totalDocs+" indexes="+totalIdx+
        " dataBytes="+dataBytes+" storageBytes="+storageBytes+
        " views="+views+" timeseries="+timeseries+" capped="+capped);
    
    print("DETAILS_START");
    collections.forEach(c => print(EJSON.stringify(c)));
    print("DETAILS_END");
})();
JS
)
JS_META="${JS_META//@@DB@@/${DB}}"

print_header "数据库元数据分析 / DATABASE METADATA ANALYSIS"
info "正在拉取数据库 '${DB}' 的统计信息... / Fetching statistics for database '${DB}'..."

set +e
META_OUT="$(mongosh --quiet --host "${HOST}" --port "${PORT}" \
    --username "${USER}" --password "${PASS}" --authenticationDatabase "${AUTH_DB}" \
    --eval "${JS_META}")"
META_RC=$?
set -e

if [[ ${META_RC} -ne 0 || -z "${META_OUT}" ]]; then
    warn "未能获取元数据 (可能权限不足), 将跳过分析直接备份 / Could not fetch metadata (permissions?), skipping analysis and proceeding with backup."
else
    SUMMARY_LINE="$(grep -m1 '^SUMMARY ' <<< "${META_OUT}" || true)"
    if [[ -n "${SUMMARY_LINE}" ]]; then
        # 解析摘要行 / Parse summary line
        COLL_TOTAL=$(echo "$SUMMARY_LINE" | sed -n 's/.*collections=\([^ ]*\).*/\1/p')
        DOC_TOTAL=$(echo "$SUMMARY_LINE" | sed -n 's/.*documents=\([^ ]*\).*/\1/p')
        IDX_TOTAL=$(echo "$SUMMARY_LINE" | sed -n 's/.*indexes=\([^ ]*\).*/\1/p')
        DATA_BYTES=$(echo "$SUMMARY_LINE" | sed -n 's/.*dataBytes=\([^ ]*\).*/\1/p')
        STORAGE_BYTES=$(echo "$SUMMARY_LINE" | sed -n 's/.*storageBytes=\([^ ]*\).*/\1/p')
        VIEWS=$(echo "$SUMMARY_LINE" | sed -n 's/.*views=\([^ ]*\).*/\1/p')
        TSERIES=$(echo "$SUMMARY_LINE" | sed -n 's/.*timeseries=\([^ ]*\).*/\1/p')
        CAPPED=$(echo "$SUMMARY_LINE" | sed -n 's/.*capped=\([^ ]*\).*/\1/p')

        printf "  %b📊 数据库摘要 / Database Summary%b\n" "${BOLD}${CLR_GRN}" "${CLR_RESET}"
        print_kv "总集合数 / Collections" "${COLL_TOTAL} (其中 / incl: ${VIEWS} 视图/views, ${TSERIES} 时序/timeseries, ${CAPPED} 封顶/capped)"
        print_kv "总文档数 / Documents" "${DOC_TOTAL}"
        print_kv "总索引数 / Indexes" "${IDX_TOTAL}"
        print_kv "逻辑大小 / Data Size" "$(format_bytes "$DATA_BYTES") (${DATA_BYTES} B)"
        print_kv "存储大小 / Storage Size" "$(format_bytes "$STORAGE_BYTES") (${STORAGE_BYTES} B)"
        
        printf "\n  %b📦 集合详情 (按存储大小排序) / Collection Details (by storage size)%b\n" "${BOLD}${CLR_SKY}" "${CLR_RESET}"
        printf "  %b%-40s %-15s %-15s %-10s%b\n" "${CLR_YLW}" "Collection Name" "Documents" "Storage Size" "Indexes" "${CLR_RESET}"
        printf "  %b%s%b\n" "${CLR_GRY}" "----------------------------------------------------------------------------------" "${CLR_RESET}"
        
        # 提取并打印集合详情 / Extract and print collection details
        echo "${META_OUT}" | sed -n '/DETAILS_START/,/DETAILS_END/p' | grep '^{' | while IFS= read -r line; do
            C_NAME=$(echo "$line" | sed -n 's/.*"name":"\([^"]*\)".*/\1/p')
            C_COUNT=$(echo "$line" | sed -n 's/.*"count":{\"$numberLong\":\"\([0-9]*\)\"}.*/\1/p')
            C_STORAGE=$(echo "$line" | sed -n 's/.*"storageSize":{\"$numberLong\":\"\([0-9]*\)\"}.*/\1/p')
            C_IDX=$(echo "$line" | sed -n 's/.*"indexes":\([0-9]*\).*/\1/p')
            printf "  %-40s %-15s %-15s %-10s\n" "$C_NAME" "$C_COUNT" "$(format_bytes "$C_STORAGE")" "$C_IDX"
        done
    fi
fi

print_header "备份执行 / BACKUP EXECUTION"

STAMP="$(date +%Y%m%d-%H%M%S)"
OUT_FILE="${DEST}/${DB}-${STAMP}.archive.gz"
umask 0077

info "备份任务启动 / Backup job started."
print_kv "数据库 / Database" "${DB}"
print_kv "输出文件 / Output File" "${OUT_FILE}"

START_TS="$(date +%s)"
(mongodump \
    --host "${HOST}" --port "${PORT}" \
    --username "${USER}" --password "${PASS}" \
    --authenticationDatabase "${AUTH_DB}" \
    --db "${DB}" \
    --dumpDbUsersAndRoles \
    --archive="${OUT_FILE}" \
    --gzip) >/dev/null 2>&1 &
spinner
wait $!
RET=$?
END_TS="$(date +%s)"
ELAPSED=$((END_TS - START_TS))

print_header "结果验证与总结 / VERIFICATION & SUMMARY"

if [[ $RET -ne 0 ]]; then
    err "备份失败! mongodump 进程返回非零退出码: ${RET} / Backup FAILED! mongodump exited with code: ${RET}"
    rm -f "${OUT_FILE}"
    exit $RET
fi
if [[ ! -s "${OUT_FILE}" ]]; then
    err "备份失败! 产物文件不存在或为空: ${OUT_FILE} / Backup FAILED! Artifact is missing or empty: ${OUT_FILE}"
    exit 4
fi

FILE_SIZE_BYTES="$(stat -c '%s' "${OUT_FILE}")"
FILE_MODE="$(stat -c '%A' "${OUT_FILE}")"
SHA256="$(sha256sum "${OUT_FILE}" | awk '{print $1}')"
DISK_INFO="$(df -h "${DEST}" | awk 'NR==2{printf "已用/Used: %s, 可用/Avail: %s, 使用率/Use%%: %s", $3, $4, $5}')"

ok "备份进程成功完成 / Backup process completed successfully."
printf "\n%b  ✅ 备份产物详情 / Backup Artifact Details %b\n" "${BOLD}${CLR_GRN}" "${CLR_RESET}"
print_kv "文件路径 / File Path" "${OUT_FILE}"
print_kv "文件大小 / File Size" "$(format_bytes "${FILE_SIZE_BYTES}") (${FILE_SIZE_BYTES} B)"
print_kv "文件权限 / File Mode" "${FILE_MODE}"
print_kv "校验和 / Checksum (SHA256)" "${SHA256}"
print_kv "执行耗时 / Elapsed Time" "${ELAPSED}s"
print_kv "目标磁盘 / Destination Disk" "${DISK_INFO}"

printf "\n%b╭──────────────────────────────────────────────────────────────────────────────╮%b\n" "${CLR_GRN}" "${CLR_RESET}"
printf "%b│ %b%-75s%b │%b\n" "${CLR_GRN}" "${BOLD}" "  SUCCESS: 备份任务已成功完成并通过验证。" "${CLR_GRN}" "${CLR_RESET}"
printf "%b│ %b%-75s%b │%b\n" "${CLR_GRN}" "${BOLD}" "  SUCCESS: Backup task completed and verified successfully." "${CLR_GRN}" "${CLR_RESET}"
printf "%b╰──────────────────────────────────────────────────────────────────────────────╯%b\n" "${CLR_GRN}" "${CLR_RESET}"
info "恢复提示 / Restore tip: mongorestore --archive='${OUT_FILE}' --gzip --drop --nsInclude '${DB}.*' --restoreDbUsersAndRoles"

exit 0
