/*
 * 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.skywalking.oap.server.storage.plugin.elasticsearch.base;

import java.util.ArrayList;
import java.util.List;
import lombok.Setter;
import org.apache.skywalking.oap.server.core.Const;
import org.apache.skywalking.oap.server.core.UnexpectedException;
import org.apache.skywalking.oap.server.core.analysis.DownSampling;
import org.apache.skywalking.oap.server.core.analysis.TimeBucket;
import org.apache.skywalking.oap.server.core.query.enumeration.Step;
import org.apache.skywalking.oap.server.core.storage.model.Model;
import org.apache.skywalking.oap.server.library.util.StringUtil;
import org.joda.time.DateTime;
import org.joda.time.Days;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

/**
 * TimeSeriesUtils sets up and splits the time suffix of index name.
 */
public class TimeSeriesUtils {
    private static DateTimeFormatter TIME_BUCKET_FORMATTER = DateTimeFormat.forPattern("yyyyMMdd");
    /**
     * We are far from the first day of 2000, so we set it as the day one to make sure the index based on {@link
     * #DAY_STEP} is consistently no matter whenever the OAP starts up.
     */
    private static final DateTime DAY_ONE = TIME_BUCKET_FORMATTER.parseDateTime("20000101");
    @Setter
    private static int DAY_STEP = 1;
    @Setter
    private static int SUPER_DATASET_DAY_STEP = 1;

    /**
     * @return formatted latest index name, based on current timestamp.
     */
    public static String latestWriteIndexName(Model model) {
        String tableName = IndexController.INSTANCE.getTableName(model);
        long dayTimeBucket = TimeBucket.getTimeBucket(System.currentTimeMillis(), DownSampling.Day);
        if (model.isRecord() && model.isSuperDataset()) {
            return tableName + Const.LINE + compressTimeBucket(dayTimeBucket, SUPER_DATASET_DAY_STEP);
        }
        return tableName + Const.LINE + compressTimeBucket(dayTimeBucket, DAY_STEP);
    }

    /**
     * @return Concrete index name for super dataset index
     */
    public static String[] superDatasetIndexNames(String indexName, long startSecondTB, long endSecondTB) {
        if (startSecondTB == 0 || endSecondTB == 0) {
            return new String[] {indexName};
        }
        DateTime startDateTime = TIME_BUCKET_FORMATTER.parseDateTime(startSecondTB / 1000000 + "");
        DateTime endDateTime = TIME_BUCKET_FORMATTER.parseDateTime(endSecondTB / 1000000 + "");
        List<DateTime> timeRanges = new ArrayList<>(16);
        for (int i = 0; i <= Days.daysBetween(startDateTime, endDateTime).getDays(); i++) {
            timeRanges.add(startDateTime.plusDays(i));
        }
        if (timeRanges.isEmpty()) {
            return new String[] {indexName};
        } else {
            return timeRanges.stream()
                             .map(item -> indexName + Const.LINE + compressDateTime(item, SUPER_DATASET_DAY_STEP))
                             .distinct()
                             .toArray(String[]::new);
        }
    }

    public static String queryIndexName(String tableName,
                                        long pointOfTB,
                                        Step step,
                                        boolean isRecord,
                                        boolean isSuperDataSet) {
        if (StringUtil.isBlank(tableName) || pointOfTB <= 0) {
            throw new IllegalArgumentException(
                "Arguments [tableName]: " + tableName + " can not be blank and [pointOfTB]: " + pointOfTB + " can not <= 0");
        }
        if (isRecord && isSuperDataSet) {
            return tableName + Const.LINE + compressTimeBucket(pointOfTB / 1000000, SUPER_DATASET_DAY_STEP);
        }
        switch (step) {
            case DAY:
                return tableName + Const.LINE + compressTimeBucket(pointOfTB, DAY_STEP);
            case HOUR:
                return tableName + Const.LINE + compressTimeBucket(pointOfTB / 100, DAY_STEP);
            case MINUTE:
                return tableName + Const.LINE + compressTimeBucket(pointOfTB / 10000, DAY_STEP);
            case SECOND:
                return tableName + Const.LINE + compressTimeBucket(pointOfTB / 1000000, DAY_STEP);
        }

        throw new UnexpectedException("Failed to get the index name from tableName:" + tableName + ", pointOfTB:" + pointOfTB + ", step:" + step.name());
    }

    /**
     * @return index name based on model definition and given time bucket.
     */
    static String writeIndexName(Model model, long timeBucket) {
        String tableName = IndexController.INSTANCE.getTableName(model);
        if (model.isRecord() && model.isSuperDataset()) {
            return tableName + Const.LINE + compressTimeBucket(timeBucket / 1000000, SUPER_DATASET_DAY_STEP);
        } else {
            switch (model.getDownsampling()) {
                case None:
                    return tableName;
                case Hour:
                    return tableName + Const.LINE + compressTimeBucket(timeBucket / 100, DAY_STEP);
                case Minute:
                    return tableName + Const.LINE + compressTimeBucket(timeBucket / 10000, DAY_STEP);
                case Day:
                    return tableName + Const.LINE + compressTimeBucket(timeBucket, DAY_STEP);
                case Second:
                    return tableName + Const.LINE + compressTimeBucket(timeBucket / 1000000, DAY_STEP);
                default:
                    throw new UnexpectedException("Unexpected down sampling value, " + model.getDownsampling());
            }
        }
    }

    /**
     * @return the index represented time, which is included in the index name.
     */
    static long isolateTimeFromIndexName(String indexName) {
        return Long.parseLong(indexName.substring(indexName.lastIndexOf(Const.LINE) + 1));
    }

    /**
     * Follow the dayStep to re-format the time bucket literal long value.
     *
     * Such as, in dayStep == 11,
     *
     * 20000105 re-formatted time bucket is 20000101, 20000115 re-formatted time bucket is 20000112, 20000123
     * re-formatted time bucket is 20000123
     */
    static long compressTimeBucket(long timeBucket, int dayStep) {
        if (dayStep > 1) {
            DateTime time = TIME_BUCKET_FORMATTER.parseDateTime("" + timeBucket);
            int days = Days.daysBetween(DAY_ONE, time).getDays();
            int groupBucketOffset = days % dayStep;
            return Long.parseLong(time.minusDays(groupBucketOffset).toString(TIME_BUCKET_FORMATTER));
        } else {
            /*
             * No calculation required. dayStep is for lower traffic. For normally configuration, there is pointless to calculate.
             */
            return timeBucket;
        }
    }

    static long compressDateTime(DateTime time, int dayStep) {
        if (dayStep > 1) {
            int days = Days.daysBetween(DAY_ONE, time).getDays();
            int groupBucketOffset = days % dayStep;
            return Long.parseLong(time.minusDays(groupBucketOffset).toString(TIME_BUCKET_FORMATTER));
        } else {
            /**
             * No calculation required. dayStep is for lower traffic. For normally configuration, there is pointless to calculate.
             */
            return Long.parseLong(time.toString(TIME_BUCKET_FORMATTER));
        }
    }

}
