package com.eactive.testmaster.loadtest.service;

import com.eactive.testmaster.client.dto.ApiRequestDTO;
import com.eactive.testmaster.client.dto.internal.ScenarioNode;
import com.eactive.testmaster.client.entity.ApiRequestInfo;
import com.eactive.testmaster.client.entity.ApiScenario;
import com.eactive.testmaster.client.mapper.ApiRequestMapper;
import com.eactive.testmaster.client.mapper.ApiScenarioMapper;
import com.eactive.testmaster.client.service.NettyApiClient;
import com.eactive.testmaster.loadtest.dto.ApiRequestEvent;
import com.eactive.testmaster.loadtest.dto.LoadTestDTO;
import com.eactive.testmaster.loadtest.dto.LoadTestRequestAndResponse;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Service
//@Scope("session")
public class LoadTestService extends TextWebSocketHandler {

    //logger
    private static final Logger logger = LoggerFactory.getLogger(LoadTestService.class);
    private static long MINUTE = 10 * 1000L;

    private SimpMessagingTemplate template;

    private LoadTestMonitorMap loadTestMonitorMap;

    private NettyApiClient nettyApiClient;

    private ApiScenarioMapper apiScenarioMapper;

    private ApiRequestMapper apiRequestMapper;

    private final RingBuffer<ApiRequestEvent> ringBuffer;

    public LoadTestService(SimpMessagingTemplate template, LoadTestMonitorMap loadTestMonitorMap, NettyApiClient nettyApiClient, ApiScenarioMapper apiScenarioMapper, ApiRequestMapper apiRequestMapper) {
        this.template = template;
        this.loadTestMonitorMap = loadTestMonitorMap;
        this.nettyApiClient = nettyApiClient;
        this.apiScenarioMapper = apiScenarioMapper;
        this.apiRequestMapper = apiRequestMapper;

        Disruptor<ApiRequestEvent> disruptor = new Disruptor<>(ApiRequestEvent::new, 1024, Executors.defaultThreadFactory());
        disruptor.handleEventsWith(this::processApiRequestEvent);
        ringBuffer = disruptor.start();
    }

    private void processApiRequestEvent(ApiRequestEvent event, long sequence, boolean endOfBatch) {
        LoadTestRequestAndResponse requestAndResponse = event.getRequestAndResponse();
        String testId = event.getTestId();
        List<Long> responseTimes = new ArrayList<>();
        insertSorted(responseTimes, requestAndResponse.getResponseTime());

        template.convertAndSend("/topic/" + testId, requestAndResponse);
    }

    public static void insertSorted(List<Long> list, long value) {
        if (list.isEmpty() || value > list.get(list.size() - 1)) {
            list.add(value);
            return;
        }

        int insertionPoint = findInsertionPoint(list, value);
        list.add(insertionPoint, value);
    }

    private static int findInsertionPoint(List<Long> list, long value) {
        int low = 0;
        int high = list.size() - 1;

        while (low <= high) {
            int mid = low + (high - low) / 2;
            if (value == list.get(mid)) {
                return mid; // Optional: Handle duplicate values as needed
            } else if (value > list.get(mid)) {
                low = mid + 1;
            } else {
                high = mid - 1;
            }
        }

        return low;
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String clientMessage = message.getPayload();
        System.out.println("Received message: " + clientMessage);
        session.sendMessage(new TextMessage("Hello from server!"));
    }


    public void sendLoadTestUpdate(String topic, LoadTestRequestAndResponse requestAndResponse) {
        long sequence = ringBuffer.next();
        ApiRequestEvent event = ringBuffer.get(sequence);
        event.setTestId(topic);
        event.setRequestAndResponse(requestAndResponse);
        ringBuffer.publish(sequence);
    }

    public void sendLoadTestError(String topic, String errorMessage) {
        template.convertAndSend("/topic/" + topic, errorMessage);

    }

    public void startLoadTest(LoadTestDTO loadTestDTO) {
        int numberOfUsers = loadTestDTO.getThreadCount();
        int maxCount = loadTestDTO.getLoopCount();
        long rampUpTimeMillis = loadTestDTO.getRampUpTime();
        long taskDurationMillis = loadTestDTO.getDuration() * MINUTE;
        CountDownLatch latch = new CountDownLatch(numberOfUsers);

        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(numberOfUsers + 1); // Add extra space for cancellation tasks
        taskScheduler.initialize();
        ApiScenario apiScenario = apiScenarioMapper.map(loadTestDTO.getScenarioId());
        ScenarioNode scenario = LoadTestGraphBuilder.getTopNode(apiScenario.getScenario());
        traverseDFS(scenario);

        List<ScheduledFuture<?>> scheduledFutures = new ArrayList<>();
        List<VirtualUser> virtualUsers = new ArrayList<>();
        for (int i = 0; i < numberOfUsers; i++) {
            final VirtualUser virtualUser = new VirtualUser(loadTestDTO.getId(), maxCount, scenario, this, nettyApiClient, latch);
            virtualUsers.add(virtualUser);

            Instant startTime = Instant.now().plusMillis(i * rampUpTimeMillis);
            ScheduledFuture<?> userTask = taskScheduler.schedule(virtualUser, Date.from(startTime));
            scheduledFutures.add(userTask);
        }

        if (taskDurationMillis > 0) {
            taskScheduler.schedule(() -> {
                stopLoadTest(loadTestDTO.getId());
            }, new Date(Instant.now().toEpochMilli() + taskDurationMillis)); // Schedule cancellation task at startTime + taskDurationMillis
        }

        loadTestMonitorMap.put(loadTestDTO.getId(), scheduledFutures, virtualUsers, taskScheduler);

        try {
            latch.await();
            logger.debug("All virtual users have completed");
            stopLoadTest(loadTestDTO.getId());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public void traverseDFS(ScenarioNode node) {
        if (node == null) {
            return;
        }

        if (node.getClassName() != null && node.getClassName().equalsIgnoreCase("api")) {
            ApiRequestInfo apiRequestInfo = apiRequestMapper.map(node.getData().get("id"));
            ApiRequestDTO apiRequestDTO = apiRequestMapper.map(apiRequestInfo);
            node.setApiRequest(apiRequestDTO);
        }

        // Visited set to avoid cycles
        Set<String> visited = new HashSet<>();
        visited.add(node.getId());

        dfsHelper(node, visited);
    }

    private void dfsHelper(ScenarioNode node, Set<String> visited) {
        if (node.getConnections() == null) {
            return;
        }

        for (ScenarioNode adjacentNode : node.getConnections()) {
            if (!visited.contains(adjacentNode.getId())) {
                // Process the adjacent node
                System.out.println(adjacentNode.getName());
                if (adjacentNode.getClassName() != null && adjacentNode.getClassName().equalsIgnoreCase("api")) {
                    ApiRequestInfo apiRequestInfo = apiRequestMapper.map(adjacentNode.getData().get("id"));
                    ApiRequestDTO apiRequestDTO = apiRequestMapper.map(apiRequestInfo);
                    adjacentNode.setApiRequest(apiRequestDTO);
                }
                visited.add(adjacentNode.getId());

                // Recurse into adjacent node
                dfsHelper(adjacentNode, visited);
            }
        }
    }


    public void stopLoadTest(String id) {
        logger.debug("Stopping load test with id: " + id);
        List<VirtualUser> virtualUsers = loadTestMonitorMap.getVirtualUsers(id);
        if (virtualUsers != null) {
            virtualUsers.forEach(VirtualUser::stop);
        }

        List<ScheduledFuture<?>> futures = loadTestMonitorMap.getFutures(id);
        if (futures != null) {
            futures.forEach(future -> {
                future.cancel(true);
            });
        }

        ThreadPoolTaskScheduler scheduler = loadTestMonitorMap.getScheduler(id);
        if (scheduler != null) {
            scheduler.shutdown();
        }

        loadTestMonitorMap.remove(id);
    }


}
