package sd.dashboard;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Orquestrador de processos para o ambiente de simulação distribuída.
 * <p>
 * Esta classe atua como um supervisor (Process Manager), responsável pelo <i>bootstrapping</i>
 * e <i>teardown</i> das múltiplas Java Virtual Machines (JVMs) que compõem o sistema.
 * <p>
 * Funcionalidades principais:
 * <ul>
 * <li><b>Isolamento:</b> Cada nó (Interseção, Coordinator, ExitNode) corre no seu próprio processo OS.</li>
 * <li><b>Ordem de Arranque:</b> Garante que os servidores (Interseções) estão online antes dos clientes (Coordenador).</li>
 * <li><b>Gestão de Logs:</b> Redireciona stdout/stderr de cada processo filho para ficheiros temporários para facilitar o debug.</li>
 * </ul>
 */
public class SimulationProcessManager {

    private final List<Process> runningProcesses;
    private final String classpath;
    private String configFile;

    /**
     * Inicializa o gestor capturando o classpath da JVM atual.
     * Isto garante que os processos filhos herdam as mesmas dependências e configurações de ambiente.
     */
    public SimulationProcessManager() {
        this.runningProcesses = new ArrayList<>();
        this.classpath = System.getProperty("java.class.path");
        this.configFile = "src/main/resources/simulation.properties";
    }

    /**
     * Define o perfil de configuração a ser injetado nos processos filhos.
     * Útil para alternar entre cenários (Low/Medium/High Load) dinamicamente.
     * * @param configFile Nome do ficheiro de propriedades (ex: "simulation-low.properties").
     */
    public void setConfigFile(String configFile) {
        this.configFile = "src/main/resources/" + configFile;
        System.out.println("Configuration file set to: " + this.configFile);
    }

    /**
     * Executa o procedimento de arranque (Bootstrap) da simulação distribuída.
     * <p>
     * A ordem de inicialização é crítica para evitar <i>Race Conditions</i> na conexão TCP:
     * <ol>
     * <li><b>Workers (Interseções):</b> Iniciam os ServerSockets.</li>
     * <li><b>Sink (Exit Node):</b> Prepara-se para receber métricas finais.</li>
     * <li><b>Delay de Estabilização:</b> Pausa de 1s para garantir que os sockets estão em LISTENING.</li>
     * <li><b>Source (Coordinator):</b> Inicia a geração de carga e conecta-se aos nós.</li>
     * </ol>
     * * @throws IOException Se falhar o fork de algum processo.
     */
    public void startSimulation() throws IOException {
        if (!runningProcesses.isEmpty()) {
            stopSimulation();
        }

        System.out.println("Starting simulation processes...");

        // 1. Start Intersections (Cr1 - Cr5)
        String[] intersectionIds = { "Cr1", "Cr2", "Cr3", "Cr4", "Cr5" };
        for (String id : intersectionIds) {
            startProcess("sd.IntersectionProcess", id);
        }

        // 2. Start Exit Node
        startProcess("sd.ExitNodeProcess", null);

        // 3. Start Coordinator (Wait a bit for others to initialize)
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        startProcess("sd.coordinator.CoordinatorProcess", null);

        System.out.println("All simulation processes started.");
    }

    /**
     * Verifica o estado de "liveness" da simulação monitorizando o processo Coordenador.
     * <p>
     * Como o Coordenador gere o relógio DES e a geração de eventos, a sua terminação
     * (após o drain time) sinaliza o fim efetivo da simulação.
     * * @return true se o Coordenador ainda estiver ativo (alive).
     */
    public boolean isSimulationRunning() {
        if (runningProcesses.isEmpty()) {
            return false;
        }
        // Coordinator is the last process in the list
        Process coordinator = runningProcesses.get(runningProcesses.size() - 1);
        return coordinator.isAlive();
    }
    
    /**
     * Bloqueia a thread atual até que a simulação termine naturalmente ou ocorra timeout.
     * * @param timeoutSeconds Tempo máximo de espera.
     * @return true se terminou, false se o timeout expirou.
     * @throws InterruptedException Se a espera for interrompida.
     */
    public boolean waitForCompletion(long timeoutSeconds) throws InterruptedException {
        if (runningProcesses.isEmpty()) {
            return true;
        }
        
        Process coordinator = runningProcesses.get(runningProcesses.size() - 1);
        return coordinator.waitFor(timeoutSeconds, java.util.concurrent.TimeUnit.SECONDS);
    }

    /**
     * Executa o procedimento de encerramento (Teardown) de todos os processos.
     * <p>
     * Tenta primeiro uma paragem graciosa (`SIGTERM`), aguarda meio segundo, e
     * força a paragem (`SIGKILL`) para processos persistentes, garantindo que não
     * ficam processos órfãos no SO.
     */
    public void stopSimulation() {
        System.out.println("Stopping simulation processes...");

        for (Process process : runningProcesses) {
            if (process.isAlive()) {
                process.destroy(); // Try graceful termination first
            }
        }

        // Wait a bit and force kill if necessary
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        for (Process process : runningProcesses) {
            if (process.isAlive()) {
                process.destroyForcibly();
            }
        }

        runningProcesses.clear();
        System.out.println("All simulation processes stopped.");
    }

    /**
     * Helper de baixo nível para construção e lançamento de processos Java.
     * Configura o redirecionamento de I/O para ficheiros de log na diretoria temporária do SO.
     */
    private void startProcess(String className, String arg) throws IOException {
        String javaBin = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";

        ProcessBuilder builder;
        if (arg != null) {
            builder = new ProcessBuilder(javaBin, "-cp", classpath, className, arg, configFile);
        } else {
            builder = new ProcessBuilder(javaBin, "-cp", classpath, className, configFile);
        }

        // get the OS temp folder
        // Linux: /tmp/
        // Windows: %AppData%\Local\Temp\
        String tempDir = System.getProperty("java.io.tmpdir");

        String logName = className.substring(className.lastIndexOf('.') + 1) + (arg != null ? "-" + arg : "") + ".log";

        // use the (File parent, String child) constructor to handle slash/backslash
        // automatically
        File logFile = new File(tempDir, logName);

        builder.redirectOutput(logFile);
        builder.redirectError(logFile);

        Process process = builder.start();
        runningProcesses.add(process);
        System.out.println("Started " + className + (arg != null ? " " + arg : ""));
        // print where the logs are actually going
        System.out.println("Logs redirected to: " + logFile.getAbsolutePath());
    }
}