"""
    pke_solver(plant, Q_sg, ctrl_fn, ref, tspan; x0=nothing, verbose=true)

Closed-loop simulation of the PKE + thermal-hydraulics plant with a
named controller.  Analog of `plant-model/pke_solver.m` but using
Julia's OrdinaryDiffEq under the hood (Rodas5 for stiff systems,
matches MATLAB's ode15s behavior).

Arguments:
- `plant`    : parameter NamedTuple from `pke_params()`
- `Q_sg`     : callable `Q_sg(t)` returning SG heat removal [W]
- `ctrl_fn`  : callable `u = ctrl_fn(t, x, plant, ref)`
- `ref`      : any ref struct (NamedTuple or nothing) — passed to controller
- `tspan`    : `(t_start, t_end)` tuple
- `x0`       : optional initial state (default: operating-point equilibrium)
- `verbose`  : print steady-state and final-state banners (default: true)

Returns `(t, X, U)`:
- `t :: Vector{Float64}` — time grid from the solver
- `X :: Matrix{Float64}` — M × 10 state matrix (rows are time, cols are states)
- `U :: Vector{Float64}` — M-element control trajectory

The state column indices match `pke_th_rhs!`: `[n, C1..C6, T_f, T_c, T_cold]`.
"""
function pke_solver(plant, Q_sg, ctrl_fn, ref, tspan; x0=nothing, verbose=true)
    if x0 === nothing
        x0 = pke_initial_conditions(plant)
    end

    CtoF(T) = T * 9 / 5 + 32

    if verbose
        println("=== Steady-State Conditions ===")
        @printf "  beta       = %.5f  (%.1f pcm)\n"  plant.beta  plant.beta*1e5
        @printf "  Lambda     = %.1e s\n"            plant.Lambda
        @printf "  P0         = %.0f MWth\n"         plant.P0/1e6
        @printf "  T_cold     = %.1f F\n"            CtoF(plant.T_cold0)
        @printf "  T_avg      = %.1f F\n"            CtoF(plant.T_c0)
        @printf "  T_hot      = %.1f F\n"            CtoF(plant.T_hot0)
        @printf "  T_fuel     = %.1f F\n"            CtoF(plant.T_f0)
        @printf "  Core dT    = %.1f F\n"            plant.dT_core * 9/5
        println("================================\n")
    end

    # Closed-loop RHS: controller provides u, then plant RHS.
    function rhs!(dx, x, p, t)
        u = ctrl_fn(t, x, plant, ref)
        pke_th_rhs!(dx, x, t, plant, Q_sg, u)
    end

    prob = ODEProblem(rhs!, x0, tspan)
    sol  = solve(prob, Rodas5(); reltol=1e-8, abstol=1e-10)

    t_arr = sol.t
    X = reduce(hcat, sol.u)'  # M × 10
    U = [ctrl_fn(sol.t[k], sol.u[k], plant, ref) for k in 1:length(sol.t)]

    if verbose
        n_f    = X[end, 1]
        T_f_f  = X[end, 8]
        T_c_f  = X[end, 9]
        T_col_f = X[end, 10]
        T_hot_f = 2 * T_c_f - T_col_f
        rho_tot = U[end] + plant.alpha_f*(T_f_f - plant.T_f0) +
                           plant.alpha_c*(T_c_f - plant.T_c0)
        println("=== Final State (t = $(round(t_arr[end]; digits=1)) s) ===")
        @printf "  Power  = %.1f MWth  (%.3f x nominal)\n"   n_f*plant.P0/1e6  n_f
        @printf "  T_fuel = %.1f F\n"                         CtoF(T_f_f)
        @printf "  T_hot  = %.1f F\n"                         CtoF(T_hot_f)
        @printf "  T_avg  = %.1f F\n"                         CtoF(T_c_f)
        @printf "  T_cold = %.1f F\n"                         CtoF(T_col_f)
        @printf "  u      = %.4f \$  (external reactivity)\n" U[end]/plant.beta
        @printf "  rho    = %.4f \$  (total, incl. T-feedback)\n"  rho_tot/plant.beta
    end

    return t_arr, Matrix(X), U
end
