#!/bin/bash
# _lib_pg_apply.sh — pg_apply / pg_rollback / pg_history 共用函式
#
# 設計原則：
# - 在 ctrl 機器跑（受 ajax 呼叫，sudo -u n1admin -i）
# - SSH 出去 target machine 做 backup / sed / restart
# - audit log 雙寫：檔案永遠寫，DB 失敗時 fallback 不擋操作
#
# Source 方式：source "$(dirname "${BASH_SOURCE[0]}")/_lib_pg_apply.sh"

# ── 常數 ─────────────────────────────────────────────────────────────────────

# 統一白名單（共 17 項，原 pgtune 分類含 reload-safe + restart-required）
# 工具定位 maintenance-only：preflight 確認 Game Server offline → 一律走 systemctl restart
# 不再區分 reload vs restart，邏輯單一、保證生效、rollback 不會踩 reload 對 restart-required 沒效的坑
readonly PGAPPLY_WHITELIST=(
    work_mem maintenance_work_mem autovacuum_work_mem
    effective_cache_size default_statistics_target
    random_page_cost effective_io_concurrency
    wal_compression checkpoint_completion_target jit
    min_wal_size max_wal_size
    shared_buffers max_connections max_worker_processes
    huge_pages max_wal_senders
)

# 風險分組（M4「套低風險」/「全套用」用，跟 restart 流程無關）
readonly PGAPPLY_LOW_RISK=(
    jit wal_compression min_wal_size max_wal_size
    random_page_cost effective_io_concurrency
    default_statistics_target checkpoint_completion_target
)

# Audit log 檔案路徑（對齊現有慣例：files/<feature>/）
# 從 BASH_SOURCE 推算，自動對齊 dev / prod 部署路徑
_PGAPPLY_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly PGAPPLY_LOG_FILE="$(cd "${_PGAPPLY_LIB_DIR}/../../.." 2>/dev/null && pwd)/files/pg_apply_log/pg_apply.log"

# SSH 選項（同 pg_analyzer.sh）
readonly PGAPPLY_SSH_OPTS=(
    -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no
    -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR
)

# ── 白名單檢查 ───────────────────────────────────────────────────────────────

# 0 if 在白名單，1 if 不在
pgapply_is_whitelisted() {
    local param="$1"
    local p
    for p in "${PGAPPLY_WHITELIST[@]}"; do
        [[ "$p" == "$param" ]] && return 0
    done
    return 1
}

# 0 if 是 low-risk，1 if 不是
pgapply_is_low_risk() {
    local param="$1"
    local p
    for p in "${PGAPPLY_LOW_RISK[@]}"; do
        [[ "$p" == "$param" ]] && return 0
    done
    return 1
}

# ── SPEC 收集 ────────────────────────────────────────────────────────────────

# 從 target 收集 nproc / mem_bytes / pg_version
# 輸出 KEY=VALUE 三行（NPROC / MEM_BYTES / PG_VERSION）
pgapply_collect_spec() {
    local target="$1"
    ssh "${PGAPPLY_SSH_OPTS[@]}" "$target" bash 2>/dev/null <<'REMOTE' || true
echo "NPROC=$(nproc 2>/dev/null || echo 0)"
echo "MEM_BYTES=$(free -b 2>/dev/null | awk '/^Mem:/ {print $2}')"
PG_VER=$(ls -d /etc/postgresql/[0-9]* 2>/dev/null | sed 's|.*/||' | sort -n | tail -1)
echo "PG_VERSION=${PG_VER:-}"
REMOTE
}

# 取 postgresql.conf 完整路徑（透過 SHOW config_file）
pgapply_get_conf_path() {
    local target="$1"
    ssh "${PGAPPLY_SSH_OPTS[@]}" "$target" bash 2>/dev/null <<'REMOTE' || true
sudo -n -u postgres psql -AtX -c "SHOW config_file;" 2>/dev/null
REMOTE
}

# ── Pre-flight ───────────────────────────────────────────────────────────────

# 0 if pass，非 0 if fail（同時 echo 錯誤訊息到 stderr）
# 中等 1B：PG running + Game Server stopped
pgapply_preflight() {
    local target="$1"
    local result
    result=$(ssh "${PGAPPLY_SSH_OPTS[@]}" "$target" bash 2>&1 <<'REMOTE' || true
set +e

# 1. PG 連得上
if ! sudo -n -u postgres psql -AtX -c "SELECT 1" >/dev/null 2>&1; then
    echo "FAIL_PG_DOWN"
    exit 1
fi

# 2. Game Server 已停（用 ps 抓 *Server process，排除 SKServer 常駐）
# 判斷依據：N1 環境的 Game Server 是 process（如 GameServer / WorldServer / ChatServer 等），
# 不是 systemd unit。SKServer 是常駐元件不算 Game Server。
GAME_PROCS=$(ps -Ao cmd 2>/dev/null | grep -oE '[A-Za-z]+Server' | grep -v '^SKServer$' | sort -u)
if [[ -n "$GAME_PROCS" ]]; then
    echo "FAIL_GAME_RUNNING:$(echo "$GAME_PROCS" | tr '\n' ',' | sed 's/,$//')"
    exit 2
fi

echo "PASS"
REMOTE
)

    case "$result" in
        PASS|*PASS*)
            return 0 ;;
        *FAIL_PG_DOWN*)
            echo "Pre-flight 失敗：PG 沒在 RUNNING 或無法連線" >&2
            return 1 ;;
        FAIL_GAME_RUNNING:*)
            local svc="${result#*FAIL_GAME_RUNNING:}"
            svc="${svc%%[[:space:]]*}"
            echo "Pre-flight 失敗：Game Server [$svc] 還在跑，請先停服" >&2
            return 2 ;;
        *)
            echo "Pre-flight 失敗：SSH 失敗或未知錯誤 ($result)" >&2
            return 3 ;;
    esac
}

# ── Audit log 雙寫 ───────────────────────────────────────────────────────────

# 寫 audit log（檔案永遠寫；DB 失敗只 stderr 警告，不擋）
# 用法：pgapply_log_audit target=GAMEDB10 target_ip=10.150.10.10 \
#                       cpu=8 ram_gb=32 pg_version=16 \
#                       param_name=jit old_value=on new_value=off \
#                       applied_by=xvincent status=success \
#                       backup_file=/etc/postgresql/16/main/postgresql.conf.bak.20260428-1030 \
#                       batch_id=B20260428103045 [error_msg=...]
pgapply_log_audit() {
    local target="" target_ip="" cpu="" ram_gb="" pg_version=""
    local param_name="" old_value="" new_value=""
    local applied_by="" status="" backup_file="" error_msg="" batch_id="" reverted_audit_id="" conf_sha256="" sha_verify="" backup_sha256=""

    local kv
    for kv in "$@"; do
        local k="${kv%%=*}" v="${kv#*=}"
        case "$k" in
            target)            target="$v" ;;
            target_ip)         target_ip="$v" ;;
            cpu)               cpu="$v" ;;
            ram_gb)            ram_gb="$v" ;;
            pg_version)        pg_version="$v" ;;
            param_name)        param_name="$v" ;;
            old_value)         old_value="$v" ;;
            new_value)         new_value="$v" ;;
            applied_by)        applied_by="$v" ;;
            status)            status="$v" ;;
            backup_file)       backup_file="$v" ;;
            error_msg)         error_msg="$v" ;;
            batch_id)          batch_id="$v" ;;
            reverted_audit_id) reverted_audit_id="$v" ;;
            conf_sha256)       conf_sha256="$v" ;;
            sha_verify)        sha_verify="$v" ;;
            backup_sha256)     backup_sha256="$v" ;;
        esac
    done

    local ts
    ts=$(date '+%Y-%m-%d %H:%M:%S %z')

    # ── 檔案寫入（永遠寫，先建目錄）──
    mkdir -p "$(dirname "$PGAPPLY_LOG_FILE")" 2>/dev/null || true
    {
        printf '{"ts":"%s","target":"%s","target_ip":"%s","cpu":"%s","ram_gb":"%s","pg_version":"%s","param":"%s","old":%s,"new":%s,"by":"%s","status":"%s","backup":"%s","batch":"%s","reverted":"%s","err":%s}\n' \
            "$ts" "$target" "$target_ip" "$cpu" "$ram_gb" "$pg_version" "$param_name" \
            "$(_pgapply_json_str "$old_value")" "$(_pgapply_json_str "$new_value")" \
            "$applied_by" "$status" "$backup_file" "$batch_id" "$reverted_audit_id" \
            "$(_pgapply_json_str "$error_msg")"
    } >> "$PGAPPLY_LOG_FILE" 2>/dev/null || echo "WARN: 寫 $PGAPPLY_LOG_FILE 失敗" >&2

    # ── DB 寫入（失敗只警告）──
    _pgapply_log_audit_db \
        "$target" "$target_ip" "$cpu" "$ram_gb" "$pg_version" \
        "$param_name" "$old_value" "$new_value" \
        "$applied_by" "$status" "$backup_file" "$error_msg" "$batch_id" \
        "$reverted_audit_id" "$conf_sha256" "$sha_verify" "$backup_sha256"
}

# ── 同機並發鎖（B 套組 #4）─────────────────────────────────────────────────
# acquire 成功 → echo "OK"，回 0
# 衝突（已被別人鎖、未過期）→ echo "BUSY|<locked_by>|<expires_at>"，回 1
# 用法：pgapply_acquire_lock GAMEDB10 apply xvincent
pgapply_acquire_lock() {
    # 在 caller 的 set -euo pipefail 下，任何子命令 fail 都可能 silent kill 整個 script
    # → 函式內 disable strict mode、純用 string match（不用 pipeline / grep）避開所有 trap
    set +eo pipefail
    local target="$1" operation="$2" locked_by="$3"
    local conn_args
    conn_args=$(_pgapply_tools_db_args 2>/dev/null)
    if [[ -z "$conn_args" ]]; then
        echo "OK"
        return 0
    fi

    # 重要：_pgapply_tools_db_args 在 $() subshell 內 export PGPASSWORD 失效，這裡重新 export
    if [[ -n "${PGAPPLY_DB_PASS:-}" ]]; then
        export PGPASSWORD="$PGAPPLY_DB_PASS"
    fi

    local target_esc="${target//\'/\'\'}"
    local op_esc="${operation//\'/\'\'}"
    local by_esc="${locked_by//\'/\'\'}"

    local result
    result=$(psql $conn_args -AtX 2>&1 <<SQL
DELETE FROM public.pg_apply_lock WHERE expires_at < CURRENT_TIMESTAMP;
INSERT INTO public.pg_apply_lock (target, locked_by, operation)
VALUES ('$target_esc', '$by_esc', '$op_esc')
ON CONFLICT (target) DO NOTHING
RETURNING 'OK';
SELECT COALESCE(NULLIF(MAX(locked_by), ''), '?') || '|' || to_char(MAX(expires_at), 'YYYY-MM-DD HH24:MI:SS')
FROM public.pg_apply_lock WHERE target='$target_esc';
SQL
)

    # psql 連線失敗（FATAL/error/timeout 等） → fallback 不擋
    case "$result" in
        *FATAL*|*"psql:"*|*"could not"*|*"server closed"*)
            echo "OK"
            return 0
            ;;
    esac
    # INSERT 成功 → result 第一行是 'OK'（接著 SELECT 一行 by|expires）
    case "$result" in
        OK$'\n'*|"OK")
            echo "OK"
            return 0
            ;;
    esac
    # INSERT conflict → 取最後一行作 info
    local info="${result##*$'\n'}"
    [[ -z "$info" ]] && info="$result"
    echo "BUSY|${info}"
    return 1
}

pgapply_release_lock() {
    local target="$1"
    local conn_args
    conn_args=$(_pgapply_tools_db_args)
    [[ -z "$conn_args" ]] && return
    local target_esc="${target//\'/\'\'}"
    psql $conn_args -AtX -c "DELETE FROM public.pg_apply_lock WHERE target='$target_esc'" >/dev/null 2>&1
}

# ── conf 內容 sha256（B 套組 #6 舊算法，已不用）─────────────────────────────
# 整檔 hash 會把 Ops 對白名單外參數的合法編輯誤判為 unverified，已停用
# 保留函式作為相容性備援，但 audit 寫入流程不再呼叫
pgapply_compute_conf_sha256() {
    local target="$1" conf_path="$2"
    [[ -z "$target" || -z "$conf_path" ]] && return
    ssh "${PGAPPLY_SSH_OPTS[@]}" "$target" \
        "sudo -n cat $(printf '%q' "$conf_path") 2>/dev/null | sha256sum | awk '{print \$1}'" 2>/dev/null
}

# ── 白名單參數指紋（Option D，B 套組 #6 第二次修正）───────────────────────────
# SSH 過去 SHOW 所有 PGAPPLY_WHITELIST 參數，排序後 sha256
# 取代整檔 hash：Ops 編輯白名單外參數（如 log_*）不會影響此指紋，避免誤判
# 用法：fp=$(pgapply_compute_managed_fingerprint GAMEDB10)
pgapply_compute_managed_fingerprint() {
    local target="$1"
    [[ -z "$target" ]] && return
    local params_csv
    params_csv=$(IFS=','; echo "${PGAPPLY_WHITELIST[*]}")
    ssh "${PGAPPLY_SSH_OPTS[@]}" "$target" \
        PARAM_LIST="$params_csv" bash 2>/dev/null <<'REMOTE'
IFS=',' read -ra PARAMS_ARR <<< "$PARAM_LIST"
for p in "${PARAMS_ARR[@]}"; do
    v=$(sudo -n -u postgres psql -AtX -c "SHOW $p" 2>/dev/null)
    echo "$p=$v"
done | LC_ALL=C sort | sha256sum | awk '{print $1}'
REMOTE
}

# 查 (target, param_name) 最近一筆 status='success' 的 audit id，rollback 時帶入 reverted_audit_id
# echo audit_id（找不到回空字串）
pgapply_find_success_audit_id() {
    local target="$1" param_name="$2"
    local conn_args
    conn_args=$(_pgapply_tools_db_args)
    [[ -z "$conn_args" ]] && return

    local target_esc="${target//\'/\'\'}"
    local param_esc="${param_name//\'/\'\'}"
    psql $conn_args -AtX -c \
        "SELECT id FROM public.pg_apply_audit
         WHERE target='$target_esc' AND param_name='$param_esc' AND status='success'
         ORDER BY id DESC LIMIT 1" 2>/dev/null
}

# 內部：JSON-escape 字串（NULL 用 "null"）
_pgapply_json_str() {
    local s="$1"
    if [[ -z "$s" ]]; then
        echo "null"
        return
    fi
    # escape backslash, quote, newline
    s="${s//\\/\\\\}"
    s="${s//\"/\\\"}"
    s="${s//$'\n'/\\n}"
    echo "\"$s\""
}

# 內部：DB 寫入（連 tools DB）
_pgapply_log_audit_db() {
    local target="$1" target_ip="$2" cpu="$3" ram_gb="$4" pg_version="$5"
    local param_name="$6" old_value="$7" new_value="$8"
    local applied_by="$9" status="${10}" backup_file="${11}" error_msg="${12}" batch_id="${13}"
    local reverted_audit_id="${14:-}" conf_sha256="${15:-}" sha_verify="${16:-}" backup_sha256="${17:-}"

    # 抓 tools DB 連線（從 ~/.gamerc 或 config）
    local conn_args
    conn_args=$(_pgapply_tools_db_args)
    if [[ -z "$conn_args" ]]; then
        echo "WARN: 找不到 tools DB 連線設定，audit DB 寫入跳過" >&2
        return
    fi

    # 用 psql -v 把參數安全傳入
    psql $conn_args -v ON_ERROR_STOP=1 -AtX <<SQL >/dev/null 2>&1 || \
        echo "WARN: audit DB 寫入失敗（檔案 log 已寫）" >&2
INSERT INTO public.pg_apply_audit
    (target, target_ip, cpu, ram_gb, pg_version, param_name, old_value, new_value,
     applied_by, status, backup_file, error_msg, batch_id, reverted_audit_id, conf_sha256, sha_verify, backup_sha256)
VALUES
    ($(_pgapply_sql_str "$target"),
     $(_pgapply_sql_inet "$target_ip"),
     $(_pgapply_sql_int "$cpu"),
     $(_pgapply_sql_int "$ram_gb"),
     $(_pgapply_sql_str "$pg_version"),
     $(_pgapply_sql_str "$param_name"),
     $(_pgapply_sql_str "$old_value"),
     $(_pgapply_sql_str "$new_value"),
     $(_pgapply_sql_str "$applied_by"),
     $(_pgapply_sql_str "$status"),
     $(_pgapply_sql_str "$backup_file"),
     $(_pgapply_sql_str "$error_msg"),
     $(_pgapply_sql_str "$batch_id"),
     $(_pgapply_sql_int "$reverted_audit_id"),
     $(_pgapply_sql_str "$conf_sha256"),
     $(_pgapply_sql_str "$sha_verify"),
     $(_pgapply_sql_str "$backup_sha256"));
SQL
}

# 內部：SQL string literal（含 NULL 處理 + 跳脫）
_pgapply_sql_str() {
    local s="$1"
    if [[ -z "$s" ]]; then
        echo "NULL"
    else
        s="${s//\'/\'\'}"
        echo "'$s'"
    fi
}

_pgapply_sql_int() {
    local s="$1"
    if [[ -z "$s" || ! "$s" =~ ^[0-9]+$ ]]; then
        echo "NULL"
    else
        echo "$s"
    fi
}

_pgapply_sql_inet() {
    local s="$1"
    if [[ -z "$s" ]]; then
        echo "NULL"
    else
        s="${s//\'/\'\'}"
        echo "'$s'::inet"
    fi
}

# 內部：取 tools DB psql 連線參數
# 優先讀 env var（PGAPPLY_DB_IP/NAME/USER/PASS，由 ajax 端注入）；
# 沒有才 fallback 讀 config.php（CLI 直接呼叫的情境）
_pgapply_tools_db_args() {
    local ip="${PGAPPLY_DB_IP:-}"
    local name="${PGAPPLY_DB_NAME:-}"
    local user="${PGAPPLY_DB_USER:-}"
    local pass="${PGAPPLY_DB_PASS:-}"

    # env var 都齊了，直接用
    if [[ -n "$ip" && -n "$name" && -n "$user" ]]; then
        export PGPASSWORD="$pass"
        echo "-h $ip -d $name -U $user"
        return
    fi

    # Fallback：grep config.php（CLI 直接呼叫的情境）
    local cfg_files=(
        "${HOME}/www/tools/config.php"
        "${HOME}/www/tools-dev/config.php"
        "/var/www/html/tools/config.php"
        "/var/www/html/tools-dev/config.php"
    )
    local cfg=""
    local f
    for f in "${cfg_files[@]}"; do
        [[ -r "$f" ]] && { cfg="$f"; break; }
    done
    [[ -z "$cfg" ]] && return

    ip=$(grep -oP "tools_db_ip[^=]*=\s*'\K[^']+" "$cfg" 2>/dev/null | head -1)
    name=$(grep -oP "tools_db_name[^=]*=\s*'\K[^']+" "$cfg" 2>/dev/null | head -1)
    user=$(grep -oP "tools_db_user[^=]*=\s*'\K[^']+" "$cfg" 2>/dev/null | head -1)
    pass=$(grep -oP "tools_db_pass[^=]*=\s*'\K[^']+" "$cfg" 2>/dev/null | head -1)

    [[ -z "$ip" || -z "$name" || -z "$user" ]] && return

    export PGPASSWORD="$pass"
    echo "-h $ip -d $name -U $user"
}

# ── batch_id 產生 ───────────────────────────────────────────────────────────

pgapply_new_batch_id() {
    echo "B$(date '+%Y%m%d%H%M%S')$$"
}

# ── timestamp suffix（給 backup 檔案用） ────────────────────────────────────

pgapply_ts_suffix() {
    date '+%Y%m%d-%H%M%S'
}
