#!/bin/bash
# pg_apply.sh — pg_analyzer Phase 2a 套用單一參數到 target 機器
#
# 用法：
#   pg_apply.sh TARGET PARAM NEW_VALUE [--batch-id ID]   # 完整套用
#   pg_apply.sh --check-only TARGET                       # 只檢查不操作
#   pg_apply.sh --dry-run TARGET PARAM NEW_VALUE          # 乾跑（不 sed/restart/audit）
#
# 流程（完整套用）：
#   1. SPEC 收集
#   2. Pre-flight（PG running + Game Server stopped）
#   3. 取 postgresql.conf 路徑
#   4. 取 OLD value（SHOW）
#   5. Backup conf
#   6. sed in-place 改值（修最後一個 active；都沒有則 append）
#   7. systemctl restart（preflight 已確認 Game Server offline）
#   8. SHOW 新值驗證
#   9. 失敗 auto-rollback
#  10. 寫 audit log
#
# 輸出：JSON（單行）給 ajax 解析

set -euo pipefail

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

usage() {
    sed -n '2,8p' "$0" | sed 's/^# //'
    exit 0
}

# ── 解析 flag ───────────────────────────────────────────────────────────────

CHECK_ONLY=0
DRY_RUN=0
TARGET=""
PARAM=""
NEW_VALUE=""
BATCH_ID=""

while [[ $# -gt 0 ]]; do
    case "$1" in
        --check-only) CHECK_ONLY=1; shift ;;
        --dry-run)    DRY_RUN=1; shift ;;
        --batch-id)   BATCH_ID="$2"; shift 2 ;;
        --help|-h)    usage ;;
        --*)          echo "ERROR: 未知 flag: $1" >&2; exit 1 ;;
        *)
            if   [[ -z "$TARGET" ]];    then TARGET="$1"
            elif [[ -z "$PARAM" ]];     then PARAM="$1"
            elif [[ -z "$NEW_VALUE" ]]; then NEW_VALUE="$1"
            else echo "ERROR: 多餘參數: $1" >&2; exit 1
            fi
            shift
            ;;
    esac
done

[[ -z "$TARGET" ]] && { echo "ERROR: 必須指定 TARGET" >&2; usage; }

# ── JSON 輸出 helper ────────────────────────────────────────────────────────
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"
}

# ── --check-only 流程（保留 M1 行為） ───────────────────────────────────────

if (( CHECK_ONLY == 1 )); then
    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 ))

    if ! pf_msg=$(pgapply_preflight "$TARGET" 2>&1); then
        emit_json "fail" "preflight 失敗" detail "$pf_msg"
        exit 2
    fi

    conf_path=$(pgapply_get_conf_path "$TARGET")
    [[ -z "$conf_path" ]] && { emit_json "fail" "取不到 config_file" ; exit 3; }

    emit_json "ok" "ready to apply" \
        cpu "$nproc" ram_gb "$ram_gb" pg_version "$pg_version" conf_path "$conf_path"
    exit 0
fi

# ── 完整 apply 流程 ──────────────────────────────────────────────────────────

[[ -z "$PARAM" || -z "$NEW_VALUE" ]] && { echo "ERROR: 完整套用需 TARGET PARAM NEW_VALUE" >&2; exit 1; }

# 0. 白名單檢查（避免 caller 帶 restart-required param 進來）
if ! pgapply_is_whitelisted "$PARAM"; then
    emit_json "fail" "param 不在白名單" param "$PARAM"
    exit 1
fi

# 0.1 簡單 sanity：NEW_VALUE 不該含換行 / 引號 / 分號
if [[ "$NEW_VALUE" =~ [$'\n;'\'] ]]; then
    emit_json "fail" "new_value 含非法字元" value "$NEW_VALUE"
    exit 1
fi


# 1. SPEC 收集
spec_data=$(pgapply_collect_spec "$TARGET")
[[ -z "$spec_data" ]] && { emit_json "fail" "SSH 失敗或 spec 收集失敗"; 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
    if ! pf_msg=$(pgapply_preflight "$TARGET" 2>&1); then
        pgapply_log_audit \
            target="$TARGET" cpu="$NPROC" ram_gb="$RAM_GB" pg_version="$PG_VERSION" \
            param_name="$PARAM" new_value="$NEW_VALUE" \
            applied_by="${SUDO_USER:-${USER:-unknown}}" \
            status="fail" error_msg="preflight: $pf_msg" batch_id="$BATCH_ID"
        emit_json "fail" "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; }

# ── --dry-run 流程：抓 OLD + grep sed 行號，不 sed / restart / audit ──────────
if (( DRY_RUN == 1 )); then
    dry_result=$(ssh "${PGAPPLY_SSH_OPTS[@]}" "$TARGET" \
        PARAM="$PARAM" CONF_PATH="$CONF_PATH" \
        bash 2>&1 <<'REMOTE' || true
set +e
OLD_VALUE=$(sudo -n -u postgres psql -AtX -c "SHOW $PARAM" 2>/dev/null)
if [[ -z "$OLD_VALUE" ]]; then
    echo "FAIL_PG_DOWN"
    exit 1
fi
LAST_LINE=$(sudo grep -nE "^[[:space:]]*${PARAM}[[:space:]]*=" "$CONF_PATH" 2>/dev/null | tail -1 | cut -d: -f1)
if [[ -n "$LAST_LINE" ]]; then
    EXISTING=$(sudo sed -n "${LAST_LINE}p" "$CONF_PATH" 2>/dev/null)
    EXISTING_B64=$(printf '%s' "$EXISTING" | base64 -w0)
    echo "DRY|OLD=${OLD_VALUE}|LINE=${LAST_LINE}|ACTION=modify|EXISTING_B64=${EXISTING_B64}"
else
    echo "DRY|OLD=${OLD_VALUE}|LINE=0|ACTION=append|EXISTING_B64="
fi
REMOTE
    )
    case "$dry_result" in
        DRY\|*)
            OLD_VALUE=$(echo "$dry_result"   | sed -n 's/.*OLD=\([^|]*\).*/\1/p')
            LINE_NUM=$(echo "$dry_result"    | sed -n 's/.*LINE=\([^|]*\).*/\1/p')
            ACTION=$(echo "$dry_result"      | sed -n 's/.*ACTION=\([^|]*\).*/\1/p')
            EXISTING_B64=$(echo "$dry_result"| sed -n 's/.*EXISTING_B64=\([^|]*\).*/\1/p')
            EXISTING_LINE=""
            [[ -n "$EXISTING_B64" ]] && EXISTING_LINE=$(printf '%s' "$EXISTING_B64" | base64 -d 2>/dev/null)
            NEW_LINE_TEXT="${PARAM} = ${NEW_VALUE}  # pg_apply (dry-run preview)"

            emit_json "dry_run_ok" "preview" \
                old_value "$OLD_VALUE" new_value "$NEW_VALUE" \
                sed_line "$LINE_NUM" action "$ACTION" \
                existing_line "$EXISTING_LINE" new_line "$NEW_LINE_TEXT" \
                conf_path "$CONF_PATH"
            exit 0 ;;
        FAIL_PG_DOWN)
            emit_json "fail" "PG not responding（SHOW 失敗，無法取 OLD）"
            exit 4 ;;
        *)
            emit_json "fail" "dry-run unexpected" detail "$dry_result"
            exit 9 ;;
    esac
fi

# 4-9. 在 target 機器上執行：取 OLD / backup / sed / restart / SHOW / auto-rollback
TS=$(pgapply_ts_suffix)
APPLIED_BY="${SUDO_USER:-${USER:-unknown}}"

# Apply 前算白名單參數指紋（= rollback 之後若還原到此狀態應對到的 backup_sha256）
PRE_FINGERPRINT=$(pgapply_compute_managed_fingerprint "$TARGET")

remote_result=$(ssh "${PGAPPLY_SSH_OPTS[@]}" "$TARGET" \
    PARAM="$PARAM" NEW_VALUE="$NEW_VALUE" CONF_PATH="$CONF_PATH" TS="$TS" \
    bash 2>&1 <<'REMOTE' || true
set +e

# 4. 取 OLD value
OLD_VALUE=$(sudo -n -u postgres psql -AtX -c "SHOW $PARAM" 2>/dev/null)
if [[ -z "$OLD_VALUE" ]]; then
    echo "FAIL_GET_OLD_VALUE"
    exit 4
fi

# 5. Backup（放在 conf_backups/ 子目錄，跟 active conf 分開、權限 postgres:postgres）
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

# 6. sed in-place
LAST_LINE=$(sudo grep -nE "^[[:space:]]*${PARAM}[[:space:]]*=" "$CONF_PATH" | tail -1 | cut -d: -f1)
if [[ -n "$LAST_LINE" ]]; then
    if ! sudo sed -i "${LAST_LINE}s|.*|${PARAM} = ${NEW_VALUE}  # pg_apply ${TS}|" "$CONF_PATH" 2>&1; then
        echo "FAIL_SED"
        sudo cp -p "$BACKUP_FILE" "$CONF_PATH"
        exit 6
    fi
else
    if ! echo "${PARAM} = ${NEW_VALUE}  # pg_apply ${TS}" | sudo tee -a "$CONF_PATH" >/dev/null 2>&1; then
        echo "FAIL_APPEND"
        sudo cp -p "$BACKUP_FILE" "$CONF_PATH"
        exit 6
    fi
fi

# 偵測 PG cluster version + name（Debian: /etc/postgresql/16/main → 16/main）
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')
PGDATA_DIR=$(sudo -n -u postgres psql -AtX -c "SHOW data_directory" 2>/dev/null)

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

# 等 PG 真的接受連線（成功回 0）
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
}

# PG restart wrapper：用 systemctl，失敗時診斷 PG 實際狀態強救
do_pg_restart() {
    local out
    out=$(sudo -n systemctl restart "$PG_UNIT" 2>&1)
    if wait_pg_ready; then
        echo "$out"
        return 0
    fi
    # 失敗救火：systemd state 可能跟 PG 不同步。看 PG process 是不是還活著
    if pgrep -f "postgres -D /var/lib/postgresql/${PGCLUSTER_VER:-16}" >/dev/null 2>&1; then
        # PG 還在跑（zombie state），優雅 stop
        sudo -n -u postgres "$PG_BIN" -D "${PGDATA_DIR:-/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
}

# 7. 套用：一律 restart（工具 maintenance-only，preflight 已確認 server stop）
OP_OUT=$(do_pg_restart)
OP_RC=$?

if [[ "$OP_RC" -ne 0 ]]; then
    # 失敗 → revert conf 後再 restart 救火
    sudo cp -p "$BACKUP_FILE" "$CONF_PATH"
    do_pg_restart >/dev/null 2>&1
    REVERT_RC=$?
    if [[ "$REVERT_RC" -ne 0 ]]; then
        echo "FAIL_RESTART_AFTER_REVERT:${OP_OUT}"
        exit 99
    fi
    echo "FAIL_RESTART:${OP_OUT}"
    exit 7
fi

# 8. SHOW 新值驗證（先 PG 活、SHOW 非空、值有變）
if ! wait_pg_ready; then
    sudo cp -p "$BACKUP_FILE" "$CONF_PATH"
    do_pg_restart >/dev/null 2>&1
    echo "FAIL_VERIFY_PG_DEAD"
    exit 8
fi
NEW_ACTUAL=$(sudo -n -u postgres psql -AtX -c "SHOW $PARAM" 2>/dev/null)
if [[ -z "$NEW_ACTUAL" ]]; then
    sudo cp -p "$BACKUP_FILE" "$CONF_PATH"
    do_pg_restart >/dev/null 2>&1
    echo "FAIL_VERIFY_EMPTY:OLD=${OLD_VALUE}"
    exit 8
fi
if [[ "$NEW_ACTUAL" == "$OLD_VALUE" ]]; then
    sudo cp -p "$BACKUP_FILE" "$CONF_PATH"
    do_pg_restart >/dev/null 2>&1
    echo "FAIL_VERIFY_NO_CHANGE:OLD=${OLD_VALUE}|NEW=${NEW_ACTUAL}"
    exit 8
fi

echo "OK|OLD=${OLD_VALUE}|NEW=${NEW_ACTUAL}|BACKUP=${BACKUP_FILE}"
REMOTE
)

# 解析 remote_result
case "$remote_result" in
    OK\|*)
        # OK|OLD=on|NEW=off|BACKUP=...
        OLD_VALUE=$(echo "$remote_result" | sed -n 's/.*OLD=\([^|]*\).*/\1/p')
        NEW_ACTUAL=$(echo "$remote_result" | sed -n 's/.*NEW=\([^|]*\).*/\1/p')
        BACKUP_FILE=$(echo "$remote_result" | sed -n 's/.*BACKUP=\([^|]*\).*/\1/p')

        # Apply 後白名單參數指紋（rollback 還原到此狀態應對到的 conf_sha256）
        POST_FINGERPRINT=$(pgapply_compute_managed_fingerprint "$TARGET")

        pgapply_log_audit \
            target="$TARGET" cpu="$NPROC" ram_gb="$RAM_GB" pg_version="$PG_VERSION" \
            param_name="$PARAM" old_value="$OLD_VALUE" new_value="$NEW_ACTUAL" \
            applied_by="$APPLIED_BY" status="success" \
            backup_file="$BACKUP_FILE" batch_id="$BATCH_ID" \
            conf_sha256="$POST_FINGERPRINT" backup_sha256="$PRE_FINGERPRINT"

        emit_json "ok" "applied" \
            old_value "$OLD_VALUE" new_value "$NEW_ACTUAL" backup_file "$BACKUP_FILE" \
            conf_path "$CONF_PATH"
        exit 0 ;;
    FAIL_*)
        pgapply_log_audit \
            target="$TARGET" cpu="$NPROC" ram_gb="$RAM_GB" pg_version="$PG_VERSION" \
            param_name="$PARAM" new_value="$NEW_VALUE" \
            applied_by="$APPLIED_BY" status="fail" \
            error_msg="$remote_result" batch_id="$BATCH_ID"

        emit_json "fail" "apply failed" detail "$remote_result"
        exit 8 ;;
    *)
        pgapply_log_audit \
            target="$TARGET" cpu="$NPROC" ram_gb="$RAM_GB" pg_version="$PG_VERSION" \
            param_name="$PARAM" new_value="$NEW_VALUE" \
            applied_by="$APPLIED_BY" status="fail" \
            error_msg="unexpected: $remote_result" batch_id="$BATCH_ID"

        emit_json "fail" "unexpected result" detail "$remote_result"
        exit 9 ;;
esac
