package ru.yandex.webmaster3.monitoring.solomon;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.NavigableSet;
import java.util.Optional;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.google.common.base.Preconditions;
import org.apache.commons.collections4.CollectionUtils;
import org.joda.time.Duration;

import ru.yandex.solomon_api.model.SolomonAlert;
import ru.yandex.solomon_api.model.SolomonExpression;
import ru.yandex.solomon_api.model.SolomonType;
import ru.yandex.webmaster3.core.solomon.metric.SolomonKey;
import ru.yandex.webmaster3.core.solomon.metric.SolomonTimerConfiguration;
import ru.yandex.webmaster3.monitoring.solomon.trigger.PropertyKey;
import ru.yandex.webmaster3.monitoring.solomon.trigger.SolomonTriggerPropertiesSet;
import ru.yandex.webmaster3.monitoring.solomon.trigger.data.CustomTrigger;
import ru.yandex.webmaster3.monitoring.solomon.trigger.data.DataAgeTrigger;
import ru.yandex.webmaster3.monitoring.solomon.trigger.data.ErrorsRatioTrigger;
import ru.yandex.webmaster3.monitoring.solomon.trigger.data.NoDataTrigger;
import ru.yandex.webmaster3.monitoring.solomon.trigger.data.QueueSizeTrigger;
import ru.yandex.webmaster3.monitoring.solomon.trigger.data.TimingsTrigger;
import ru.yandex.webmaster3.monitoring.solomon.trigger.data.WebmasterTrigger;
import ru.yandex.webmaster3.monitoring.solomon.trigger.data.WebmasterTriggerInfo;
import ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder;
import ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonProgramBuilder;
import ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonTriggerExpression;
import ru.yandex.webmaster3.storage.util.JsonDBMapping;

import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.boolVar;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.count;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.div;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.duration;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.eq;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.gte;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.last;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.lt;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.mul;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.num;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.numVar;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.shift;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.string;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.sum;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.ternary;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.toFixed;
import static ru.yandex.webmaster3.monitoring.solomon.trigger.expressions.SolomonExpressionsBuilder.variable;

/**
 * @author avhaliullin
 */
public class WebmasterTriggerUtil {
    private static final ObjectMapper OM = JsonDBMapping.OM;

    private static final String PROP_SEND_YELLOW = "send-yellow";
    private static final String PROP_SEND_GREEN = "send-green";
    private static final String PROP_DATA_DURATION = "data-duration";
    private static final String PROP_REPEAT_INTERVAL_RED = "repeat-interval-red";
    private static final String PROP_PERMANENT_SUBSCRIBERS = "permanent-subscribers";
    private static final String PROP_MUTE = "mute";

    private static final String PROP_TIMINGS_TIME_BUCKETS = "timings.time-buckets";
    private static final String PROP_TIMINGS_DURATION_THRESHOLD = "timings.duration-threshold";
    private static final String PROP_TIMINGS_YELLOW_PERCENTILE = "timings.yellow-percentile";
    private static final String PROP_TIMINGS_RED_PERCENTILE = "timings.red-percentile";

    private static final String PROP_ERRORS_RATIO_RED = "errors-ratio.red";
    private static final String PROP_ERRORS_RATIO_YELLOW = "errors-ratio.yellow";
    private static final String PROP_ERRORS_RATIO_ERROR_RESULTS = "errors-ratio.error-results";
    private static final String PROP_ERRORS_RATIO_NORMAL_RESULTS = "errors-ratio.normal-results";

    private static final String PROP_DATA_AGE_RED_THRESHOLD = "data-age.red-threshold";
    private static final String PROP_DATA_AGE_YELLOW_THRESHOLD = "data-age.yellow-threshold";

    private static final String PROP_QUEUE_SIZE_RED_THRESHOLD = "queue-size.red-threshold";
    private static final String PROP_QUEUE_SIZE_YELLOW_THRESHOLD = "queue-size.yellow-threshold";

    private static final String PROP_NO_DATA_PERIOD_RED = "no-data.period-red";
    private static final String PROP_NO_DATA_PERIOD_YELLOW = "no-data.period-yellow";

    private static final String PROP_RESULT_LABEL_NAME = "result-label-name";
    private static final String PROP_CLUSTER_LABEL_VALUE = "cluster-label-value";
    private static final String PROP_HOST_LABEL_VALUE = "host-label-value";

    private static final String LABEL_TIME_BUCKET = "time_bucket";

    private static final String VAR_IS_RED = "is_red";
    private static final String VAR_IS_YELLOW = "is_yellow";
    private static final String VAR_TRAFFIC_COLOR = "trafficColor";
    private static final String VAR_TRAFFIC_VALUE = "trafficValue";
    private static final String COLOR_RED = "red";
    private static final String COLOR_YELLOW = "yellow";
    private static final String COLOR_GREEN = "green";
    private static final SolomonTriggerExpression<String> TRAFFIC_COLOR_EXPRESSION =
            ternary(boolVar(VAR_IS_RED), string(COLOR_RED),
            ternary(boolVar(VAR_IS_YELLOW), string(COLOR_YELLOW), string(COLOR_GREEN)));

    public static SolomonAlert buildSolomonAlert(String environmentType, SolomonTriggerPropertiesSet props,
                                                 WebmasterTriggerInfo triggerInfo) {
        WebmasterTrigger trigger = triggerInfo.getTrigger();
        PropertyKey propsSelectionKey = new PropertyKey(triggerInfo.getCommonLabels())
                .withSolomonLabel("project", triggerInfo.getProject())
                .withSolomonLabel("service", triggerInfo.getService())
                .withLabel("trigger_type", trigger.getType().name())
                .withLabel("dashboard_category", triggerInfo.getDashboardCategory());
        String cluster = getProperty(props, propsSelectionKey, PROP_CLUSTER_LABEL_VALUE, String.class).orElse(environmentType);
        propsSelectionKey = propsSelectionKey.withLabel("cluster", cluster);

        SolomonKey commonLabels = triggerInfo.getCommonLabels()
                .withLabel("cluster", cluster)
                .withLabel("service", triggerInfo.getService());

        String host = Optional.ofNullable(triggerInfo.getHost())
                .orElse(getProperty(props, propsSelectionKey, PROP_HOST_LABEL_VALUE, String.class).orElse(null));
        if (host != null) {
            commonLabels = commonLabels.withLabel("host", host);
            propsSelectionKey = propsSelectionKey.withSolomonLabel("host", host);
        }
        Duration dataInterval = Optional.ofNullable(triggerInfo.getDataDuration())
                .orElse(getProperty(props, propsSelectionKey, PROP_DATA_DURATION, Duration.class).orElse(Duration.standardMinutes(10L)));

        boolean mute = getProperty(props, propsSelectionKey, PROP_MUTE, Boolean.class).orElse(false);
        List<String> notificationsChannels = new ArrayList<>();
        if (!mute) {
            // TODO
            notificationsChannels.add("webmaster-duty-channel-sms");
        }

        SolomonAlert alert = new SolomonAlert()
                .id(triggerInfo.getId())
                .name(triggerInfo.getName())
                .projectId(triggerInfo.getProject())
                .periodMillis(dataInterval.getMillis())
                .notificationChannels(notificationsChannels)
                .putAnnotationsItem("trafficLight.color", "{{expression.trafficColor}}")
                .putAnnotationsItem("trafficLight.value", "{{expression.trafficValue}}")
                .putAnnotationsItem("name", "{{alert.name}}");

        switch (trigger.getType()) {
            case TIMINGS:
                alert = buildExpression(alert, (TimingsTrigger) trigger, propsSelectionKey, props, commonLabels);
                break;
            case ERROR_RATIO:
                alert = buildExpression(alert, (ErrorsRatioTrigger) trigger, propsSelectionKey, props, commonLabels);
                break;
            case DATA_AGE:
                alert = buildExpression(alert, (DataAgeTrigger) trigger, propsSelectionKey, props, commonLabels);
                break;
            case NO_DATA:
                alert = buildExpression(alert, (NoDataTrigger) trigger, propsSelectionKey, props, commonLabels);
                break;
            case QUEUE_SIZE:
                alert = buildExpression(alert, (QueueSizeTrigger) trigger, propsSelectionKey, props, commonLabels);
                break;
            case CUSTOM:
                alert = buildExpression(alert, (CustomTrigger) trigger, propsSelectionKey, props, commonLabels);
                break;
            default:
                throw new RuntimeException("Unknown webmaster trigger type " + trigger.getType());
        }

        // тип алерта со всей логикой
        return alert;
    }

    private static SolomonAlert buildExpression(SolomonAlert alert, TimingsTrigger timingsTrigger, PropertyKey propsSelectionKey,
                                                     SolomonTriggerPropertiesSet props, SolomonKey commonsLabels) {
        SolomonProgramBuilder program = new SolomonProgramBuilder();

        Duration durationThreshold = timingsTrigger.getDurationThreshold()
                .orElse(getProperty(props, propsSelectionKey, PROP_TIMINGS_DURATION_THRESHOLD, Duration.class)
                        .orElse(Duration.standardSeconds(2))
                );
        NavigableSet<Duration> buckets;
        if (!CollectionUtils.isEmpty(timingsTrigger.getTimeBuckets())) {
            buckets = new TreeSet<>(timingsTrigger.getTimeBuckets());
        } else {
            buckets = getProperty(props, propsSelectionKey, PROP_TIMINGS_TIME_BUCKETS, new TypeReference<NavigableSet<Duration>>() {})
                    .orElse(SolomonTimerConfiguration.DEFAULT_BUCKETS);
        }
        int yellowPercentile = timingsTrigger.getYellowStatePercentile().orElse(
                getProperty(props, propsSelectionKey, PROP_TIMINGS_YELLOW_PERCENTILE, Integer.class).orElse(90)
        );
        int redPercentile = timingsTrigger.getRedStatePercentile().orElse(
                getProperty(props, propsSelectionKey, PROP_TIMINGS_RED_PERCENTILE, Integer.class).orElse(85)
        );

        // generate program
        List<String> goodBuckets = new ArrayList<>();
        List<String> allBuckets = new ArrayList<>();
        for (Duration bucket : buckets) {
            String bucketName = "ms" + bucket.getMillis();
            program.addVariable(bucketName, commonsLabels.withLabel(LABEL_TIME_BUCKET, "<" + bucket.getMillis() + "ms"));
            if (!bucket.isLongerThan(durationThreshold)) {
                goodBuckets.add(bucketName);
            }
            allBuckets.add(bucketName);
        }
        program.addExpression("good", sum(goodBuckets.stream().map(SolomonExpressionsBuilder::variable)
                .map(SolomonExpressionsBuilder::sum).collect(Collectors.toList())));
        program.addExpression("all", sum(allBuckets.stream().map(SolomonExpressionsBuilder::variable)
                .map(SolomonExpressionsBuilder::sum).collect(Collectors.toList())));
        program.addExpression("percentile", mul(num(100), div(numVar("good"), numVar("all"))));
        program.addExpression(VAR_IS_RED, lt(numVar("percentile"), num(redPercentile)));
        program.addExpression(VAR_IS_YELLOW, lt(numVar("percentile"), num(yellowPercentile)));
        program.addExpression(VAR_TRAFFIC_COLOR, TRAFFIC_COLOR_EXPRESSION);
        program.addExpression(VAR_TRAFFIC_VALUE, toFixed(numVar("percentile"), num(1)));

        SolomonExpression expression = new SolomonExpression().checkExpression(VAR_IS_RED).program(program.build());
        return alert.type(new SolomonType().expression(expression));
    }

    private static SolomonAlert buildExpression(SolomonAlert alert, ErrorsRatioTrigger errorsRatioTrigger, PropertyKey propsSelectionKey,
                                                      SolomonTriggerPropertiesSet props, SolomonKey commonLabels) {
        int redRatioThreshold = errorsRatioTrigger.getRedRatioThreshold().orElse(
                getProperty(props, propsSelectionKey, PROP_ERRORS_RATIO_RED, Integer.class).orElse(10)
        );
        int yellowRatioThreshold = errorsRatioTrigger.getYellowRatioThreshold().orElse(
                getProperty(props, propsSelectionKey, PROP_ERRORS_RATIO_YELLOW, Integer.class).orElse(1)
        );
        String resultLabelName = errorsRatioTrigger.getResultLabelName().orElse(
                getProperty(props, propsSelectionKey, PROP_RESULT_LABEL_NAME, String.class).orElse("result")
        );
        List<String> errorLabelValues = errorsRatioTrigger.getErrorValues();
        if (CollectionUtils.isEmpty(errorLabelValues)) {
            errorLabelValues = getProperty(props, propsSelectionKey, PROP_ERRORS_RATIO_ERROR_RESULTS, new TypeReference<List<String>>() {})
                    .orElse(Collections.singletonList("internal_error"));
        }
        List<String> normalLabelValues = errorsRatioTrigger.getNormalValues();
        if (CollectionUtils.isEmpty(normalLabelValues)) {
            normalLabelValues = getProperty(props, propsSelectionKey, PROP_ERRORS_RATIO_NORMAL_RESULTS, new TypeReference<List<String>>() {})
                    .orElse(Collections.singletonList("success"));
        }

        SolomonProgramBuilder program = new SolomonProgramBuilder();
        Stream.concat(normalLabelValues.stream(), errorLabelValues.stream()).forEach(labelValue ->
                  program.addVariable(labelValue, commonLabels.withLabel(resultLabelName, labelValue))
        );
        program.addExpression("errors", sum(errorLabelValues.stream()
                .map(SolomonExpressionsBuilder::variable).map(SolomonExpressionsBuilder::sum).collect(Collectors.toList())));
        program.addExpression("all", sum(Stream.concat(errorLabelValues.stream(), normalLabelValues.stream())
                .map(SolomonExpressionsBuilder::variable).map(SolomonExpressionsBuilder::sum).collect(Collectors.toList())));
        program.addExpression("errors_ratio", mul(num(100), div(numVar("errors"), numVar("all"))));
        program.addExpression(VAR_IS_RED, gte(numVar("errors_ratio"), num(redRatioThreshold)));
        program.addExpression(VAR_IS_YELLOW, gte(numVar("errors_ratio"), num(yellowRatioThreshold)));
        program.addExpression(VAR_TRAFFIC_COLOR, TRAFFIC_COLOR_EXPRESSION);
        program.addExpression(VAR_TRAFFIC_VALUE, toFixed(numVar("errors_ratio"), num(1)));

        SolomonExpression expression = new SolomonExpression().checkExpression(VAR_IS_RED).program(program.build());
        return alert.type(new SolomonType().expression(expression));
    }

    private static SolomonAlert buildExpression(SolomonAlert alert, DataAgeTrigger dataAgeTrigger, PropertyKey propsSelectionKey,
                                                     SolomonTriggerPropertiesSet props, SolomonKey commonLabels) {

        Duration redStateThreshold = dataAgeTrigger.getRedThreshold()
                .orElse(getProperty(props, propsSelectionKey, PROP_DATA_AGE_RED_THRESHOLD, Duration.class).orElse(Duration.standardDays(2)));
        Duration yellowStateThreshold = dataAgeTrigger.getYellowThreshold()
                .orElse(getProperty(props, propsSelectionKey, PROP_DATA_AGE_YELLOW_THRESHOLD, Duration.class).orElse(Duration.standardDays(1)));

        SolomonProgramBuilder program = new SolomonProgramBuilder();
        program.addVariable("age", commonLabels);
        program.addExpression("last_age", SolomonExpressionsBuilder.last(variable("age")));
        program.addExpression(VAR_IS_RED, gte(numVar("last_age"), num(redStateThreshold.getStandardSeconds())));
        program.addExpression(VAR_IS_YELLOW, gte(numVar("last_age"), num(yellowStateThreshold.getStandardSeconds())));
        program.addExpression(VAR_TRAFFIC_COLOR, TRAFFIC_COLOR_EXPRESSION);
        program.addExpression(VAR_TRAFFIC_VALUE, toFixed(numVar("last_age"), num(0)));

        SolomonExpression expression = new SolomonExpression().checkExpression(VAR_IS_RED).program(program.build());
        return alert.type(new SolomonType().expression(expression));
    }

    private static SolomonAlert buildExpression(SolomonAlert alert, NoDataTrigger noDataTrigger, PropertyKey propsSelectionKey,
                                                      SolomonTriggerPropertiesSet props, SolomonKey commonLabels) {
        Duration yellowPeriod = noDataTrigger.getPeriodYellow().orElse(
                getProperty(props, propsSelectionKey, PROP_NO_DATA_PERIOD_YELLOW, Duration.class).orElse(Duration.standardDays(1))
        );
        Duration redPeriod = noDataTrigger.getPeriodRed().orElse(
                getProperty(props, propsSelectionKey, PROP_NO_DATA_PERIOD_RED, Duration.class).orElse(Duration.standardDays(2))
        );
        Preconditions.checkState(!yellowPeriod.isLongerThan(redPeriod), "Red period must be greater or equal than yellow period");

        Duration yellowShift = yellowPeriod.minus(redPeriod);
        SolomonProgramBuilder program = new SolomonProgramBuilder();
        program.addVariable("cnt", commonLabels);
        program.addExpression(VAR_IS_RED, eq(num(0), count(variable("cnt"))));
        program.addExpression(VAR_IS_YELLOW, eq(num(0), count(shift(variable("cnt"), duration(yellowShift)))));
        program.addExpression(VAR_TRAFFIC_COLOR, TRAFFIC_COLOR_EXPRESSION);

        SolomonExpression expression = new SolomonExpression().checkExpression(VAR_IS_RED).program(program.build());
        return alert.periodMillis(redPeriod.getMillis()).type(new SolomonType().expression(expression));
    }

    private static SolomonAlert buildExpression(SolomonAlert alert, QueueSizeTrigger queueSizeTrigger, PropertyKey propsSelectionKey,
                                                SolomonTriggerPropertiesSet props, SolomonKey commonLabels) {
        long yellowThreshold = queueSizeTrigger.getYellowThreshold().orElse(
                getProperty(props, propsSelectionKey, PROP_QUEUE_SIZE_YELLOW_THRESHOLD, Long.class).orElse(5000L)
        );
        long redThreshold = queueSizeTrigger.getRedThreshold().orElse(
                getProperty(props, propsSelectionKey, PROP_QUEUE_SIZE_RED_THRESHOLD, Long.class).orElse(10_000L)
        );

        SolomonProgramBuilder program = new SolomonProgramBuilder();
        program.addVariable("queue_size", commonLabels);
        program.addExpression("last_size", last(variable("queue_size")));
        program.addExpression(VAR_IS_RED, gte(numVar("last_size"), num(redThreshold)));
        program.addExpression(VAR_IS_YELLOW, gte(numVar("last_size"), num(yellowThreshold)));
        program.addExpression(VAR_TRAFFIC_COLOR, TRAFFIC_COLOR_EXPRESSION);
        program.addExpression(VAR_TRAFFIC_VALUE, toFixed(numVar("last_size"), num(0)));

        SolomonExpression expression = new SolomonExpression().checkExpression(VAR_IS_RED).program(program.build());
        return alert.type(new SolomonType().expression(expression));
    }

    private static SolomonAlert buildExpression(SolomonAlert alert, CustomTrigger customTrigger, PropertyKey propsSelectionKey,
                                                SolomonTriggerPropertiesSet props, SolomonKey commonLabels) {
        throw new UnsupportedOperationException("Custom alerts not yet supported: " + alert.getId());
/*        programDisplay = customTrigger.getProgramDisplay().map(SolomonExpressionsBuilder::<Number>customProgram).orElse(null);
        programRed = SolomonExpressionsBuilder.customProgram(customTrigger.getProgramRed());
        programYellow = SolomonExpressionsBuilder.customProgram(customTrigger.getProgramYellow());
        variables = customTrigger.getVariables().stream().map(SerializableTriggerVariable::toApiDto).collect(Collectors.toList());
        parameters = customTrigger.getParameters().stream().map(SerializableTriggerParameter::toApiDto).collect(Collectors.toList());*/
    }

    private static <T> Optional<T> getProperty(SolomonTriggerPropertiesSet props, PropertyKey key, String propertyName, ObjectReader objectReader) {
        return props.findProperty(key, propertyName).map(s -> {
            try {
                return objectReader.readValue(s);
            } catch (IOException e) {
                throw new IllegalArgumentException("Failed to parse property " + key + " " + propertyName + " value " + s, e);
            }
        });
    }

    private static <T> Optional<T> getProperty(SolomonTriggerPropertiesSet props, PropertyKey key, String propertyName, TypeReference<T> typeReference) {
        return getProperty(props, key, propertyName, OM.readerFor(typeReference));
    }

    private static <T> Optional<T> getProperty(SolomonTriggerPropertiesSet props, PropertyKey key, String propertyName, Class<T> valueClass) {
        return getProperty(props, key, propertyName, OM.readerFor(valueClass));
    }

}
