package sd.coordinator;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.LockSupport;

import sd.config.SimulationConfig;
import sd.dashboard.DashboardStatistics;
import sd.dashboard.StatsUpdatePayload;
import sd.des.DESEventType;
import sd.des.EventQueue;
import sd.des.SimulationClock;
import sd.des.SimulationEvent;
import sd.logging.EventLogger;
import sd.model.Message;
import sd.model.MessageType;
import sd.model.Vehicle;
import sd.routing.LeastCongestedRouteSelector;
import sd.routing.RandomRouteSelector;
import sd.routing.RouteSelector;
import sd.routing.RoutingPolicy;
import sd.routing.ShortestPathRouteSelector;
import sd.serialization.SerializationException;
import sd.util.VehicleGenerator;

/**
 * Coordenador central da arquitetura de simulação distribuída.
 * <p>
 * Este processo atua como o "cérebro" da simulação, sendo responsável por:
 * <ol>
 * <li><b>Orquestração DES:</b> Gerir o relógio global ({@link SimulationClock}) e a fila de eventos prioritária.</li>
 * <li><b>Geração de Carga:</b> Injetar veículos na malha viária seguindo distribuições estocásticas (Poisson) ou determinísticas.</li>
 * <li><b>Encaminhamento Dinâmico:</b> Decidir as rotas dos veículos com base na política ativa (Random, Shortest Path, Least Congested).</li>
 * <li><b>Sincronização:</b> Garantir que todos os nós (Interseções e Dashboard) operem em uníssono.</li>
 * </ol>
 */
public class CoordinatorProcess {

    private final SimulationConfig config;
    private final VehicleGenerator vehicleGenerator;
    
    /** Mapa de clientes TCP persistentes para cada interseção (Worker Nodes). */
    private final Map<String, SocketClient> intersectionClients;
    private SocketClient dashboardClient;

    // Componentes DES (Discrete Event Simulation)
    private final SimulationClock clock;
    private final EventQueue eventQueue;
    private final EventLogger eventLogger;

    // Estado da simulação
    private int vehicleCounter;
    private boolean running;
    private double timeScale;
    private RouteSelector currentRouteSelector;
    
    /** Referência para estatísticas do dashboard para polling de mudanças de política. */
    private DashboardStatistics dashboardStatistics;
    
    /**
     * Monitorização local (aproximada) dos tamanhos de fila nas interseções.
     * <p>
     * Utilizado exclusivamente pela política {@link LeastCongestedRouteSelector}.
     * O coordenador incrementa este contador ao enviar um veículo para uma interseção.
     * Nota: Esta é uma visão "borda" (edge) e pode não refletir a saída em tempo real
     * dos veículos, mas serve como heurística suficiente para balanceamento de carga.
     */
    private final Map<String, Integer> intersectionQueueSizes;

    /**
     * Ponto de entrada do processo Coordenador.
     * Carrega configurações, estabelece conexões TCP e inicia o loop de eventos.
     */
    public static void main(String[] args) {
        System.out.println("=".repeat(60));
        System.out.println("COORDINATOR PROCESS - DISTRIBUTED TRAFFIC SIMULATION");
        System.out.println("=".repeat(60));

        try {
            // 1. Load configuration
            String configFile = args.length > 0 ? args[0] : "src/main/resources/simulation.properties";
            System.out.println("Loading configuration from: " + configFile);

            SimulationConfig config = new SimulationConfig(configFile);
            CoordinatorProcess coordinator = new CoordinatorProcess(config);

            // 2. Connect to intersection processes
            System.out.println("\n" + "=".repeat(60));
            coordinator.initialize();

            // 3. Run the sim
            System.out.println("\n" + "=".repeat(60));
            coordinator.run();

        } catch (IOException e) {
            System.err.println("Failed to load configuration: " + e.getMessage());
            System.exit(1);
        } catch (Exception e) {
            System.err.println("Coordinator error: " + e.getMessage());
            System.exit(1);
        }
    }

    /**
     * Inicializa o coordenador com a configuração fornecida.
     * Configura o motor DES, logging e o seletor de rotas inicial.
     *
     * @param config Objeto de configuração carregado.
     */
    public CoordinatorProcess(SimulationConfig config) {
        this.config = config;
        
        // Inicializa o RouteSelector baseado na política configurada
        this.currentRouteSelector = createRouteSelector(config.getRoutingPolicy());
        
        this.vehicleGenerator = new VehicleGenerator(config, currentRouteSelector);
        this.intersectionClients = new HashMap<>();
        this.vehicleCounter = 0;
        this.running = false;
        this.timeScale = config.getTimeScale();
        this.intersectionQueueSizes = new HashMap<>();

        this.clock = new SimulationClock();
        this.eventQueue = new EventQueue(true);

        this.eventLogger = EventLogger.getInstance();
        eventLogger.log(sd.logging.EventType.PROCESS_STARTED, "Coordinator",
                "Coordinator process initialized with DES architecture");

        System.out.println("Coordinator initialized with configuration:");
        System.out.println("  - Simulation duration: " + config.getSimulationDuration() + "s");
        System.out.println("  - Arrival model: " + config.getArrivalModel());
        System.out.println("  - Arrival rate: " + config.getArrivalRate() + " vehicles/s");
        System.out.println("  - Routing policy: " + config.getRoutingPolicy());
        System.out.println("  - DES Mode: ENABLED (Event-driven, no time-stepping)");
    }

    /**
     * Fábrica de {@link RouteSelector} baseada no nome da política.
     * * @param policyName Nome da política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED).
     * @return Uma instância da estratégia de roteamento.
     */
    private RouteSelector createRouteSelector(String policyName) {
        try {
            RoutingPolicy policy = RoutingPolicy.valueOf(policyName.toUpperCase());
            
            switch (policy) {
                case RANDOM:
                    System.out.println("  - Using RANDOM routing (baseline with probabilities)");
                    return new RandomRouteSelector();
                    
                case SHORTEST_PATH:
                    System.out.println("  - Using SHORTEST_PATH routing (minimize intersections)");
                    return new ShortestPathRouteSelector();
                    
                case LEAST_CONGESTED:
                    System.out.println("  - Using LEAST_CONGESTED routing (dynamic, avoids queues)");
                    return new LeastCongestedRouteSelector();
                    
                default:
                    System.err.println("  ! Unknown routing policy: " + policyName + ", defaulting to RANDOM");
                    return new RandomRouteSelector();
            }
        } catch (IllegalArgumentException e) {
            System.err.println("  ! Invalid routing policy: " + policyName + ", defaulting to RANDOM");
            return new RandomRouteSelector();
        }
    }

    /**
     * Estabelece conexões TCP com o Dashboard e todas as Interseções (Worker Nodes).
     * Essencial para o envio de comandos de controle e injeção de veículos.
     */
    public void initialize() {
        // Connect to dashboard first
        connectToDashboard();

        System.out.println("Connecting to intersection processes...");

        String[] intersectionIds = { "Cr1", "Cr2", "Cr3", "Cr4", "Cr5" };

        for (String intersectionId : intersectionIds) {
            try {
                String host = config.getIntersectionHost(intersectionId);
                int port = config.getIntersectionPort(intersectionId);

                SocketClient client = new SocketClient(intersectionId, host, port);
                client.connect();
                intersectionClients.put(intersectionId, client);

            } catch (IOException e) {
                System.err.println("Failed to connect to " + intersectionId + ": " + e.getMessage());
            }
        }

        System.out.println("Successfully connected to " + intersectionClients.size() + " intersection(s)");

        if (intersectionClients.isEmpty()) {
            System.err.println("WARNING: No intersections connected. Simulation cannot proceed.");
        }
    }

    /**
     * Loop principal da simulação (DES Engine).
     * <p>
     * Executa a sequência:
     * 1. Retira o próximo evento da fila prioritária.
     * 2. Avança o relógio virtual para o timestamp do evento.
     * 3. Aplica escala temporal (Time Scale) para visualização, se necessário.
     * 4. Processa o evento.
     */
    public void run() {
        double duration = config.getSimulationDuration();
        double drainTime = config.getDrainTime();
        double totalDuration = duration + drainTime;
        running = true;

        System.out.println("Starting DES-based vehicle generation simulation...");
        System.out.println("Duration: " + duration + "s (+ " + drainTime + "s drain)");
        System.out.println();

        // Log simulation start
        eventLogger.log(sd.logging.EventType.SIMULATION_STARTED, "Coordinator",
                String.format("Starting simulation - Duration: %.1fs", duration));

        // Send simulation start time to all processes for synchronization
        sendSimulationStartTime();

        // Schedule first vehicle generation event
        double firstArrivalTime = vehicleGenerator.getNextArrivalTime(clock.getCurrentTime());
        eventQueue.schedule(new SimulationEvent(
                firstArrivalTime,
                DESEventType.VEHICLE_GENERATION,
                null,
                "Coordinator"));

        // Schedule simulation end event
        eventQueue.schedule(new SimulationEvent(
                totalDuration,
                DESEventType.SIMULATION_END,
                null,
                "Coordinator"));

        System.out.printf("Initial event scheduled at t=%.3fs\n", firstArrivalTime);
        System.out.println("Entering DES event loop...\n");

        // Main DES loop - process events in chronological order
        double lastTime = 0.0;
        while (running && !eventQueue.isEmpty()) {
            SimulationEvent event = eventQueue.poll();

            // Apply time scaling for visualization
            if (timeScale > 0) {
                double simTimeDelta = event.getTimestamp() - lastTime;
                long realDelayMs = (long) (simTimeDelta * timeScale * 1000);
                if (realDelayMs > 0) {
                    try {
                        Thread.sleep(realDelayMs);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
                lastTime = event.getTimestamp();
            }

            // Advance simulation time to event time
            clock.advanceTo(event.getTimestamp());

            // Process the event
            processEvent(event, duration);
        }

        System.out.println();
        System.out.printf("Simulation complete at t=%.2fs\n", clock.getCurrentTime());
        System.out.println("Total vehicles generated: " + vehicleCounter);
        System.out.println("Total events processed: " + eventQueue.getProcessedCount());

        // Log simulation end
        eventLogger.log(sd.logging.EventType.SIMULATION_STOPPED, "Coordinator",
                String.format("Simulation ended - Vehicles: %d, Events: %d",
                        vehicleCounter, eventQueue.getProcessedCount()));

        // Export event history (spec requirement: view complete event list)
        exportEventHistory();

        shutdown();
    }

    /**
     * Trata o processamento de um evento DES retirado da fila.
     * * @param event O evento a ser processado.
     * @param generationDuration Duração da fase de geração ativa (antes do 'drain time').
     */
    private void processEvent(SimulationEvent event, double generationDuration) {
        double currentTime = clock.getCurrentTime();

        switch (event.getType()) {
            case VEHICLE_GENERATION:
                // Only generate if we're still in the generation phase
                if (currentTime < generationDuration) {
                    // Check for routing policy changes from dashboard
                    checkForPolicyChanges();
                    
                    generateAndSendVehicle();

                    // Schedule next vehicle generation (Recursive scheduling)
                    double nextArrivalTime = vehicleGenerator.getNextArrivalTime(currentTime);
                    eventQueue.schedule(new SimulationEvent(
                            nextArrivalTime,
                            DESEventType.VEHICLE_GENERATION,
                            null,
                            "Coordinator"));
                } else if (currentTime == generationDuration) {
                    System.out.printf("\n[t=%.2f] Generation phase complete. Entering DRAIN MODE...\n",
                            currentTime);
                }
                break;

            case SIMULATION_END:
                System.out.printf("[t=%.2f] Simulation end event reached\n", currentTime);
                running = false;
                break;

            default:
                System.err.println("WARNING: Unknown event type: " + event.getType());
        }
    }

    /**
     * Exporta o log completo de eventos DES para auditoria e debug.
     * Caminho: {@code logs/coordinator-event-history.txt}.
     */
    private void exportEventHistory() {
        try (java.io.PrintWriter writer = new java.io.PrintWriter(
                new java.io.FileWriter("logs/coordinator-event-history.txt"))) {
            String history = eventQueue.exportEventHistory();
            writer.println(history);
            System.out.println("\nEvent history exported to: logs/coordinator-event-history.txt");
        } catch (IOException e) {
            System.err.println("Failed to export event history: " + e.getMessage());
        }
    }

    /**
     * Gera um novo veículo e envia-o via TCP para a interseção de entrada apropriada.
     * Também atualiza o rastreio local de filas para balanceamento de carga.
     */
    private void generateAndSendVehicle() {
        double currentTime = clock.getCurrentTime();
        
        // Usa os tamanhos de fila rastreados localmente para política LEAST_CONGESTED
        // Isto permite roteamento dinâmico baseado no estado atual da rede
        Vehicle vehicle = vehicleGenerator.generateVehicle("V" + (++vehicleCounter), currentTime, intersectionQueueSizes);

        System.out.printf("[t=%.2f] Vehicle %s generated (type=%s, route=%s)%n",
                currentTime, vehicle.getId(), vehicle.getType(), vehicle.getRoute());

        // Log to event logger
        eventLogger.log(sd.logging.EventType.VEHICLE_GENERATED, "Coordinator",
                String.format("[%s] Type: %s, Route: %s", vehicle.getId(), vehicle.getType(), vehicle.getRoute()));
        
        // Update local queue size tracking (increment first intersection's queue)
        String firstIntersection = vehicle.getRoute().get(0);
        intersectionQueueSizes.put(firstIntersection, 
            intersectionQueueSizes.getOrDefault(firstIntersection, 0) + 1);

        // Send generation count to dashboard
        sendGenerationStatsToDashboard();

        if (vehicle.getRoute().isEmpty()) {
            System.err.println("ERROR: Vehicle " + vehicle.getId() + " has empty route!");
            return;
        }

        String entryIntersection = vehicle.getRoute().get(0);
        sendVehicleToIntersection(vehicle, entryIntersection);
        LockSupport.parkNanos(100000); // 100us
    }

    /**
     * Serializa e transmite o objeto Veículo para o nó (interseção) de destino.
     */
    private void sendVehicleToIntersection(Vehicle vehicle, String intersectionId) {
        SocketClient client = intersectionClients.get(intersectionId);

        if (client == null || !client.isConnected()) {
            System.err.println("ERROR: No connection to " + intersectionId + " for vehicle " + vehicle.getId());
            return;
        }

        try {
            Message message = new Message(
                    MessageType.VEHICLE_SPAWN,
                    "COORDINATOR",
                    intersectionId,
                    vehicle);

            client.send(message);
            System.out.printf("->Sent to %s%n", intersectionId);

        } catch (SerializationException | IOException e) {
            System.err.println("ERROR: Failed to send vehicle " + vehicle.getId() + " to " + intersectionId);
            System.err.println("Reason: " + e.getMessage());
        }
    }

    /**
     * Encerra graciosamente a simulação, enviando sinais de SHUTDOWN para todos os nós.
     */
    public void shutdown() {
        System.out.println();
        System.out.println("=".repeat(60));
        System.out.println("Shutting down coordinator...");

        for (Map.Entry<String, SocketClient> entry : intersectionClients.entrySet()) {
            String intersectionId = entry.getKey();
            SocketClient client = entry.getValue();

            try {
                if (client.isConnected()) {
                    Message personalizedShutdown = new Message(
                            MessageType.SHUTDOWN,
                            "COORDINATOR",
                            intersectionId,
                            "Simulation complete");
                    client.send(personalizedShutdown);
                    System.out.println("Sent shutdown message to " + intersectionId);
                }
            } catch (SerializationException | IOException e) {
                System.err.println("Error sending shutdown to " + intersectionId + ": " + e.getMessage());
            } finally {
                client.close();
            }
        }

        System.out.println("Coordinator shutdown complete");
        System.out.println("=".repeat(60));
    }

    public void stop() {
        System.out.println("\nStop signal received...");
        running = false;
    }

    /**
     * Altera dinamicamente a política de roteamento durante a simulação (Hot-swap).
     * Thread-safe.
     * * @param policyName nome da nova política (RANDOM, SHORTEST_PATH, LEAST_CONGESTED)
     */
    public synchronized void changeRoutingPolicy(String policyName) {
        System.out.println("\n" + "=".repeat(60));
        System.out.println("ROUTING POLICY CHANGE REQUEST");
        System.out.println("=".repeat(60));
        System.out.println("Current policy: " + getCurrentPolicyName());
        System.out.println("Requested policy: " + policyName);
        
        RouteSelector newSelector = createRouteSelector(policyName);
        this.currentRouteSelector = newSelector;
        this.vehicleGenerator.setRouteSelector(newSelector);
        
        System.out.println("Routing policy successfully changed to: " + policyName);
        System.out.println("  - New vehicles will use the updated policy");
        System.out.println("=".repeat(60) + "\n");
        
        eventLogger.log(sd.logging.EventType.CONFIG_CHANGED, "Coordinator",
                "Routing policy changed to: " + policyName);
    }

    /**
     * Retorna o nome da política de roteamento atual.
     */
    private String getCurrentPolicyName() {
        if (currentRouteSelector instanceof RandomRouteSelector) {
            return "RANDOM";
        } else if (currentRouteSelector instanceof ShortestPathRouteSelector) {
            return "SHORTEST_PATH";
        } else if (currentRouteSelector instanceof LeastCongestedRouteSelector) {
            return "LEAST_CONGESTED";
        }
        return "UNKNOWN";
    }

    /**
     * Verifica se há solicitação de mudança de política proveniente do dashboard
     * e aplica a alteração se houver.
     */
    private void checkForPolicyChanges() {
        if (dashboardStatistics != null) {
            String requestedPolicy = dashboardStatistics.getAndClearRequestedRoutingPolicy();
            if (requestedPolicy != null && !requestedPolicy.isEmpty()) {
                changeRoutingPolicy(requestedPolicy);
            }
        }
    }
    
    /**
     * Injeta a referência para as estatísticas do dashboard.
     * Permite que o coordenador consuma intenções de mudança de política do utilizador.
     */
    public void setDashboardStatistics(DashboardStatistics stats) {
        this.dashboardStatistics = stats;
    }

    private void connectToDashboard() {
        try {
            String host = config.getDashboardHost();
            int port = config.getDashboardPort();

            System.out.println("Connecting to dashboard at " + host + ":" + port);
            dashboardClient = new SocketClient("Dashboard", host, port);
            dashboardClient.connect();
            System.out.println("Successfully connected to dashboard\n");
        } catch (IOException e) {
            System.err.println("WARNING: Failed to connect to dashboard: " + e.getMessage());
            System.err.println("Coordinator will continue without dashboard connection\n");
        }
    }

    private void sendGenerationStatsToDashboard() {
        if (dashboardClient == null || !dashboardClient.isConnected()) {
            return;
        }

        try {
            // Create stats payload with vehicle generation count
            StatsUpdatePayload payload = new StatsUpdatePayload();
            payload.setTotalVehiclesGenerated(vehicleCounter);

            Message message = new Message(
                    MessageType.STATS_UPDATE,
                    "COORDINATOR",
                    "Dashboard",
                    payload);

            dashboardClient.send(message);
        } catch (Exception e) { // This is fine - can add IOException if need be
            // Don't crash if dashboard update fails
            System.err.println("Failed to send stats to dashboard: " + e.getMessage());
        }
    }

    /**
     * Sincronização Global: Envia o timestamp de início (System.currentTimeMillis)
     * para todos os componentes distribuídos, garantindo uma base de tempo comum
     * para métricas de latência.
     */
    private void sendSimulationStartTime() {
        long startTimeMillis = System.currentTimeMillis();

        // Send to all intersections
        for (Map.Entry<String, SocketClient> entry : intersectionClients.entrySet()) {
            try {
                Message message = new Message(
                        MessageType.SIMULATION_START,
                        "COORDINATOR",
                        entry.getKey(),
                        startTimeMillis);
                entry.getValue().send(message);
            } catch (Exception e) { // Same thing here
                System.err.println("Failed to send start time to " + entry.getKey() + ": " + e.getMessage());
            }
        }

        // Send to dashboard
        if (dashboardClient != null && dashboardClient.isConnected()) {
            try {
                Message message = new Message(
                        MessageType.SIMULATION_START,
                        "COORDINATOR",
                        "Dashboard",
                        startTimeMillis);
                dashboardClient.send(message);
            } catch (Exception e) { // And here
                // Don't crash
            }
        }
    }
}