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

"""
numerical EC.6.1 consider cross-period and cross-product constraints
name: Constr 
"""
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
    Clist = [5, 10 ,15, 30 ]
    Klist = [2,3,4,5] 
    testn = 5
    time_bound = 60*60
else
    Clist = [15,30]
    Klist = [4,5] 
    testn = 5
    time_bound = 60*60
end

time_bound_bmk = 2*60*60
# =======================================================
 

using JuMP,CSV,DataFrames,Gurobi,BenchmarkTools,MosekTools, XLSX
import LinearAlgebra 
import Random
Random.seed!(314)
import MathOptInterface as MOI 
using Dates, JSON 
 
# --- 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", "Conic_Env.jl"))  

# -------------- Set parameters --------------
println("=" ^ 50)
println("Set optimization parameters ...")
testname = "Constrained"
M = 1 
T = 5 
N = 30 
strong = 0  
posi = 0 
mix = 0 
posi_ratio = 0    
 
# opt parameters 
rank_set = [1]
v0 = 1  
stop_gap = 0.005
init = 0
init_X = Any[]
output = 1
IP = 1  # change IP to 0 to obtain continuous relaxation 

println("Numerical study: ", testname)
println("Number of products N = ", N)
println("Memory length M = ", M)
println("Planning horizon T = ", T) 
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", "TableEC2_ec6_1_Constraints")
logs_folder = joinpath(results_folder, "logs")
mkpath(logs_folder)  


test_data = open(  joinpath(PROJECT_ROOT, "data", "Constraint_paras.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 cardinal in Clist 
        for showtime in Klist  
            log_filename = "cardinal"*string(cardinal)*"_showtime"*string(showtime)*".txt"
            logname = joinpath(logs_folder, log_filename)
            env = MIP(T,N,M,r,beta,u,v0,IP, time_bound,stop_gap,init,init_X, output, logname, s, betanew,rank_set, cardinal, showtime , 0 )  

            
            env_lp = MIP(T,N,M,r,beta,u,v0, 0, time_bound,stop_gap,init,init_X, output, "", s, betanew,rank_set, cardinal, showtime , 0 ) 
            
            root_gap = 0 
            # check if not obtain optimal in one hour, then run two hours 
            if env[3] > stop_gap + 0.0001 # not optimal 
                log_filename_opt = "cardinal"*string(cardinal)*"_showtime"*string(showtime)*"_opt.txt"
                logname_opt = joinpath(logs_folder, log_filename_opt )
                env_opt = MIP(T,N,M,r,beta,u,v0, IP, time_bound_bmk,stop_gap,1, env[4] , output, logname_opt, s, betanew,rank_set, cardinal, showtime , 0 )  # solve within 2 hours and use previous optimal solution as init solution 
                root_gap = (env_lp[1] - env_opt[1])/env_opt[1]
            else 
                root_gap = (env_lp[1] - env[1])/env[1]
            end 

            push!(Result,[cardinal, showtime, env[1], env[2], env[3] , root_gap ])
            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, [ "cardsize", "showtime", "obj","runtime","gap" , "root_gap"])
CSV.write(f1,  temp_data, writeheader=false) 


# ========================== compute table ec.2  ========================= 
summary = [ ] 
for C in Clist
    for K in Klist   
        filterdata = filter(row -> row.cardsize == C && row.showtime == K , temp_data ) 

        solved = 0 
        total_time = 0 
        total_endgap = 0  
        for (index, row) in enumerate(eachrow(filterdata))
            if row.gap <= stop_gap 
                solved +=1 
                total_time += row.runtime 
            else 
                total_endgap += row.gap
            end
        end 
        if solved ==0 
            push!(summary, [C,K, solved, time_bound, round(100*total_endgap/testn, digits=4) , round(100*mean(filterdata.root_gap),digits=4) ])  
        elseif solved == testn 
            push!(summary, [C,K, solved, round(total_time/testn, digits=4), round(100*total_endgap/testn )  , round(100*mean(filterdata.root_gap),digits=4)] )  
        else
            push!(summary, [C,K, solved, round(total_time/solved, digits=4), round(100*total_endgap/(testn-solved), digits=4) , round(100*mean(filterdata.root_gap),digits=4)] ) 
        end
    end 
end 

summary = tupe2mat(summary) 
summary = DataFrame(summary, :auto)   
rename!(summary, ["C","K", "solved", "time", "end_gap" , "root_gap"])

# Get current time
t = Dates.now() 
timestr = Dates.format(t, "HHMM")
ec2 = joinpath(results_folder,"Table_ec2_"*timestr*".xlsx" )
XLSX.writetable(ec2, summary)

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