#!/usr/bin/env julia
#
# reach_heatup_pj.jl — nonlinear reach on heatup, prompt-jump model.
#
# Reduced from 10-state to 9-state (n is algebraic). Removes the Λ⁻¹
# stiffness that capped the full-state reach at ~10 s.
#
# State (10D with augmented time):
#   x[1..6] = C_1..C_6   (delayed-neutron precursors)
#   x[7]    = T_f
#   x[8]    = T_c
#   x[9]    = T_cold
#   x[10]   = t (augmented time, dt/dt = 1)
#
# n is algebraic:  n = Λ·Σ λ_i C_i / (β - ρ),  ρ = K_p·(T_ref - T_c).
#
# Controller reference: T_ref(t) = T_REF_START_C + RAMP_RATE_CS · t,
# linear (no clamp at T_c0 — the reach probes when it enters X_exit;
# clamping would introduce a non-smooth piecewise dynamics @taylorize
# can't handle).  T_REF_START_C must be at-or-below the X_entry T_c
# lower bound to keep the controller heating, not cooling.
#
# Configuration-driven: pass a TOML config path as the first CLI arg,
# or omit for the baseline config.
#
#   julia --project=. scripts/reach/reach_heatup_pj.jl                  # baseline
#   julia --project=. scripts/reach/reach_heatup_pj.jl configs/heatup/tight.toml
#
# Configs live in code/configs/heatup/*.toml. See baseline.toml for
# the full schema.

using Pkg
Pkg.activate(joinpath(@__DIR__, "..", ".."))

using LinearAlgebra
using ReachabilityAnalysis, LazySets
using JSON
using MAT
using TOML

# --- Plant constants (must match pke_params) ---
const LAMBDA  = 1e-4
const BETA_1, BETA_2, BETA_3, BETA_4, BETA_5, BETA_6 =
    0.000215, 0.001424, 0.001274, 0.002568, 0.000748, 0.000273
const BETA    = BETA_1 + BETA_2 + BETA_3 + BETA_4 + BETA_5 + BETA_6
const LAM_1, LAM_2, LAM_3, LAM_4, LAM_5, LAM_6 =
    0.0124, 0.0305, 0.111, 0.301, 1.14, 3.01

const P0   = 1e9
const M_F, C_F, M_C, C_C, HA, W_M, M_SG =
    50000.0, 300.0, 20000.0, 5450.0, 5e7, 5000.0, 30000.0

const T_COLD0 = 290.0
const DT_CORE = P0 / (W_M * C_C)
const T_HOT0  = T_COLD0 + DT_CORE
const T_C0    = (T_HOT0 + T_COLD0) / 2
const T_F0    = T_C0 + P0 / HA

const T_STANDBY    = T_C0 - 33.333333
const RAMP_RATE_CS = 28.0 / 3600
const KP_HEATUP    = 1e-4

# Controller reference starting temperature.  Must be at-or-below the
# X_entry T_c lower bound so the controller's first move is HEATING, not
# cooling.  Originally was T_STANDBY = 275, but the heatup X_entry
# polytope has T_c >= 281 (post-t_avg_above_min), so the FL controller
# was commanding cooling for the first ~60 s before the ramp caught up.
# 285 matches `configs/heatup/tight.toml` T_c_lo.  If the entry box
# changes, update here too.
const T_REF_START_C = 285.0

# --- Taylorized heatup PJ RHS ---
@taylorize function rhs_heatup_pj_taylor!(dx, x, p, t)
    rho = KP_HEATUP * (T_REF_START_C + RAMP_RATE_CS * x[10] - x[8])
    sum_lam_C = LAM_1*x[1] + LAM_2*x[2] + LAM_3*x[3] +
                LAM_4*x[4] + LAM_5*x[5] + LAM_6*x[6]
    denom = BETA - rho
    n = LAMBDA * sum_lam_C / denom
    inv_factor = sum_lam_C / denom

    dx[1] = BETA_1 * inv_factor - LAM_1 * x[1]
    dx[2] = BETA_2 * inv_factor - LAM_2 * x[2]
    dx[3] = BETA_3 * inv_factor - LAM_3 * x[3]
    dx[4] = BETA_4 * inv_factor - LAM_4 * x[4]
    dx[5] = BETA_5 * inv_factor - LAM_5 * x[5]
    dx[6] = BETA_6 * inv_factor - LAM_6 * x[6]

    dx[7]  = (P0 * n - HA * (x[7] - x[8])) / (M_F * C_F)
    dx[8]  = (HA * (x[7] - x[8]) - 2 * W_M * C_C * (x[8] - x[9])) / (M_C * C_C)
    dx[9]  = (2 * W_M * C_C * (x[8] - x[9])) / (M_SG * C_C)

    dx[10] = one(x[1])
    return nothing
end

# --- Config loader ---
function load_config(config_path)
    if isfile(config_path)
        return TOML.parsefile(config_path)
    else
        error("Config file not found: $config_path")
    end
end

function build_entry_box(config)
    if get(config, "use_predicates_entry", false)
        pred_path = joinpath(@__DIR__, "..", "..", "..", "reachability", "predicates.json")
        pred_raw  = JSON.parsefile(pred_path)
        entry     = pred_raw["mode_boundaries"]["q_heatup"]["X_entry_polytope"]
        n_lo, n_hi           = entry["n_range"]
        T_f_lo, T_f_hi       = entry["T_f_range_C"]
        T_c_lo, T_c_hi       = entry["T_c_range_C"]
        T_cold_lo, T_cold_hi = entry["T_cold_range_C"]
    else
        e = config["entry"]
        n_lo, n_hi           = e["n_range"]
        T_f_lo, T_f_hi       = e["T_f_range_C"]
        T_c_lo, T_c_hi       = e["T_c_range_C"]
        T_cold_lo, T_cold_hi = e["T_cold_range_C"]
    end

    n_mid = 0.5 * (n_lo + n_hi)
    C_mid = [BETA_1/(LAM_1*LAMBDA), BETA_2/(LAM_2*LAMBDA),
             BETA_3/(LAM_3*LAMBDA), BETA_4/(LAM_4*LAMBDA),
             BETA_5/(LAM_5*LAMBDA), BETA_6/(LAM_6*LAMBDA)] .* n_mid

    x_lo = [C_mid[1]*(n_lo/n_mid), C_mid[2]*(n_lo/n_mid),
            C_mid[3]*(n_lo/n_mid), C_mid[4]*(n_lo/n_mid),
            C_mid[5]*(n_lo/n_mid), C_mid[6]*(n_lo/n_mid),
            T_f_lo, T_c_lo, T_cold_lo, 0.0]
    x_hi = [C_mid[1]*(n_hi/n_mid), C_mid[2]*(n_hi/n_mid),
            C_mid[3]*(n_hi/n_mid), C_mid[4]*(n_hi/n_mid),
            C_mid[5]*(n_hi/n_mid), C_mid[6]*(n_hi/n_mid),
            T_f_hi, T_c_hi, T_cold_hi, 0.0]

    return Hyperrectangle(low=x_lo, high=x_hi),
           (n_lo=n_lo, n_hi=n_hi,
            T_f_lo=T_f_lo, T_f_hi=T_f_hi,
            T_c_lo=T_c_lo, T_c_hi=T_c_hi,
            T_cold_lo=T_cold_lo, T_cold_hi=T_cold_hi)
end

# --- Per-step envelope extraction ---
function extract_envelopes(flow_hr)
    n_steps = length(flow_hr)
    t_arr     = zeros(n_steps)
    Tc_lo_ts  = zeros(n_steps); Tc_hi_ts  = zeros(n_steps)
    Tf_lo_ts  = zeros(n_steps); Tf_hi_ts  = zeros(n_steps)
    Tco_lo_ts = zeros(n_steps); Tco_hi_ts = zeros(n_steps)
    n_lo_ts   = zeros(n_steps); n_hi_ts   = zeros(n_steps)
    rho_lo_ts = zeros(n_steps); rho_hi_ts = zeros(n_steps)
    for (k, R) in enumerate(flow_hr)
        s = set(R)
        t_arr[k]    = high(s, 10)
        Tc_lo_ts[k] = low(s, 8);  Tc_hi_ts[k] = high(s, 8)
        Tf_lo_ts[k] = low(s, 7);  Tf_hi_ts[k] = high(s, 7)
        Tco_lo_ts[k] = low(s, 9); Tco_hi_ts[k] = high(s, 9)
        sumLC_lo = LAM_1*low(s,1) + LAM_2*low(s,2) + LAM_3*low(s,3) +
                   LAM_4*low(s,4) + LAM_5*low(s,5) + LAM_6*low(s,6)
        sumLC_hi = LAM_1*high(s,1) + LAM_2*high(s,2) + LAM_3*high(s,3) +
                   LAM_4*high(s,4) + LAM_5*high(s,5) + LAM_6*high(s,6)
        t_hi_here = high(s, 10)
        t_lo_here = low(s, 10)
        Tref_lo = T_REF_START_C + RAMP_RATE_CS * t_lo_here
        Tref_hi = T_REF_START_C + RAMP_RATE_CS * t_hi_here
        rho_lo_here = KP_HEATUP * (Tref_lo - high(s, 8))
        rho_hi_here = KP_HEATUP * (Tref_hi - low(s, 8))
        rho_lo_ts[k] = rho_lo_here
        rho_hi_ts[k] = rho_hi_here
        denom_lo = BETA - rho_hi_here
        denom_hi = BETA - rho_lo_here
        if denom_lo > 0
            n_lo_ts[k] = LAMBDA * sumLC_lo / denom_hi
            n_hi_ts[k] = LAMBDA * sumLC_hi / denom_lo
        end
    end
    return (; t_arr,
              Tc_lo_ts, Tc_hi_ts, Tf_lo_ts, Tf_hi_ts,
              Tco_lo_ts, Tco_hi_ts, n_lo_ts, n_hi_ts,
              rho_lo_ts, rho_hi_ts)
end

# --- Main ---
function main()
    default_config = joinpath(@__DIR__, "..", "..", "configs", "heatup", "baseline.toml")
    config_path = length(ARGS) > 0 ? ARGS[1] : default_config
    # Allow a config path relative to repo root or code/.
    if !isfile(config_path)
        alt = joinpath(@__DIR__, "..", "..", config_path)
        isfile(alt) && (config_path = alt)
    end
    config = load_config(config_path)

    println("\n=== Heatup PJ reach — config: $(config["name"]) ===")
    println("  $(get(config, "description", ""))")

    X0, entry_info = build_entry_box(config)
    println("  X_entry: n ∈ [$(entry_info.n_lo), $(entry_info.n_hi)], " *
            "T_c ∈ [$(entry_info.T_c_lo), $(entry_info.T_c_hi)] °C")

    tmjets_cfg = config["tmjets"]
    probes = config["probes"]["horizons_seconds"]

    results = Dict{Float64, Any}()

    for T_probe in probes
        println("\n--- Probe T = $T_probe s ($(round(T_probe/60; digits=1)) min) ---")
        sys  = BlackBoxContinuousSystem(rhs_heatup_pj_taylor!, 10)
        prob = InitialValueProblem(sys, X0)
        try
            alg = TMJets(
                orderT   = tmjets_cfg["orderT"],
                orderQ   = tmjets_cfg["orderQ"],
                abstol   = tmjets_cfg["abstol"],
                maxsteps = tmjets_cfg["maxsteps"],
            )
            t_start = time()
            sol = solve(prob; T=Float64(T_probe), alg=alg)
            elapsed = time() - t_start
            flow = flowpipe(sol)
            n_sets = length(flow)
            println("  TMJets: $n_sets reach-sets, wall $(round(elapsed; digits=1)) s")

            flow_hr = overapproximate(flow, Hyperrectangle)
            env = extract_envelopes(flow_hr)

            println("  n envelope:   [$(round(minimum(env.n_lo_ts); sigdigits=4)), $(round(maximum(env.n_hi_ts); sigdigits=4))]")
            println("  T_c envelope: [$(round(minimum(env.Tc_lo_ts); digits=2)), $(round(maximum(env.Tc_hi_ts); digits=2))] °C")
            println("  T_f envelope: [$(round(minimum(env.Tf_lo_ts); digits=2)), $(round(maximum(env.Tf_hi_ts); digits=2))] °C")
            println("  T_cold env:   [$(round(minimum(env.Tco_lo_ts); digits=2)), $(round(maximum(env.Tco_hi_ts); digits=2))] °C")
            println("  rho env:      [$(round(minimum(env.rho_lo_ts); sigdigits=4)), $(round(maximum(env.rho_hi_ts); sigdigits=4))]")

            results[T_probe] = (status="OK", n_sets=n_sets, elapsed=elapsed, env=env)
        catch err
            msg = sprint(showerror, err)
            println("  FAILED: ", first(msg, 300))
            results[T_probe] = (status="FAILED", err=first(msg, 300))
            break
        end
    end

    println("\n=== Summary ===")
    for T_probe in probes
        haskey(results, T_probe) || continue
        r = results[T_probe]
        if r.status == "OK"
            println("  T = $(T_probe) s: OK, $(r.n_sets) sets, $(round(r.elapsed; digits=1))s wall")
        else
            println("  T = $(T_probe) s: FAILED")
        end
    end

    # --- Save ---
    if get(config, "output", Dict()) |> (o -> get(o, "save_per_step", false))
        result_file = config["output"]["result_file"]
        mat_out = joinpath(@__DIR__, "..", "..", "..", "results", result_file)
        saved = Dict{String, Any}(
            "config_name"   => config["name"],
            "probe_horizons" => collect(probes),
            "beta"          => BETA,
            "Kp"            => KP_HEATUP,
            "T_c0"          => T_C0,
            "T_cold0"       => T_COLD0,
            "T_standby"     => T_STANDBY,
        )
        for T_probe in probes
            haskey(results, T_probe) || continue
            r = results[T_probe]
            r.status == "OK" || continue
            pre = "T_$(Int(T_probe))_"
            env = r.env
            saved[pre * "t_arr"]     = env.t_arr
            saved[pre * "Tc_lo_ts"]  = env.Tc_lo_ts;  saved[pre * "Tc_hi_ts"]  = env.Tc_hi_ts
            saved[pre * "Tf_lo_ts"]  = env.Tf_lo_ts;  saved[pre * "Tf_hi_ts"]  = env.Tf_hi_ts
            saved[pre * "Tco_lo_ts"] = env.Tco_lo_ts; saved[pre * "Tco_hi_ts"] = env.Tco_hi_ts
            saved[pre * "n_lo_ts"]   = env.n_lo_ts;   saved[pre * "n_hi_ts"]   = env.n_hi_ts
            saved[pre * "rho_lo_ts"] = env.rho_lo_ts; saved[pre * "rho_hi_ts"] = env.rho_hi_ts
        end
        matwrite(mat_out, saved)
        println("\nSaved per-step envelopes to $mat_out")
    end
end

main()
