package com.r35157.jupiterperpsalarm.impl.ref;

import com.r35157.cryptowallet.solana.SolanaWallet;
import com.r35157.jupiterperpsalarm.JupiterPerpsAlarm;
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
import com.r35157.nenjim.component.NenjimApplication;
import org.jetbrains.annotations.NotNull;

import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;

public final class JupiterPerpsAlarmImpl implements
        JupiterPerpsAlarm,
        NenjimApplication {

    public JupiterPerpsAlarmImpl(
            @NotNull SolanaWallet solanaWallet,
            @NotNull JupiterPerpsService jupiterServicePerps,
            @NotNull String configFilename
    ) {
        this.solanaWallet = Objects.requireNonNull(
                solanaWallet,
                "solanaWallet"
        );

        this.jupiterServicePerps = Objects.requireNonNull(
                jupiterServicePerps,
                "jupiterServicePerps"
        );

        this.configFilename = Objects.requireNonNull(
                configFilename,
                "configFilename"
        );

        this.thread = new Thread(() -> {
            try {
                run();
            } catch (Throwable throwable) {
                throwable.printStackTrace(System.err);
            }
        }, "Nenjim Component - JupiterPerpsAlarm");
        this.thread.setDaemon(false);
    }

    @Override
    public void start() {
        thread.start();
    }

    private void run() throws Exception {
        Config config;
        System.out.println("Starting Jupiter Perps Alarms...");
        try {
            System.out.print("Parsing commandline parameters... ");
            config = Config.parse(new String[] { configFilename }, System.getenv());
            System.out.println("Done.");
        } catch (IllegalArgumentException exception) {
            printUsage();
            String errMsg = "Could not parse configuration for JupiterPerpsAlarm: " + exception.getMessage() + "!";
            throw new IllegalStateException(errMsg, exception);
        }

        List<PriceAlarmDefinition> definitions;
        AlarmConfiguration alarmConfiguration;

        try {
            System.out.print("Parsing alarm configuration... ");
            alarmConfiguration = AlarmConfigurationParser.parse(config.alarmConfiguration());
            System.out.println("Done.");
            definitions = alarmConfiguration.definitions();
        } catch (Exception exception) {
            String errMsg = "Could not load alarm configuration: " + exception.getMessage() + "!";
            throw new IllegalStateException(errMsg, exception);
        }

        AlarmVariableResolver variableResolver = new AlarmVariableResolver(
                alarmConfiguration.variables()
        );

        Path alarmConfigurationDirectory = config.alarmConfiguration()
                .toAbsolutePath()
                .getParent();

        System.out.print("Initializing dependencies... ");

        JupiterPerpsPositionIncreaseAlarmActionConfiguration
                positionIncreaseConfiguration =
                JupiterPerpsPositionIncreaseAlarmActionConfigurationParser
                        .parse(
                                alarmConfigurationDirectory.resolve(
                                        JupiterPerpsPositionIncreaseAlarmAction
                                                .CONFIGURATION_FILE_NAME
                                ),
                                variableResolver
                        );

        JupiterPerpsPositionDecreaseAlarmActionConfiguration
                positionDecreaseConfiguration =
                JupiterPerpsPositionDecreaseAlarmActionConfigurationParser
                        .parse(
                                alarmConfigurationDirectory.resolve(
                                        JupiterPerpsPositionDecreaseAlarmAction
                                                .CONFIGURATION_FILE_NAME
                                ),
                                variableResolver
                        );

        JupiterPerpsEntryPriceVariableRefresher entryPriceVariableRefresher =
                new JupiterPerpsEntryPriceVariableRefresher(
                        alarmConfiguration.variables(),
                        jupiterServicePerps,
                        solanaWallet.getAddress()
                );
        System.out.println("Done.");

        System.out.println("Executing first pre-run data fetch:");
        entryPriceVariableRefresher.refresh();
        System.out.println("Fetching done.");

        System.out.print("Validating alarm conditions... ");
        validateAlarmConditions(definitions, variableResolver);
        System.out.println("Done.");

        entryPriceVariableRefresher.startPeriodicRefresh();

        JupiterPerpsEntryPriceVariableRefreshWatcher entryPriceVariableRefreshWatcher =
                new JupiterPerpsEntryPriceVariableRefreshWatcher(
                        config.alarmConfiguration().getParent(),
                        entryPriceVariableRefresher
                );

        entryPriceVariableRefreshWatcher.start();

        List<AlarmAction> actions = new ArrayList<>();
        actions.add(new ConsoleAlarmAction(alarmConfigurationDirectory));
        actions.add(new JupiterPerpsPositionIncreaseAlarmAction(
                positionIncreaseConfiguration,
                solanaWallet,
                jupiterServicePerps,
                variableResolver
        ));
        actions.add(new JupiterPerpsPositionDecreaseAlarmAction(
                positionDecreaseConfiguration,
                solanaWallet,
                jupiterServicePerps,
                variableResolver
        ));
        actions.add(new PushoverAlarmAction(
                alarmConfigurationDirectory,
                variableResolver
        ));

        AlarmAction action = new CompositeAlarmAction(actions);
        Map<JupiterPerpsAsset, List<PriceAlarmDefinition>> definitionsByAsset =
                groupByAsset(definitions);

        Map<JupiterPerpsAsset, AssetPriceAlarmMonitor> monitors = new EnumMap<>(
                JupiterPerpsAsset.class
        );

        definitionsByAsset.forEach((asset, assetDefinitions) -> monitors.put(
                asset,
                new AssetPriceAlarmMonitor(asset, assetDefinitions, variableResolver, action)
        ));

        List<OracleWebSocketClient> clients = new ArrayList<>();
        for (Map.Entry<JupiterPerpsAsset, AssetPriceAlarmMonitor> entry : monitors.entrySet()) {
            for (URI endpoint : config.webSocketEndpoints()) {
                clients.add(new OracleWebSocketClient(
                        endpoint,
                        entry.getKey(),
                        entry.getValue()::accept
                ));
            }
        }

        Runtime.getRuntime().addShutdownHook(new Thread(
                () -> {
                    entryPriceVariableRefresher.close();
                    clients.forEach(OracleWebSocketClient::close);
                },
                "shutdown"
        ));

        System.out.println("Alarm configuration: " + config.alarmConfiguration().toAbsolutePath());
        System.out.println("Loaded alarms: " + definitions.size());
        definitionsByAsset.forEach((asset, assetDefinitions) -> {
            System.out.printf(
                    "  %s: %d alarm(s), oracle account %s%n",
                    asset,
                    assetDefinitions.size(),
                    asset.oracleAccount()
            );
            assetDefinitions.forEach(definition -> System.out.printf(
                    "    %s USD, %s%n",
                    definition.conditionExpression(),
                    definition.trigger()
            ));
        });
        System.out.println("RPC endpoints per asset: " + config.webSocketEndpoints().size());
        System.out.println("Total WebSocket connections: " + clients.size());

        clients.forEach(OracleWebSocketClient::start);
        new CountDownLatch(1).await();
    }

    private void validateAlarmConditions(
            List<PriceAlarmDefinition> definitions,
            AlarmVariableResolver variableResolver
    ) {
        for (PriceAlarmDefinition definition : definitions) {
            try {
                String resolvedConditionExpression = variableResolver.resolve(
                        definition.conditionExpression()
                );

                if (definition.trigger() == AlarmTrigger.CROSSING) {
                    AlarmConditionParser.parseCrossing(resolvedConditionExpression);
                } else {
                    AlarmConditionParser.parse(resolvedConditionExpression);
                }
            } catch (RuntimeException exception) {
                throw new IllegalArgumentException(
                        "Invalid condition for alarm "
                                + definition.id()
                                + " ("
                                + definition.conditionExpression()
                                + "): "
                                + exception.getMessage(),
                        exception
                );
            }
        }
    }

    private Map<JupiterPerpsAsset, List<PriceAlarmDefinition>> groupByAsset(
            List<PriceAlarmDefinition> definitions
    ) {
        Map<JupiterPerpsAsset, List<PriceAlarmDefinition>> result = new EnumMap<>(
                JupiterPerpsAsset.class
        );
        for (PriceAlarmDefinition definition : definitions) {
            result.computeIfAbsent(definition.asset(), ignored -> new ArrayList<>())
                    .add(definition);
        }
        result.replaceAll((asset, alarms) -> List.copyOf(alarms));
        return result;
    }

    private void printUsage() {
        System.err.println("""
                JupiterPerpsAlarmImpl is constructed and registered by the Nenjim Registry service manager.
                It is intentionally excluded from the current explicit startup list.

                Current registered configuration:
                  --config=conf/alarms.conf

                Environment:
                  SOLANA_WS_URLS             Comma-separated RPC WebSocket endpoints
                                              Default: wss://api.mainnet-beta.solana.com
                """);
    }

    private record Config(
            Path alarmConfiguration,
            List<URI> webSocketEndpoints
    ) {
        private static Config parse(String[] args, Map<String, String> environment) {
            Map<String, String> options = Arrays.stream(args)
                    .filter(argument -> argument.startsWith("--") && argument.contains("="))
                    .map(argument -> argument.substring(2).split("=", 2))
                    .collect(java.util.stream.Collectors.toMap(
                            parts -> parts[0],
                            parts -> parts[1],
                            (first, second) -> second
                    ));

            String configurationText = options.get("config");
            if (configurationText == null || configurationText.isBlank()) {
                configurationText = environment.getOrDefault(
                        "PRICE_ALARMS_CONFIG",
                        "price-alarms.conf"
                );
            }

            String endpointText = options.get("ws");
            if (endpointText == null || endpointText.isBlank()) {
                endpointText = environment.getOrDefault(
                        "SOLANA_WS_URLS",
                        "wss://api.mainnet-beta.solana.com"
                );
            }

            List<URI> endpoints = Arrays.stream(endpointText.split(","))
                    .map(String::trim)
                    .filter(value -> !value.isBlank())
                    .map(URI::create)
                    .toList();

            if (endpoints.isEmpty()) {
                throw new IllegalArgumentException("At least one WebSocket endpoint is required");
            }

            return new Config(
                    Path.of(configurationText),
                    endpoints
            );
        }
    }

    private final SolanaWallet solanaWallet;
    private final JupiterPerpsService jupiterServicePerps;
    private final String configFilename;

    private final Thread thread;
}
