/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jmeter.visualizers.backend.influxdb;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.visualizers.backend.BackendListenerClient;
import org.apache.jmeter.visualizers.backend.BackendListenerContext;
import org.jetbrains.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.auto.service.AutoService;

/**
 * Implementation of {@link BackendListenerClient} to write the response times
 * of every sample to InfluxDB. If more "raw" information is required in InfluxDB
 * then this class can be extended or another BackendListener
 * {@link InfluxdbBackendListenerClient} can be used to send aggregate information
 * to InfluxDB.
 *
 * @since 5.3
 */
@AutoService(BackendListenerClient.class)
public class InfluxDBRawBackendListenerClient implements BackendListenerClient {

    private static final Logger log = LoggerFactory.getLogger(InfluxDBRawBackendListenerClient.class);

    private static final Object LOCK = new Object();

    private static final String TAG_OK = "ok";
    private static final String TAG_KO = "ko";
    private static final String DEFAULT_MEASUREMENT = "jmeter";

    private static final Map<String, String> DEFAULT_ARGS = new LinkedHashMap<>();

    static {
        DEFAULT_ARGS.put("influxdbMetricsSender", HttpMetricsSender.class.getName());
        DEFAULT_ARGS.put("influxdbUrl", "http://host_to_change:8086/write?db=jmeter");
        DEFAULT_ARGS.put("influxdbToken", "");
        DEFAULT_ARGS.put("measurement", DEFAULT_MEASUREMENT);
    }

    private InfluxdbMetricsSender influxDBMetricsManager;
    private String measurement;

    public InfluxDBRawBackendListenerClient() {
        // default constructor
    }

    /**
     * Used for testing.
     *
     * @param sender the {@link InfluxdbMetricsSender} to use
     */
    public InfluxDBRawBackendListenerClient(InfluxdbMetricsSender sender) {
        influxDBMetricsManager = sender;
    }

    @VisibleForTesting
    String getMeasurement() {
        return measurement;
    }

    @VisibleForTesting
    InfluxdbMetricsSender getInfluxDBMetricsManager() {
        return influxDBMetricsManager;
    }

    @Override
    public void setupTest(BackendListenerContext context) throws Exception {
        initInfluxDBMetricsManager(context);
        measurement = context.getParameter("measurement", DEFAULT_MEASUREMENT);
    }

    private void initInfluxDBMetricsManager(BackendListenerContext context) throws Exception {
        influxDBMetricsManager = Class
                .forName(context.getParameter("influxdbMetricsSender"))
                .asSubclass(InfluxdbMetricsSender.class)
                .getDeclaredConstructor()
                .newInstance();

        influxDBMetricsManager.setup(
                context.getParameter("influxdbUrl"),
                context.getParameter("influxdbToken"));
    }

    @Override
    public void teardownTest(BackendListenerContext context) {
        influxDBMetricsManager.destroy();
    }

    @Override
    public void handleSampleResults(
            List<SampleResult> sampleResults, BackendListenerContext context) {
        log.debug("Handling {} sample results", sampleResults.size());
        synchronized (LOCK) {
            for (SampleResult sampleResult : sampleResults) {
                addMetricFromSampleResult(sampleResult);
            }
            influxDBMetricsManager.writeAndSendMetrics();
        }
    }

    private void addMetricFromSampleResult(SampleResult sampleResult) {
        String tags = "," + createTags(sampleResult);
        String fields = createFields(sampleResult);
        long timestamp = sampleResult.getTimeStamp();

        influxDBMetricsManager.addMetric(measurement, tags, fields, timestamp);
    }

    @VisibleForTesting
    static String createTags(SampleResult sampleResult) {
        boolean isError = sampleResult.getErrorCount() != 0;
        String status = isError ? TAG_KO : TAG_OK;
        // remove surrounding quotes and spaces from sample label
        String label = StringUtils.strip(sampleResult.getSampleLabel(), "\" ");
        String transaction = AbstractInfluxdbMetricsSender.tagToStringValue(label);
        String threadName = StringUtils.deleteWhitespace(sampleResult.getThreadName());
        return "status=" + status
                + ",transaction=" + transaction
                + ",threadName=" + threadName;
    }

    @VisibleForTesting
    static String createFields(SampleResult sampleResult) {
        long duration = sampleResult.getTime();
        long latency = sampleResult.getLatency();
        long connectTime = sampleResult.getConnectTime();
        return "duration=" + duration
                + ",ttfb=" + latency
                + ",connectTime=" + connectTime;
    }

    @Override
    public Arguments getDefaultParameters() {
        Arguments arguments = new Arguments();
        DEFAULT_ARGS.forEach(arguments::addArgument);
        return arguments;
    }
}
