package ru.yandex.webmaster3.storage.sitemap.dao;

import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import lombok.RequiredArgsConstructor;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import ru.yandex.webmaster3.core.data.HttpCodeInfo;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.sitemap.HostSitemap;
import ru.yandex.webmaster3.core.sitemap.SitemapErrorCode;
import ru.yandex.webmaster3.core.sitemap.SitemapErrorInfo;
import ru.yandex.webmaster3.core.sitemap.SitemapInfo;
import ru.yandex.webmaster3.core.sitemap.raw.HostRelatedSitemaps.SitemapSource;
import ru.yandex.webmaster3.core.util.json.JsonMapping;
import ru.yandex.webmaster3.storage.abt.AbtService;
import ru.yandex.webmaster3.storage.abt.model.Experiment;
import ru.yandex.webmaster3.storage.clickhouse.TableProvider;
import ru.yandex.webmaster3.storage.clickhouse.TableType;
import ru.yandex.webmaster3.storage.util.clickhouse2.AbstractClickhouseDao;
import ru.yandex.webmaster3.storage.util.clickhouse2.CHField;
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 ru.yandex.webmaster3.storage.util.clickhouse2.query.RawStatement;
import ru.yandex.wmtools.common.util.http.YandexHttpStatus;

/**
 * Created by Oleg Bazdyrev on 14/07/2021.
 */
@Repository
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class SitemapsCHDao extends AbstractClickhouseDao {
    private final AbtService abtService;

    private static final UUID NULL_PARENT_SITEMAP_UUID = UUID.fromString("00000000-0000-4000-a000-000000000000");
    private static final int PARTITIONS_COUNT = 16;
    public static final CHTable TABLE = CHTable.builder()
            .database(DB_WEBMASTER3_INDEXING)
            .sharded(true)
            .parts(1)
            .name("sitemaps_%s")
            .partitionBy("cityHash64(" + F.HOST_ID + ") % " + PARTITIONS_COUNT)
            .keyField(F.HOST_ID, CHPrimitiveType.String)
            .keyField(F.ID, CHPrimitiveType.String)
            .keyField(F.PARENT_ID, CHPrimitiveType.String)
            .field(F.URL, CHPrimitiveType.String)
            .field(F.REFERRER, CHPrimitiveType.String)
            .field(F.HTTP_CODE, CHPrimitiveType.Int32)
            .field(F.LAST_ACCESS, CHPrimitiveType.Int64)
            .field(F.LAST_CHANGE, CHPrimitiveType.Int64)
            .field(F.LAST_URLS_CHANGE, CHPrimitiveType.Int64)
            .field(F.URL_COUNT, CHPrimitiveType.Int64)
            .field(F.ERROR_COUNT, CHPrimitiveType.Int64)
            .field(F.SOURCE_ID, CHPrimitiveType.String)
            .field(F.REDIR_TARGET, CHPrimitiveType.String)
            .field(F.IS_SITEMAP_PARSED, CHPrimitiveType.Int8)
            .field(F.IS_SITEMAP_INDEX, CHPrimitiveType.Int8)
            .field(F.URLS, CHPrimitiveType.String)
            .field(F.ERRORS, CHPrimitiveType.String)
            .field(F.CHILDREN_COUNT, CHPrimitiveType.Int64)
            .partsToThrowInsert(5000)
            .build();

    private static final List<String> ALL_FIELDS = TABLE.getFields().stream().map(CHField::getName).collect(Collectors.toList());

    private final TableProvider legacyMdbTableStorage;

    public List<HostSitemap> getSitemaps(WebmasterHostId hostId, UUID parentId, UUID fromId, int limit) {
        return getSitemapInfos(hostId, parentId, fromId, limit).stream().map(SitemapInfo::toHostSitemap).collect(Collectors.toList());
    }

    public List<SitemapInfo> getSitemapInfos(WebmasterHostId hostId, UUID parentId, UUID fromId, int limit) {
        var table = legacyMdbTableStorage.getTable(TableType.SITEMAPS);
        if (parentId == null) {
            parentId = NULL_PARENT_SITEMAP_UUID;
        }

        String subQueryForChild = "(" + F.PARENT_ID + " = '" + parentId + "' OR " + F.URL + " = (" +
                QueryBuilder.select(F.REDIR_TARGET).from(table.getLocalTableName())
                        .where(QueryBuilder.eq(F.HOST_ID, hostId))
                        .and(QueryBuilder.eq(F.ID, parentId)).limit(1).toString() +
                "))";
        var st = QueryBuilder.select(ALL_FIELDS)
                .from(table.getLocalTableName())
                .where(QueryBuilder.eq(F.HOST_ID, hostId))
                .and(new RawStatement(subQueryForChild));

        if (fromId != null) {
            var subquery = QueryBuilder.select(F.URL).from(table.getLocalTableName())
                    .where(QueryBuilder.eq(F.HOST_ID, hostId))
                    .and(QueryBuilder.eq(F.ID, fromId))
                    .and(QueryBuilder.eq(F.PARENT_ID, parentId));
            st = st.and(new RawStatement(F.URL + " > (" + subquery.toString() + ")"));
        }

        List<SitemapInfo> sitemapInfoList = getClickhouseServer().queryAll(table.chContext(getClickhouseServer(), hostId),
                st.orderBy(F.URL, OrderBy.Direction.ASC).limit(limit), SitemapsCHDao::rowToSitemapInfo);

        // Ждет поддержку ERR_INVALID_MIME_TYPE
        sitemapInfoList = sitemapInfoList.stream()
                .map(
                        sitemapInfo -> {
                            if (!abtService.isInExperiment(sitemapInfo.getHostId(), Experiment.SITEMAP_INVALID_MIMETYPE)) {
                                List<SitemapErrorInfo> errors = sitemapInfo
                                        .getErrors();
                                if (errors.removeIf(it -> it.getCode() == SitemapErrorCode.ERR_INVALID_MIME_TYPE)) {
                                    sitemapInfo =
                                            sitemapInfo.withErrorCount(sitemapInfo.getErrorCount() - 1).withErrors(errors);
                                }
                            }
                            return sitemapInfo;
                        }
                )
                .toList();

        return sitemapInfoList;
    }

    public Optional<SitemapInfo> getSitemap(WebmasterHostId hostId, UUID parentId, UUID id) {
        var table = legacyMdbTableStorage.getTable(TableType.SITEMAPS);
        var st = QueryBuilder.select(ALL_FIELDS)
                .from(table.getLocalTableName())
                .where(QueryBuilder.eq(F.HOST_ID, hostId))
                .and(QueryBuilder.eq(F.ID, id));
        if (parentId != null) {
            st = st.and(QueryBuilder.eq(F.PARENT_ID, parentId));
        }
        Optional<SitemapInfo> sitemapInfo = getClickhouseServer().queryOne(table.chContext(getClickhouseServer(), hostId), st, SitemapsCHDao::rowToSitemapInfo);

        // Ждет поддержку ERR_INVALID_MIME_TYPE
        if (sitemapInfo.isPresent() && !abtService.isInExperiment(sitemapInfo.get().getHostId(), Experiment.SITEMAP_INVALID_MIMETYPE)){
            List<SitemapErrorInfo> errors = sitemapInfo.get()
                    .getErrors();
            if (errors.removeIf(it -> it.getCode() == SitemapErrorCode.ERR_INVALID_MIME_TYPE)) {
                sitemapInfo = Optional.ofNullable(sitemapInfo.get().withErrorCount(sitemapInfo.get().getErrorCount() - 1).withErrors(errors));
            }
        }

        return sitemapInfo;
    }

    // TODO избавиться от всех упоминаний поколений
    private static SitemapInfo rowToSitemapInfo(CHRow row) {
        Set<SitemapSource> sources = EnumSet.noneOf(SitemapSource.class);
        for (int sourceId : JsonMapping.readValue(row.getString(F.SOURCE_ID), JsonMapping.INTEGER_LIST_REFERENCE)) {
            switch (sourceId) {
                case 0:
                case 4:
                    sources.add(SitemapSource.ROBOTS_TXT);
                    break;
                case 1:
                    sources.add(SitemapSource.WEBMASTER);
                    break;
                case 2:
                    sources.add(SitemapSource.SITEMAP_INDEX);
                    break;
                default:
                    sources.add(SitemapSource.UNKNOWN);
                    break;
            }
        }
        return SitemapInfo.builder()
                .hostId(row.getHostId(F.HOST_ID))
                .id(row.getStringUUID(F.ID))
                .parentId(row.getStringUUID(F.PARENT_ID))
                .url(row.getString(F.URL))
                .referrer(row.getString(F.REFERRER))
                .httpCodeInfo(HttpCodeInfo.fromHttpStatus(YandexHttpStatus.parseCode(row.getInt(F.HTTP_CODE))))
                .lastAccess(new DateTime(row.getLong(F.LAST_ACCESS) * 1000L))
                .lastChange(new DateTime(row.getLong(F.LAST_CHANGE) * 1000L))
                .lastUrlsChange(new DateTime(row.getLong(F.LAST_URLS_CHANGE) * 1000L))
                .urlCount(row.getLong(F.URL_COUNT))
                .errorCount(row.getLong(F.ERROR_COUNT))
                .sources(sources)
                .redirTarget(row.getString(F.REDIR_TARGET))
                .parsed(row.getInt(F.IS_SITEMAP_PARSED) == 1)
                .index(row.getInt(F.IS_SITEMAP_INDEX) == 1)
                .urls(JsonMapping.readValue(row.getString(F.URLS), JsonMapping.STRING_LIST_REFERENCE))
                .errors(JsonMapping.readValue(row.getString(F.ERRORS), SitemapErrorInfo.LIST_REFERENCE))
                .errorCount(row.getLong(F.ERROR_COUNT))
                .childrenCount(row.getLong(F.CHILDREN_COUNT))
                .build();
    }

    public interface F {
        String HOST_ID = "host_id";
        String ID = "id";
        String PARENT_ID = "parent_id";
        String URL = "url";
        String REFERRER = "referrer";
        String HTTP_CODE = "http_code";
        String LAST_ACCESS = "last_access";
        String LAST_CHANGE = "last_change";
        String LAST_URLS_CHANGE = "last_urls_change";
        String URL_COUNT = "url_count";
        String ERROR_COUNT = "error_count";
        String SOURCE_ID = "source_id";
        String REDIR_TARGET = "redir_target";
        String IS_SITEMAP_PARSED = "is_sitemap_parsed";
        String IS_SITEMAP_INDEX = "is_sitemap_index";
        String URLS = "urls";
        String ERRORS = "errors";
        String CHILDREN_COUNT = "children_count";
    }
}
