# SPDX-FileCopyrightText: 2026 Yating Zhang
# SPDX-License-Identifier: MIT

"""
numerical EC.6.3 compute revenue of (M+1)-cyclic policy 
name: CycleRev 
"""
function enable_stay_awake()
    if Sys.iswindows()
        # Windows: Call Kernel32 API
        # ES_CONTINUOUS (0x80000000) | ES_SYSTEM_REQUIRED (0x00000001)
        # Tells the system that this thread requires the system to be in the working state.
        try
            ccall((:SetThreadExecutionState, "kernel32"), UInt32, (UInt32,), 0x80000001)
            println(">> Windows: System sleep prevention enabled.")
        catch
            println(">> Windows: Failed to set sleep prevention via API.")
        end
    elseif Sys.isapple()
        # macOS: Use caffeinate
        # -i: prevent idle sleep
        # -w: wait for the process with the given PID to exit
        run(`caffeinate -i -w $(getpid())`, wait=false)
        println(">> macOS: System sleep prevention enabled (caffeinate).")
    elseif Sys.islinux()
        # Linux: Use systemd-inhibit
        # It creates a lock that prevents sleep/idle.
        try
            run(`systemd-inhibit --why="Numerical Experiments" --who="Julia" --what=idle:sleep sleep infinity`, wait=false)
            println(">> Linux: System sleep prevention enabled (systemd-inhibit).")
        catch
            println(">> Linux: systemd-inhibit not found. System may still sleep.")
        end
    end
end

enable_stay_awake()
# ================== set parameters ==================
mode_idx = findfirst(x -> x == "--mode", ARGS)
is_full = mode_idx !== nothing && mode_idx < length(ARGS) && ARGS[mode_idx+1] == "full"
if is_full
    @info "RUNNING IN FULL MODE: This will reproduce the full tables."
else
    @warn "RUNNING IN TEST MODE (Default): Only a subset of instances will be reproduced. To reproduce full results, add '--mode full' at the end of the command."
end
if is_full
    Mlist = [1,2] 
    Tlist = [10,30,50]
    testn = 5
    time_bound = 2*60*60
else
    Mlist = [1,2] 
    Tlist = [10]
    testn = 5
    time_bound = 2*60*60
end

# =======================================================

using JuMP,CSV,DataFrames,Gurobi,BenchmarkTools,MosekTools, Dates, JSON , XLSX
import LinearAlgebra, Random 
Random.seed!(314)
import MathOptInterface as MOI    

# --- PATH SETUP ---
const SCRIPT_DIR = @__DIR__
const PROJECT_ROOT = dirname(SCRIPT_DIR) 
# ------------------

include(joinpath(PROJECT_ROOT, "src", "Gene_data.jl")) 
include(joinpath(PROJECT_ROOT, "src", "Project_CuttingPlane.jl")) 
include(joinpath(PROJECT_ROOT, "src", "Conic_Env.jl"))
include(joinpath(PROJECT_ROOT, "src", "Heuristics.jl"))

# ==================== Set parameters  ==================== 
name = "CycleRev"
 
N = 30
posi = 0 # consider satiation case 
mix = 0
strong = 1 #  strong satiation 
posi_ratio = 0  
# opt parameters 
v0 = 1 
stop_gap = 0.005
init = 0
init_X = Any[]
output = 1
IP = 1   

println("Time limit = ", time_bound, " seconds ")
println("Stop optimality gap = ", stop_gap*100, "%")



println("Start solving HAP problems...")
println("=" ^ 50)

Result = [ ]

# This ensures code doesn't crash if folder is missing
results_folder = joinpath(PROJECT_ROOT, "results", "TableEC4_ec_6_3_CyclicRevenue")
logs_folder = joinpath(results_folder, "logs")
mkpath(logs_folder)  
 
for M in Mlist  
    if M == 1
        rank_set = [[1]]
    else
        rank_set = [ [1,2], [2,1]]
    end  
    test_data = open( joinpath(PROJECT_ROOT, "data","CycleRev_M"*string(M)*".json"), "r") do io
        JSON.parse(read(io, String))
    end  
        
    for k in 1:testn 
        r = test_data[k][1]
        u = test_data[k][2]
        beta0 = test_data[k][3] 
        beta = zeros(N,M)
        for i in 1:N 
            for m in 1:M 
                beta[i,m] = beta0[m][i]
            end 
        end  
        
        s = zeros(N,M)
        for i in 1:N 
            for m in 1:M 
                if beta[i,m]>0 
                    s[i,m]=1
                end 
            end
        end  

        betanew = zeros(N,M,M+1)
        for t in 2:M+1
            for i in 1:N  
                if (t<=M) 
                    for m in 1:t-1 
                        betanew[i,m,t] = beta[i,m]
                    end 
                else 
                    for m in 1:M 
                        betanew[i,m,t] = beta[i,m] 
                    end 
                end 
            end 
        end  
    
        for T in Tlist 
            # unconstrained 
            cardinal = N
            showtime = T 
    
            # solve M+1 cyclic  
            log_filename = "bf_M"*string(M)*"_inst"*string(k)*"_T"*string(T)*".txt"
            logname1 = joinpath(logs_folder, log_filename)
            resbf1 = BoundFreewithSepa(N,M,r,u,v0,beta, IP ,1, stop_gap, time_bound, logname1 , s,cardinal )
            # generate solution for T periods 
            cycle_solu = zeros(N,T)
            for t in 1:T 
                ind = t%(M+1)
                if ind == 0
                    cycle_solu[:, t] = resbf1[2][:,M+1]
                else
                    cycle_solu[:, t] = resbf1[2][:, ind]
                end 
            end 
            cycle_res = FeasibleVarValue(N,T,M,u,beta,v0,cycle_solu,r) 

            # solve env for benchmark  
            log_filename2 = "env_M"*string(M)*"_inst"*string(k)*"_T"*string(T)*".txt"
            logname2 = joinpath(logs_folder, log_filename2)
            env = MIP(T,N,M,r,beta,u,v0,IP, time_bound,stop_gap,init,init_X, output, logname2, s, betanew,rank_set, cardinal, showtime , 0 ) 

            # sequential ro 
            ro = SequentialRO(v0,u,beta,r,N,M,T) 
            # sequential lospo
            oneout = ro_oneout(N,M,T, r, u, beta, v0 )

            push!(Result, [M, k, T, cycle_res[1],env[1], env[3], ro[1], oneout[1]])
            GC.gc()
        end  
    end  
end  

temp_matrix  = tupe2mat(Result) 
f1 = joinpath(results_folder, "result_detail.csv")
temp_data = DataFrame(temp_matrix, :auto) 
rename!(temp_data, ["M",  "k","T","cycle_obj","env", "env_time", "ro_obj", "oneout_obj"])
CSV.write(f1,  temp_data, writeheader=false)

# ================ compute table =================

println("=" ^ 50)
println("Compute revenue loss ...")


temp_data.cycle = (temp_data.env  .- temp_data.cycle_obj ) ./ temp_data.env      
temp_data.ro = (temp_data.env  .- temp_data.ro_obj ) ./ temp_data.env      
temp_data.oneout = (temp_data.env  .- temp_data.oneout_obj ) ./ temp_data.env      


sumdata = [ ] 
 
for M in Mlist       
    for T in Tlist
        filterdata = filter(row -> row.M == M   && row.T == T, temp_data)
        push!(sumdata, [M, T, round(100*mean(filterdata.cycle),digits=2), round(100*mean(filterdata.ro),digits=2), round(100*mean(filterdata.oneout),digits=2) ])
    end 
end 

sumdata = tupe2mat(sumdata) 
sumdata = DataFrame(sumdata, :auto)   
rename!(sumdata, ["M", "T", "cyclic_gap", "sequential_ro_gap", "sequential_lospo_cap"])

# Get current time
t = Dates.now() 
timestr = Dates.format(t, "HHMM")
tb =  joinpath(results_folder,"Table_ec4_revgap_"*timestr*".xlsx")
XLSX.writetable(tb, sumdata)
 


println("Finish numerical study ", name, "!")