#!/usr/bin/env julia
#
# barrier_polytopic.jl — polytopic barrier attempt for operation mode.
#
# Idea: instead of the loose quadratic-Lyapunov ellipsoid, use the
# polytope P = inv2_holds ∩ (precursor bounds) and verify
# forward-invariance face-by-face via LP.
#
# Nagumo's theorem for linear systems with bounded disturbance: a
# polytope P = {x : Ax ≤ b} is forward-invariant under dx/dt = A_cl x +
# B_w w iff for each face i of P (where a_i' x = b_i is active),
#   max over (x in P with a_i'x=b_i, w in W) of a_i'(A_cl x + B_w w) ≤ 0.
# This is one LP per face.
#
# The issue with inv2_holds alone: it's unbounded in the 6 precursor
# directions, so the LP is unbounded.  Fix: add precursor bounds
# inferred from the reach-tube envelope (which we already computed in
# reach_operation.jl).  The resulting augmented polytope is bounded
# and the LPs have finite solutions.
#
# Uses JuMP + HiGHS (open-source, ships with no license trouble).

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

using Printf
using LinearAlgebra
using MatrixEquations
using MAT
using JSON
using JuMP
using HiGHS

include(joinpath(@__DIR__, "..", "..", "src", "pke_params.jl"))
include(joinpath(@__DIR__, "..", "..", "src", "pke_th_rhs.jl"))
include(joinpath(@__DIR__, "..", "..", "src", "pke_linearize.jl"))
include(joinpath(@__DIR__, "..", "..", "src", "load_predicates.jl"))

plant = pke_params()
x_op  = pke_initial_conditions(plant)
pred  = load_predicates(plant)

# --- Closed-loop A_cl (LQR) ---
A, B, B_w, _, _, _ = pke_linearize(plant)
Q_lqr = Diagonal([1.0, 1e-3, 1e-3, 1e-3, 1e-3, 1e-3, 1e-3, 1e-2, 1e2, 1.0])
R_lqr = 1e6 * ones(1, 1)
X_ric, _, _ = arec(A, reshape(B, :, 1), R_lqr, Matrix(Q_lqr))
K    = (R_lqr \ reshape(B, 1, :)) * X_ric
A_cl = A - reshape(B, :, 1) * K

# --- inv2_holds halfspaces (from JSON) ---
inv2 = pred.mode_invariants[:inv2_holds]
A_inv2 = inv2.A_poly        # 6 × 10
b_inv2 = inv2.b_poly        # 6

# --- Precursor bounds from reach-tube envelope ---
# Read reach_operation_result.mat; take min/max of X_lo, X_hi on precursors.
reach_mat_path = joinpath(@__DIR__, "..", "..", "..", "reachability",
                          "reach_operation_result.mat")
reach = matread(reach_mat_path)
X_lo = reach["X_lo"]; X_hi = reach["X_hi"]

# Build the augmented polytope: inv2_holds ∪ (C_i in tube bounds).
# Precursor halfspaces: C_i ≤ max(X_hi[i+1, :]), -C_i ≤ -min(X_lo[i+1, :])
# for i = 1..6 (rows 2..7 of state).
A_aug = copy(A_inv2)
b_aug = copy(b_inv2)
labels = copy(inv2.components)

for i in 1:6
    local idx = i + 1
    local C_min = minimum(X_lo[idx, :])
    local C_max = maximum(X_hi[idx, :])
    local row_hi = zeros(1, 10); row_hi[1, idx] = 1.0
    global A_aug = vcat(A_aug, row_hi); global b_aug = vcat(b_aug, C_max)
    push!(labels, "C$(i)_upper")
    local row_lo = zeros(1, 10); row_lo[1, idx] = -1.0
    global A_aug = vcat(A_aug, row_lo); global b_aug = vcat(b_aug, -C_min)
    push!(labels, "C$(i)_lower")
end

# (Skipping tube-based bounds on n, T_f, T_cold — those are REACH
#  envelopes, not forward-invariant limits.  We rely on the inv2_holds
#  halfspaces for n/T_f/T_cold and the precursor tube-bounds above to
#  keep the polytope bounded in all 10 dimensions.)

n_faces = size(A_aug, 1)
println("\n=== Polytopic barrier check ===")
println("  Polytope P = inv2_holds ∩ (precursor + n/T_f/T_cold tube bounds)")
println("  Total faces: $n_faces")
println("  Disturbance: Q_sg ∈ [0.85, 1.00]·P_0")

# --- Disturbance envelope ---
Q_nom = plant.P0
w_lo  = 0.85 * plant.P0 - Q_nom
w_hi  = 0.00 * plant.P0 + (plant.P0 - Q_nom)  # = 0
# Actually the disturbance is Q_sg deviation from nominal.
# Q_sg ∈ [0.85, 1.00]·P0 → w ∈ [-0.15·P0, 0].
w_lo = -0.15 * plant.P0
w_hi = 0.0

# --- Per-face LP check ---
# For face i: max over x, w of a_i' (A_cl x + B_w w) s.t. Ax ≤ b, a_i'x = b_i,
#             w ∈ [w_lo, w_hi].  All in DEVIATION coordinates (dx = x - x_op).
# Need to convert polytope: in absolute coords, P is {x : A_aug x ≤ b_aug}.
# Deviation polytope: P_dev = {dx : A_aug (dx + x_op) ≤ b_aug} = {dx : A_aug dx ≤ b_aug - A_aug x_op}.
b_dev = b_aug .- A_aug * x_op

results = []
for i in 1:n_faces
    a_i = A_aug[i, :]
    rhs_i = b_dev[i]

    # Worst-case disturbance contribution a_i' B_w w over w ∈ [w_lo, w_hi].
    # a_i' B_w is a scalar; max w is w_hi if that scalar > 0 else w_lo.
    aB = a_i' * B_w
    dist_worst = aB > 0 ? aB * w_hi : aB * w_lo

    # LP: maximize a_i' (A_cl dx) + dist_worst
    # subject to A_aug dx ≤ b_dev, a_i' dx = rhs_i.
    model = Model(HiGHS.Optimizer)
    set_silent(model)
    @variable(model, dx[1:10])
    @constraint(model, A_aug * dx .<= b_dev)
    @constraint(model, a_i' * dx == rhs_i)
    grad = (A_cl' * a_i)
    @objective(model, Max, grad' * dx + dist_worst)
    optimize!(model)

    status = termination_status(model)
    if status == MOI.OPTIMAL
        obj = objective_value(model)
        margin = -obj   # forward invariance requires obj ≤ 0
        badge = margin >= 0 ? "✅ forward-invariant" :
                "❌ can escape (a_i'·ẋ = $(round(obj; digits=4)))"
        @printf "  [%-28s]  max a_i'·ẋ = %+.4e  %s\n" labels[i] obj badge
        push!(results, (label=labels[i], obj=obj, status=status))
    else
        @printf "  [%-28s]  LP status: %s\n" labels[i] string(status)
        push!(results, (label=labels[i], obj=NaN, status=status))
    end
end

n_ok = count(r -> isfinite(r.obj) && r.obj <= 1e-10, results)
println("\n=== Summary ===")
println("  Faces forward-invariant:    $n_ok / $n_faces")
println("  Faces that can violate:     $(n_faces - n_ok)")

if n_ok == n_faces
    println("\n  ✅ Polytope P is forward-invariant under A_cl + bounded Q_sg.")
    println("     Polytopic barrier B(x) = max_i (a_i'(x - x_op) - b_dev_i) satisfies")
    println("     B(x₀) ≤ 0 on P, and dB/dt ≤ 0 on {B = 0}.")
else
    println("\n  ❌ Some faces can be crossed under A_cl; P as constructed is not a")
    println("     valid barrier.  Expected: safety halfspaces + tube-envelope bounds")
    println("     together form a set MUCH larger than what LQR can contract to, so")
    println("     the worst-case point on a face can have derivative outward.")
    println()
    println("     The right approach is the Blanchini pre-image algorithm:")
    println("       P₀ = safety polytope (inv2_holds + precursor bounds)")
    println("       P_{k+1} = P_k ∩ {x : A_cl x + B_w w ∈ P_k for all w ∈ W}")
    println("     iterating until fixed point or timeout.  The fixed point is the")
    println("     maximal robustly controllable invariant set inside the safety polytope.")
    println("     Each iteration adds faces; polytope grows combinatorially in size.")
    println("     Tools: Polyhedra.jl + CDDLib for the set operations, HiGHS for LPs.")
    println()
    println("     Rough effort estimate: 2-3 days focused to get a working implementation")
    println("     + a thesis-defensible result on operation mode.  Deferred for now.")
end
