// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.cosmos.implementation;

import com.azure.cosmos.implementation.apachecommons.lang.tuple.ImmutablePair;
import com.azure.cosmos.implementation.query.metrics.ClientSideMetrics;
import com.azure.cosmos.implementation.query.metrics.FetchExecutionRange;
import com.azure.cosmos.implementation.query.metrics.SchedulingTimeSpan;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.azure.cosmos.implementation.guava27.Strings.lenientFormat;

/**
 * Query metrics in the Azure Cosmos database service.
 * This metric represents a moving average for a set of queries whose metrics have been aggregated together.
 */
public final class QueryMetrics {

    private static final Logger LOGGER = LoggerFactory.getLogger(QueryMetrics.class);
    private final long retrievedDocumentCount;
    private final long retrievedDocumentSize;
    private final long outputDocumentCount;
    private final long outputDocumentSize;
    private final long indexHitDocumentCount;
    private final Duration totalQueryExecutionTime;
    private final QueryPreparationTimes queryPreparationTimes;
    private final Duration indexLookupTime;
    private final Duration documentLoadTime;
    private final Duration vmExecutionTime;
    private final RuntimeExecutionTimes runtimeExecutionTimes;
    private final Duration documentWriteTime;
    private final ClientSideMetrics clientSideMetrics;
    private final List<String> activityIds;
    private final IndexUtilizationInfo indexUtilizationInfo;

    public QueryMetrics(List<String> activities, long retrievedDocumentCount, long retrievedDocumentSize, long outputDocumentCount,
                        long outputDocumentSize, long indexHitCount, Duration totalQueryExecutionTime,
                        QueryPreparationTimes queryPreparationTimes, Duration indexLookupTime, Duration documentLoadTime,
                        Duration vmExecutionTime, RuntimeExecutionTimes runtimeExecutionTimes, Duration documentWriteTime,
                        ClientSideMetrics clientSideMetrics, IndexUtilizationInfo indexUtilizationInfo) {
        this.retrievedDocumentCount = retrievedDocumentCount;
        this.retrievedDocumentSize = retrievedDocumentSize;
        this.outputDocumentCount = outputDocumentCount;
        this.outputDocumentSize = outputDocumentSize;
        this.indexHitDocumentCount = indexHitCount;
        this.totalQueryExecutionTime = totalQueryExecutionTime;
        this.queryPreparationTimes = queryPreparationTimes;
        this.indexLookupTime = indexLookupTime;
        this.documentLoadTime = documentLoadTime;
        this.vmExecutionTime = vmExecutionTime;
        this.runtimeExecutionTimes = runtimeExecutionTimes;
        this.documentWriteTime = documentWriteTime;
        this.clientSideMetrics = clientSideMetrics;
        this.activityIds = activities;
        this.indexUtilizationInfo = indexUtilizationInfo;
    }

    /**
     * @return the retrievedDocumentCount
     */
    public long getRetrievedDocumentCount() {
        return retrievedDocumentCount;
    }

    /**
     * @return the retrievedDocumentSize
     */
    public long getRetrievedDocumentSize() {
        return retrievedDocumentSize;
    }

    /**
     * @return the outputDocumentCount
     */
    public long getOutputDocumentCount() {
        return outputDocumentCount;
    }

    /**
     * @return the outputDocumentSize
     */
    public long getOutputDocumentSize() {
        return outputDocumentSize;
    }

    /**
     * @return the indexHitDocumentCount
     */
    public long getIndexHitDocumentCount() {
        return indexHitDocumentCount;
    }

    /**
     * Gets the index hit ratio by query in the Azure Cosmos database service.
     *
     * @return the IndexHitRatio
     */
    public double getIndexHitRatio() {
        return this.retrievedDocumentCount == 0 ? 1 : (double) this.indexHitDocumentCount / this.retrievedDocumentCount;
    }

    /**
     * @return the totalQueryExecutionTime
     */
    public Duration getTotalQueryExecutionTime() {
        return totalQueryExecutionTime;
    }

    /**
     * @return the queryPreparationTimes
     */
    public QueryPreparationTimes getQueryPreparationTimes() {
        return queryPreparationTimes;
    }

    /**
     * @return the indexLookupTime
     */
    public Duration getIndexLookupTime() {
        return indexLookupTime;
    }

    /**
     * @return the documentLoadTime
     */
    public Duration getDocumentLoadTime() {
        return documentLoadTime;
    }

    /**
     * @return the vmExecutionTime
     */
    public Duration getVMExecutionTime() {
        return vmExecutionTime;
    }

    /**
     * @return the runtimeExecutionTimes
     */
    public RuntimeExecutionTimes getRuntimeExecutionTimes() {
        return runtimeExecutionTimes;
    }

    /**
     * @return the documentWriteTime
     */
    public Duration getDocumentWriteTime() {
        return documentWriteTime;
    }

    /**
     * @return the clientSideMetrics
     */
    public ClientSideMetrics getClientSideMetrics() {
        return clientSideMetrics;
    }

    /**
     * @return the indexUtilizationInfo
     */
    public IndexUtilizationInfo getIndexUtilizationInfo() {
        return indexUtilizationInfo;
    }

    /**
     * @return number of reties in the Azure Cosmos database service.
     */
    public long getRetries() {
        return this.clientSideMetrics.getRetries();
    }

    public static QueryMetrics addQueryMetrics(QueryMetrics... additionalQueryMetrics) {
        List<QueryMetrics> queryMetricsList = new ArrayList<>(Arrays.asList(additionalQueryMetrics));

        return QueryMetrics.createFromCollection(queryMetricsList);
    }

    /**
     * Utility method to merge two query metrics map.
     * @param base metrics map which will be updated with new values.
     * @param addOn metrics map whose values will be merge in base map.
     */
    public static void mergeQueryMetricsMap(Map<String, QueryMetrics> base, Map<String, QueryMetrics> addOn) {
        for (Map.Entry<String, QueryMetrics> entry : addOn.entrySet()) {
            base.compute(entry.getKey(), (key, value) -> {
                if (value == null) {
                    return entry.getValue();
                } else {
                    return QueryMetrics.addQueryMetrics(value, entry.getValue());
                }
            });
        }
    }

    public static QueryMetrics createFromCollection(Collection<QueryMetrics> queryMetricsCollection) {
        long retrievedDocumentCount = 0;
        long retrievedDocumentSize = 0;
        long outputDocumentCount = 0;
        long outputDocumentSize = 0;
        long indexHitDocumentCount = 0;
        Duration totalQueryExecutionTime = Duration.ZERO;
        Collection<QueryPreparationTimes> queryPreparationTimesCollection = new ArrayList<QueryPreparationTimes>();
        Duration indexLookupTime = Duration.ZERO;
        Duration documentLoadTime = Duration.ZERO;
        Duration vmExecutionTime = Duration.ZERO;
        Collection<RuntimeExecutionTimes> runtimeExecutionTimesCollection = new ArrayList<RuntimeExecutionTimes>();
        Duration documentWriteTime = Duration.ZERO;
        Collection<ClientSideMetrics> clientSideMetricsCollection = new ArrayList<ClientSideMetrics>();
        List<String> activityIds = new ArrayList<>();
        Collection<IndexUtilizationInfo> indexUtilizationInfoCollection = new ArrayList<IndexUtilizationInfo>();

        for (QueryMetrics queryMetrics : queryMetricsCollection) {
            if (queryMetrics == null) {
                throw new NullPointerException("queryMetricsList can not have null elements");
            }
            activityIds.addAll(queryMetrics.activityIds);
            retrievedDocumentCount += queryMetrics.retrievedDocumentCount;
            retrievedDocumentSize += queryMetrics.retrievedDocumentSize;
            outputDocumentCount += queryMetrics.outputDocumentCount;
            outputDocumentSize += queryMetrics.outputDocumentSize;
            indexHitDocumentCount += queryMetrics.indexHitDocumentCount;
            totalQueryExecutionTime = totalQueryExecutionTime.plus(queryMetrics.totalQueryExecutionTime);
            queryPreparationTimesCollection.add(queryMetrics.queryPreparationTimes);
            indexLookupTime = indexLookupTime.plus(queryMetrics.indexLookupTime);
            documentLoadTime = documentLoadTime.plus(queryMetrics.documentLoadTime);
            vmExecutionTime = vmExecutionTime.plus(queryMetrics.vmExecutionTime);
            runtimeExecutionTimesCollection.add(queryMetrics.runtimeExecutionTimes);
            documentWriteTime = documentWriteTime.plus(queryMetrics.documentWriteTime);
            clientSideMetricsCollection.add(queryMetrics.clientSideMetrics);
            indexUtilizationInfoCollection.add(queryMetrics.indexUtilizationInfo);
        }

        return new QueryMetrics(activityIds, retrievedDocumentCount, retrievedDocumentSize, outputDocumentCount,
                outputDocumentSize,
                indexHitDocumentCount, totalQueryExecutionTime,
                QueryPreparationTimes.createFromCollection(queryPreparationTimesCollection), indexLookupTime, documentLoadTime,
                vmExecutionTime, RuntimeExecutionTimes.createFromCollection(runtimeExecutionTimesCollection),
                documentWriteTime, ClientSideMetrics.createFromCollection(clientSideMetricsCollection), IndexUtilizationInfo.createFromCollection(indexUtilizationInfoCollection));
    }

    public static QueryMetrics createFromDelimitedString(String delimitedString) {
        return QueryMetrics.createFromDelimitedStringAndClientSideMetrics(delimitedString,
                new ClientSideMetrics(0, 0, new ArrayList<FetchExecutionRange>(),
                        new ArrayList<ImmutablePair<String, SchedulingTimeSpan>>()), "", "");
    }

    public static QueryMetrics createFromDelimitedStringAndClientSideMetrics(String delimitedString, ClientSideMetrics clientSideMetrics,
                                                                      String activityId, String indexUtilizationInfoJSONString) {
        HashMap<String, Double> metrics = QueryMetricsUtils.parseDelimitedString(delimitedString);
        double indexHitRatio;
        double retrievedDocumentCount;
        indexHitRatio = metrics.get(QueryMetricsConstants.IndexHitRatio);
        retrievedDocumentCount = metrics.get(QueryMetricsConstants.RetrievedDocumentCount);
        long indexHitCount = (long) (indexHitRatio * retrievedDocumentCount);
        double outputDocumentCount = metrics.get(QueryMetricsConstants.OutputDocumentCount);
        double outputDocumentSize = metrics.get(QueryMetricsConstants.OutputDocumentSize);
        double retrievedDocumentSize = metrics.get(QueryMetricsConstants.RetrievedDocumentSize);
        Duration totalQueryExecutionTime = QueryMetricsUtils.getDurationFromMetrics(metrics, QueryMetricsConstants.TotalQueryExecutionTimeInMs);
        IndexUtilizationInfo indexUtilizationInfo = null;
        if (indexUtilizationInfoJSONString != null) {
            indexUtilizationInfo = IndexUtilizationInfo.createFromJSONString(Utils.decodeBase64String(indexUtilizationInfoJSONString));
        }

        List<String> activities = new ArrayList<>();
        activities.add(activityId);

        return new QueryMetrics(
                activities,
                (long) retrievedDocumentCount,
                (long) retrievedDocumentSize,
                (long) outputDocumentCount,
                (long) outputDocumentSize,
                indexHitCount,
                totalQueryExecutionTime,
                QueryPreparationTimes.createFromDelimitedString(delimitedString),
                QueryMetricsUtils.getDurationFromMetrics(metrics, QueryMetricsConstants.IndexLookupTimeInMs),
                QueryMetricsUtils.getDurationFromMetrics(metrics, QueryMetricsConstants.DocumentLoadTimeInMs),
                QueryMetricsUtils.getDurationFromMetrics(metrics, QueryMetricsConstants.VMExecutionTimeInMs),
                RuntimeExecutionTimes.createFromDelimitedString(delimitedString),
                QueryMetricsUtils.getDurationFromMetrics(metrics, QueryMetricsConstants.DocumentWriteTimeInMs),
                clientSideMetrics,
                indexUtilizationInfo);
    }

    @Override
    public String toString() {
        try {
            return Utils.getDurationEnabledObjectMapper().writeValueAsString(this);
        } catch (final JsonProcessingException error) {
            LOGGER.debug("could not convert {} value to JSON due to:", this.getClass(), error);
            try {
                return lenientFormat("{\"error\":%s}", Utils.getDurationEnabledObjectMapper().writeValueAsString(error.toString()));
            } catch (final JsonProcessingException exception) {
                return "null";
            }
        }
    }
}
