package sd.logging;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Motor de logging assíncrono e thread-safe para a simulação distribuída.
 * <p>
 * Implementa o padrão <i>Singleton</i> para garantir um ponto centralizado de registo.
 * Utiliza o padrão <i>Producer-Consumer</i> com uma {@link BlockingQueue} para desacoplar
 * a geração de eventos (crítica para a performance da simulação) da persistência em disco
 * (operação de I/O lenta).
 * <p>
 * <b>Garantias:</b>
 * <ul>
 * <li>Non-blocking writes (para a thread chamadora, na maioria dos casos).</li>
 * <li>Ordering cronológico aproximado (FIFO na fila).</li>
 * <li>Graceful Shutdown (flush de logs pendentes ao terminar).</li>
 * </ul>
 */
public class EventLogger {
    
    private static EventLogger instance;
    private static final Object instanceLock = new Object();
    
    private final PrintWriter writer;
    
    /** Buffer de memória para absorver picos de eventos (Burst traffic). */
    private final BlockingQueue<LogEntry> logQueue;
    
    /** Thread dedicada (Consumer) para escrita em ficheiro. */
    private final Thread writerThread;
    
    private final AtomicBoolean running;
    private final SimpleDateFormat timestampFormat;
    private final long simulationStartMillis;
    
    /**
     * Inicializa o sistema de logs.
     * Abre o ficheiro, escreve o cabeçalho e inicia a thread consumidora.
     *
     * @param logFilePath Caminho relativo ou absoluto do ficheiro de log.
     * @throws IOException Se não for possível criar ou escrever no ficheiro.
     */
    private EventLogger(String logFilePath) throws IOException {
        // Auto-flush ativado para garantir persistência, mas gerido pelo buffer do BufferedWriter
        this.writer = new PrintWriter(new BufferedWriter(new FileWriter(logFilePath, false)), true);
        this.logQueue = new LinkedBlockingQueue<>(10000); // Backpressure: limita a 10k eventos pendentes
        this.running = new AtomicBoolean(true);
        this.timestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        this.simulationStartMillis = System.currentTimeMillis();
        
        // Header inicial do log
        writer.println("=".repeat(80));
        writer.println("SIMULATION EVENT LOG");
        writer.println("Started: " + timestampFormat.format(new Date()));
        writer.println("=".repeat(80));
        writer.println();
        writer.printf("%-23s | %-8s | %-20s | %-15s | %s\n", 
            "TIMESTAMP", "REL_TIME", "EVENT_TYPE", "COMPONENT", "DESCRIPTION");
        writer.println("-".repeat(80));
        writer.flush();
        
        this.writerThread = new Thread(this::processLogQueue, "EventLogger-Writer");
        this.writerThread.setDaemon(true); // Permite que a JVM termine se apenas esta thread sobrar
        this.writerThread.start();
    }
    
    /**
     * Obtém a instância única do logger (Lazy Initialization).
     * Se não existir, cria uma predefinida em "logs/simulation-events.log".
     *
     * @return A instância singleton.
     */
    public static EventLogger getInstance() {
        if (instance == null) {
            synchronized (instanceLock) {
                if (instance == null) {
                    try {
                        String logFile = "logs/simulation-events.log";
                        java.nio.file.Files.createDirectories(
                            java.nio.file.Paths.get("logs"));
                        instance = new EventLogger(logFile);
                    } catch (IOException e) {
                        System.err.println("Failed to initialize EventLogger: " + e.getMessage());
                        e.printStackTrace();
                    }
                }
            }
        }
        return instance;
    }
    
    /**
     * Reinicializa o logger com um ficheiro específico.
     * Útil para testes ou configurações personalizadas.
     */
    public static void initialize(String logFilePath) throws IOException {
        synchronized (instanceLock) {
            if (instance != null) {
                instance.shutdown();
            }
            instance = new EventLogger(logFilePath);
        }
    }
    
    /**
     * Regista um evento genérico.
     * Esta operação é não-bloqueante (retorna imediatamente após colocar na fila),
     * exceto se a fila estiver cheia (backpressure).
     *
     * @param eventType Categoria do evento.
     * @param component Nome do componente (ex: "Coordinator", "IntersectionProcess").
     * @param description Detalhes do evento.
     */
    public void log(EventType eventType, String component, String description) {
        if (!running.get()) return;
        
        LogEntry entry = new LogEntry(
            System.currentTimeMillis(),
            eventType,
            component,
            description
        );
        
        // Non-blocking offer - if queue is full, drop oldest or warn
        if (!logQueue.offer(entry)) {
            // Queue full - this shouldn't happen with 10k buffer, but handle gracefully
            System.err.println("EventLogger queue full - dropping event: " + eventType);
        }
    }
    
    /**
     * Regista um evento associado a um veículo específico (Helper method).
     */
    public void logVehicle(EventType eventType, String component, String vehicleId, String description) {
        log(eventType, component, "[" + vehicleId + "] " + description);
    }
    
    /**
     * Regista um erro ou exceção com formatação apropriada.
     */
    public void logError(String component, String description, Exception e) {
        String fullDescription = description + (e != null ? ": " + e.getMessage() : "");
        log(EventType.ERROR, component, fullDescription);
    }
    
    /**
     * Lógica da thread consumidora (Worker Thread).
     * Retira eventos da fila e escreve no disco continuamente.
     */
    private void processLogQueue() {
        while (running.get() || !logQueue.isEmpty()) {
            try {
                // Poll com timeout para permitir verificar a flag 'running' periodicamente
                LogEntry entry = logQueue.poll(100, java.util.concurrent.TimeUnit.MILLISECONDS);
                if (entry != null) {
                    writeEntry(entry);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        
        // Flush final: garantir que eventos restantes na fila são escritos antes de morrer
        while (!logQueue.isEmpty()) {
            LogEntry entry = logQueue.poll();
            if (entry != null) {
                writeEntry(entry);
            }
        }
    }
    
    /**
     * Formata e escreve uma entrada de log no PrintWriter.
     */
    private void writeEntry(LogEntry entry) {
        String timestamp = timestampFormat.format(new Date(entry.timestampMillis));
        double relativeTime = (entry.timestampMillis - simulationStartMillis) / 1000.0;
        
        writer.printf("%-23s | %8.3fs | %-20s | %-15s | %s\n",
            timestamp,
            relativeTime,
            entry.eventType.toString(),
            truncate(entry.component, 15),
            entry.description
        );
        
        // Flush periódico inteligente: se a carga for baixa, garante que vemos logs em tempo real
        if (logQueue.size() < 10) {
            writer.flush();
        }
    }
    
    private String truncate(String str, int maxLength) {
        if (str == null) return "";
        return str.length() <= maxLength ? str : str.substring(0, maxLength);
    }
    
    /**
     * Encerra o logger de forma segura.
     * Desativa a aceitação de novos eventos, aguarda que a fila esvazie (flush)
     * e fecha o ficheiro.
     */
    public void shutdown() {
        if (!running.compareAndSet(true, false)) {
            return;  // Já encerrado
        }
        
        try {
            // Wait for writer thread to finish flushing
            writerThread.join(5000);  // Wait up to 5 seconds
            
            // Write footer
            writer.println();
            writer.println("-".repeat(80));
            writer.println("SIMULATION ENDED");
            writer.println("Ended: " + timestampFormat.format(new Date()));
            writer.println("=".repeat(80));
            
            writer.close();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    /**
     * DTO interno imutável para armazenar dados do evento na fila.
     */
    private static class LogEntry {
        final long timestampMillis;
        final EventType eventType;
        final String component;
        final String description;
        
        LogEntry(long timestampMillis, EventType eventType, String component, String description) {
            this.timestampMillis = timestampMillis;
            this.eventType = eventType;
            this.component = component;
            this.description = description;
        }
    }
}