package ru.yandex.webmaster3.storage.feeds.logs;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.base.Preconditions;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.apache.commons.lang3.tuple.Triple;
import org.codehaus.jackson.map.annotate.JsonDeserialize;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import ru.yandex.webmaster3.core.feeds.feed.FeedsErrorSeverity;
import ru.yandex.webmaster3.core.feeds.feed.NativeFeedStatus;
import ru.yandex.webmaster3.core.feeds.feed.NativeFeedType;
import ru.yandex.webmaster3.core.util.json.JsonMapping;
import ru.yandex.webmaster3.storage.clickhouse.ClickhouseTableInfo;
import ru.yandex.webmaster3.storage.clickhouse.TableProvider;
import ru.yandex.webmaster3.storage.clickhouse.TableType;
import ru.yandex.webmaster3.storage.feeds.logs.FeedsOffersLogsHistoryCHDao.FeedRecord;
import ru.yandex.webmaster3.storage.feeds.logs.FeedsOffersLogsHistoryCHDao.OfferErrorInfo;
import ru.yandex.webmaster3.storage.feeds.models.FeedStats;
import ru.yandex.webmaster3.storage.util.clickhouse2.AbstractClickhouseDao;
import ru.yandex.webmaster3.storage.util.clickhouse2.CHPrimitiveType;
import ru.yandex.webmaster3.storage.util.clickhouse2.CHRow;
import ru.yandex.webmaster3.storage.util.clickhouse2.CHTable;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.OrderBy;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.QueryBuilder;

import static java.util.Objects.requireNonNullElse;
import static ru.yandex.webmaster3.core.feeds.feed.NativeFeedStatus.FAILED;
import static ru.yandex.webmaster3.core.feeds.feed.NativeFeedStatus.IN_PROGRESS;
import static ru.yandex.webmaster3.core.feeds.feed.NativeFeedStatus.SUCCESS;

/**
 * Created by Oleg Bazdyrev on 16/06/2022.
 */
@Repository
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class GoodsOffersLogsHistoryCHDao extends AbstractClickhouseDao {

    public static final String TABLE_NAME = "goods_offers_logs_history";
    public static final CHTable TABLE = CHTable.builder()
            .database(DB_WEBMASTER3_FEEDS)
            .name(TABLE_NAME + "_%s")
            .partitionBy("cityHash64(partner_id) % 16")
            .keyField(F.BUSINESS_ID, CHPrimitiveType.Int64)
            .keyField(F.PARTNER_ID, CHPrimitiveType.Int64)
            .keyField(F.FEED_ID, CHPrimitiveType.Int64)
            .keyField(F.UPDATE_TIME, CHPrimitiveType.Int64)
            .field(F.ERRORS, CHPrimitiveType.String)
            .field(F.FEED_URL, CHPrimitiveType.String)
            .field(F.STATS, CHPrimitiveType.String)
            .field(F.STATUS, CHPrimitiveType.String)
            .build();

    private static final String RESULT_SUFFIX = "_r";
    private static final List<String> ALL_RESULT_FIELDS = TABLE.getFields().stream().map(field -> field.getName() + " as " + field.getName() + RESULT_SUFFIX)
            .collect(Collectors.toList());
    private static final Function<CHRow, GoodsFeedRecord> RECORD_MAPPER = chRow -> {
        final long ts = chRow.getLong(F.UPDATE_TIME + RESULT_SUFFIX);
        return new GoodsFeedRecord(
                chRow.getLong(F.BUSINESS_ID + RESULT_SUFFIX),
                chRow.getLong(F.PARTNER_ID + RESULT_SUFFIX),
                chRow.getLong(F.FEED_ID + RESULT_SUFFIX),
                new DateTime(ts),
                chRow.getString(F.FEED_URL + RESULT_SUFFIX),
                JsonMapping.readValue(chRow.getString(F.ERRORS + RESULT_SUFFIX), GoodsError.LIST_REFERENCE),
                JsonMapping.readValue(chRow.getString(F.STATS + RESULT_SUFFIX), GoodsFeedStats.class),
                GoodsFeedStatus.valueOf(chRow.getString(F.STATUS + RESULT_SUFFIX))
        );
    };

    private final TableProvider tableProvider;

    public List<GoodsFeedRecord> getHistory(String domain, Triple<Long, Long, Long> feed, DateTime fromDate, DateTime toDate) {
        Preconditions.checkArgument(fromDate != null && toDate != null);
        ClickhouseTableInfo table = tableProvider.getTable(TableType.GOODS_OFFERS_LOGS_HISTORY);
        var st = QueryBuilder.select(ALL_RESULT_FIELDS)
                .from(table.getLocalTableName())
                .where(QueryBuilder.eq(F.BUSINESS_ID, feed.getLeft()))
                .and(QueryBuilder.eq(F.PARTNER_ID, feed.getMiddle()))
                .and(QueryBuilder.eq(F.FEED_ID, feed.getRight()))
                .and(QueryBuilder.gte(F.UPDATE_TIME, fromDate.getMillis()))
                .and(QueryBuilder.lt(F.UPDATE_TIME, toDate.getMillis()))
                .orderBy(F.UPDATE_TIME, OrderBy.Direction.DESC);

        return getClickhouseServer().queryAll(table.chContext(getClickhouseServer(), String.valueOf(feed.getLeft())), st, RECORD_MAPPER);
    }

    public List<GoodsFeedRecord> getLastState(Collection<Triple<Long, Long, Long>> feeds) {
        if (feeds.isEmpty()) {
            return Collections.emptyList();
        }
        Map<Integer, List<Triple<Long, Long, Long>>> feedsByShard = feeds.stream().
                collect(Collectors.groupingBy(f -> getShard(String.valueOf(f.getLeft()))));
        ClickhouseTableInfo table = tableProvider.getTable(TableType.GOODS_OFFERS_LOGS_HISTORY);
        return feedsByShard.values().parallelStream().flatMap(list -> {
            var st = QueryBuilder.select(TABLE.getFields().stream()
                            .map(field -> QueryBuilder.argMax(field.getName(), F.UPDATE_TIME) + " as " + field.getName() + RESULT_SUFFIX)
                            .collect(Collectors.toList()))
                    .from(table.getLocalTableName())
                    .where(QueryBuilder.tripleIn(F.BUSINESS_ID, F.PARTNER_ID, F.FEED_ID, list))
                    .groupBy(F.BUSINESS_ID, F.PARTNER_ID, F.FEED_ID);
            return getClickhouseServer().queryAll(chContext(String.valueOf(list.get(0).getLeft())), st, RECORD_MAPPER).stream()
                    .filter(Objects::nonNull);
        }).collect(Collectors.toList());
    }

    public enum GoodsFeedStatus {
        UNKNOWN,
        COULD_NOT_DOWNLOAD,
        FATAL,
        ERROR,
        OK,
        FEED_NOT_MODIFIED,
        WARNING,
        ;

        public NativeFeedStatus getFeedStatus() {
            return switch (this) {
                case OK -> SUCCESS;
                case ERROR, WARNING -> NativeFeedStatus.WARNING;
                case FATAL, COULD_NOT_DOWNLOAD -> FAILED;
                case UNKNOWN, FEED_NOT_MODIFIED -> IN_PROGRESS;
            };
        }
    }

    public enum GoodsErrorLevel {
        WARNING,
        ERROR,
        FATAL,
    }

    public interface F {
        String BUSINESS_ID = "business_id";
        String PARTNER_ID = "partner_id";
        String FEED_ID = "feed_id";
        String UPDATE_TIME = "update_time";
        String ERRORS = "errors";
        String FEED_URL = "feed_url";
        String STATS = "stats";
        String STATUS = "status";
    }

    @Value
    @AllArgsConstructor(onConstructor_ = @JsonCreator)
    public static final class GoodsFeedRecord {
        long businessId;
        long partnerId;
        long feedId;
        DateTime updateTime;
        String feedUrl;
        List<GoodsError> errors;
        GoodsFeedStats stats;
        GoodsFeedStatus status;

        public FeedRecord toFeedRecord() {
            Long warningOffers = requireNonNullElse(this.stats.warningOffers, 0L);
            Long errorOffers = requireNonNullElse(this.stats.errorOffers, 0L);
            Long totalOffers = requireNonNullElse(this.stats.totalOffers, 0L);
            final FeedStats stats = new FeedStats(
                    errorOffers,
                    0L,
                    warningOffers,
                    totalOffers - warningOffers - errorOffers
            );
            // convert errors
            Map<String, List<OfferErrorInfo>> errors = new HashMap<>();
            for (GoodsError goodsError : this.errors) {
                Map<String, String> details = goodsError.details.stream()
                        .collect(Collectors.toMap(GoodsErrorDetails::getName, GoodsErrorDetails::getValue));
                final String code = "Goods." + goodsError.code;
                errors.computeIfAbsent(code, k -> new ArrayList<>()).add(
                        new OfferErrorInfo(code, "", goodsError.text, JsonMapping.OM.valueToTree(details),
                                goodsError.level == GoodsErrorLevel.FATAL
                                        ? FeedsErrorSeverity.FATAL
                                        : FeedsErrorSeverity.WARNING)
                );
            }
            var feedStatus = status.getFeedStatus();

            return new FeedRecord(null, feedUrl, NativeFeedType.STORES.getTypeOfferBase(), updateTime, updateTime, errors, stats, stats, feedStatus);
        }
    }

    @Value
    @AllArgsConstructor(onConstructor_ = @JsonCreator)
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static final class GoodsError {
        public static final TypeReference<List<GoodsError>> LIST_REFERENCE = new TypeReference<>() {
        };
        String code;
        @JsonDeserialize
        List<GoodsErrorDetails> details;
        GoodsErrorLevel level;
        String namespace;
        String text;
    }

    @Value
    @AllArgsConstructor(onConstructor_ = @JsonCreator)
    public static final class GoodsErrorDetails {
        String name;
        String value;
    }

    @Value
    @AllArgsConstructor(onConstructor_ = @JsonCreator)
    public static final class GoodsFeedStats {
        Long errorOffers;
        Long loadedOffers;
        Long totalOffers;
        Long unloadedOffers;
        Long warningOffers;
    }

}
