package ru.yandex.autotests.direct.scriptrunner.service.scripts;

import java.net.URI;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.autotests.direct.scriptrunner.service.utils.Environments;

public class BulkScriptRunner {

    private final Logger log = LoggerFactory.getLogger(BulkScriptRunner.class);
    private static final String CID = "--cid";

    private final String scriptName;

    private Map<Long, ScriptRunParams> tasks = new LinkedHashMap<>();
    private Lock tasksLock = new ReentrantLock();

    private Map<Long, ScriptRunResult> results = new HashMap<>();
    private Lock resultsLock = new ReentrantLock();
    private Condition resultsCondition = resultsLock.newCondition();

    private Lock executionLock = new ReentrantLock();

    public BulkScriptRunner(String scriptName) {
        this.scriptName = scriptName;
    }

    public ScriptRunResult runScript(ScriptRunParams scriptRunParams) {
        Long key = addTask(scriptRunParams);
        startExecution();
        return waitForResult(key);
    }

    public int getQueueSize() {
        return tasks.size();
    }

    private void startExecution() {
        if (tasks.isEmpty()) {
            return;
        }

        new Thread(() -> {
            if (executionLock.tryLock()) {

                try {

                    Map<Long, ScriptRunParams> bulk = extractBulk();
                    if (bulk.isEmpty()) {
                        return;
                    }

                    URI jsonRpcUrl;
                    String bulkCmdLine;
                    try {
                        jsonRpcUrl = getJsonRpcUrl(bulk);
                        bulkCmdLine = buildBulkCmdLine(bulk.values());
                    } catch (Throwable t) {
                        addResults(null, null, bulk.keySet(), t);
                        log.error("Error while obtaining script run params", t);
                        return;
                    }

                    try {
                        JsonRpcScriptExecutor executor = new JsonRpcScriptExecutor(jsonRpcUrl);
                        log.info("running " + scriptName + ", jsonRpcUrl: " + jsonRpcUrl + ", cmdLine: " + bulkCmdLine);
                        TestScriptRunResponse response = executor.runScript(scriptName, bulkCmdLine);
                        addResults(jsonRpcUrl.toString(), bulkCmdLine, bulk.keySet(), response);
                    } catch (Throwable t) {
                        addResults(jsonRpcUrl.toString(), bulkCmdLine, bulk.keySet(), t);
                        log.error("Error while script execution. ", t);
                    }

                } finally {
                    executionLock.unlock();
                    startExecution();
                }

            }
        }).start();
    }

    private Long addTask(ScriptRunParams scriptRunParams) {
        Long key = randomKey();
        tasksLock.lock();
        try {
            tasks.put(key, scriptRunParams);
        } finally {
            tasksLock.unlock();
        }
        return key;
    }

    private ScriptRunResult waitForResult(Long key) {
        resultsLock.lock();
        try {
            while (true) {  // не уйдет в бесконечный цикл т.к. используется упорядоченный LinkedHashMap
                resultsCondition.await();
                ScriptRunResult result = results.remove(key);
                if (result != null) {
                    return result;
                }
            }
        } catch (InterruptedException e) {
            return new ScriptRunResult(null, null, "Error while waiting for result: " + e);
        } finally {
            resultsLock.unlock();
        }
    }

    private void addResults(String url, String cmdLine, Set<Long> keys, TestScriptRunResponse response) {
        resultsLock.lock();
        try {
            for (Long key : keys) {
                results.put(key, new ScriptRunResult(url, cmdLine, response));
            }
            resultsCondition.signalAll();
        } finally {
            resultsLock.unlock();
        }
    }

    private void addResults(String url, String cmdLine, Set<Long> keys, Throwable t) {
        resultsLock.lock();
        try {
            for (Long key : keys) {
                results.put(key, new ScriptRunResult(url, cmdLine, t.toString()));
            }
            resultsCondition.signalAll();
        } finally {
            resultsLock.unlock();
        }
    }

    private Map<Long, ScriptRunParams> extractBulk() {
        tasksLock.lock();
        try {
            ScriptRunParams firstParamsNoCid = null;

            Map<Long, ScriptRunParams> bulk = new HashMap<>();
            for (Map.Entry<Long, ScriptRunParams> entry : new HashSet<>(tasks.entrySet())) {
                if (firstParamsNoCid == null) {
                    bulk.put(entry.getKey(), entry.getValue());
                    tasks.remove(entry.getKey());
                    firstParamsNoCid = new ScriptRunParams(entry.getValue());
                    firstParamsNoCid.getScriptParams().remove(CID);
                    continue;
                }

                ScriptRunParams curParamsNoCid = new ScriptRunParams(entry.getValue());
                curParamsNoCid.getScriptParams().remove(CID);
                if (curParamsNoCid.equals(firstParamsNoCid)) {
                    bulk.put(entry.getKey(), entry.getValue());
                    tasks.remove(entry.getKey());
                }

            }

            return bulk;
        } finally {
            tasksLock.unlock();
        }
    }

    private String buildBulkCmdLine(Collection<ScriptRunParams> scriptRunParamsList) {
        try {
            StringBuilder builder = new StringBuilder();
            boolean first = true;
            for (ScriptRunParams scriptRunParams : scriptRunParamsList) {
                if (first) {
                    first = false;
                    builder.append(scriptRunParams.getScriptParams().toString()).append(" ");
                    continue;
                }
                ScriptParams curParamsCidOnly = new ScriptParams();
                Object cid = scriptRunParams.getScriptParams().get(CID);
                if(cid instanceof List){
                    for(Object cidItem:(List)cid){
                        curParamsCidOnly.put(CID, String.valueOf(cidItem));
                        builder.append(curParamsCidOnly.toString());
                    }
                } else {
                    curParamsCidOnly.put(CID, String.valueOf(cid));
                    builder.append(curParamsCidOnly.toString());
                }
            }
            return builder.toString();
        } catch (Exception e) {
            throw new IllegalStateException("Error while building bulk cmd line: " + e, e);
        }
    }

    private URI getJsonRpcUrl(Map<Long, ScriptRunParams> bulk) {
        try {
            Iterator<ScriptRunParams> i = bulk.values().iterator();
            ScriptRunParams params = i.next();
            return Environments.getJsonRpcServiceUrl(params.getHost());
        } catch (Exception e) {
            throw new IllegalStateException("Error while getting JsonRPC service url from bulk: " + e, e);
        }
    }

    private Long randomKey() {
        return System.currentTimeMillis() * 1000 + Math.round(Math.random() * 1000);
    }
}
