#!/bin/bash
# pg_analyzer.sh — 掃描 N1 DB 機器，比對當前 postgresql.conf vs pgtune 建議值
#
# 用法：
#   pg_analyzer.sh                     # 掃所有 DB 機器
#   pg_analyzer.sh ACCOUNTDB GAMEDB10  # 指定 alias
#   pg_analyzer.sh 10.150.10.10        # 指定 IP
#
# 輸出：每台機器一個 section，顯示 SPEC + 目前值 vs 建議值 vs 差距

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

PGTUNE="${PGTUNE_CLI:-${SCRIPT_DIR}/pgtune_cli.py}"
if [[ ! -x "$PGTUNE" ]]; then
    echo "ERROR: $PGTUNE not found or not executable" >&2
    exit 1
fi

HOSTS_FILE="${HOME}/bin/hosts"
DB_ALIAS_PATTERN='ACCOUNTDB|GAMEDB[0-9]+|WORLDDB[0-9]+|SOCIETYDB[0-9]+'

# conf 解析（CONFIG_FILE / config_lookup / classify_workload / PGCONF_OVERRIDE_KEYS）
# 抽到 _lib_pg_conf.sh 與 pg_recommend_for_spec.sh 共用，確保兩條路徑解讀一致
# shellcheck source=./_lib_pg_conf.sh
source "${SCRIPT_DIR}/_lib_pg_conf.sh"

# ── Helpers ──────────────────────────────────────────────────────────────────
get_db_ips() {
    grep -E "$DB_ALIAS_PATTERN" "$HOSTS_FILE" 2>/dev/null | awk '{print $1}' | sort -u
}

get_aliases_for_ip() {
    local ip="$1"
    awk -v ip="$ip" '$1==ip {$1=""; print $0}' "$HOSTS_FILE" | xargs
}


# 主要 SSH 收集：nproc + mem + pg_settings 查詢
# 每一步都 || true，用 __PG_ANALYZER_END__ 標記確認 SSH 走完整段
collect_machine_data() {
    local target="$1"
    ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no \
        -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
        "$target" bash 2>&1 <<'REMOTE' || true
set +e
source ~/.gamerc 2>/dev/null
echo "NPROC=$(nproc 2>/dev/null || echo 0)"
echo "MEM_BYTES=$(free -b 2>/dev/null | awk '/^Mem:/ {print $2}')"
# 一律當 HDD 處理：GCP pd-standard 是真 HDD；OCI 雖底層 NVMe SSD，但 N1 Game DB
# 工作負載用不到 SSD 調校，統一走保守值（random_page_cost=4 / effective_io_concurrency=2）。
# 與 resize 路徑（pg_recommend_for_spec.sh --hd-type hdd）對齊。
echo "HD_TYPE=hdd"

# PG 版本偵測：先看 /etc/postgresql/ 目錄（Debian 標準）
PG_VER=$(ls -d /etc/postgresql/[0-9]* 2>/dev/null | sed 's|.*/||' | sort -n | tail -1)
echo "PG_VERSION=${PG_VER:-}"

# 試多種 psql 連線方式，用第一個成功的
PSQL_OK=""
PSQL_QUERY="SELECT 'CONF:' || name || '|' || setting || '|' || coalesce(unit,'') FROM pg_settings WHERE name IN ('max_connections','shared_buffers','effective_cache_size','maintenance_work_mem','checkpoint_completion_target','wal_buffers','default_statistics_target','random_page_cost','effective_io_concurrency','work_mem','huge_pages','jit','wal_compression','autovacuum_max_workers','autovacuum_work_mem','min_wal_size','max_wal_size','max_worker_processes','max_parallel_workers_per_gather','max_parallel_workers','max_parallel_maintenance_workers') ORDER BY name;"

# 方法 1：sudo postgres（peer auth）
if [[ -z "$PSQL_OK" ]]; then
    OUT=$(sudo -n -u postgres psql -AtXF '|' -c "$PSQL_QUERY" 2>/dev/null)
    if [[ -n "$OUT" ]]; then echo "$OUT"; PSQL_OK=sudo-postgres; fi
fi
# 方法 2：unix socket with PGUSER
if [[ -z "$PSQL_OK" ]]; then
    OUT=$(psql -U "${PGUSER:-spiritking}" -AtXF '|' -c "$PSQL_QUERY" 2>/dev/null)
    if [[ -n "$OUT" ]]; then echo "$OUT"; PSQL_OK=socket; fi
fi
# 方法 3：TCP localhost
if [[ -z "$PSQL_OK" ]]; then
    OUT=$(psql -U "${PGUSER:-spiritking}" -h localhost -AtXF '|' -c "$PSQL_QUERY" 2>/dev/null)
    if [[ -n "$OUT" ]]; then echo "$OUT"; PSQL_OK=tcp; fi
fi

# 連線統計：current / max / reserved / peak / start_time，5 欄一行用 | 分隔
CONN_QUERY="SELECT (SELECT count(*) FROM pg_stat_activity)::text || '|' || (SELECT setting FROM pg_settings WHERE name = 'max_connections') || '|' || (SELECT setting FROM pg_settings WHERE name = 'superuser_reserved_connections') || '|' || COALESCE((SELECT max(numbackends)::text FROM pg_stat_database), '') || '|' || to_char(pg_postmaster_start_time(), 'YYYY-MM-DD HH24:MI:SS')"
CONN_OUT=""
case "$PSQL_OK" in
    sudo-postgres) CONN_OUT=$(sudo -n -u postgres psql -AtX -c "$CONN_QUERY" 2>/dev/null) ;;
    socket)        CONN_OUT=$(psql -U "${PGUSER:-spiritking}" -AtX -c "$CONN_QUERY" 2>/dev/null) ;;
    tcp)           CONN_OUT=$(psql -U "${PGUSER:-spiritking}" -h localhost -AtX -c "$CONN_QUERY" 2>/dev/null) ;;
esac
echo "CONN_STATS=${CONN_OUT}"

# 若 PG_VER 是空但 /etc/postgresql/ 有目錄，重試偵測
if [[ -z "$PG_VER" ]]; then
    if command -v pg_lsclusters &>/dev/null; then
        PG_VER=$(pg_lsclusters 2>/dev/null | awk 'NR==2 {print $1}')
        echo "PG_VERSION=${PG_VER:-}"
    fi
fi

echo "PSQL_METHOD=${PSQL_OK:-none}"
echo "__PG_ANALYZER_END__"
REMOTE
}

# 把 pg_settings 原始值（含 unit）轉成 pgtune 格式（GB/MB/kB）
pg_to_pgtune_format() {
    local name="$1" setting="$2" unit="$3"
    # autovacuum_work_mem 的 -1 是 sentinel value（繼承 maintenance_work_mem），
    # 不是「-1 千位元組」，特殊顯示避免使用者誤解成負記憶體
    if [[ "$name" == "autovacuum_work_mem" && "$setting" == "-1" ]]; then
        echo "auto (=maintenance)"
        return
    fi
    # pg_settings 單位會是：8kB, 1, s, '', ms...
    # 我們只關心記憶體相關的 (kB, MB, 8kB, 16kB)，其他直接顯示
    case "$unit" in
        "8kB")
            # shared_buffers 類：value * 8kB → kB
            local kb=$((setting * 8))
            if ((kb % 1048576 == 0)); then echo "$((kb / 1048576))GB"
            elif ((kb % 1024 == 0)); then echo "$((kb / 1024))MB"
            else echo "${kb}kB"
            fi
            ;;
        "kB")
            if ((setting % 1048576 == 0)); then echo "$((setting / 1048576))GB"
            elif ((setting % 1024 == 0)); then echo "$((setting / 1024))MB"
            else echo "${setting}kB"
            fi
            ;;
        "MB")
            if ((setting % 1024 == 0)); then echo "$((setting / 1024))GB"
            else echo "${setting}MB"
            fi
            ;;
        *)
            echo "$setting"
            ;;
    esac
}

# MemTotal 通常比實體 RAM 少 ~1GB（kernel 保留）。取最接近的標準規格。
# 例：31GB → 32GB、15GB → 16GB、62GB → 64GB
# 吃 MB 精度：小 RAM 機器先 floor 成整數 GB 會補不回標準規格
# （8GB 機器 MemTotal ≈ 7813MB，floor 成 7 後 8/7 ≈ 1.143 超出 1.1 倍容忍度）
round_to_standard_ram() {
    local detected_mb=$1
    local sizes=(1 2 4 6 8 12 16 24 32 48 64 96 128 192 256 384 512 768 1024)
    for s in "${sizes[@]}"; do
        # 若標準規格 >= detected 且在 detected 的 1.1 倍以內，採用
        if (( s * 1024 >= detected_mb )) && (( s * 1024 * 10 <= detected_mb * 11 )); then
            echo "$s"
            return
        fi
    done
    echo $(( detected_mb / 1024 ))  # 無匹配，floor 成 GB（維持原行為）
}

# 優先序：CLI env var > config 檔 per-alias > config 檔 default > 空（讓 pgtune 用預設）
get_connections_for_aliases() {
    local aliases="$1"
    [[ -n "${MAX_CONNECTIONS:-}" ]] && { echo "$MAX_CONNECTIONS"; return; }
    config_lookup "max_connections" "$aliases"
}

# 產生該機器的 pgtune 建議
# SPEC override env var（給 UI 動態假設模式用，全部 optional）：
#   CPU_OVERRIDE / RAM_GB_OVERRIDE / WORKLOAD_OVERRIDE / MAX_CONNECTIONS
gen_recommendations() {
    local pg_ver="$1" cpu="$2" ram_bytes="$3" hd_type="$4" workload="$5" aliases="$6"

    # SPEC override（假設 SPEC 模式）
    [[ -n "${CPU_OVERRIDE:-}" ]] && cpu="$CPU_OVERRIDE"
    [[ -n "${WORKLOAD_OVERRIDE:-}" ]] && workload="$WORKLOAD_OVERRIDE"

    local ram_gb
    if [[ -n "${RAM_GB_OVERRIDE:-}" ]]; then
        ram_gb="$RAM_GB_OVERRIDE"
    else
        ram_gb=$(round_to_standard_ram "$((ram_bytes / 1048576))")
    fi
    local conns
    conns=$(get_connections_for_aliases "$aliases")
    local conn_arg=()
    [[ -n "$conns" ]] && conn_arg=(--connections "$conns")

    # db_size 覆寫：DB_SIZE=less_ram / mid_ram / greater_ram
    local db_size="${DB_SIZE:-mid_ram}"

    "$PGTUNE" \
        --db-version "$pg_ver" \
        --os linux \
        --db-type "$workload" \
        --cpu "$cpu" \
        --ram "$ram_gb" \
        --ram-unit GB \
        --hd-type "${hd_type:-hdd}" \
        --db-size "$db_size" \
        "${conn_arg[@]}" \
        2>/dev/null | grep -E '^[a-z_]+ *=' | sed 's/ *= */=/'
}

# 比對輸出
# 由 OUTPUT_FORMAT env var 控制：text（預設，給人看）或 tsv（給程式解析）
compare_and_print() {
    local machine="$1" aliases="$2" data="$3" workload="$4" ip="${5:-}"
    local format="${OUTPUT_FORMAT:-text}"

    local nproc="" mem_bytes="" hd_type="" pg_ver="" psql_method="" conn_stats=""
    declare -A current

    while IFS= read -r line; do
        case "$line" in
            NPROC=*)       nproc="${line#NPROC=}" ;;
            MEM_BYTES=*)   mem_bytes="${line#MEM_BYTES=}" ;;
            HD_TYPE=*)     hd_type="${line#HD_TYPE=}" ;;
            PG_VERSION=*)  pg_ver="${line#PG_VERSION=}" ;;
            PSQL_METHOD=*) psql_method="${line#PSQL_METHOD=}" ;;
            CONN_STATS=*)  conn_stats="${line#CONN_STATS=}" ;;
            CONF:*)
                local rest="${line#CONF:}"
                local n="${rest%%|*}" ; rest="${rest#*|}"
                local s="${rest%%|*}" ; rest="${rest#*|}"
                local u="$rest"
                current[$n]=$(pg_to_pgtune_format "$n" "$s" "$u")
                ;;
        esac
    done <<< "$data"

    if [[ -z "$nproc" || -z "$mem_bytes" ]]; then
        if [[ "$format" == "tsv" ]]; then
            printf "ERROR\t無法收集資料（SSH 失敗或 psql 不可用）\n"
        else
            echo "  ⚠  無法收集資料（SSH 失敗或 psql 不可用）"
        fi
        return 1
    fi

    local ram_gb_raw=$((mem_bytes / 1073741824))
    local ram_gb
    ram_gb=$(round_to_standard_ram "$((mem_bytes / 1048576))")

    if [[ "$format" == "text" ]]; then
        echo "  SPEC: CPU=${nproc} / RAM=${ram_gb}GB (detected ${ram_gb_raw}GB，四捨五入到標準規格) / Storage=${hd_type:-?} / PG=${pg_ver:-?}"
        echo "  Workload classification: ${workload}"
        if [[ -n "$conn_stats" ]]; then
            IFS='|' read -r cs_cur cs_max cs_res cs_peak cs_uptime <<< "$conn_stats"
            echo "  Connections: now=${cs_cur} / max=${cs_max} (reserved ${cs_res}) | peak=${cs_peak:-?} since ${cs_uptime:-?}"
        fi
        echo
    fi

    if [[ -z "$pg_ver" ]]; then
        if [[ "$format" == "tsv" ]]; then
            printf "ERROR\t無法偵測 PG 版本\n"
        else
            echo "  ⚠  無法偵測 PG 版本，跳過建議產生"
        fi
        return 1
    fi

    declare -A recommend
    while IFS='=' read -r k v; do
        recommend[$k]="$v"
    done < <(gen_recommendations "$pg_ver" "$nproc" "$mem_bytes" "$hd_type" "$workload" "$aliases")

    for key in $PGCONF_OVERRIDE_KEYS; do
        local env_var_name="${key^^}"
        local val=""
        if [[ -n "${!env_var_name:-}" ]]; then
            val="${!env_var_name}"
        else
            val=$(config_lookup "$key" "$aliases" 2>/dev/null || true)
        fi
        if [[ -n "$val" ]]; then
            recommend[$key]="$val"
        fi
    done

    local params="max_connections shared_buffers effective_cache_size maintenance_work_mem checkpoint_completion_target wal_buffers default_statistics_target random_page_cost effective_io_concurrency work_mem huge_pages jit wal_compression autovacuum_max_workers autovacuum_work_mem min_wal_size max_wal_size max_worker_processes max_parallel_workers_per_gather max_parallel_workers max_parallel_maintenance_workers"

    if [[ "$format" == "tsv" ]]; then
        printf "META\ttarget\t%s\n" "$machine"
        printf "META\tip\t%s\n" "$ip"
        printf "META\taliases\t%s\n" "$aliases"
        printf "SPEC\tcpu\t%s\n" "$nproc"
        printf "SPEC\tram_gb_detected\t%s\n" "$ram_gb_raw"
        printf "SPEC\tram_gb_rounded\t%s\n" "$ram_gb"
        printf "SPEC\thd_type\t%s\n" "$hd_type"
        printf "SPEC\tpg_version\t%s\n" "$pg_ver"
        printf "SPEC\tpsql_method\t%s\n" "$psql_method"
        printf "SPEC\tworkload\t%s\n" "$workload"
        if [[ -n "$conn_stats" ]]; then
            IFS='|' read -r cs_cur cs_max cs_res cs_peak cs_uptime <<< "$conn_stats"
            printf "CONN\tcurrent\t%s\n"    "$cs_cur"
            printf "CONN\tmax\t%s\n"        "$cs_max"
            printf "CONN\treserved\t%s\n"   "$cs_res"
            printf "CONN\tpeak\t%s\n"       "$cs_peak"
            printf "CONN\tstart_time\t%s\n" "$cs_uptime"
        fi
        for p in $params; do
            local cur="${current[$p]:-}"
            local rec="${recommend[$p]:-}"
            local status
            if [[ -z "$rec" ]]; then
                status="na"
            elif [[ "$cur" == "$rec" ]]; then
                status="ok"
            else
                status="diff"
            fi
            printf "PARAM\t%s\t%s\t%s\t%s\n" "$p" "$cur" "$rec" "$status"
        done
    else
        printf "  %-35s %-20s %-20s %s\n" "Parameter" "Current" "Recommended" "Status"
        printf "  %-35s %-20s %-20s %s\n" "-----------------------------------" "--------------------" "--------------------" "------"
        for p in $params; do
            local cur="${current[$p]:-—}"
            local rec="${recommend[$p]:-—}"
            local status
            if [[ "$rec" == "—" ]]; then
                status=""
            elif [[ "$cur" == "$rec" ]]; then
                status="✓ ok"
            else
                status="≠ diff"
            fi
            printf "  %-35s %-20s %-20s %s\n" "$p" "$cur" "$rec" "$status"
        done
    fi
}

# ── Main ─────────────────────────────────────────────────────────────────────
main() {
    # 解析 flag
    local targets=()
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --tsv)      export OUTPUT_FORMAT=tsv; shift ;;
            --text)     export OUTPUT_FORMAT=text; shift ;;
            --help|-h)
                sed -n '2,10p' "$0" | sed 's/^# //'
                echo "  --tsv     輸出 TSV 格式（機器可讀，給網頁版用）"
                echo "  --text    輸出人類可讀表格（預設）"
                exit 0
                ;;
            *)          targets+=("$1"); shift ;;
        esac
    done

    if [[ ${#targets[@]} -eq 0 ]]; then
        readarray -t targets < <(get_db_ips)
    fi

    if [[ ${#targets[@]} -eq 0 ]]; then
        echo "No DB machines found." >&2
        exit 1
    fi

    local format="${OUTPUT_FORMAT:-text}"
    if [[ "$format" == "text" ]]; then
        echo "Analyzing ${#targets[@]} target(s)..."
        echo
    fi

    for t in "${targets[@]}"; do
        local ssh_target="$t"
        local ip="$t"
        if ! [[ "$t" =~ ^[0-9]+\. ]]; then
            ip=$(awk -v a="$t" '{for(i=2;i<=NF;i++) if($i==a){print $1; exit}}' "$HOSTS_FILE")
            if [[ -z "$ip" ]]; then
                if [[ "$format" == "tsv" ]]; then
                    printf "ERROR\tUnknown alias: %s\n" "$t"
                else
                    echo "Unknown alias: $t" >&2
                fi
                continue
            fi
        fi
        local aliases
        aliases=$(get_aliases_for_ip "$ip")
        local workload
        workload=$(classify_workload "$aliases")

        if [[ "$format" == "text" ]]; then
            echo "================================================================="
            echo "Machine: $ssh_target (IP=$ip)"
            echo "Aliases: $aliases"
            echo "================================================================="
        fi

        local data
        data=$(collect_machine_data "$ssh_target" 2>&1)

        if ! grep -q '__PG_ANALYZER_END__' <<< "$data"; then
            if [[ "$format" == "tsv" ]]; then
                printf "META\ttarget\t%s\n" "$ssh_target"
                printf "META\tip\t%s\n" "$ip"
                printf "META\taliases\t%s\n" "$aliases"
                printf "ERROR\tSSH 失敗或 remote script 未結束\n"
            else
                echo "  ❌ SSH / remote script 未正常結束"
                echo "  收集到的輸出："
                echo "$data" | head -20 | sed 's/^/    /'
            fi
            echo
            continue
        fi

        compare_and_print "$ssh_target" "$aliases" "$data" "$workload" "$ip"
        [[ "$format" == "text" ]] && echo
    done
}

main "$@"
