/*
 * 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.jdbc.common;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.oap.server.core.Const;
import org.apache.skywalking.oap.server.core.CoreModule;
import org.apache.skywalking.oap.server.core.analysis.DownSampling;
import org.apache.skywalking.oap.server.core.analysis.FunctionCategory;
import org.apache.skywalking.oap.server.core.analysis.TimeBucket;
import org.apache.skywalking.oap.server.core.config.ConfigService;
import org.apache.skywalking.oap.server.core.storage.model.Model;
import org.apache.skywalking.oap.server.library.client.jdbc.hikaricp.JDBCClient;
import org.apache.skywalking.oap.server.library.module.ModuleManager;
import org.apache.skywalking.oap.server.library.util.StringUtil;
import org.apache.skywalking.oap.server.storage.plugin.jdbc.TableMetaInfo;

import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.LongStream;

import static java.util.stream.Collectors.toList;

/**
 * Utility class to get table name for a given model.
 */
@Slf4j
@RequiredArgsConstructor
public class TableHelper {
    private final ModuleManager moduleManager;
    private final JDBCClient jdbcClient;

    @Getter(lazy = true, value = AccessLevel.PRIVATE)
    private final ConfigService configService = moduleManager.find(CoreModule.NAME).provider().getService(ConfigService.class);

    private final LoadingCache<String, Boolean> tableExistence =
        CacheBuilder.newBuilder()
                    .expireAfterAccess(Duration.ofMinutes(10))
                    .build(new CacheLoader<>() {
                        @Override
                        public @NonNull Boolean load(@NonNull String tableName) throws Exception {
                            return jdbcClient.tableExists(tableName);
                        }
                    });

    public static String getTableName(Model model) {
        final var aggFuncName = FunctionCategory.uniqueFunctionName(model.getStreamClass()).replaceAll("-", "_");
        return StringUtil.isNotBlank(aggFuncName) ? aggFuncName : model.getName();
    }

    public static String getLatestTableForWrite(Model model) {
        final var tableName = getTableName(model);

        if (!model.isTimeSeries()) {
            return tableName;
        }

        final var dayTimeBucket = TimeBucket.getTimeBucket(System.currentTimeMillis(), DownSampling.Day);
        return tableName + Const.UNDERSCORE + dayTimeBucket;
    }

    public static String getTable(Model model, long timeBucket) {
        final var tableName = getTableName(model);
        if (timeBucket == 0) {
            timeBucket = TimeBucket.getTimeBucket(System.currentTimeMillis(), DownSampling.Day);
        }

        if (!model.isTimeSeries()) {
            return tableName;
        }

        return tableName + Const.UNDERSCORE + TimeBucket.getTimeBucket(TimeBucket.getTimestamp(timeBucket), DownSampling.Day);
    }

    public static String getTable(String rawTableName, long timeBucket) {
        if (timeBucket == 0) {
            timeBucket = TimeBucket.getTimeBucket(System.currentTimeMillis(), DownSampling.Day);
        }

        return rawTableName + Const.UNDERSCORE + TimeBucket.getTimeBucket(TimeBucket.getTimestamp(timeBucket), DownSampling.Day);
    }

    public List<String> getTablesForRead(String modelName, long timeBucketStart, long timeBucketEnd) {
        final var model = TableMetaInfo.get(modelName);
        final var rawTableName = getTableName(model);

        if (!model.isTimeSeries()) {
            return Collections.singletonList(rawTableName);
        }

        final var ttlTables = getTablesWithinTTL(modelName);
        return getTablesInTimeBucketRange(modelName, timeBucketStart, timeBucketEnd)
            .stream()
            .filter(ttlTables::contains)
            .filter(table -> {
                try {
                    return tableExistence.get(table);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            })
            .collect(toList());
    }

    /**
     * Similar to {@link #getTablesForRead(String, long, long)}, but don't check the table existence.
     */
    public List<String> getTablesInTimeBucketRange(String modelName, long timeBucketStart, long timeBucketEnd) {
        final var model = TableMetaInfo.get(modelName);
        final var rawTableName = getTableName(model);

        if (!model.isTimeSeries()) {
            return Collections.singletonList(rawTableName);
        }

        final var timestampStart = TimeBucket.getTimestamp(timeBucketStart);
        final var timestampEnd = TimeBucket.getTimestamp(timeBucketEnd);
        final var timeBuckets = LongStream.builder();
        for (var timestamp = timestampStart; timestamp <= timestampEnd; timestamp += TimeUnit.DAYS.toMillis(1)) {
            timeBuckets.add(TimeBucket.getTimeBucket(timestamp, DownSampling.Day));
        }

        return timeBuckets
            .build()
            .distinct()
            .mapToObj(timeBucket -> getTable(rawTableName, timeBucket))
            .collect(toList());
    }

    public List<String> getTablesWithinTTL(String modelName) {
        final var model = TableMetaInfo.get(modelName);
        final var rawTableName = getTableName(model);

        if (!model.isTimeSeries()) {
            return Collections.singletonList(rawTableName);
        }

        final var ttlTimeBuckets = getTTLTimeBuckets(model);
        return ttlTimeBuckets
            .stream()
            .map(it -> getTable(rawTableName, it))
            .filter(table -> {
                try {
                    return tableExistence.get(table);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            })
            .collect(toList());
    }

    public static String generateId(Model model, String originalID) {
        if (model.isRecord() && !model.isSuperDataset()) {
            return generateId(model.getName(), originalID);
        }
        if (!model.isMetric() || !isFunctionMetric(model)) {
            return originalID;
        }
        return generateId(model.getName(), originalID);
    }

    public static String generateId(String modelName, String originalID) {
        return modelName + Const.ID_CONNECTOR + originalID;
    }

    public static boolean isFunctionMetric(Model model) {
        return StringUtil.isNotBlank(FunctionCategory.uniqueFunctionName(model.getStreamClass()));
    }

    public static long getTimeBucket(String table) {
        final var split = table.split("_");
        return Long.parseLong(split[split.length - 1]);
    }

    List<Long> getTTLTimeBuckets(Model model) {
        final var ttl = model.isRecord() ?
            getConfigService().getRecordDataTTL() :
            getConfigService().getMetricsDataTTL();
        return LongStream
            .rangeClosed(0, ttl)
            .mapToObj(it -> TimeBucket.getTimeBucket(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(it), DownSampling.Day))
            .distinct()
            .collect(toList());
    }
}
