#!/bin/bash
# pg_apply_batch.sh — 一次性套用多個 param + 單一 restart（效率優化版）
#
# 用法：
#   pg_apply_batch.sh TARGET PARAMS_JSON [--batch-id ID]
#   pg_apply_batch.sh TARGET PARAMS_JSON --dry-run        # 乾跑（不 sed/restart/audit）
#
# PARAMS_JSON 格式：[{"name":"jit","value":"off"},{"name":"max_connections","value":"100"}]
#
# 流程（all-or-nothing）：
#   1. SPEC 收集
#   2. Pre-flight
#   3. 取 conf 路徑
#   4. SSH 過去：backup → loop sed N 個 param → restart 一次 → SHOW per param → audit per param
#   5. 任一步失敗 → revert backup + restart 救火，所有 param 記 fail
#
# 輸出 JSON 摘要 + per-param 結果

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./_lib_pg_apply.sh
source "${SCRIPT_DIR}/_lib_pg_apply.sh"

emit_json() {
    local status="$1" message="$2"
    shift 2
    local extra=""
    while [[ $# -gt 0 ]]; do
        local k="$1" v="$2"; shift 2
        local v_esc="${v//\\/\\\\}"
        v_esc="${v_esc//\"/\\\"}"
        extra+=",\"$k\":\"$v_esc\""
    done
    printf '{"status":"%s","message":"%s"%s}\n' "$status" "$message" "$extra"
}

TARGET="${1:-}"
PARAMS_JSON="${2:-}"
BATCH_ID=""
DRY_RUN=0
shift 2 2>/dev/null || true
while [[ $# -gt 0 ]]; do
    case "$1" in
        --batch-id) BATCH_ID="$2"; shift 2 ;;
        --dry-run)  DRY_RUN=1; shift ;;
        *)          shift ;;
    esac
done

[[ -z "$TARGET" || -z "$PARAMS_JSON" ]] && { echo "Usage: $0 TARGET PARAMS_JSON [--batch-id ID]" >&2; exit 1; }
[[ -z "$BATCH_ID" ]] && BATCH_ID=$(pgapply_new_batch_id)

# 解析 PARAMS_JSON 成 name|value 形式（用 python，跨平台穩定）
PARAMS_KV=$(python3 -c "
import json, sys
data = json.loads(sys.argv[1])
for item in data:
    n = item.get('name','')
    v = item.get('value','')
    if not n or not v: continue
    # 簡單防禦：name / value 不該含換行 / 控制字元
    if any(c in n+v for c in '\n\r;'): continue
    print(f'{n}|{v}')
" "$PARAMS_JSON" 2>/dev/null)

[[ -z "$PARAMS_KV" ]] && { emit_json "fail" "PARAMS_JSON 解析失敗或空"; exit 1; }

# 白名單檢查
while IFS='|' read -r p v; do
    [[ -z "$p" ]] && continue
    if ! pgapply_is_whitelisted "$p"; then
        emit_json "fail" "param 不在白名單" param "$p"
        exit 1
    fi
done <<< "$PARAMS_KV"

APPLIED_BY="${SUDO_USER:-${USER:-unknown}}"

# 1. SPEC
spec_data=$(pgapply_collect_spec "$TARGET")
[[ -z "$spec_data" ]] && { emit_json "fail" "SSH 失敗"; exit 1; }
NPROC=$(echo "$spec_data" | awk -F= '/^NPROC=/ {print $2}')
MEM_BYTES=$(echo "$spec_data" | awk -F= '/^MEM_BYTES=/ {print $2}')
PG_VERSION=$(echo "$spec_data" | awk -F= '/^PG_VERSION=/ {print $2}')
RAM_GB=$(( MEM_BYTES / 1073741824 ))

# 2. Pre-flight（dry-run 跳過：可任意時段預覽，不要求 game server 已停）
if (( DRY_RUN == 0 )); then
    # preflight fail = 環境因素（PG down / Game Server 在跑），跳過該機，不寫 fail audit
    if ! pf_msg=$(pgapply_preflight "$TARGET" 2>&1); then
        emit_json "skipped" "preflight failed" detail "$pf_msg"
        exit 2
    fi
fi

# 3. conf path
CONF_PATH=$(pgapply_get_conf_path "$TARGET")
[[ -z "$CONF_PATH" ]] && { emit_json "fail" "取不到 config_file"; exit 3; }

TS=$(pgapply_ts_suffix)

# ── --dry-run 流程：per-param SHOW + grep sed 行號，不 sed / restart / audit ──
if (( DRY_RUN == 1 )); then
    PARAMS_KV_B64=$(printf '%s' "$PARAMS_KV" | base64 -w0)
    dry_result=$(ssh "${PGAPPLY_SSH_OPTS[@]}" "$TARGET" \
        CONF_PATH="$CONF_PATH" PARAMS_KV_B64="$PARAMS_KV_B64" \
        bash 2>&1 <<'REMOTE' || true
set +e
PARAMS_KV=$(printf '%s' "$PARAMS_KV_B64" | base64 -d)
echo "DRY_BEGIN"
while IFS='|' read -r p v; do
    [[ -z "$p" ]] && continue
    old=$(sudo -n -u postgres psql -AtX -c "SHOW $p" 2>/dev/null)
    if [[ -z "$old" ]]; then
        echo "DRY|$p|FAIL_PG_DOWN"
        continue
    fi
    last=$(sudo grep -nE "^[[:space:]]*${p}[[:space:]]*=" "$CONF_PATH" 2>/dev/null | tail -1 | cut -d: -f1)
    if [[ -n "$last" ]]; then
        existing=$(sudo sed -n "${last}p" "$CONF_PATH" 2>/dev/null)
        existing_b64=$(printf '%s' "$existing" | base64 -w0)
        echo "DRY|$p|OLD=${old}|NEW_INTENDED=${v}|LINE=${last}|ACTION=modify|EXISTING_B64=${existing_b64}"
    else
        echo "DRY|$p|OLD=${old}|NEW_INTENDED=${v}|LINE=0|ACTION=append|EXISTING_B64="
    fi
done <<< "$PARAMS_KV"
echo "DRY_END"
echo "OK_DRY|CONF=${CONF_PATH}"
REMOTE
    )

    if [[ "$dry_result" != *"OK_DRY|"* ]]; then
        emit_json "fail" "dry-run unexpected" detail "$dry_result" batch_id "$BATCH_ID"
        exit 9
    fi

    # 解析 per-param dry 結果 → 組 JSON array
    items_json=""
    while IFS= read -r line; do
        case "$line" in
            "DRY|"*)
                rest="${line#DRY|}"
                p="${rest%%|*}"; payload="${rest#*|}"
                if [[ "$payload" == "FAIL_PG_DOWN" ]]; then
                    item="{\"param\":\"$p\",\"status\":\"fail\",\"error\":\"PG SHOW failed\"}"
                else
                    old=$(echo "$payload"      | sed -n 's/.*OLD=\([^|]*\).*/\1/p')
                    intended=$(echo "$payload" | sed -n 's/.*NEW_INTENDED=\([^|]*\).*/\1/p')
                    line_no=$(echo "$payload"  | sed -n 's/.*LINE=\([^|]*\).*/\1/p')
                    action=$(echo "$payload"   | sed -n 's/.*ACTION=\([^|]*\).*/\1/p')
                    existing_b64=$(echo "$payload" | sed -n 's/.*EXISTING_B64=\([^|]*\).*/\1/p')
                    existing=""
                    [[ -n "$existing_b64" ]] && existing=$(printf '%s' "$existing_b64" | base64 -d 2>/dev/null)
                    new_line_text="${p} = ${intended}  # pg_apply_batch (dry-run preview)"
                    # JSON escape
                    e_old=$(printf '%s' "$old" | sed 's/\\/\\\\/g; s/"/\\"/g')
                    e_int=$(printf '%s' "$intended" | sed 's/\\/\\\\/g; s/"/\\"/g')
                    e_exi=$(printf '%s' "$existing" | sed 's/\\/\\\\/g; s/"/\\"/g')
                    e_new=$(printf '%s' "$new_line_text" | sed 's/\\/\\\\/g; s/"/\\"/g')
                    item="{\"param\":\"$p\",\"status\":\"ok\",\"old_value\":\"$e_old\",\"new_value\":\"$e_int\",\"sed_line\":$line_no,\"action\":\"$action\",\"existing_line\":\"$e_exi\",\"new_line\":\"$e_new\"}"
                fi
                [[ -n "$items_json" ]] && items_json+=","
                items_json+="$item"
                ;;
        esac
    done <<< "$dry_result"

    # 直接 printf 帶 raw items_json，繞過 emit_json 的 string 包裝
    printf '{"status":"dry_run_ok","message":"preview","conf_path":"%s","items":[%s]}\n' \
        "$CONF_PATH" "$items_json"
    exit 0
fi

# 4. SSH 過去：backup → loop sed → restart → SHOW per param
# PARAMS_KV 含 `|` 會被 SSH env var passing 當 shell pipe，base64 編碼避開
PARAMS_KV_B64=$(printf '%s' "$PARAMS_KV" | base64 -w0)
remote_result=$(ssh "${PGAPPLY_SSH_OPTS[@]}" "$TARGET" \
    CONF_PATH="$CONF_PATH" TS="$TS" PARAMS_KV_B64="$PARAMS_KV_B64" \
    bash 2>&1 <<'REMOTE' || true
set +e

# 還原 PARAMS_KV
PARAMS_KV=$(printf '%s' "$PARAMS_KV_B64" | base64 -d)

# Backup
BACKUP_DIR="$(dirname "$CONF_PATH")/conf_backups"
sudo mkdir -p "$BACKUP_DIR" 2>/dev/null
sudo chown postgres:postgres "$BACKUP_DIR" 2>/dev/null
sudo chmod 750 "$BACKUP_DIR" 2>/dev/null
BACKUP_FILE="${BACKUP_DIR}/$(basename "$CONF_PATH").bak.${TS}"
if ! sudo cp -p "$CONF_PATH" "$BACKUP_FILE" 2>/dev/null; then
    echo "FAIL_BACKUP"
    exit 5
fi

# 取 OLD values（per param）
echo "OLD_BEGIN"
while IFS='|' read -r p v; do
    [[ -z "$p" ]] && continue
    old=$(sudo -n -u postgres psql -AtX -c "SHOW $p" 2>/dev/null)
    echo "OLD|$p|$old"
done <<< "$PARAMS_KV"
echo "OLD_END"

# Loop sed：每 param 改最後一個 active 行，沒有則 append
while IFS='|' read -r p v; do
    [[ -z "$p" ]] && continue
    LAST_LINE=$(sudo grep -nE "^[[:space:]]*${p}[[:space:]]*=" "$CONF_PATH" | tail -1 | cut -d: -f1)
    if [[ -n "$LAST_LINE" ]]; then
        if ! sudo sed -i "${LAST_LINE}s|.*|${p} = ${v}  # pg_apply_batch ${TS}|" "$CONF_PATH" 2>&1; then
            echo "FAIL_SED:$p"
            sudo cp -p "$BACKUP_FILE" "$CONF_PATH"
            exit 6
        fi
    else
        if ! echo "${p} = ${v}  # pg_apply_batch ${TS}" | sudo tee -a "$CONF_PATH" >/dev/null 2>&1; then
            echo "FAIL_APPEND:$p"
            sudo cp -p "$BACKUP_FILE" "$CONF_PATH"
            exit 6
        fi
    fi
done <<< "$PARAMS_KV"

# Restart wrapper
PGCLUSTER_VER=$(echo "$CONF_PATH" | sed -nE 's|^/etc/postgresql/([0-9]+)/.*$|\1|p')
PGCLUSTER_NAME=$(echo "$CONF_PATH" | sed -nE 's|^/etc/postgresql/[0-9]+/([^/]+)/.*$|\1|p')
PG_UNIT="postgresql@${PGCLUSTER_VER:-16}-${PGCLUSTER_NAME:-main}.service"
PG_BIN=$(ls /usr/lib/postgresql/*/bin/pg_ctl 2>/dev/null | tail -1)
[[ -z "$PG_BIN" ]] && PG_BIN=/usr/bin/pg_ctl

wait_pg_ready() {
    local i
    for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
        sudo -u postgres pg_isready >/dev/null 2>&1 && return 0
        sleep 2
    done
    return 1
}

do_pg_restart() {
    local out
    out=$(sudo -n systemctl restart "$PG_UNIT" 2>&1)
    if wait_pg_ready; then echo "$out"; return 0; fi
    if pgrep -f "postgres -D /var/lib/postgresql/${PGCLUSTER_VER:-16}" >/dev/null 2>&1; then
        sudo -n -u postgres "$PG_BIN" -D "/var/lib/postgresql/${PGCLUSTER_VER:-16}/main" stop -m fast >/dev/null 2>&1
        sleep 3
    fi
    sudo -n systemctl reset-failed "$PG_UNIT" 2>/dev/null
    out="${out}\n--- after rescue ---\n$(sudo -n systemctl start "$PG_UNIT" 2>&1)"
    if wait_pg_ready; then echo "$out"; return 0; fi
    echo "$out"
    return 1
}

# Restart
RESTART_OUT=$(do_pg_restart)
if [[ $? -ne 0 ]]; then
    sudo cp -p "$BACKUP_FILE" "$CONF_PATH"
    do_pg_restart >/dev/null 2>&1
    echo "FAIL_RESTART:${RESTART_OUT}"
    exit 7
fi

# SHOW per param 驗證新值（並 echo）
echo "NEW_BEGIN"
while IFS='|' read -r p v; do
    [[ -z "$p" ]] && continue
    new=$(sudo -n -u postgres psql -AtX -c "SHOW $p" 2>/dev/null)
    echo "NEW|$p|$new"
done <<< "$PARAMS_KV"
echo "NEW_END"

echo "OK|BACKUP=${BACKUP_FILE}"
REMOTE
)

# 5. 解析 + 寫 audit
case "$remote_result" in
    *"OK|"*)
        BACKUP_FILE=$(echo "$remote_result" | sed -n 's/.*BACKUP=\([^|]*\).*/\1/p')

        # 整批 sed + restart 完成後，計算當前 conf 的 sha256（per param 都帶同一份）
        CONF_SHA=$(pgapply_compute_conf_sha256 "$TARGET" "$CONF_PATH")

        declare -A OLD_VALS NEW_VALS
        while IFS= read -r line; do
            case "$line" in
                "OLD|"*)
                    rest="${line#OLD|}"; p="${rest%%|*}"; v="${rest#*|}"
                    OLD_VALS[$p]="$v" ;;
                "NEW|"*)
                    rest="${line#NEW|}"; p="${rest%%|*}"; v="${rest#*|}"
                    NEW_VALS[$p]="$v" ;;
            esac
        done <<< "$remote_result"

        # 解析 PARAMS_KV 拿到要套用的 param + intended_value，per-param verify + audit
        SUCCESS=0; FAIL=0
        while IFS='|' read -r p intended; do
            [[ -z "$p" ]] && continue
            old="${OLD_VALS[$p]:-}"
            new="${NEW_VALS[$p]:-}"
            if [[ -z "$new" || "$new" == "$old" ]]; then
                # 沒生效
                pgapply_log_audit \
                    target="$TARGET" cpu="$NPROC" ram_gb="$RAM_GB" pg_version="$PG_VERSION" \
                    param_name="$p" old_value="$old" new_value="$intended" \
                    applied_by="$APPLIED_BY" status="fail" \
                    error_msg="verify: NEW=${new}（無變動）" \
                    backup_file="$BACKUP_FILE" batch_id="$BATCH_ID" \
                    conf_sha256="$CONF_SHA"
                FAIL=$((FAIL+1))
            else
                pgapply_log_audit \
                    target="$TARGET" cpu="$NPROC" ram_gb="$RAM_GB" pg_version="$PG_VERSION" \
                    param_name="$p" old_value="$old" new_value="$new" \
                    applied_by="$APPLIED_BY" status="success" \
                    backup_file="$BACKUP_FILE" batch_id="$BATCH_ID" \
                    conf_sha256="$CONF_SHA"
                SUCCESS=$((SUCCESS+1))
            fi
        done <<< "$PARAMS_KV"

        emit_json "ok" "batch applied" \
            success "$SUCCESS" fail "$FAIL" backup_file "$BACKUP_FILE" batch_id "$BATCH_ID" \
            conf_path "$CONF_PATH"
        exit 0 ;;
    FAIL_*)
        # 整批失敗，所有 param 記 fail（共用 batch_id）
        while IFS='|' read -r p intended; do
            [[ -z "$p" ]] && continue
            pgapply_log_audit \
                target="$TARGET" cpu="$NPROC" ram_gb="$RAM_GB" pg_version="$PG_VERSION" \
                param_name="$p" new_value="$intended" \
                applied_by="$APPLIED_BY" status="fail" \
                error_msg="$remote_result" batch_id="$BATCH_ID"
        done <<< "$PARAMS_KV"

        emit_json "fail" "batch failed" detail "$remote_result" batch_id "$BATCH_ID"
        exit 8 ;;
    *)
        emit_json "fail" "unexpected result" detail "$remote_result"
        exit 9 ;;
esac
