package ru.yandex.webmaster3.storage.searchurl.samples.dao;

import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.joda.time.Instant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.metrika.counters.MetrikaCountersUtil;
import ru.yandex.webmaster3.core.searchbase.SearchBaseDates;
import ru.yandex.webmaster3.core.searchbase.SearchBaseUpdateInfo;
import ru.yandex.webmaster3.core.sitestructure.RawSearchUrlStatusEnum;
import ru.yandex.webmaster3.core.sitestructure.SearchUrlStatusEnum;
import ru.yandex.webmaster3.core.sitestructure.SearchUrlStatusUtil;
import ru.yandex.webmaster3.core.util.IdUtils;
import ru.yandex.webmaster3.storage.AbstractFilter;
import ru.yandex.webmaster3.storage.TextFilterUtil;
import ru.yandex.webmaster3.storage.metrika.MetrikaCrawlStateService;
import ru.yandex.webmaster3.storage.searchurl.SearchUrlEventSampleField;
import ru.yandex.webmaster3.storage.searchurl.samples.dao.SearchUrlEventSamplesCHDao.F;
import ru.yandex.webmaster3.storage.searchurl.samples.data.SearchUrlCanonicalStatus;
import ru.yandex.webmaster3.storage.searchurl.samples.data.SearchUrlEventType;
import ru.yandex.webmaster3.storage.util.clickhouse2.condition.*;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * @author avhaliullin
 */
@Component
@Slf4j
public class SearchUrlEventConditions {
    private final MetrikaCrawlStateService metrikaCrawlStateService;

    @Autowired
    public SearchUrlEventConditions(MetrikaCrawlStateService metrikaCrawlStateService) {
        this.metrikaCrawlStateService = metrikaCrawlStateService;
    }

    public static Condition lastAccessCondition(Operator op, DateTime value) {
        return new TimestampCondition(F.LAST_ACCESS.getName(), op, value);
    }

    public static Condition searchBaseDateCondition(Operator op, DateTime value, SearchBaseDates searchBaseDates) {
        Map.Entry<Instant, SearchBaseUpdateInfo> baseEntry;
        Instant switchDate = value.toInstant();
        switch (op) {
            case LESS_THAN:
                baseEntry = searchBaseDates.getSwitchDate2Info().lowerEntry(switchDate);
                break;
            case LESS_EQUAL:
                baseEntry = searchBaseDates.getSwitchDate2Info().floorEntry(switchDate);
                break;
            case GREATER_THAN:
                baseEntry = searchBaseDates.getSwitchDate2Info().higherEntry(switchDate);
                break;
            case GREATER_EQUAL:
                baseEntry = searchBaseDates.getSwitchDate2Info().ceilingEntry(switchDate);
                break;
            default:
                throw new IllegalArgumentException("Unknown op " + op + " for search base date");
        }
        if (baseEntry == null) {
            return Condition.falseCondition();
        }
        return new TimestampCondition(F.SEARCH_BASE_DATE.getName(), op, baseEntry.getValue().getBaseCollectionDate().toDateTime());
    }

    public static Condition titleContains(String value) {
        return new TextContainsCondition(F.TITLE.getName(), value);
    }

    public static Condition pathContains(String value) {
        return new TextContainsCondition(F.PATH.getName(), value);
    }

    public static Condition eventTypeEquals(SearchUrlEventType eventType) {
        return new IntEqualCondition(F.EVENT_TYPE.getName(), eventType.value());
    }

    public static Condition urlStatusEquals(RawSearchUrlStatusEnum status) {
        return new IntEqualCondition(F.STATUS.getName(), status.value());
    }

    public static PathLikeConditionBuilder pathLikeConditionBuilder() {
        return new PathLikeConditionBuilder();
    }

    public static TextLikeCondition buildPathCondition(TextLikeCondition.Builder builder) {
        return builder.build(F.PATH.getName());
    }

    public static String getTitleFieldName() {
        return F.TITLE.getName();
    }

    public static String getUrlFieldName() {
        return F.PATH.getName();
    }

    public static String getSitemapFieldName() {
        return F.EX_IS_FROM_SITEMAP.getName();
    }

    public static String getTurboFieldName() {
        return F.EX_IS_TURBO.getName();
    }

    @SuppressWarnings("Duplicates")
    public static Condition canonicalStatusEquals(WebmasterHostId hostId, boolean fresh, SearchUrlCanonicalStatus canonicalStatus) {
        String hostUrl = IdUtils.hostIdToUrl(hostId);
        return new Condition(ConditionType.INT_EQUAL) {
            @Override
            public String toQuery() {
                switch (canonicalStatus) {
                    case UNDEFINED_CANONICAL:
                        return F.EX_REL_CANONICAL_TARGET.getName() + " == ''";
                    case CANONICAL:
                        return F.EX_REL_CANONICAL_TARGET.getName() + " == '" + hostUrl + "' || " + F.PATH.getName();
                    case NOT_CANONICAL:
                        return F.EX_REL_CANONICAL_TARGET.getName() + " <> '" + hostUrl + "' || " + F.PATH.getName() +
                                " AND " + F.EX_REL_CANONICAL_TARGET.getName() + " <> ''";
                    case FRESH_CANONICAL:
                        return fresh ? trueCondition().toQuery() : falseCondition().toQuery();
                }
                return null;
            }

            @Override
            public <T> Predicate<T> toPredicate(ConditionFieldExtractor<T> fieldExtractor) {
                return null;
            }
        };
    }


    public Condition makeCondition(WebmasterHostId hostId,
                                          List<? extends AbstractFilter<SearchUrlEventSampleField>> filters,
                                          boolean isFresh,
                                          SearchBaseDates searchBaseDates) {
        Condition condition = Condition.trueCondition();
        if (filters == null) {
            return condition;
        }
        for (AbstractFilter<SearchUrlEventSampleField> filter : filters) {
            Operator dateOp = Operator.fromFilterOperation(filter.getOperation());

            switch (filter.getIndicator()) {
                case TURBO:
                    condition = condition.andThen(new IntCondition(getTurboFieldName(), Operator.fromFilterOperation(filter.getOperation()), Integer.parseInt(filter.getValue())));
                    break;
                case LAST_ACCESS_DATE:
                    if (dateOp == null) {
                        throw filter.invalidFilterException();
                    } else {
                        condition = condition.andThen(SearchUrlEventConditions.lastAccessCondition(dateOp, filter.parseDate()));
                    }
                    break;
                case SEARCH_BASE_DATE:
                    if (dateOp == null) {
                        throw filter.invalidFilterException();
                    } else {
                        condition = condition.andThen(SearchUrlEventConditions.searchBaseDateCondition(dateOp, filter.parseDate(), searchBaseDates));
                    }
                    break;
                case TITLE:
                    condition = condition.andThen(TextFilterUtil.getTextCondition(filter, SearchUrlEventConditions.getTitleFieldName()));
                    break;
                case URL:
                    condition = condition.andThen(TextFilterUtil.getTextCondition(filter, SearchUrlEventConditions.getUrlFieldName()));
                    break;
                case EVENT_TYPE:
                    if (filter.getOperation() == AbstractFilter.Operation.EQUAL) {
                        SearchUrlEventType eventType = SearchUrlEventType.R.valueOf(filter.getValue());
                        condition = condition.andThen(SearchUrlEventConditions.eventTypeEquals(eventType));
                    } else {
                        throw filter.invalidFilterException();
                    }
                    break;
                case URL_STATUS:
                    if (filter.getOperation() == AbstractFilter.Operation.EQUAL) {
                        SearchUrlStatusEnum status = SearchUrlStatusEnum.R.valueOf(filter.getValue());
                        if (!isFresh) {
                            Set<RawSearchUrlStatusEnum> rawStatuses = SearchUrlStatusUtil.view2AllRaw(status);
                            // Пока что статусы мы показываем только для GONE урлов - поэтому на всякий случай дофильтровываем вручную
                            condition = condition.andThen(SearchUrlEventConditions.eventTypeEquals(SearchUrlEventType.GONE))
                                    .andThen(Condition.or(
                                            rawStatuses.stream().map(SearchUrlEventConditions::urlStatusEquals).collect(Collectors.toList())
                                    ));
                        } else {
                            //Для свежести пока есть только статус в поиске, в любом другом случае в свежесть не лезем.
                            condition = status == SearchUrlStatusEnum.INDEXED_SEARCHABLE ? condition.andThen(Condition.trueCondition()) : condition.andThen(Condition.falseCondition());
                        }
                    } else {
                        throw filter.invalidFilterException();
                    }
                    break;
                case CANONICAL_STATUS:
                    if (isFresh) {
                        condition = condition.andThen(Condition.falseCondition());
                    } else {
                        if (filter.getOperation() == AbstractFilter.Operation.EQUAL) {
                            SearchUrlCanonicalStatus status = SearchUrlCanonicalStatus.valueOf(filter.getValue());
                            condition = condition.andThen(SearchUrlEventConditions.canonicalStatusEquals(hostId, isFresh, status));
                            // only for NEW events
                            condition = condition.andThen(SearchUrlEventConditions.eventTypeEquals(SearchUrlEventType.NEW));
                        } else {
                            throw filter.invalidFilterException();
                        }
                    }
                    break;
                case FROM_SITEMAP:
                case VALID_FROM_METRIKA:
                    String domain = MetrikaCountersUtil.hostToPunycodeDomain(hostId);
                    boolean isMetrikaCrawlEnabled = metrikaCrawlStateService.getDomainCrawlStateCached(domain).hasEnabled();
                    condition = condition.andThen(ConditionUtils.makeValidFromMetrikaCondition(filter, isMetrikaCrawlEnabled, isFresh));
                    break;
                default:
                    throw new RuntimeException("Not implemented filter for indicator " + filter.getIndicator());
            }
        }
        return condition;
    }

    public static class PathLikeConditionBuilder {
        private final TextLikeCondition.Builder builder = TextLikeCondition.newBuilder();

        public PathLikeConditionBuilder append(String s) {
            builder.append(s);
            return this;
        }

        public PathLikeConditionBuilder appendWildcard() {
            builder.appendWildcard();
            return this;
        }

        public Condition build() {
            return builder.build(F.PATH.getName());
        }
    }
}
