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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
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.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import lombok.Value;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.joda.time.DateTime;
import org.springframework.stereotype.Repository;

import ru.yandex.autodoc.common.doc.annotation.Description;
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.IdUtils;
import ru.yandex.webmaster3.core.util.WwwUtil;
import ru.yandex.webmaster3.core.util.json.JsonMapping;
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;

/**
 * Created by Oleg Bazdyrev on 16/12/2021.
 */
@Repository
public class FeedsOffersLogsHistoryCHDao extends AbstractClickhouseDao {

    public static final String TABLE_NAME = "feeds_offers_logs_history";
    public static final CHTable TABLE = CHTable.builder()
            .database(DB_WEBMASTER3_FEEDS)
            .name("tmp_" + TABLE_NAME + "_%s")
            .partitionBy("cityHash64(feed) % 16")
            .keyField(F.HOST, CHPrimitiveType.String)
            .keyField(F.FEED, CHPrimitiveType.String)
            .keyField(F.TIMESTAMP, CHPrimitiveType.Int64)
            .field(F.STATS, CHPrimitiveType.String)
            .field(F.ERROR_STATS, CHPrimitiveType.String)
            .field(F.ERRORS, CHPrimitiveType.String)
            .field(F.LAST_ACCESS, CHPrimitiveType.Int64)
            .field(F.TYPE, 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, FeedRecord> RECORD_MAPPER = chRow -> {
        final long ts = chRow.getLong(F.TIMESTAMP + RESULT_SUFFIX);
        if (ts == 0L) {
            return null;
        }
        String errors = chRow.getString(F.ERRORS + RESULT_SUFFIX);
        String stats = chRow.getString(F.STATS + RESULT_SUFFIX);
        String errorStats = chRow.getString(F.ERROR_STATS + RESULT_SUFFIX);
        if (Strings.isNullOrEmpty(errorStats)) {
            errorStats = stats;
        }

        return new FeedRecord(chRow.getString(F.HOST + RESULT_SUFFIX), chRow.getString(F.FEED + RESULT_SUFFIX), chRow.getString(F.TYPE + RESULT_SUFFIX),
                new DateTime(chRow.getLong(F.LAST_ACCESS + RESULT_SUFFIX) * 1000L),
                new DateTime(ts * 1000L),
                errors.isEmpty() ? null : JsonMapping.readValue(errors, OfferErrorInfo.MAP_REFERENCE),
                stats.isEmpty() ? null : JsonMapping.readValue(stats, FeedStats.class),
                errorStats.isEmpty() ? null : JsonMapping.readValue(errorStats, FeedStats.class),
                null
        );
    };

    protected String getTableName() {
        return TABLE_NAME;
    }

    public List<FeedRecord> getHistory(String domain, String feedUrl, DateTime fromDate, DateTime toDate) {
        Preconditions.checkArgument(fromDate != null && toDate != null);
        var st = QueryBuilder.select(ALL_RESULT_FIELDS)
                .from(DB_WEBMASTER3_FEEDS, getTableName())
                .where(QueryBuilder.eq(F.FEED, feedUrl))
                .and(QueryBuilder.eq(F.HOST, domain))
                .and(QueryBuilder.gte(F.TIMESTAMP, fromDate.getMillis() / 1000L))
                .and(QueryBuilder.lt(F.TIMESTAMP, toDate.getMillis() / 1000L))
                .orderBy(F.TIMESTAMP, OrderBy.Direction.DESC);

        return getClickhouseServer().queryAll(chContext(domain), st, RECORD_MAPPER).stream()
                .collect(Collectors.collectingAndThen(Collectors.toCollection(() ->
                        new TreeSet<>(Comparator.comparing(FeedRecord::getTimestamp).reversed())), ArrayList::new)); // removing duplicates by timestamp
    }

    public List<FeedRecord> getLastState(Collection<String> feeds) {
        Map<Integer, List<Pair<String, String>>> feedsByShard = new HashMap<>();
        for (String feed : feeds) {
            String domain = WwwUtil.cutWWWAndM(IdUtils.urlToHostId(feed));
            feedsByShard.computeIfAbsent(getShard(domain), (k) -> new ArrayList<>()).add(Pair.of(domain, feed));
        }

        return feedsByShard.values().parallelStream().flatMap(list -> {
            Set<String> shardFeeds = list.stream().map(Pair::getRight).collect(Collectors.toSet());
            var st = QueryBuilder.select(TABLE.getFields().stream()
                            .map(field -> QueryBuilder.argMax(field.getName(), F.TIMESTAMP) + " as " + field.getName() + RESULT_SUFFIX)
                            .collect(Collectors.toList()))
                    .from(DB_WEBMASTER3_FEEDS, getTableName())
                    .where(QueryBuilder.in(F.HOST, list.stream().map(Pair::getLeft).collect(Collectors.toSet())))
                    .groupBy(F.HOST, F.FEED);
            return getClickhouseServer().queryAll(chContext(list.get(0).getLeft()), st, RECORD_MAPPER).stream()
                    .filter(Objects::nonNull)
                    .filter(record -> shardFeeds.contains(record.url));
        }).collect(Collectors.toList());
    }

    /**
     * Получение мапы фид/статус самых последних загруженных статусов
     *
     * @param domain
     * @param feeds
     * @return
     */
    public List<FeedRecord> getLastState(String domain, NativeFeedType feedType, Collection<String> feeds) {
        var st = QueryBuilder.select(TABLE.getFields().stream()
                        .map(field -> QueryBuilder.argMax(field.getName(), F.TIMESTAMP) + " as " + field.getName() + RESULT_SUFFIX)
                        .collect(Collectors.toList()))
                .from(DB_WEBMASTER3_FEEDS, getTableName())
                .where(QueryBuilder.eq(F.HOST, domain));
        if (feedType != null) {
            st = st.and(QueryBuilder.eq(F.TYPE, feedType.getTypeOfferBase()));
        }
        if (!CollectionUtils.isEmpty(feeds)) {
            st = st.and(QueryBuilder.in(F.FEED, feeds));
        }
        return getClickhouseServer().queryAll(chContext(domain), st.groupBy(F.HOST, F.FEED), RECORD_MAPPER)
                .stream().filter(Objects::nonNull).collect(Collectors.toList());
    }

    public interface F {
        String FEED = "feed";
        String HOST = "host";
        String TIMESTAMP = "timestamp";
        String STATS = "stats";
        String ERROR_STATS = "error_stats";
        String ERRORS = "errors";
        String LAST_ACCESS = "last_access";
        String TYPE = "type";
    }

    @Value
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class OfferErrorInfo {
        public static final TypeReference<Map<String, List<OfferErrorInfo>>> MAP_REFERENCE = new TypeReference<>() {
        };
        @Description("код ошибки")
        @JsonProperty("code_public")
        String code;
        @Description("url документа (оффера)")
        @JsonProperty("item_url")
        String url;
        @Description("описание ошибки (на английском)")
        @JsonProperty("message")
        String message;
        @Description("доп.инфа по ошибке")
        @JsonProperty("details")
        ObjectNode details;
        @Description("важность ошибки")
        @JsonProperty("severity")
        FeedsErrorSeverity severity;

        @JsonCreator
        public OfferErrorInfo(@JsonProperty("code_public") String code,
                              @JsonProperty("item_url") String url,
                              @JsonProperty("message") String message,
                              @JsonProperty("details") ObjectNode details,
                              @JsonProperty("severity") FeedsErrorSeverity severity) {
            this.code = code;
            this.url = url;
            this.message = message;
            this.details = details;
            this.severity = severity;
        }
    }

    @Value
    public static class FeedRecord {
        String domain;
        String url;
        String type;
        DateTime lastAccess;
        DateTime timestamp;
        Map<String, List<OfferErrorInfo>> errors;
        FeedStats stats;
        FeedStats errorStats;
        NativeFeedStatus status;
    }

}
