package com.eactive.testmaster.loadtest.service;

import com.eactive.testmaster.client.dto.ApiLayoutItemDTO;
import com.eactive.testmaster.client.dto.ApiRequestDTO;
import com.eactive.testmaster.client.dto.ApiRequestKeyValueDTO;
import com.eactive.testmaster.client.dto.ApiResponse;
import com.eactive.testmaster.client.dto.internal.ScenarioNode;
import com.eactive.testmaster.client.service.ApiClientFactory;
import com.eactive.testmaster.loadtest.dto.LoadTestRequestAndResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.regex.Pattern;
import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
import org.apache.commons.lang3.SerializationUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class VirtualUser implements Runnable {

    //logger
    private static final Logger logger = LoggerFactory.getLogger(VirtualUser.class);
    private ScriptEngine scriptEngine;
    private Console console;
    private final Object pauseLock = new Object();
    private final Object scriptLock = new Object();  // New lock for script execution

    private boolean paused = false;
    private volatile boolean running = true;

    private int maxCount;

    private Thread runningThread;

    private LoadTestService loadTestService;
    private ApiClientFactory apiClientFactory;

    private static Map<String, String> globals = new ConcurrentHashMap<>();
    private Map<String, String> variables = new ConcurrentHashMap<>();

    private String testId;
    private ScenarioNode scenario;
    private final ThreadLocal<Set<ScenarioNode>> visitedNodes = ThreadLocal.withInitial(HashSet::new);


    private CountDownLatch latch;

    public VirtualUser(String testId, int maxCount, ScenarioNode scenarioDTO, LoadTestService loadTestService, ApiClientFactory apiClientFactory, CountDownLatch latch) {
        this.testId = testId;
        this.scenario = scenarioDTO;
        this.loadTestService = loadTestService;
        this.maxCount = maxCount;
        this.apiClientFactory = apiClientFactory;
        this.latch = latch;

        NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
        this.scriptEngine = factory.getScriptEngine(new String[]{"--language=es5"});
        this.console = new Console();
    }

    public VirtualUser(String testId, ApiClientFactory apiClientFactory) {
        this.testId = testId;
        this.apiClientFactory = apiClientFactory;

        NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
        this.scriptEngine = factory.getScriptEngine(new String[]{"--language=es5"});
        this.console = new Console();
    }

    public void pause() {
        synchronized (pauseLock) {
            paused = true;
        }
    }

    public void resume() {
        synchronized (pauseLock) {
            paused = false;
            pauseLock.notifyAll();
        }
    }

    public void stop() {
        running = false;
        latch.countDown();
        resume();
        if (runningThread != null) {
            runningThread.interrupt();
        }
    }

    @Override
    public void run() {
        try {
            runningThread = Thread.currentThread();
            int count = 0;
            while (maxCount == 0 || running && count < maxCount) {
                synchronized (pauseLock) {
                    if (paused) {
                        try {
                            pauseLock.wait();
                        } catch (InterruptedException ex) {
                            Thread.currentThread().interrupt(); // Preserve interrupt status
                            break;
                        }
                    }
                }
                if (!running) {
                    break;
                }
                performAction();
                logger.debug("Count: " + (count + 1) + " of " + maxCount);
                count++;
            }
        } finally {
            latch.countDown();
        }
    }

    private void performAction() {
        Set<ScenarioNode> threadLocalVisited = visitedNodes.get();
        threadLocalVisited.clear();
        ScenarioNode currentNode = scenario;

        while (running) {
            if (Thread.interrupted()) {
                break;
            }
            if (currentNode != null) {
                if (threadLocalVisited.contains(currentNode)) {
                    logger.debug("Loop detected, stopping");
                    break;
                }
                threadLocalVisited.add(currentNode);

                if (currentNode.getClassName().equalsIgnoreCase("api")) {
                    processApi(currentNode.getApiRequest());
                }

                if (currentNode.getConnections() != null && !currentNode.getConnections().isEmpty()) {
                    currentNode = currentNode.getConnections().get(0);
                } else {
                    break;
                }
            }
        }
    }


    public ApiResponse processApiAndReturn(ApiRequestDTO apiRequest) {
        ApiRequestDTO cloned = SerializationUtils.clone(apiRequest);
        ApiRequestDTO replaced = null;

        executePreRequestScript(cloned);
        replaced = replacePlaceHoldersWithVariables(cloned);

        ApiResponse response = null;
        try {
            response = apiClientFactory.getClient(replaced).handleRequest(replaced).get();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }

        executePostRequestScript(replaced, response);

        return response;
    }

    public void processApi(ApiRequestDTO apiRequest) {
        try {
            if (apiRequest != null) {
                ApiRequestDTO cloned = SerializationUtils.clone(apiRequest);
                ApiRequestDTO replaced = null;

                executePreRequestScript(cloned);
                replaced = replacePlaceHoldersWithVariables(cloned);

                long startTime = System.currentTimeMillis();
                ApiResponse response = apiClientFactory.getClient(replaced).handleRequest(replaced).get();
                long endTime = System.currentTimeMillis();
                long responseTime = endTime - startTime;

                executePostRequestScript(replaced, response);
                LoadTestRequestAndResponse requestAndResponse = new LoadTestRequestAndResponse(replaced, response, responseTime);
                loadTestService.sendLoadTestUpdate(testId, requestAndResponse);

            }
        } catch (InterruptedException | ExecutionException e) {
            loadTestService.sendLoadTestError(testId, "An error occurred: " + e.getMessage());
        }
    }

    private void executeScript(Map<String, Object> bindings, String script) {
        if (script == null || script.isEmpty()) {
            return;
        }

        synchronized (scriptLock) {
            try {
                Bindings scriptBindings = new SimpleBindings();
                scriptBindings.put("console", console);
                scriptBindings.put("globals", globals);
                scriptBindings.put("variables", variables);

                if (bindings != null) {
                    scriptBindings.putAll(bindings);
                }
                scriptEngine.eval(script, scriptBindings);
            } catch (ScriptException e) {
                logger.error("Error executing script", e);
                e.printStackTrace();
            }
        }
    }

    private void executePreRequestScript(ApiRequestDTO apiRequest) {
        if (apiRequest.getPreRequestScript() == null) {
            return;
        }

        Map<String, Object> bindings = new HashMap<>();
        bindings.put("request", apiRequest);
        executeScript(bindings, apiRequest.getPreRequestScript());
    }

    private void executePostRequestScript(ApiRequestDTO apiRequest, ApiResponse response) {
        if (apiRequest.getPostRequestScript() == null) {
            return;
        }

        Map<String, Object> bindings = new HashMap<>();
        bindings.put("request", apiRequest);
        bindings.put("response", response);
        executeScript(bindings, apiRequest.getPostRequestScript());
    }

    private ApiRequestDTO replacePlaceHoldersWithVariables(ApiRequestDTO apiRequest) {
        Map<String, String> mergedVariables = new HashMap<>();

        // Merge globals and variables
        if (globals != null) {
            mergedVariables.putAll(globals);
        }
        if (variables != null) {
            mergedVariables.putAll(variables);
        }

        for (Map.Entry<String, String> entry : mergedVariables.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            String placeholder = "\\{\\{" + key + "\\}\\}"; // Escape braces for regex

            apiRequest.setPath(apiRequest.getPath().replaceAll(placeholder, value));

            if (apiRequest.getRequestBody() != null) {
                apiRequest.setRequestBody(apiRequest.getRequestBody().replaceAll(placeholder, value));
            }

            if (apiRequest.getHeaders() != null) {
                apiRequest.setHeaders(replaceInList(apiRequest.getHeaders(), placeholder, value));
            }

            if (apiRequest.getQueryParams() != null) {
                apiRequest.setQueryParams(replaceInList(apiRequest.getQueryParams(), placeholder, value));
            }

            if (apiRequest.getLayout() != null && apiRequest.getLayout().getLayoutItems() != null) {
                apiRequest.getLayout().setLayoutItems(replaceInLayoutItems(apiRequest.getLayout().getLayoutItems(), placeholder, value));
            }
            if (apiRequest.getLayout() != null && apiRequest.getLayout().getComputedLayoutItemRoot() != null) {
                replaceLayoutItemTree(apiRequest.getLayout().getComputedLayoutItemRoot(), placeholder, value);
            }
        }

        return apiRequest;
    }

    private void replaceLayoutItemTree(ApiLayoutItemDTO computedLayoutItemRoot, String placeholder, String value) {
        if (computedLayoutItemRoot == null) {
            return;  // Base case: if the node is null, just return.
        }

        // Replace the value in the current node if it contains the placeholder
        if (computedLayoutItemRoot.getValue() != null) {
            String replacedValue = computedLayoutItemRoot.getValue().replaceAll(placeholder, value);
            computedLayoutItemRoot.setValue(replacedValue);
        }

        // Recursively replace the value in each child
        if (computedLayoutItemRoot.getChildren() != null) {
            for (ApiLayoutItemDTO child : computedLayoutItemRoot.getChildren()) {
                replaceLayoutItemTree(child, placeholder, value);
            }
        }
    }


    private List<ApiLayoutItemDTO> replaceInLayoutItems(List<ApiLayoutItemDTO> originalList, String placeholder, String value) {
        List<ApiLayoutItemDTO> replacedList = new ArrayList<>();
        Pattern pattern = Pattern.compile(placeholder);

        for (ApiLayoutItemDTO item : originalList) {
            String replacedValue = pattern.matcher(item.getValue()).replaceAll(value);
            item.setValue(replacedValue);
            replacedList.add(item);
        }

        return replacedList;
    }

    private List<ApiRequestKeyValueDTO> replaceInList(List<ApiRequestKeyValueDTO> originalList, String placeholder, String value) {
        List<ApiRequestKeyValueDTO> replacedList = new ArrayList<>();
        Pattern pattern = Pattern.compile(placeholder);

        originalList.forEach(kv -> {
            String replacedKey = pattern.matcher(kv.getKey()).replaceAll(value);
            String replacedValue = pattern.matcher(kv.getValue()).replaceAll(value);
            replacedList.add(new ApiRequestKeyValueDTO(kv.isEnabled(), replacedKey, replacedValue));
        });

        return replacedList;
    }


}
