package ru.yandex.webmaster3.storage.niche2;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.util.IdUtils;
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.niche2.filters.FilterEntity;
import ru.yandex.webmaster3.storage.util.clickhouse2.AbstractClickhouseDao;
import ru.yandex.webmaster3.storage.util.clickhouse2.CHRow;
import ru.yandex.webmaster3.storage.util.clickhouse2.condition.Operator;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.From;
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.webmaster3.storage.util.clickhouse2.query.Select;
import ru.yandex.webmaster3.storage.util.clickhouse2.query.Where;

import static java.lang.Math.min;
import static ru.yandex.webmaster3.core.util.WwwUtil.cutWWWAndM;

@Repository
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@Slf4j
public class Niche2QueriesCHDao extends AbstractClickhouseDao {
    public static final String TABLE_NAME = "query_report";
    private static final int MAX_QUERIES_PER_LANDING = 10;
    private static final int MAX_SIMILAR_URLS_PER_LANDING = 10;
    private static final String NEW_MARKETS_GROUP_ID = "NEW_MARKETS";
    private final TableProvider tableProvider;

    private static final ObjectMapper OM = new ObjectMapper()
            .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
            .registerModules(new JodaModule(), new ParameterNamesModule());

    private static List<String> extractSimilarPages(List<String> similarPages) {
        if (similarPages == null) {
            return List.of();
        }
        return similarPages
                .stream()
                .distinct()
                .collect(Collectors.groupingBy(x -> IdUtils.withSchema(IdUtils.urlToHostId(x),
                        WebmasterHostId.Schema.HTTP)))
                .values()
                .stream()
                .map(strings -> strings.get(0))
                .limit(MAX_SIMILAR_URLS_PER_LANDING)
                .collect(Collectors.toList());
    }

    public List<QueryReportRecord> getQueries(WebmasterHostId hostId, String groupId, List<FilterEntity> filters,
                                              List<ProblemEnum> problems, int p, int pSize) {
        if (groupId.equals(NEW_MARKETS_GROUP_ID)) {
            return getQueriesForNewMarkets(hostId, groupId, filters, p, pSize);
        } else {
            return getQueriesForAnyGroup(hostId, groupId, filters, problems, p, pSize);
        }
    }

    private List<QueryReportRecord> getQueriesForNewMarkets(WebmasterHostId hostId, String groupId,
                                                            List<FilterEntity> filters,
                                                            int p, int pSize) {
        ClickhouseTableInfo table = tableProvider.getTable(TableType.NICHE2_QUERY_REPORT);
        String domain = cutWWWAndM(hostId);
//        здесь просто берем записи as it is и не показываем посадочную, то есть 1 запрос = 1 строка в выдаче
        Select select = QueryBuilder.select(F.DOMAIN, F.GROUP_NAME, F.QUERY, F.SHOWS,
                F.AVERAGE_POSITION, F.CLICKS, F.URL,
                F.PROBLEMS, F.SIMILAR_URLS, F.WEIGHT);
        Where where = createQueryFilter(select, filters);
        var baseline = where
                .and(QueryBuilder.eq(F.DOMAIN, domain))
                .and(QueryBuilder.eq(F.GROUP_NAME, groupId));
        var context = table.chContext(getClickhouseServer(), domain);
        var st = baseline
                .orderBy(List.of(Pair.of(F.WEIGHT, OrderBy.Direction.DESC),
                        Pair.of(F.SHOWS, OrderBy.Direction.DESC))) //вероятность совпасть WEIGHT +- 0, но всё равно
                // пусть будет
                .limit(p * pSize, pSize);
        return getClickhouseServer().queryAll(context, st.toString(), chRow -> {
            List<String> similarPages = List.of();
            try {
                similarPages = extractSimilarPages(JsonMapping.readValue(chRow.getString(F.SIMILAR_URLS),
                        JsonMapping.STRING_LIST_REFERENCE));
            } catch (WebmasterException exception) {
                log.error("Could not parse {}, {}", chRow.getString(F.SIMILAR_URLS), exception.getError());
            }
            return new QueryReportRecord(
                    List.of(new QueryInfo(chRow.getString(F.QUERY), chRow.getLong(F.SHOWS),
                            -1L, 0L)),
                    "", // не показываем посадочную
                    List.of(), // проблем посадочной быть не может, если нет посадочной
                    similarPages
            );
        });
    }

    private List<QueryReportRecord> getQueriesForAnyGroup(WebmasterHostId hostId, String groupId,
                                                          List<FilterEntity> filters,
                                                          List<ProblemEnum> problems, int p, int pSize) {
        ClickhouseTableInfo table = tableProvider.getTable(TableType.NICHE2_QUERY_REPORT);
        String domain = cutWWWAndM(hostId);
        var context = table.chContext(getClickhouseServer(), domain);
//        хотим получить структуру вида url, [[query, shows, position, clicks, [similar_url]]] отсортированную по
//        сумме показов, чтобы группировать по посадочной и показывать часть сводной информации
        Select select = QueryBuilder.select("key",
                "groupArray(array(" + escape(F.QUERY) + ", cast(" + F.SHOWS + " as String), cast(" + F.AVERAGE_POSITION + " " +
                        "as String), cast(" + F.CLICKS + " as String), " + escape(F.SIMILAR_URLS) + ")) " +
                        "as query_info",
                "sum(" + F.SHOWS + ") as total_shows",
                "any(" + F.PROBLEMS + ") as _problems");
        Where where = createQueryFilter(select, filters);
        var baseline = addProblems(where, problems)
                .and(QueryBuilder.eq(F.DOMAIN, domain))
                .and(QueryBuilder.eq(F.GROUP_NAME, groupId));
        var st =
                baseline
                        .groupBy("if(url != '', url, 'nohttps://' || " + "cast(generateUUIDv4() as String)" + ") as " +
                                "key")
                        .orderBy("total_shows", OrderBy.Direction.DESC)
                        .limit(p * pSize, pSize);
        return getClickhouseServer().queryAll(context, st.toString(), QueryReportRecord::build);
    }

    public int getQueriesCount(WebmasterHostId hostId, String groupId, List<FilterEntity> filters,
                               List<ProblemEnum> problems) {
        ClickhouseTableInfo table = tableProvider.getTable(TableType.NICHE2_QUERY_REPORT);
        String domain = cutWWWAndM(hostId);
        var context = table.chContext(getClickhouseServer(), domain);
        if (groupId.equals(NEW_MARKETS_GROUP_ID)) {
            Select select = QueryBuilder.select("count(*)");
            Where where = createQueryFilter(select, filters);
            var baseline = where
                    .and(QueryBuilder.eq(F.DOMAIN, domain))
                    .and(QueryBuilder.eq(F.GROUP_NAME, groupId));
            return getClickhouseServer().queryOne(context, baseline.toString(), chRow ->
                    (int) chRow.getLongUnsafe(0)).orElse(0);
        } else {
            Select select = QueryBuilder.select("count(distinct if(url != '', url, 'nohttps://' || cast" +
                    "(generateUUIDv4() as String)))");
            Where where = createQueryFilter(select, filters);
            var baseline = addProblems(where, problems)
                    .and(QueryBuilder.eq(F.DOMAIN, domain))
                    .and(QueryBuilder.eq(F.GROUP_NAME, groupId))
                    .and(QueryBuilder.eq("position(query, '\\'')", 0));
            return getClickhouseServer().queryOne(context, baseline.toString(), chRow ->
                    (int) chRow.getLongUnsafe(0)).orElse(0);
        }
    }

    private Where createQueryFilter(Select select, List<FilterEntity> filters) {
//        добавляем в ch-запрос фильтры по урлу/запросу
        var from = getFromQuery(select).where(QueryBuilder.trueCondition());
        if (filters != null) {
            for (FilterEntity filter : filters) {
                Operator op = Operator.fromFilterOperation(filter.getOperation());
                switch (filter.getIndicator()) {
                    case BY_QUERY:
                        if (op == Operator.TEXT_CONTAINS) {
                            if (filter.getValue() != null) {
                                from = from.and(QueryBuilder.containsSubstring(F.QUERY, filter.getValue()));
                            }
                        } else {
                            throw filter.invalidFilterException();
                        }
                        break;
                    case BY_URL:
                        if (op == Operator.TEXT_CONTAINS) {
                            if (filter.getValue() != null) {
                                from = from.and(QueryBuilder.containsSubstring(F.URL, filter.getValue()));
                            }
                        } else {
                            throw filter.invalidFilterException();
                        }
                        break;
                    default:
                        throw new RuntimeException("Not implemented filter for indicator " + filter.getIndicator());
                }
            }
        }
        return from;
    }

    private Where addProblems(Where where, List<ProblemEnum> problems) {
//        добавляем в ch-запрос фильтр по проблемам
        if (problems == null) {
            return where;
        }
        StringBuilder rawStatementString = new StringBuilder("(0");
        for (ProblemEnum problem : problems) {
            rawStatementString.append(" or ").append(QueryBuilder.containsSubstring(F.PROBLEMS, problem.name()));
        }
        rawStatementString.append(")");
        return where.and(new RawStatement(rawStatementString.toString()));
    }

    private From getFromQuery(Select select) {
//        генерируем начало ch-запроса
        ClickhouseTableInfo table = tableProvider.getTable(TableType.NICHE2_QUERY_REPORT);
        return select
                .from(table.getLocalTableName());
    }

    @Value
    public static class QueryInfo {
        String query;
        Long shows;
        Long averagePosition;
        Long clicks;

        public static QueryInfo build(List<String> rawQueryInfo) {
            var position = Long.parseLong(rawQueryInfo.get(2));
            if (position != -1) {
                position += 1; // в реальном мире нумерация с 1
            }
            if (rawQueryInfo.get(0).contains("\"")) {
                log.debug("contains \": {}", rawQueryInfo.get(0));
            }
            return new QueryInfo(rawQueryInfo.get(0), Long.valueOf(rawQueryInfo.get(1)),
                    position, Long.valueOf(rawQueryInfo.get(3)));
        }
    }


    @Value
    public static class QueryReportRecord {
        List<QueryInfo> queryInfos;
        String url;
        List<ProblemEnum> problems;
        List<String> similarPages;

        public static QueryReportRecord build(CHRow chRow) {
            String url = chRow.getString("key");
            if (url.startsWith("nohttps://")) {
                url = "";
            }
            log.info("url : {}", url);
            List<ProblemEnum> problems = List.of();
            try {
                problems = JsonMapping.readValue(chRow.getString("_problems"), PROBLEM_LIST_REFERENCE);
            } catch (WebmasterException exception) {
                log.error("Could not parse {}, {}", chRow.getString("_problems"), exception.getError());
            }
            log.info("problems : {}", problems);
            log.info("trying to read: {}", chRow.getString("query_info"));
            List<List<String>> queryInfosRaw;
            try {
                queryInfosRaw = OM.readValue(chRow.getString("query_info"),
                        LIST_LIST_REFERENCE);
            } catch (IOException e) {
//                если не спарсили, то по-честному надо кинуть ошибку и пойти смотреть что не так
                throw new WebmasterException("Could not parse",
                        new WebmasterErrorResponse.UnableToReadJsonRequestResponse(JsonMapping.class, e), e);
            }
            log.info("""
                    parsed:\s
                    url: {}
                    problems: {}
                    queryInfos: {}""", url, problems, queryInfosRaw);
            List<String> similarPages = new ArrayList<>();
            List<QueryInfo> queryInfos = new ArrayList<>();
            queryInfosRaw.forEach(rawQueryInfo -> {
                assert (rawQueryInfo.size() == 5);
                queryInfos.add(QueryInfo.build(rawQueryInfo));
                var similarUrlRaw = rawQueryInfo.get(4);
                log.debug("going to parse {}", similarUrlRaw);
                try {
                    var similarPagesRead = JsonMapping.readValue(
                            similarUrlRaw,
                            JsonMapping.STRING_LIST_REFERENCE);
                    log.debug("parsed {}", similarPagesRead);
                    if (similarPagesRead != null) {
                        similarPagesRead
                                .stream()
                                .filter(x -> !x.equals("null"))
                                .distinct()
                                .forEach(similarPages::add);
                    }
                } catch (WebmasterException exception) {
                    log.error("Could not parse {}, {}", similarUrlRaw, exception);
                }
            });
            queryInfos.sort((x, y) -> y.shows.compareTo(x.shows));
            log.debug("""
                    Got:
                    QueryInfos: {}
                    SimilarPages: {}
                    """, queryInfos, similarPages);
            return new QueryReportRecord(
                    queryInfos.subList(0, min(MAX_QUERIES_PER_LANDING, queryInfos.size())),
                    url,
                    problems,
                    extractSimilarPages(similarPages)
            );
        }
    }

    private String escape(String field) {
        return "replaceAll(replaceAll(" + field + ", '\\\\', '\\\\\\\\'), '\\'', '\\\\\\'')";
    }

    public interface F {
        String DOMAIN = "domain";
        String GROUP_NAME = "group_name";
        String QUERY = "query";
        String SHOWS = "shows";
        String AVERAGE_POSITION = "average_position";
        String CLICKS = "clicks";
        String URL = "url";
        String PROBLEMS = "problems";
        String SIMILAR_URLS = "similar_urls";
        String WEIGHT = "weight";
    }

    private static final TypeReference<List<List<String>>> LIST_LIST_REFERENCE = new TypeReference<>() {
    };

    private static final TypeReference<List<ProblemEnum>> PROBLEM_LIST_REFERENCE = new TypeReference<>() {
    };

    public enum ProblemEnum {
        NON_CANONICAL,
        DUPLICATION,
        BAD_RESPONSE,
        EMPTY_TITLE,
        EMPTY_META_DESCRIPTION,
        DUPLICATE_TITLE,
        DUPLICATE_META_DESCRIPTION,
    }

    @AllArgsConstructor
    @Getter
    public enum FilterEnum {
        BY_QUERY(F.QUERY),
        BY_URL(F.URL),
        ;
        private final String field;
    }
}
