package io.github.delirius325.jmeter.backendlistener.elasticsearch;

import java.net.InetAddress;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

import org.apache.jmeter.assertions.AssertionResult;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.visualizers.backend.BackendListenerContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.apache.commons.lang3.math.NumberUtils.isCreatable;

public class ElasticSearchMetric {
    private static final Logger logger = LoggerFactory.getLogger(ElasticSearchMetric.class);
    private SampleResult sampleResult;
    private String esTestMode;
    private String esTimestamp;
    private int ciBuildNumber;
    private HashMap<String, Object> json;
    private Set<String> fields;
    private boolean allReqHeaders;
    private boolean allResHeaders;

    public ElasticSearchMetric(
            SampleResult sr, String testMode, String timeStamp, int buildNumber,
            boolean parseReqHeaders, boolean parseResHeaders, Set<String> fields) {
        this.sampleResult = sr;
        this.esTestMode = testMode.trim();
        this.esTimestamp = timeStamp.trim();
        this.ciBuildNumber = buildNumber;
        this.json = new HashMap<>();
        this.allReqHeaders = parseReqHeaders;
        this.allResHeaders = parseResHeaders;
        this.fields = fields;
    }

    /**
     * This method returns the current metric as a Map(String, Object) for the provided sampleResult
     *
     * @param context BackendListenerContext
     * @return a JSON Object as Map(String, Object)
     */
    public Map<String, Object> getMetric(BackendListenerContext context) throws Exception {
        SimpleDateFormat sdf = new SimpleDateFormat(this.esTimestamp);

        //add all the default SampleResult parameters
        addFilteredJSON("AllThreads", this.sampleResult.getAllThreads());
        addFilteredJSON("BodySize", this.sampleResult.getBodySizeAsLong());
        addFilteredJSON("Bytes", this.sampleResult.getBytesAsLong());
        addFilteredJSON("SentBytes", this.sampleResult.getSentBytes());
        addFilteredJSON("ConnectTime", this.sampleResult.getConnectTime());
        addFilteredJSON("ContentType", this.sampleResult.getContentType());
        addFilteredJSON("DataType", this.sampleResult.getDataType());
        addFilteredJSON("ErrorCount", this.sampleResult.getErrorCount());
        addFilteredJSON("GrpThreads", this.sampleResult.getGroupThreads());
        addFilteredJSON("IdleTime", this.sampleResult.getIdleTime());
        addFilteredJSON("Latency", this.sampleResult.getLatency());
        addFilteredJSON("ResponseTime", this.sampleResult.getTime());
        addFilteredJSON("SampleCount", this.sampleResult.getSampleCount());
        addFilteredJSON("SampleLabel", this.sampleResult.getSampleLabel());
        addFilteredJSON("ThreadName", this.sampleResult.getThreadName());
        addFilteredJSON("URL", this.sampleResult.getURL());
        addFilteredJSON("ResponseCode", this.sampleResult.getResponseCode());
        addFilteredJSON("TestStartTime", JMeterContextService.getTestStartTime());
        addFilteredJSON("SampleStartTime", sdf.format(new Date(this.sampleResult.getStartTime())));
        addFilteredJSON("SampleEndTime", sdf.format(new Date(this.sampleResult.getEndTime())));
        addFilteredJSON("Timestamp", this.sampleResult.getTimeStamp());
        addFilteredJSON("InjectorHostname", InetAddress.getLocalHost().getHostName());

        // Add the details according to the mode that is set
        switch (this.esTestMode) {
            case "debug":
                addDetails();
                break;
            case "error":
                addDetails();
                break;
            case "info":
                if (!this.sampleResult.isSuccessful())
                    addDetails();
                break;
            default:
                break;
        }

        addAssertions();
        addElapsedTime();
        addCustomFields(context);
        parseHeadersAsJsonProps(this.allReqHeaders, this.allResHeaders);

        return this.json;
    }

    /**
     * This method adds all the assertions for the current sampleResult
     */
    private void addAssertions() {
        AssertionResult[] assertionResults = this.sampleResult.getAssertionResults();
        if (assertionResults != null) {
            Map<String, Object>[] assertionArray = new HashMap[assertionResults.length];
            Integer i = 0;
            String failureMessage = "";
            boolean isFailure = false;
            for (AssertionResult assertionResult : assertionResults) {
                HashMap<String, Object> assertionMap = new HashMap<>();
                boolean failure = assertionResult.isFailure() || assertionResult.isError();
                isFailure = isFailure || assertionResult.isFailure() || assertionResult.isError();
                assertionMap.put("failure", failure);
                assertionMap.put("failureMessage", assertionResult.getFailureMessage());
                failureMessage += assertionResult.getFailureMessage() + "\n";
                assertionMap.put("name", assertionResult.getName());
                assertionArray[i] = assertionMap;
                i++;
            }
            addFilteredJSON("AssertionResults", assertionArray);
            addFilteredJSON("FailureMessage", failureMessage);
            addFilteredJSON("Success", !isFailure);
        }
    }

    /**
     * This method adds the ElapsedTime as a key:value pair in the JSON object. Also, depending on whether or not the
     * tests were launched from a CI tool (i.e Jenkins), it will add a hard-coded version of the ElapsedTime for results
     * comparison purposes
     */
    private void addElapsedTime() {
        Date elapsedTime;

        if (this.ciBuildNumber != 0) {
            elapsedTime = getElapsedTime(true);
            addFilteredJSON("BuildNumber", this.ciBuildNumber);

            if (elapsedTime != null)
                addFilteredJSON("ElapsedTimeComparison", elapsedTime.getTime());
        }

        elapsedTime = getElapsedTime(false);
        if (elapsedTime != null)
            addFilteredJSON("ElapsedTime", elapsedTime.getTime());
    }

    /**
     * Methods that add all custom fields added by the user in the Backend Listener's GUI panel
     *
     * @param context BackendListenerContext
     */
    private void addCustomFields(BackendListenerContext context) {
        Iterator<String> pluginParameters = context.getParameterNamesIterator();
        String parameter;
        while (pluginParameters.hasNext()) {
            String parameterName = pluginParameters.next();

            if (!parameterName.startsWith("es.") && context.containsParameter(parameterName)
                    && !"".equals(parameter = context.getParameter(parameterName).trim())) {
                if (isCreatable(parameter)) {
                    addFilteredJSON(parameterName, Long.parseLong(parameter));
                } else {
                    addFilteredJSON(parameterName, parameter);
                }
            }
        }
    }

    /**
     * Method that adds the request and response's body/headers
     */
    private void addDetails() {
        addFilteredJSON("RequestHeaders", this.sampleResult.getRequestHeaders());
        addFilteredJSON("RequestBody", this.sampleResult.getSamplerData());
        addFilteredJSON("ResponseHeaders", this.sampleResult.getResponseHeaders());
        addFilteredJSON("ResponseBody", this.sampleResult.getResponseDataAsString());
        addFilteredJSON("ResponseMessage", this.sampleResult.getResponseMessage());
    }

    /**
     * This method will parse the headers and look for custom variables passed through as header. It can also seperate
     * all headers into different ElasticSearch document properties by passing "true" This is a work-around the native
     * behaviour of JMeter where variables are not accessible within the backend listener.
     *
     * @param allReqHeaders boolean to determine if the user wants to separate ALL request headers into different ES JSON
     *                      properties.
     * @param allResHeaders boolean to determine if the user wants to separate ALL response headers into different ES JSON
     *                      properties.
     *                      <p>
     *                      NOTE: This will be fixed as soon as a patch comes in for JMeter to change the behaviour.
     */
    private void parseHeadersAsJsonProps(boolean allReqHeaders, boolean allResHeaders) {
        LinkedList<String[]> headersArrayList = new LinkedList<String[]>();

        if (allReqHeaders) {
            headersArrayList.add(this.sampleResult.getRequestHeaders().split("\n"));
        }

        if (allResHeaders) {
            headersArrayList.add(this.sampleResult.getResponseHeaders().split("\n"));
        }

        if (!allReqHeaders && !allResHeaders) {
            headersArrayList.add(this.sampleResult.getRequestHeaders().split("\n"));
            headersArrayList.add(this.sampleResult.getResponseHeaders().split("\n"));
        }

        for (String[] lines : headersArrayList) {
            for (int i = 0; i < lines.length; i++) {
                String[] header = lines[i].split(":", 2);

                // if not all res/req headers and header contains special X-tag
                if (!allReqHeaders && !allResHeaders && header.length > 1) {
                    if (header[0].startsWith("X-es-backend-")) {
                        this.json.put(header[0].replaceAll("X-es-backend-", "").trim(), header[1].trim());
                    }
                }

                if ((allReqHeaders || allResHeaders) && header.length > 1) {
                    this.json.put(header[0].trim(), header[1].trim());
                }
            }
        }
    }

    /**
     * Adds a given key-value pair to JSON if the key is contained in the field filter or in case of empty field filter
     *
     * @param key
     * @param value
     */
    private void addFilteredJSON(String key, Object value) {
        if (this.fields.size() == 0 || this.fields.contains(key.toLowerCase())) {
            this.json.put(key, value);
        }
    }

    /**
     * This method is meant to return the elapsed time in a human readable format. The purpose of this is mostly for
     * build comparison in Kibana. By doing this, the user is able to set the X-axis of his graph to this date and split
     * the series by build numbers. It allows him to overlap test results and see if there is regression or not.
     *
     * @param forBuildComparison boolean to determine if there is CI (continuous integration) or not
     * @return The elapsed time in YYYY-MM-dd HH:mm:ss format
     */
    public Date getElapsedTime(boolean forBuildComparison) {
        String sElapsed;
        //Calculate the elapsed time (Starting from midnight on a random day - enables us to compare of two loads over their duration)
        long start = JMeterContextService.getTestStartTime();
        long end = System.currentTimeMillis();
        long elapsed = (end - start);
        long minutes = (elapsed / 1000) / 60;
        long seconds = (elapsed / 1000) % 60;

        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.HOUR_OF_DAY, 0); //If there is more than an hour of data, the number of minutes/seconds will increment this
        cal.set(Calendar.MINUTE, (int) minutes);
        cal.set(Calendar.SECOND, (int) seconds);

        if (forBuildComparison) {
            sElapsed = String.format("2017-01-01 %02d:%02d:%02d", cal.get(Calendar.HOUR_OF_DAY),
                    cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND));
        } else {
            sElapsed = String.format("%s %02d:%02d:%02d",
                    DateTimeFormatter.ofPattern("yyyy-mm-dd").format(LocalDateTime.now()),
                    cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND));
        }

        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
        try {
            return formatter.parse(sElapsed);
        } catch (ParseException e) {
            logger.error("Unexpected error occured computing elapsed date", e);
            return null;
        }
    }

}