#!/usr/bin/env julia
#############################################################################
## validate_uncertainty.jl
## Uncertainty validation: Posterior Predictive Coverage (PPC)
##
## Following Claassen (2019), this script computes:
## - PPC: What % of expert values fall within 95% posterior predictive interval?
## - Also computes 80% PPC for direct Claassen comparison (he reports 60.3%)
## - Wilson score CI for coverage proportion
## - Breakdown by survey source and decade
##
## Unlike credible interval coverage (which checks θ-CIs), posterior predictive
## coverage simulates what a new expert observation would look like given the
## model's beta likelihood, accounting for both position uncertainty AND
## measurement noise. Well-calibrated models should yield ~95% PPC at 95%.
##
## No model re-run needed: reads θ, γ, and φ from existing chain CSV files.
#############################################################################

using CSV, DataFrames, Statistics, Dates, Printf, Random, JSON

# =============================================================================
# Utility: Wilson score CI for a proportion
# =============================================================================

function wilson_ci(p, n; alpha=0.05)
    if n == 0
        return (lower=NaN, upper=NaN, se=NaN)
    end
    z = 1.96  # For 95% CI
    denominator = 1 + z^2/n
    center = (p + z^2/(2n)) / denominator
    margin = z * sqrt((p*(1-p) + z^2/(4n))/n) / denominator
    se = sqrt(p * (1-p) / n)
    return (lower=center - margin, upper=center + margin, se=se)
end

# =============================================================================
# STEP 0: Find latest model run
# =============================================================================

function find_latest_run(base_dir::String="model_outputs")
    if !isdir(base_dir)
        error("Model outputs directory not found: $base_dir")
    end
    runs = filter(d -> startswith(d, "run_") && isdir(joinpath(base_dir, d)), readdir(base_dir))
    if isempty(runs)
        error("No runs found in $base_dir")
    end
    sort!(runs, rev=true)
    latest = joinpath(base_dir, runs[1])
    println("Using latest run: $latest")
    return latest
end

# =============================================================================
# STEP 1: Load expert_dim.csv from model run data
# =============================================================================

function load_expert_dim(run_dir::String)
    expert_dim_file = joinpath(run_dir, "data", "expert_dim.csv")
    if !isfile(expert_dim_file)
        error("expert_dim.csv not found in $run_dir/data/")
    end
    expert_dim = CSV.read(expert_dim_file, DataFrame)
    println("Loaded expert_dim.csv: $(nrow(expert_dim)) observations")
    println("  Unique rr values: $(length(unique(expert_dim.rr_exp_dim)))")
    println("  Item indices (var_exp_dim): $(sort(unique(expert_dim.var_exp_dim)))")
    println("  Dimensions (dim_idx_exp): $(sort(unique(expert_dim.dim_idx_exp)))")
    return expert_dim
end

# =============================================================================
# STEP 2: Selectively load chain columns
# =============================================================================

function load_chains_selective(run_dir::String, needed_rr::Set{Int}, K::Int)
    """Load only the chain columns we need for posterior predictive checks."""
    chains_dir = joinpath(run_dir, "chains")
    chain_files = sort(filter(f -> endswith(f, ".csv") && startswith(f, "chain_"), readdir(chains_dir)))

    if isempty(chain_files)
        error("No chain files found in $chains_dir")
    end
    println("\nLoading $(length(chain_files)) chain files (selective columns)...")

    # Build the set of column names we need
    needed_cols = Set{String}()

    # theta columns: economic_lr.{rr} and galtan.{rr} for each unique rr
    for rr in needed_rr
        push!(needed_cols, "economic_lr.$rr")
        push!(needed_cols, "galtan.$rr")
    end

    # Item parameters: gamma_exp_intercept.1-K, gamma_exp_slope.1-K
    for k in 1:K
        push!(needed_cols, "gamma_exp_intercept.$k")
        push!(needed_cols, "gamma_exp_slope.$k")
    end

    # Precision parameter
    push!(needed_cols, "phi_exp_dim")

    println("  Need $(length(needed_cols)) columns ($(length(needed_rr)) rr × 2 dims + $(2*K) item params + 1 phi)")

    # Read the header from first chain to identify column indices
    first_chain_path = joinpath(chains_dir, chain_files[1])
    header_line = ""
    open(first_chain_path) do f
        for line in eachline(f)
            if !startswith(line, "#")
                header_line = line
                break
            end
        end
    end

    all_cols = split(header_line, ",")
    col_indices = Int[]
    col_names = String[]

    for (i, col) in enumerate(all_cols)
        if col in needed_cols
            push!(col_indices, i)
            push!(col_names, col)
        end
    end

    println("  Found $(length(col_indices))/$(length(needed_cols)) columns in chains")

    if length(col_indices) < length(needed_cols)
        missing_cols = setdiff(needed_cols, Set(col_names))
        n_missing = length(missing_cols)
        sample = collect(missing_cols)[1:min(5, n_missing)]
        println("  WARNING: Missing columns (showing $( min(5, n_missing))/$n_missing): $sample")
    end

    # Build a type specification for selective reading
    # We'll use CSV.read with select parameter
    select_symbols = Symbol.(col_names)

    all_chains = DataFrame[]
    for (i, cf) in enumerate(chain_files)
        path = joinpath(chains_dir, cf)
        print("  Loading chain $i: $(cf)... ")
        t = @elapsed begin
            chain = CSV.read(path, DataFrame; comment="#", select=select_symbols)
        end
        println("$(nrow(chain)) samples, $(round(t, digits=1))s")
        push!(all_chains, chain)
    end

    combined = vcat(all_chains...)
    println("Combined: $(nrow(combined)) total posterior draws")
    return combined
end

# =============================================================================
# STEP 3: Compute posterior predictive coverage
# =============================================================================

function compute_posterior_predictive_cic(chains::DataFrame, expert_dim::DataFrame;
                                          ci_level::Float64=0.95, seed::Int=42)
    """
    Compute posterior predictive coverage for expert dimension observations.

    For each expert observation n with observed value y_n:
    1. For each posterior draw s:
       - Get theta_s = theta[dim, rr] (on logit scale, but chains store inv_logit)
       - Compute mu_s = invlogit(gamma_intercept[k] + gamma_slope[k] * logit(theta_s))
       - V4 (Beta):          Draw y_pred_s ~ Beta(phi * mu_s, phi * (1 - mu_s))
       - V5 (Beta-Binomial): Draw y_pred_s ~ Beta(phi * K * mu_s, phi * K * (1 - mu_s))
         where K = n_experts for that observation
    2. Compute quantile interval of y_pred draws
    3. Check if y_n falls within interval

    Returns DataFrame with one row per observation plus coverage indicator.
    """
    rng = MersenneTwister(seed)

    alpha_lower = (1 - ci_level) / 2
    alpha_upper = 1 - alpha_lower

    N = nrow(expert_dim)
    S = nrow(chains)  # total posterior draws

    # Detect V5 (Beta-Binomial with K-scaling) by presence of n_experts column
    has_k_scaling = hasproperty(expert_dim, :n_experts)
    if has_k_scaling
        k_vec = expert_dim.n_experts
        println("\nV5 detected: using Beta(phi*K*mu, phi*K*(1-mu)) with per-observation K")
    else
        println("\nV4 detected: using Beta(phi*mu, phi*(1-mu))")
    end

    println("Computing posterior predictive coverage ($(round(Int, 100*ci_level))% level)")
    println("  Expert observations: $N")
    println("  Posterior draws: $S")

    # Pre-extract phi vector
    phi_vec = chains[!, :phi_exp_dim]

    # Pre-extract gamma vectors for each item k
    K = maximum(expert_dim.var_exp_dim)
    gamma_int = Dict{Int, Vector{Float64}}()
    gamma_slope = Dict{Int, Vector{Float64}}()
    for k in 1:K
        col_int = Symbol("gamma_exp_intercept.$k")
        col_slope = Symbol("gamma_exp_slope.$k")
        if hasproperty(chains, col_int) && hasproperty(chains, col_slope)
            gamma_int[k] = chains[!, col_int]
            gamma_slope[k] = chains[!, col_slope]
        end
    end

    # Allocate result columns
    covered = BitVector(undef, N)
    pred_lower = Vector{Float64}(undef, N)
    pred_upper = Vector{Float64}(undef, N)
    pred_median = Vector{Float64}(undef, N)

    # Pre-allocate per-observation draw buffer
    y_pred = Vector{Float64}(undef, S)

    prog_interval = max(1, N ÷ 20)

    for n in 1:N
        if n % prog_interval == 0 || n == N
            pct = round(100 * n / N, digits=1)
            print("\r  Progress: $pct% ($n / $N)")
        end

        rr = expert_dim.rr_exp_dim[n]
        dim = expert_dim.dim_idx_exp[n]
        k = expert_dim.var_exp_dim[n]
        y_obs = expert_dim.val[n]

        # Get theta column (chains store inv_logit(theta), i.e. on [0,1] scale)
        theta_col = dim == 1 ? Symbol("economic_lr.$rr") : Symbol("galtan.$rr")

        if !hasproperty(chains, theta_col) || !haskey(gamma_int, k)
            # Missing chain data — mark as not covered
            covered[n] = false
            pred_lower[n] = NaN
            pred_upper[n] = NaN
            pred_median[n] = NaN
            continue
        end

        theta_star_vec = chains[!, theta_col]  # inv_logit(theta), i.e. on [0,1]
        g_int = gamma_int[k]
        g_slope = gamma_slope[k]

        # Effective concentration: phi for V4, phi * n_experts for V5
        k_mult = has_k_scaling ? Float64(k_vec[n]) : 1.0

        # For each posterior draw, simulate a predictive observation
        for s in 1:S
            theta_star = theta_star_vec[s]

            # Convert back to latent scale for linear predictor
            # theta_star is inv_logit(theta), so theta = logit(theta_star)
            # Clamp to avoid Inf
            theta_star_clamped = clamp(theta_star, 1e-10, 1 - 1e-10)
            theta_latent = log(theta_star_clamped / (1 - theta_star_clamped))

            # Linear predictor
            lin = g_int[s] + g_slope[s] * theta_latent

            # Mean of beta
            mu = 1 / (1 + exp(-lin))
            mu = clamp(mu, 1e-6, 1 - 1e-6)

            # Beta parameters: phi * K * mu for V5, phi * mu for V4
            phi = phi_vec[s] * k_mult
            a = phi * mu
            b = phi * (1 - mu)

            # Draw from Beta(a, b) via gamma method (no Distributions.jl needed)
            y_pred[s] = _rand_beta(rng, a, b)
        end

        # Compute predictive interval
        sort!(y_pred)
        idx_lo = max(1, round(Int, alpha_lower * S))
        idx_hi = min(S, round(Int, alpha_upper * S))
        idx_med = round(Int, 0.5 * S)

        pred_lower[n] = y_pred[idx_lo]
        pred_upper[n] = y_pred[idx_hi]
        pred_median[n] = y_pred[idx_med]
        covered[n] = (y_obs >= pred_lower[n]) && (y_obs <= pred_upper[n])
    end
    println()  # newline after progress

    # Add results to a copy of expert_dim
    result = DataFrame(
        rr = expert_dim.rr_exp_dim,
        dim_idx = expert_dim.dim_idx_exp,
        var_idx = expert_dim.var_exp_dim,
        val = expert_dim.val,
        party = expert_dim.party,
        country = expert_dim.country,
        year = expert_dim.year,
        project = expert_dim.project,
        var = expert_dim.var,
        pred_lower = pred_lower,
        pred_upper = pred_upper,
        pred_median = pred_median,
        covered = covered
    )

    return result
end

# =============================================================================
# Beta random variate without Distributions.jl
# =============================================================================

"""
    _rand_beta(rng, a, b)

Generate a Beta(a, b) random variate using the Gamma method:
Beta(a,b) = X/(X+Y) where X ~ Gamma(a), Y ~ Gamma(b).
Uses Marsaglia & Tsang (2000) for Gamma generation.
"""
function _rand_beta(rng::AbstractRNG, a::Float64, b::Float64)
    x = _rand_gamma(rng, a)
    y = _rand_gamma(rng, b)
    return x / (x + y)
end

"""
    _rand_gamma(rng, shape)

Generate Gamma(shape, 1) random variate using Marsaglia & Tsang (2000).
For shape < 1, uses the rejection method with shape+1 then scales.
"""
function _rand_gamma(rng::AbstractRNG, shape::Float64)
    if shape < 1.0
        # Gamma(a) = Gamma(a+1) * U^(1/a) where U ~ Uniform(0,1)
        return _rand_gamma(rng, shape + 1.0) * rand(rng)^(1.0 / shape)
    end

    # Marsaglia & Tsang (2000) for shape >= 1
    d = shape - 1.0/3.0
    c = 1.0 / sqrt(9.0 * d)

    while true
        local x::Float64
        local v::Float64

        while true
            x = randn(rng)
            v = 1.0 + c * x
            if v > 0.0
                break
            end
        end

        v = v * v * v
        u = rand(rng)

        if u < 1.0 - 0.0331 * x^2 * x^2
            return d * v
        end

        if log(u) < 0.5 * x^2 + d * (1.0 - v + log(v))
            return d * v
        end
    end
end

# =============================================================================
# STEP 4: Summarize and save results
# =============================================================================

function summarize_coverage(result::DataFrame, ci_level::Float64)
    level_pct = round(Int, 100 * ci_level)
    println("\n" * "="^60)
    println("POSTERIOR PREDICTIVE COVERAGE ($level_pct%)")
    println("="^60)

    # Overall by dimension
    dim_names = Dict(1 => "economic_lr", 2 => "galtan")

    summary_rows = []
    for dim in sort(unique(result.dim_idx))
        subset = filter(r -> r.dim_idx == dim, result)
        n = nrow(subset)
        n_covered = sum(subset.covered)
        ppc = n_covered / n
        ci = wilson_ci(ppc, n)
        dim_name = dim_names[dim]

        println(@sprintf("\n  %-15s: %.1f%% [%.1f%%, %.1f%%]  (%d/%d)",
            dim_name, 100*ppc, 100*ci.lower, 100*ci.upper, n_covered, n))

        push!(summary_rows, (
            dimension = dim_name,
            cic = ppc,
            cic_pct = round(100 * ppc, digits=1),
            ci_lower = ci.lower,
            ci_upper = ci.upper,
            n = n,
            covered = n_covered
        ))

        # By project
        println("\n  By survey source:")
        by_project = combine(groupby(subset, :project)) do df
            nc = sum(df.covered)
            DataFrame(n = nrow(df), covered = nc, cic = nc / nrow(df))
        end
        sort!(by_project, :n, rev=true)

        @printf("    %-12s %6s %8s\n", "Project", "N", "PPC")
        for row in eachrow(by_project)
            @printf("    %-12s %6d %7.1f%%\n", row.project, row.n, 100*row.cic)
        end

        # By decade
        subset_with_decade = copy(subset)
        subset_with_decade.decade = div.(subset_with_decade.year, 10) .* 10
        println("\n  By decade:")
        by_decade = combine(groupby(subset_with_decade, :decade)) do df
            nc = sum(df.covered)
            DataFrame(n = nrow(df), covered = nc, cic = nc / nrow(df))
        end
        sort!(by_decade, :decade)

        @printf("    %-8s %6s %8s\n", "Decade", "N", "PPC")
        for row in eachrow(by_decade)
            @printf("    %-8d %6d %7.1f%%\n", row.decade, row.n, 100*row.cic)
        end
    end

    return summary_rows
end

function save_results(result_95::DataFrame, summary_95, summary_80,
                      by_project_95::Dict, output_dir::String="validation")
    if !isdir(output_dir)
        mkpath(output_dir)
    end

    timestamp = Dates.format(now(), "yyyy-mm-dd_HH-MM-SS")

    # Summary table (95%)
    if !isempty(summary_95)
        summary_df = DataFrame(summary_95)
        summary_file = joinpath(output_dir, "uncertainty_cic_summary_$timestamp.csv")
        CSV.write(summary_file, summary_df)
        println("\nSaved: $summary_file")
    end

    # Also save 80% summary
    if !isempty(summary_80)
        summary80_df = DataFrame(summary_80)
        summary80_file = joinpath(output_dir, "uncertainty_cic_80pct_summary_$timestamp.csv")
        CSV.write(summary80_file, summary80_df)
        println("Saved: $summary80_file")
    end

    # By-project tables (95%)
    dim_names = Dict(1 => "economic_lr", 2 => "galtan")
    for (dim, bp) in by_project_95
        project_file = joinpath(output_dir, "uncertainty_$(dim_names[dim])_by_project_$timestamp.csv")
        CSV.write(project_file, bp)
        println("Saved: $project_file")
    end

    return summary_95
end

function print_claassen_comparison(summary_95, summary_80)
    println("\n" * "="^60)
    println("COMPARISON WITH CLAASSEN (2019) BENCHMARKS")
    println("="^60)

    println("\nClaassen's result:")
    println("  CIC (80% CI): 60.3%")
    println("  (Using credible intervals for θ, not posterior predictive)")
    println()
    println("Our results (posterior predictive):")
    println()

    println("-"^60)
    @printf("%-15s %10s %10s %8s\n", "Dimension", "PPC 95%", "PPC 80%", "Status")
    println("-"^60)

    dim_map_80 = Dict(r.dimension => r for r in summary_80)

    for r in summary_95
        ppc80 = haskey(dim_map_80, r.dimension) ? dim_map_80[r.dimension].cic : NaN
        # Well-calibrated: 95% PPC should be near 95%
        status = r.cic >= 0.90 ? "GOOD" : (r.cic >= 0.80 ? "OK" : "LOW")
        @printf("%-15s %9.1f%% %9.1f%% %8s\n",
            r.dimension, 100*r.cic, 100*ppc80, status)
    end
    println("-"^60)
    println()
    println("Interpretation:")
    println("  95% PPC ~95% = well-calibrated uncertainty")
    println("  80% PPC > 60% = exceeds Claassen (2019) benchmark")
end

# =============================================================================
# MAIN
# =============================================================================

function main()
    println("="^60)
    println("UNCERTAINTY VALIDATION: Posterior Predictive Coverage")
    println("="^60)
    println("Following Claassen (2019) validation framework")
    println("Posterior predictive intervals account for both position")
    println("uncertainty AND observation-level measurement noise.")
    println()

    # Step 0: Parse options and find run directory
    run_dir = nothing
    quick_mode = get(ENV, "QUICK_VALIDATION", "0") == "1"
    for (i, arg) in enumerate(ARGS)
        if arg == "--run-dir" && i < length(ARGS)
            run_dir = ARGS[i + 1]
        elseif startswith(arg, "--run-dir=")
            run_dir = split(arg, "=", limit=2)[2]
        elseif arg == "--quick"
            quick_mode = true
        end
    end
    if run_dir === nothing
        run_dir = find_latest_run()
    else
        println("Using specified run directory: $run_dir")
    end

    # Step 1: Load expert_dim.csv
    expert_dim = load_expert_dim(run_dir)

    # Step 2: Selectively load chains
    needed_rr = Set(expert_dim.rr_exp_dim)
    K = maximum(expert_dim.var_exp_dim)
    chains = load_chains_selective(run_dir, needed_rr, K)

    # Step 3a: Compute 95% posterior predictive coverage
    result_95 = compute_posterior_predictive_cic(chains, expert_dim; ci_level=0.95)
    summary_95 = summarize_coverage(result_95, 0.95)

    # Step 3b: Compute 80% posterior predictive coverage (Claassen benchmark)
    # Recompute coverage from the same predictive draws but with 80% quantiles
    if quick_mode
        println("\nQUICK MODE: skipping 80% PPC recomputation")
        summary_80 = [(dimension=r.dimension, n=r.n, covered=r.covered, cic=NaN, ci_level=0.80) for r in summary_95]
    else
        println("\n" * "="^60)
        println("RECOMPUTING WITH 80% LEVEL (Claassen comparison)")
        println("="^60)
        result_80 = compute_posterior_predictive_cic(chains, expert_dim; ci_level=0.80, seed=42)
        summary_80 = summarize_coverage(result_80, 0.80)
    end

    # Build by-project tables for 95%
    dim_names = Dict(1 => "economic_lr", 2 => "galtan")
    by_project_95 = Dict{Int, DataFrame}()
    for dim in sort(unique(result_95.dim_idx))
        subset = filter(r -> r.dim_idx == dim, result_95)
        bp = combine(groupby(subset, :project)) do df
            nc = sum(df.covered)
            DataFrame(n = nrow(df), covered = nc, cic = nc / nrow(df))
        end
        sort!(bp, :n, rev=true)
        by_project_95[dim] = bp
    end

    # Step 4: Save results
    save_results(result_95, summary_95, summary_80, by_project_95)

    # Step 5: Print Claassen comparison
    print_claassen_comparison(summary_95, summary_80)

    println("\n" * "="^60)
    println("VALIDATION COMPLETE")
    println("="^60)

    return (summary_95=summary_95, summary_80=summary_80)
end

if abspath(PROGRAM_FILE) == @__FILE__
    main()
end
