package sd.config;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import com.google.gson.Gson;

/**
 * Responsável pelo carregamento, validação e acesso centralizado às configurações da simulação.
 * <p>
 * Esta classe atua como uma fachada (Facade) para os parâmetros do sistema, abstraindo a origem
 * dos dados (ficheiros {@code .properties} ou JSON). Implementa uma estratégia robusta de
 * carregamento de recursos, suportando tanto caminhos absolutos do sistema de ficheiros quanto
 * recursos embutidos no <i>classpath</i>.
 * <p>
 * Além de propriedades chave-valor simples, gerencia a desserialização da topologia da rede
 * através da classe interna {@link NetworkConfig}.
 */
public class SimulationConfig {

    /** Armazenamento em memória das propriedades chave-valor carregadas. */
    private final Properties properties;
    
    /** Estrutura hierárquica da configuração da rede carregada via JSON. */
    private NetworkConfig networkConfig;

    /**
     * Objeto de transferência de dados (DTO) que representa a configuração global da rede.
     * Mapeado a partir do ficheiro {@code network_config.json}.
     */
    public static class NetworkConfig {
        private List<IntersectionConfig> intersections;

        public List<IntersectionConfig> getIntersections() {
            return intersections;
        }
    }

    /**
     * DTO que representa a configuração de uma única interseção na topologia.
     */
    public static class IntersectionConfig {
        private String id;
        private List<String> lights;
        private Map<String, String> routes;

        /** @return O identificador único da interseção (ex: "Cr1"). */
        public String getId() {
            return id;
        }

        /** @return Lista de identificadores dos semáforos associados a esta interseção. */
        public List<String> getLights() {
            return lights;
        }

        /** @return Mapa de roteamento definindo destinos alcançáveis e seus próximos saltos. */
        public Map<String, String> getRoutes() {
            return routes;
        }
    }

    /**
     * Inicializa o gestor de configuração carregando propriedades do caminho especificado.
     * * <p>Implementa uma estratégia de carregamento em cascata (fallback) para garantir robustez
     * em diferentes ambientes de execução (IDE, JAR, Docker):
     * <ol>
     * <li><b>Sistema de Ficheiros Direto:</b> Tenta carregar do caminho absoluto ou relativo.</li>
     * <li><b>Classpath (Contexto):</b> Tenta carregar via {@code Thread.currentThread().getContextClassLoader()},
     * normalizando prefixos como "src/main/resources" ou "classpath:".</li>
     * <li><b>Classpath (Classe):</b> Tenta carregar via {@code SimulationConfig.class.getResourceAsStream},
     * útil para recursos na raiz do JAR.</li>
     * </ol>
     *
     * @param filePath O caminho ou nome do recurso do ficheiro {@code .properties}.
     * @throws IOException Se o ficheiro não puder ser localizado em nenhuma das estratégias,
     * com uma mensagem detalhada das tentativas falhadas.
     */
    public SimulationConfig(String filePath) throws IOException {
        properties = new Properties();

        // List to track all attempted paths for better error reporting
        List<String> attemptedPaths = new ArrayList<>();
        IOException fileSystemException = null;

        // Strategy 1: Try to load directly from file system
        try (InputStream input = new FileInputStream(filePath)) {
            properties.load(input);
            loadNetworkConfig();
            return; // Successfully loaded from file system
        } catch (IOException e) {
            fileSystemException = e;
            attemptedPaths.add("File system: " + filePath);
        }

        // Strategy 2: Try to load from classpath with path normalization
        String resourcePath = filePath;

        // Remove common src/main/resources prefixes
        resourcePath = resourcePath.replace("src/main/resources/", "").replace("src\\main\\resources\\", "");

        // Remove classpath: prefix if provided
        if (resourcePath.startsWith("classpath:")) {
            resourcePath = resourcePath.substring("classpath:".length());
            if (resourcePath.startsWith("/")) {
                resourcePath = resourcePath.substring(1);
            }
        }

        // Try loading from classpath using thread context class loader
        InputStream resourceStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourcePath);
        attemptedPaths.add("Classpath (context): " + resourcePath);

        if (resourceStream == null) {
            // Strategy 3: Try with leading slash
            String slashPath = "/" + resourcePath;
            resourceStream = SimulationConfig.class.getResourceAsStream(slashPath);
            attemptedPaths.add("Classpath (class): " + slashPath);
        }

        if (resourceStream != null) {
            try (InputStream input = resourceStream) {
                properties.load(input);
                loadNetworkConfig();
                return; // Successfully loaded from classpath
            } catch (IOException e) {
                // Failed to read from classpath resource
                throw new IOException(
                        String.format("Failed to read properties from classpath resource '%s': %s",
                                resourcePath, e.getMessage()),
                        e);
            }
        }

        // All strategies failed - provide comprehensive error message
        StringBuilder errorMsg = new StringBuilder();
        errorMsg.append("Configuration file '").append(filePath).append("' could not be found.\n");
        errorMsg.append("Attempted locations:\n");
        for (String path : attemptedPaths) {
            errorMsg.append("  - ").append(path).append("\n");
        }

        if (fileSystemException != null) {
            errorMsg.append("\nOriginal error: ").append(fileSystemException.getMessage());
        }

        throw new IOException(errorMsg.toString(), fileSystemException);
    }

    /**
     * Carrega a configuração da topologia de rede a partir do ficheiro "network_config.json".
     * <p>
     * Utiliza a biblioteca Gson para desserialização. Em caso de falha, emite um aviso para o
     * {@code System.err} mas não aborta a execução, permitindo o uso de defaults ou redes vazias.
     */
    private void loadNetworkConfig() {
        try (InputStream is = getClass().getClassLoader().getResourceAsStream("network_config.json")) {
            if (is == null) {
                System.err.println("Warning: network_config.json not found in classpath. Using defaults/empty.");
                return;
            }
            try (Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
                Gson gson = new Gson();
                this.networkConfig = gson.fromJson(reader, NetworkConfig.class);
            }
        } catch (IOException e) {
            System.err.println("Failed to load network_config.json: " + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * Retorna a configuração estruturada da rede.
     * @return Objeto {@link NetworkConfig} ou null se o carregamento falhou.
     */
    public NetworkConfig getNetworkConfig() {
        return networkConfig;
    }

    // --- Network configurations ---

    /**
     * Obtém o endereço de host (nome DNS ou IP) para uma interseção específica.
     * * @param intersectionId O ID da interseção (ex: "Cr1").
     * @return O host configurado ou "localhost" por omissão.
     */
    public String getIntersectionHost(String intersectionId) {
        return properties.getProperty("intersection." + intersectionId + ".host", "localhost");
    }

    /**
     * Obtém a porta de escuta TCP para uma interseção específica.
     * * @param intersectionId O ID da interseção (ex: "Cr1").
     * @return O número da porta. Retorna 0 se não configurado.
     */
    public int getIntersectionPort(String intersectionId) {
        return Integer.parseInt(properties.getProperty("intersection." + intersectionId + ".port", "0"));
    }

    /**
     * Obtém o endereço de host do servidor de Dashboard (monitorização).
     * @return O host do dashboard (padrão: "localhost").
     */
    public String getDashboardHost() {
        return properties.getProperty("dashboard.host", "localhost");
    }

    /**
     * Obtém a porta de conexão do servidor de Dashboard.
     * @return A porta do dashboard (padrão: 9000).
     */
    public int getDashboardPort() {
        return Integer.parseInt(properties.getProperty("dashboard.port", "9000"));
    }

    /**
     * Obtém o endereço de host do nó de saída (Exit Node), para onde os veículos são encaminhados ao sair da malha.
     * @return O host do nó de saída (padrão: "localhost").
     */
    public String getExitHost() {
        return properties.getProperty("exit.host", "localhost");
    }

    /**
     * Obtém a porta de conexão do nó de saída.
     * @return A porta do nó de saída (padrão: 9001).
     */
    public int getExitPort() {
        return Integer.parseInt(properties.getProperty("exit.port", "9001"));
    }

    // --- Simulation configurations ---

    /**
     * Define a duração total da execução da simulação em segundos virtuais.
     * @return A duração em segundos (padrão: 3600).
     */
    public double getSimulationDuration() {
        return Double.parseDouble(properties.getProperty("simulation.duration", "3600"));
    }

    /**
     * Obtém o fator de escala temporal para visualização/execução.
     * <ul>
     * <li>0.0: Execução instantânea (DES puro, velocidade máxima).</li>
     * <li>1.0: Tempo real (1 segundo simulado = 1 segundo real).</li>
     * <li>0.01: Acelerado 100x.</li>
     * </ul>
     * @return O fator de escala.
     */
    public double getTimeScale() {
        return Double.parseDouble(properties.getProperty("simulation.time.scale", "0"));
    }

    /**
     * Obtém o tempo de "drenagem" (drain time) em segundos virtuais.
     * <p>
     * Este é o período adicional executado após o fim da geração de veículos para permitir
     * que os veículos restantes no sistema completem os seus percursos.
     * @return O tempo de drenagem (padrão: 60.0s).
     */
    public double getDrainTime() {
        return Double.parseDouble(properties.getProperty("simulation.drain.time", "60.0"));
    }

    /**
     * Determina o modelo estocástico utilizado para a chegada de veículos.
     * @return "POISSON" (distribuição exponencial) ou "FIXED" (intervalo determinístico).
     */
    public String getArrivalModel() {
        return properties.getProperty("simulation.arrival.model", "POISSON");
    }

    /**
     * Obtém a taxa média de chegada (lambda) para o modelo Poisson.
     * @return Veículos por segundo (padrão: 0.5).
     */
    public double getArrivalRate() {
        return Double.parseDouble(properties.getProperty("simulation.arrival.rate", "0.5"));
    }

    /**
     * Obtém o intervalo fixo entre chegadas para o modelo determinístico.
     * @return O intervalo em segundos (padrão: 2.0).
     */
    public double getFixedArrivalInterval() {
        return Double.parseDouble(properties.getProperty("simulation.arrival.fixed.interval", "2.0"));
    }

    /**
     * Obtém a política de roteamento utilizada pelos veículos para navegar na malha.
     * @return A política: "RANDOM", "SHORTEST_PATH" ou "LEAST_CONGESTED".
     */
    public String getRoutingPolicy() {
        return properties.getProperty("simulation.routing.policy", "RANDOM");
    }

    // --- Traffic light configurations ---

    /**
     * Obtém a duração do estado VERDE para um semáforo específico.
     * * @param intersectionId ID da interseção.
     * @param direction Direção do fluxo (ex: "North").
     * @return Duração em segundos (padrão: 30.0).
     */
    public double getTrafficLightGreenTime(String intersectionId, String direction) {
        String key = "trafficlight." + intersectionId + "." + direction + ".green";
        return Double.parseDouble(properties.getProperty(key, "30.0"));
    }

    /**
     * Obtém a duração do estado VERMELHO para um semáforo específico.
     * * @param intersectionId ID da interseção.
     * @param direction Direção do fluxo.
     * @return Duração em segundos (padrão: 30.0).
     */
    public double getTrafficLightRedTime(String intersectionId, String direction) {
        String key = "trafficlight." + intersectionId + "." + direction + ".red";
        return Double.parseDouble(properties.getProperty(key, "30.0"));
    }

    // --- Vehicle configurations ---

    /**
     * Probabilidade (0.0 a 1.0) de geração de um veículo do tipo LIGEIRO (LIGHT).
     * @return Probabilidade (padrão: 0.7).
     */
    public double getLightVehicleProbability() {
        return Double.parseDouble(properties.getProperty("vehicle.probability.light", "0.7"));
    }

    /**
     * Tempo médio necessário para um veículo LIGEIRO atravessar uma interseção.
     * @return Tempo em segundos (padrão: 2.0).
     */
    public double getLightVehicleCrossingTime() {
        return Double.parseDouble(properties.getProperty("vehicle.crossing.time.light", "2.0"));
    }

    /**
     * Probabilidade (0.0 a 1.0) de geração de um veículo do tipo BICICLETA (BIKE).
     * @return Probabilidade (padrão: 0.0).
     */
    public double getBikeVehicleProbability() {
        return Double.parseDouble(properties.getProperty("vehicle.probability.bike", "0.0"));
    }

    /**
     * Tempo médio necessário para uma BICICLETA atravessar uma interseção.
     * @return Tempo em segundos (padrão: 1.5).
     */
    public double getBikeVehicleCrossingTime() {
        return Double.parseDouble(properties.getProperty("vehicle.crossing.time.bike", "1.5"));
    }

    /**
     * Probabilidade (0.0 a 1.0) de geração de um veículo PESADO (HEAVY).
     * @return Probabilidade (padrão: 0.0).
     */
    public double getHeavyVehicleProbability() {
        return Double.parseDouble(properties.getProperty("vehicle.probability.heavy", "0.0"));
    }

    /**
     * Tempo médio necessário para um veículo PESADO atravessar uma interseção.
     * @return Tempo em segundos (padrão: 4.0).
     */
    public double getHeavyVehicleCrossingTime() {
        return Double.parseDouble(properties.getProperty("vehicle.crossing.time.heavy", "4.0"));
    }

    /**
     * Define o tempo base de viagem entre interseções para veículos padrão.
     * @return Tempo em segundos (padrão: 8.0).
     */
    public double getBaseTravelTime() {
        return Double.parseDouble(properties.getProperty("vehicle.travel.time.base", "8.0"));
    }

    /**
     * Multiplicador de tempo de viagem para bicicletas.
     * <p>Tempo efetivo = Base * Multiplicador.
     * @return Fator multiplicativo (padrão: 0.5).
     */
    public double getBikeTravelTimeMultiplier() {
        return Double.parseDouble(properties.getProperty("vehicle.travel.time.bike.multiplier", "0.5"));
    }

    /**
     * Multiplicador de tempo de viagem para veículos pesados.
     * <p>Tempo efetivo = Base * Multiplicador.
     * @return Fator multiplicativo (padrão: 4.0).
     */
    public double getHeavyTravelTimeMultiplier() {
        return Double.parseDouble(properties.getProperty("vehicle.travel.time.heavy.multiplier", "4.0"));
    }

    // --- Statistics ---

    /**
     * Intervalo de tempo (em segundos virtuais) para agregação e envio de estatísticas periódicas.
     * @return Intervalo de atualização (padrão: 1.0).
     */
    public double getStatisticsUpdateInterval() {
        return Double.parseDouble(properties.getProperty("statistics.update.interval", "1.0"));
    }

    // --- Generic getters ---

    /**
     * Recupera uma propriedade genérica como String, com valor padrão de segurança.
     * * @param key A chave da propriedade.
     * @param defaultValue O valor a retornar caso a chave não exista.
     * @return O valor da propriedade ou o default.
     */
    public String getProperty(String key, String defaultValue) {
        return properties.getProperty(key, defaultValue);
    }

    /**
     * Recupera uma propriedade genérica como String.
     * * @param key A chave da propriedade.
     * @return O valor da propriedade ou null se não encontrada.
     */
    public String getProperty(String key) {
        return properties.getProperty(key);
    }
}