package sd;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import sd.config.SimulationConfig;
import sd.coordinator.SocketClient;
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.logging.EventType;
import sd.logging.VehicleTracer;
import sd.model.Message;
import sd.model.MessageType;
import sd.model.Vehicle;
import sd.model.VehicleType;
import sd.protocol.MessageProtocol;
import sd.protocol.SocketConnection;

/**
 * Ponto terminal da malha de simulação (Sink Node).
 * <p>
 * Este processo atua como o sumidouro da rede de filas. A sua função primária é
 * a <b>coleta de telemetria final</b>. Diferente das interseções, não encaminha veículos;
 * em vez disso, retira-os do sistema, calcula as métricas de latência "end-to-end"
 * (tempo no sistema, tempo de espera acumulado) e reporta ao Dashboard.
 * <p>
 * <b>Arquitetura de Concorrência:</b>
 * Utiliza um {@link ServerSocket} multithreaded para aceitar conexões simultâneas de
 * qualquer interseção de fronteira (Cr1, Cr5, etc.) que envie veículos para fora da malha.
 */
public class ExitNodeProcess {

    // --- Configuration and Networking ---
    private final SimulationConfig config;
    private ServerSocket serverSocket;
    
    /** Pool de threads elástica para tratamento de conexões de entrada. */
    private final ExecutorService connectionHandlerPool;

    // DES components
    private final SimulationClock clock;
    private final EventQueue eventQueue;
    private final EventLogger eventLogger;
    private Thread eventProcessorThread;

    /** Flag de controlo (volatile para visibilidade entre threads de I/O e lógica). */
    private volatile boolean running;

    /** Instante de início da simulação (milissegundos) sincronizado com o Coordenador. */
    private long simulationStartMillis;

    /** Contador atómico (via synchronized) de throughput total. */
    private int totalVehiclesReceived;

    /** Tempo acumulado no sistema (System Time) de todos os veículos. */
    private double totalSystemTime;

    /** Tempo acumulado em espera (Waiting Time) de todos os veículos. */
    private double totalWaitingTime;

    /** Tempo acumulado em travessia (Service Time) de todos os veículos. */
    private double totalCrossingTime;

    /** Agregação por categoria de veículo. */
    private final Map<VehicleType, Integer> vehicleTypeCount;

    /** Latência acumulada por categoria. */
    private final Map<VehicleType, Double> vehicleTypeWaitTime;

    /** Cliente TCP persistente para push de métricas ao Dashboard. */
    private SocketClient dashboardClient;

    /**
     * Bootstrap do processo ExitNode.
     * Carrega configuração, inicializa subsistemas e entra no loop de serviço.
     * * @param args Argumentos de CLI (caminho do config).
     */
    public static void main(String[] args) {
        System.out.println("=".repeat(60));
        System.out.println("EXIT NODE PROCESS");
        System.out.println("=".repeat(60));

        try {
            EventLogger.getInstance().log(EventType.PROCESS_STARTED, "ExitNode", "Exit node process started");

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

            SimulationConfig config = new SimulationConfig(configFile);
            ExitNodeProcess exitNode = new ExitNodeProcess(config);

            System.out.println("\n" + "=".repeat(60));
            exitNode.initialize();

            System.out.println("\n" + "=".repeat(60));
            exitNode.start();

        } catch (IOException e) {
            System.err.println("Failed to start exit node: " + e.getMessage());
            EventLogger.getInstance().logError("ExitNode", "Failed to start", e);
            System.exit(1);
        } catch (Exception e) {
            System.err.println("Exit node error: " + e.getMessage());
            EventLogger.getInstance().logError("ExitNode", "Exit node error", e);
            System.exit(1);
        } finally {
            EventLogger.getInstance().log(EventType.PROCESS_STOPPED, "ExitNode", "Exit node process stopped");
        }
    }

    /**
     * Instancia o nó de saída.
     * Prepara os acumuladores estatísticos e a infraestrutura de logging distribuído.
     * * @param config A configuração global da simulação.
     */
    public ExitNodeProcess(SimulationConfig config) {
        this.config = config;
        this.connectionHandlerPool = Executors.newCachedThreadPool();
        this.running = false;

        this.totalVehiclesReceived = 0;
        this.totalSystemTime = 0.0;
        this.totalWaitingTime = 0.0;
        this.totalCrossingTime = 0.0;
        this.vehicleTypeCount = new HashMap<>();
        this.vehicleTypeWaitTime = new HashMap<>();

        // Inicializa os counters para cada tipo de veículo
        for (VehicleType type : VehicleType.values()) {
            vehicleTypeCount.put(type, 0);
            vehicleTypeWaitTime.put(type, 0.0);
        }

        // Initialize DES components
        this.clock = new SimulationClock();
        this.eventQueue = new EventQueue(true); // Track history
        this.eventLogger = EventLogger.getInstance();

        eventLogger.log(EventType.PROCESS_STARTED, "ExitNode",
                "Exit node initialized with DES architecture");

        System.out.println("Exit node initialized (DES Mode)");
        System.out.println("  - Exit port: " + config.getExitPort());
        System.out.println("  - Dashboard: " + config.getDashboardHost() + ":" + config.getDashboardPort());
    }

    /**
     * Estabelece o canal de controlo (Control Plane) com o Dashboard.
     * Essencial para a visualização em tempo real das métricas de saída.
     */
    public void initialize() {
        System.out.println("Connecting to dashboard...");

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

            dashboardClient = new SocketClient("Dashboard", host, port);
            dashboardClient.connect();

            System.out.println("Successfully connected to dashboard");
        } catch (IOException e) {
            System.err.println("WARNING: Failed to connect to dashboard: " + e.getMessage());
            System.err.println("Exit node will continue without dashboard connection");
        }
    }

    /**
     * Inicia a thread de processamento de eventos DES.
     * Embora o ExitNode seja primariamente reativo (Network-driven), o motor DES
     * é mantido para consistência de relógio e agendamento de fim de simulação.
     */
    private void startEventProcessor() {
        eventProcessorThread = new Thread(() -> {
            eventLogger.log(EventType.SIMULATION_STARTED, "ExitNode",
                    "Event processor thread started");

            // Keep running while process is active
            while (running) {
                SimulationEvent event = eventQueue.poll();
                if (event == null) {
                    // No events currently, wait before checking again
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                    continue;
                }

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

                // Process the event
                processEvent(event);
            }

            eventLogger.log(EventType.SIMULATION_STOPPED, "ExitNode",
                    String.format("Event processor thread terminated at time %.2f", clock.getCurrentTime()));
        }, "EventProcessor-ExitNode");

        eventProcessorThread.start();
    }

    /**
     * Dispatcher de eventos discretos.
     * Trata eventos de fim de simulação. Chegadas de veículos são tratadas via Socket.
     */
    private void processEvent(SimulationEvent event) {
        try {
            switch (event.getType()) {
                case VEHICLE_EXIT:
                    // Vehicle exits are handled via network messages in real-time
                    // This event type can be used for scheduled vehicle processing
                    break;

                case SIMULATION_END:
                    handleSimulationEndEvent(event);
                    break;

                default:
                    System.err.println("[ExitNode] Unknown event type: " + event.getType());
            }
        } catch (Exception e) {
            System.err.println("[ExitNode] Error processing event " + event.getType() +
                    " at time " + event.getTimestamp() + ": " + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * Executa a lógica de encerramento desencadeada pelo evento DES.
     */
    private void handleSimulationEndEvent(SimulationEvent event) {
        eventLogger.log(EventType.SIMULATION_STOPPED, "ExitNode",
                String.format("Simulation ended at time %.2f", event.getTimestamp()));
        running = false;

        // Print final statistics
        printFinalStatistics();
    }

    /**
     * Exporta o histórico completo de eventos para auditoria.
     * Requisito funcional para verificação de trace.
     */
    public void exportEventHistory(String outputPath) {
        String history = eventQueue.exportEventHistory();
        try (java.io.PrintWriter writer = new java.io.PrintWriter(outputPath)) {
            writer.println(history);
            System.out.println("[ExitNode] Event history exported to: " + outputPath);
        } catch (java.io.FileNotFoundException e) {
            System.err.println("[ExitNode] Failed to export event history: " + e.getMessage());
        }
    }

    /**
     * Agenda o fim determinístico da simulação.
     * * @param endTime Tempo virtual de paragem.
     */
    public void scheduleSimulationEnd(double endTime) {
        SimulationEvent endEvent = new SimulationEvent(
                endTime,
                DESEventType.SIMULATION_END,
                null);
        eventQueue.schedule(endEvent);
        System.out.println("[ExitNode] Simulation end scheduled at time " + endTime);
    }

    /**
     * Inicia o servidor TCP em modo de bloqueio (Blocking I/O).
     * @throws IOException Se ocorrer erro no bind da porta.
     */
    public void start() throws IOException {
        start(true); // Default to DES mode
    }

    /**
     * Inicia o processo com opção de ativar o rastreio DES.
     * * @param useDES Se verdadeiro, ativa a thread do processador de eventos.
     */
    public void start(boolean useDES) throws IOException {
        int port = config.getExitPort();
        serverSocket = new ServerSocket(port);
        running = true;
        simulationStartMillis = System.currentTimeMillis();

        System.out.println("Exit node started on port " + port);
        if (useDES) {
            System.out.println("Running in DES mode (event history tracking enabled)");
        }
        System.out.println("Waiting for vehicles...\\n");

        // Loop de aceitação principal
        while (running) {
            try {
                Socket clientSocket = serverSocket.accept();
                // Delega o processamento da conexão para o Thread Pool
                connectionHandlerPool.submit(() -> handleIncomingConnection(clientSocket));
            } catch (IOException e) {
                if (running) {
                    System.err.println("Error accepting connection: " + e.getMessage());
                }
            }
        }
    }

    /**
     * Worker method para tratar uma conexão persistente vinda de uma interseção.
     * <p>
     * Mantém o socket aberto e consome mensagens num loop até que a conexão seja fechada
     * pelo remetente. Responsável pela desserialização polimórfica (JSON/Gson).
     * * @param clientSocket O socket conectado.
     */
    private void handleIncomingConnection(Socket clientSocket) {
        String clientAddress = clientSocket.getInetAddress().getHostAddress();
        System.out.println("New connection accepted from " + clientAddress);

        try (SocketConnection connection = new SocketConnection(clientSocket)) {

            while (running && connection.isConnected()) {
                try {
                    System.out.println("[Exit] Waiting for message from " + clientAddress);
                    MessageProtocol message = connection.receiveMessage();
                    System.out.println("[Exit] Received message type: " + message.getType() +
                            " from " + message.getSourceNode());

                    if (message.getType() == MessageType.SIMULATION_START) {
                        // Sincronização de relógio com o Coordenador
                        simulationStartMillis = ((Number) message.getPayload()).longValue();
                        System.out.println("[Exit] Simulation start time synchronized");
                    } else if (message.getType() == MessageType.VEHICLE_TRANSFER) {
                        Object payload = message.getPayload();
                        System.out.println("[Exit] Payload type: " + payload.getClass().getName());

                        // Tratamento de artefatos de desserialização do Gson (LinkedTreeMap -> POJO)
                        Vehicle vehicle;
                        if (payload instanceof com.google.gson.internal.LinkedTreeMap ||
                                payload instanceof java.util.LinkedHashMap) {
                            String json = new com.google.gson.Gson().toJson(payload);
                            vehicle = new com.google.gson.Gson().fromJson(json, Vehicle.class);
                        } else {
                            vehicle = (Vehicle) payload;
                        }

                        processExitingVehicle(vehicle);
                    }

                } catch (ClassNotFoundException e) {
                    System.err.println("[Exit] Unknown message type: " + e.getMessage());
                    e.printStackTrace();
                } catch (Exception e) {
                    System.err.println("[Exit] Error processing message: " + e.getMessage());
                    e.printStackTrace();
                }
            }

            System.out.println("[Exit] Connection closed from " + clientAddress);

        } catch (IOException e) {
            if (running) {
                System.err.println("[Exit] Connection error from " + clientAddress + ": " + e.getMessage());
                e.printStackTrace();
            }
        }
    }

    /**
     * Processa atomicamente a saída de um veículo.
     * <p>
     * <b>Secção Crítica:</b> Método {@code synchronized} para garantir que a atualização
     * das estatísticas globais (totalSystemTime, contadores) é atómica, prevenindo
     * Race Conditions quando múltiplos veículos chegam simultaneamente de interseções diferentes.
     * * @param vehicle O veículo que completou a rota.
     */
    private synchronized void processExitingVehicle(Vehicle vehicle) {
        totalVehiclesReceived++;

        // Cálculo de métricas finais baseadas no tempo virtual de simulação acumulado no veículo
        double waitTime = vehicle.getTotalWaitingTime();
        double crossingTime = vehicle.getTotalCrossingTime();
        double systemTime = waitTime + crossingTime;

        totalSystemTime += systemTime;
        totalWaitingTime += waitTime;
        totalCrossingTime += crossingTime;

        VehicleType type = vehicle.getType();
        vehicleTypeCount.put(type, vehicleTypeCount.get(type) + 1);
        vehicleTypeWaitTime.put(type, vehicleTypeWaitTime.get(type) + waitTime);

        System.out.printf("[Exit] Vehicle %s completed (type=%s, system_time=%.2fs, wait=%.2fs, crossing=%.2fs)%n",
                vehicle.getId(), vehicle.getType(), systemTime, waitTime, crossingTime);

        // Logging estruturado
        EventLogger.getInstance().logVehicle(EventType.VEHICLE_EXITED, "ExitNode", vehicle.getId(),
                String.format("Completed - System: %.2fs, Wait: %.2fs, Crossing: %.2fs", systemTime, waitTime,
                        crossingTime));

        // Finaliza o trace individual do veículo
        VehicleTracer.getInstance().logExit(vehicle, systemTime);

        // Push imediato para o Dashboard para visualização em tempo real
        sendStatsToDashboard();
    }

    /**
     * Constrói e transmite o DTO de atualização de estatísticas.
     */
    private void sendStatsToDashboard() {
        if (dashboardClient == null || !dashboardClient.isConnected()) {
            return;
        }

        try {
            // Create stats payload
            StatsUpdatePayload payload = new StatsUpdatePayload();

            // Set global stats - convert seconds to milliseconds for display consistency
            payload.setTotalVehiclesCompleted(totalVehiclesReceived);
            payload.setTotalSystemTime((long) (totalSystemTime * 1000.0));
            payload.setTotalWaitingTime((long) (totalWaitingTime * 1000.0));

            // Hack: Usar campos de interseção para mostrar throughput no dashboard
            payload.setIntersectionArrivals(totalVehiclesReceived);
            payload.setIntersectionDepartures(totalVehiclesReceived);
            payload.setIntersectionQueueSize(0);

            // Detailed breakdown
            Map<VehicleType, Integer> typeCounts = new HashMap<>();
            Map<VehicleType, Long> typeWaitTimes = new HashMap<>();

            for (VehicleType type : VehicleType.values()) {
                typeCounts.put(type, vehicleTypeCount.get(type));
                typeWaitTimes.put(type, (long) (vehicleTypeWaitTime.get(type) * 1000.0));
            }

            payload.setVehicleTypeCounts(typeCounts);
            payload.setVehicleTypeWaitTimes(typeWaitTimes);

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

            dashboardClient.send(message);

            double avgWait = totalVehiclesReceived > 0 ? totalWaitingTime / totalVehiclesReceived : 0.0;
            System.out.printf("[Exit] Sent stats to dashboard (total=%d, avg_wait=%.2fs)%n",
                    totalVehiclesReceived, avgWait);

        } catch (Exception e) {
            System.err.println("[Exit] Failed to send stats to dashboard: " + e.getMessage());
        }
    }

    /**
     * Encerramento gracioso do processo.
     * Fecha sockets, termina a pool de threads e liberta recursos.
     */
    public void shutdown() {
        System.out.println("\n[Exit] Shutting down...");
        running = false;

        printFinalStatistics();

        sendStatsToDashboard();

        try {
            if (serverSocket != null && !serverSocket.isClosed()) {
                serverSocket.close();
            }
        } catch (IOException e) {
            System.err.println("Error closing server socket: " + e.getMessage());
        }

        connectionHandlerPool.shutdown();
        try {
            if (!connectionHandlerPool.awaitTermination(5, TimeUnit.SECONDS)) {
                connectionHandlerPool.shutdownNow();
            }
        } catch (InterruptedException e) {
            connectionHandlerPool.shutdownNow();
        }

        if (dashboardClient != null) {
            dashboardClient.close();
        }

        System.out.println("[Exit] Shutdown complete.");
        System.out.println("=".repeat(60));
    }

    /**
     * Imprime o relatório final no stdout.
     */
    private void printFinalStatistics() {
        System.out.println("\n=== EXIT NODE STATISTICS ===");
        System.out.printf("Total Vehicles Completed: %d%n", totalVehiclesReceived);

        if (totalVehiclesReceived > 0) {
            System.out.printf("%nAVERAGE METRICS:%n");
            System.out.printf("  System Time: %.2f seconds%n", totalSystemTime / totalVehiclesReceived);
            System.out.printf("  Waiting Time: %.2f seconds%n", totalWaitingTime / totalVehiclesReceived);
            System.out.printf("  Crossing Time: %.2f seconds%n", totalCrossingTime / totalVehiclesReceived);
        }

        System.out.println("\nVEHICLE TYPE DISTRIBUTION:");
        for (VehicleType type : VehicleType.values()) {
            int count = vehicleTypeCount.get(type);
            if (count > 0) {
                double percentage = (count * 100.0) / totalVehiclesReceived;
                double avgWait = vehicleTypeWaitTime.get(type) / count;
                System.out.printf("  %s: %d (%.1f%%), Avg Wait: %.2fs%n",
                        type, count, percentage, avgWait);
            }
        }
    }

}