#!/bin/bash
# pg_drift_monitor.sh — pg_analyzer drift 監控 cron
#
# 跑在 Tools host 上，cron 每 6 小時：
#   1. 跑 pg_analyzer.sh --tsv 收所有 DB 機器當前 drift 狀況
#   2. PARAM \t name \t cur \t rec \t diff 過濾白名單後算 drift count + params
#   3. 跟 pg_drift_state DB 比對：
#        appeared (0 → >0) | recovered (>0 → 0) | changed (drift_params set 變動) → 推 chat
#        第一次見到的 target 不推（避免新環境 spam）
#   4. UPSERT pg_drift_state（state 變才更新 last_changed_at；總會更新 last_evaluated_at）
#
# webhook 從 config.php $GLOBALS['gchat_webhook_alerter'] 抽（與 usage_alerter 共用）
# log 寫 files/pg_apply_log/pg_drift_monitor.log

set -u

TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
CONFIG_PHP="$TOOLS_DIR/config.php"
LOG_FILE="$TOOLS_DIR/files/pg_apply_log/pg_drift_monitor.log"
PG_ANALYZER="$TOOLS_DIR/scripts/bin/pg_analyzer/pg_analyzer.sh"

_log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" | tee -a "$LOG_FILE" >&2; }

_cfg_get() {
    local key="$1"
    sed -n "s/.*\$GLOBALS\['${key}'\][[:space:]]*=[[:space:]]*['\"]\([^'\"]*\).*/\1/p" "$CONFIG_PHP" 2>/dev/null | head -1
}

[[ -f "$CONFIG_PHP" ]] || { _log "❌ config.php 找不到: $CONFIG_PHP"; exit 1; }
[[ -x "$PG_ANALYZER" ]] || { _log "❌ pg_analyzer.sh 不可執行: $PG_ANALYZER"; exit 1; }

DB_IP="$(_cfg_get tools_db_ip)"
DB_NAME="$(_cfg_get tools_db_name)"
DB_USER="$(_cfg_get tools_db_user)"
DB_PASS="$(_cfg_get tools_db_pass)"
COUNTRY="$(_cfg_get country)"
MACHINE_KIND="$(_cfg_get machine)"
TOOLS_URL="$(_cfg_get tools_url)"
WEBHOOK="$(_cfg_get gchat_webhook_alerter)"

ENV_CODE="N1-$(echo "$COUNTRY" | tr 'a-z' 'A-Z')"
[[ "$MACHINE_KIND" == *DEV* || "$MACHINE_KIND" == *dev* ]] && ENV_CODE="${ENV_CODE}-DEV"

if [[ -z "$DB_IP" || -z "$DB_PASS" || -z "$DB_NAME" || -z "$DB_USER" ]]; then
    _log "❌ config.php 抽不出 tools_db_*"
    exit 1
fi
if [[ -z "$WEBHOOK" ]]; then
    _log "⚠️  gchat_webhook_alerter 未設定，仍會評估+寫 state，但不推 chat"
fi

_psql() {
    PGPASSWORD="$DB_PASS" psql -h "$DB_IP" -U "$DB_USER" -d "$DB_NAME" \
        -AtX -F '|' -v ON_ERROR_STOP=1 "$@"
}

# 白名單對齊 _lib_pg_apply.sh PGAPPLY_WHITELIST（17 項）
# 只有白名單內的 diff 才算「可套用的 drift」，超出範圍 pg_apply 不支援，沒必要 alert
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 "

is_whitelisted() {
    local p="$1"
    [[ "$WHITELIST" == *" $p "* ]]
}

push_chat() {
    local title="$1" body="$2"
    [[ -z "$WEBHOOK" ]] && return 0
    local payload
    payload=$(printf '{"text": "*%s*\\n%s"}' "$title" "$body" | sed 's/[[:cntrl:]]//g')
    curl -sS -m 8 -H 'Content-Type: application/json' -X POST -d "$payload" "$WEBHOOK" >/dev/null 2>&1 || \
        _log "⚠️  webhook 推送失敗 title='$title'"
}

# ── 1. 拉舊 state（per target） ────────────────────────────────────────
declare -A OLD_DRIFT_COUNT=() OLD_DRIFT_PARAMS=()
mapfile -t STATE_ROWS < <(_psql -c "
    SELECT target, drift_count, COALESCE(drift_params,'')
    FROM public.pg_drift_state;
")
for r in "${STATE_ROWS[@]}"; do
    [[ -z "$r" ]] && continue
    IFS='|' read -r tgt cnt prm <<< "$r"
    OLD_DRIFT_COUNT[$tgt]="$cnt"
    OLD_DRIFT_PARAMS[$tgt]="$prm"
done

# ── 2. 跑 pg_analyzer.sh --tsv 收當前狀態 ─────────────────────────────
ts_start=$(date '+%F %T')
TSV_OUTPUT=$("$PG_ANALYZER" --tsv 2>>"$LOG_FILE" || true)

if [[ -z "$TSV_OUTPUT" ]]; then
    _log "❌ pg_analyzer.sh --tsv 沒輸出，可能是 SSH 全失敗或 conf 缺失"
    exit 1
fi

# Parse TSV：META target / ip / aliases 開新 block，PARAM 累積 drift
declare -A CUR_DRIFT_COUNT=() CUR_DRIFT_PARAMS=() CUR_TARGET_IP=() CUR_HAS_ERROR=()
cur_target=""
cur_ip=""
cur_aliases=""
cur_drift_list=""
cur_error=0

flush_block() {
    [[ -z "$cur_target" ]] && return
    # 從 aliases 找 DB alias 當 state key（對齊 pg_apply target 命名）
    local db_alias=""
    for a in $cur_aliases; do
        if [[ "$a" =~ ^(WORLDDB|GAMEDB|ACCOUNTDB|SOCIETYDB)[0-9]*$ ]]; then
            db_alias="$a"; break
        fi
    done
    [[ -z "$db_alias" ]] && db_alias="$cur_target"
    if (( cur_error == 1 )); then
        CUR_HAS_ERROR[$db_alias]=1
    else
        CUR_TARGET_IP[$db_alias]="$cur_ip"
        local cnt
        cnt=$(printf '%s\n' "$cur_drift_list" | grep -cE '^[a-z_]+$' || true)
        CUR_DRIFT_COUNT[$db_alias]="$cnt"
        # 排序讓 set 比對穩定（避免 hash map 順序差異 false positive）
        local sorted
        sorted=$(printf '%s\n' "$cur_drift_list" | grep -E '^[a-z_]+$' | sort -u | tr '\n' ' ' | sed 's/ $//')
        CUR_DRIFT_PARAMS[$db_alias]="$sorted"
    fi
}

while IFS= read -r line; do
    case "$line" in
        "META"$'\t'"target"$'\t'*)
            flush_block
            cur_target="${line##*$'\t'}"
            cur_ip=""; cur_aliases=""; cur_drift_list=""; cur_error=0
            ;;
        "META"$'\t'"ip"$'\t'*)      cur_ip="${line##*$'\t'}" ;;
        "META"$'\t'"aliases"$'\t'*) cur_aliases="${line##*$'\t'}" ;;
        "ERROR"$'\t'*)              cur_error=1 ;;
        "PARAM"$'\t'*)
            IFS=$'\t' read -r _ pname _ _ pstatus <<< "$line"
            if [[ "$pstatus" == "diff" ]] && is_whitelisted "$pname"; then
                cur_drift_list+="${pname}"$'\n'
            fi
            ;;
    esac
done <<< "$TSV_OUTPUT"
flush_block

total_targets=$(( ${#CUR_DRIFT_COUNT[@]} + ${#CUR_HAS_ERROR[@]} ))
if (( total_targets == 0 )); then
    _log "⚠️  TSV 解析無 target，pg_analyzer 可能異常"
    exit 1
fi

# ── 3. 比對 old vs new，推 chat（state-change-only） ──────────────────
fired=0; unchanged=0; firstseen=0; errored=${#CUR_HAS_ERROR[@]}
declare -a UPSERT_VALUES

for tgt in "${!CUR_DRIFT_COUNT[@]}"; do
    cnt="${CUR_DRIFT_COUNT[$tgt]}"
    prm="${CUR_DRIFT_PARAMS[$tgt]}"
    ip="${CUR_TARGET_IP[$tgt]}"
    old_cnt="${OLD_DRIFT_COUNT[$tgt]-__MISSING__}"
    old_prm="${OLD_DRIFT_PARAMS[$tgt]:-}"

    state_changed=0
    change_kind=""
    if [[ "$old_cnt" == "__MISSING__" ]]; then
        # 第一次見到 → 不 fire（避免新環境 spam），只寫 state
        firstseen=$((firstseen+1))
    elif [[ "$cnt" == "0" && "$old_cnt" != "0" ]]; then
        state_changed=1; change_kind="recovered"
    elif [[ "$cnt" != "0" && "$old_cnt" == "0" ]]; then
        state_changed=1; change_kind="appeared"
    elif [[ "$cnt" != "0" && "$prm" != "$old_prm" ]]; then
        state_changed=1; change_kind="changed"
    fi

    if (( state_changed == 1 )); then
        link=""
        [[ -n "$TOOLS_URL" ]] && link=$'\n'"📊 ${TOOLS_URL%/}/admin_pg_analyzer.php#target-${tgt}"
        case "$change_kind" in
            appeared)
                title="[${ENV_CODE}][PG-DRIFT] ${tgt} 有 ${cnt} 個參數需重評"
                body="新出現 drift: ${prm}${link}"
                ;;
            recovered)
                title="[${ENV_CODE}][PG-OK] ${tgt} 全部一致"
                body="先前 drift: ${old_prm}${link}"
                ;;
            changed)
                title="[${ENV_CODE}][PG-DRIFT] ${tgt} drift 變化 (${old_cnt}→${cnt})"
                body="舊: ${old_prm}"$'\n'"新: ${prm}${link}"
                ;;
        esac
        push_chat "$title" "$body"
        _log "🔔 fire ${tgt} ${change_kind} old=${old_cnt} new=${cnt}"
        fired=$((fired+1))
    else
        unchanged=$((unchanged+1))
    fi

    tgt_e=${tgt//\'/\'\'}
    prm_e=${prm//\'/\'\'}
    if [[ -n "$ip" ]]; then
        ip_e=${ip//\'/\'\'}
        ip_sql="'$ip_e'::inet"
    else
        ip_sql="NULL"
    fi
    UPSERT_VALUES+=("('$tgt_e',${ip_sql},${cnt:-0},'$prm_e')")
done

# ── 4. UPSERT pg_drift_state ───────────────────────────────────────────
if (( ${#UPSERT_VALUES[@]} > 0 )); then
    values_csv=""
    for v in "${UPSERT_VALUES[@]}"; do values_csv+="$v,"; done
    values_csv="${values_csv%,}"
    {
        echo "INSERT INTO public.pg_drift_state"
        echo "  (target, target_ip, drift_count, drift_params)"
        echo "VALUES"
        echo "  $values_csv"
        echo "ON CONFLICT (target) DO UPDATE SET"
        echo "  target_ip       = EXCLUDED.target_ip,"
        echo "  drift_count     = EXCLUDED.drift_count,"
        echo "  drift_params    = EXCLUDED.drift_params,"
        echo "  last_changed_at = CASE WHEN public.pg_drift_state.drift_count <> EXCLUDED.drift_count"
        echo "                          OR public.pg_drift_state.drift_params <> EXCLUDED.drift_params"
        echo "                         THEN CURRENT_TIMESTAMP"
        echo "                         ELSE public.pg_drift_state.last_changed_at"
        echo "                    END,"
        echo "  last_evaluated_at = CURRENT_TIMESTAMP;"
    } | _psql >/dev/null 2>>"$LOG_FILE"
    rc=$?
    if (( rc != 0 )); then
        _log "❌ UPSERT pg_drift_state 失敗 rc=$rc"
        exit 1
    fi
fi

_log "✓ env=${ENV_CODE} targets=${total_targets} fired=${fired} unchanged=${unchanged} firstseen=${firstseen} errored=${errored} ($ts_start → $(date '+%T'))"
