package ru.yandex.webmaster3.viewer.regions;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import ru.yandex.webmaster3.core.data.L10nEnum;
import ru.yandex.webmaster3.core.data.W3RegionInfo;
import ru.yandex.webmaster3.core.regions.RegionUtils;
import ru.yandex.webmaster3.core.regions.W3RegionsTreeService;
import ru.yandex.webmaster3.storage.util.Cf;
import ru.yandex.webmaster3.viewer.regions.data.RegionWithParents;
import ru.yandex.webmaster3.viewer.util.suggest.*;
import ru.yandex.wmtools.common.error.InternalException;

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * @author avhaliullin
 */
public class RegionsIndexService {
    private static final Logger log = LoggerFactory.getLogger(RegionsIndexService.class);
    public static final Set<Integer> VISIBLE_REGION_TYPES = ru.yandex.common.util.collections.Cf.set(2, 3, 4, 5, 6, 7, 10, 12);
    public static final Predicate<W3RegionInfo> VISIBILITY_PREDICATE = RegionUtils
            .filterTypeIn(VISIBLE_REGION_TYPES).or(RegionUtils.FILTER_ALLOW_REGION_NOT_SPECIFIED);

    private static final Map<Integer, L10nEnum> COUNTRIES_TO_LOCALE = new HashMap<Integer, L10nEnum>() {{
        put(225, L10nEnum.RU);
        put(187, L10nEnum.UK);
        put(149, L10nEnum.BE);
        put(159, L10nEnum.KK);
        put(983, L10nEnum.TR);
    }};
    private static final List<L10nEnum> ENUMS_PRIORITY_ORDER =
            Cf.immutableList(L10nEnum.RU, L10nEnum.UK, L10nEnum.BE, L10nEnum.KK, L10nEnum.EN, L10nEnum.TR);
    private final Map<L10nEnum, SuggestIndex<RegionWithParents>> indexMap = new EnumMap<>(L10nEnum.class);
    private final Map<String, List<RegionWithParents>> exactMatch = new HashMap<>();
    private final Map<L10nEnum, Map<String, List<RegionWithParents>>> localizedExactMatch = new EnumMap<>(L10nEnum.class);

    private W3RegionsTreeService w3regionsTreeService;

    public List<RegionWithParents> searchRegions(String query, L10nEnum l10n, int limit) {
        SimpleSuggestResultBuilder<RegionWithParents, Integer> resultBuilder = SuggestResultBuilder.create(limit, RegionWithParents::getId);
        String lcQuery = query.toLowerCase(l10n.getLocale());
        List<RegionWithParents> localizedExactMatchRegions = localizedExactMatch.get(l10n).get(lcQuery);
        if (localizedExactMatchRegions != null) {
            resultBuilder.addAll("Localized exact match", localizedExactMatchRegions, true);
        }
        List<RegionWithParents> exactMatchRegions = exactMatch.get(lcQuery);
        if (exactMatchRegions != null) {
            resultBuilder.addAll("Exact match", exactMatchRegions, true);
        }
        indexMap.get(l10n).fillResults(lcQuery, resultBuilder);
        return resultBuilder.getRegions();
    }

    public void init() throws InternalException {
        buildIndex();
    }

    private L10nEnum localeForCountry(int id) {
        L10nEnum l10n = COUNTRIES_TO_LOCALE.get(id);
        if (l10n == null) {
            l10n = L10nEnum.EN;
        }
        return l10n;
    }

    private int setTopLevelRegion(Map<Integer, Integer> child2TopLevel, W3RegionInfo region) {
        int id = region.getId();

        if (RegionUtils.RegionTypes.COUNTRY_LIKE.contains(region.getType())) {
            return id;
        }

        Integer res = child2TopLevel.get(id);
        if (res != null) {
            return res;
        }
        W3RegionInfo parent = w3regionsTreeService.getVisibleParent(id, VISIBILITY_PREDICATE);
        if (parent == null || parent == region) {
            res = -1;
        } else {
            res = setTopLevelRegion(child2TopLevel, parent);
        }
        child2TopLevel.put(id, res);
        return res;
    }

    private String regionWithParents2Name(L10nEnum l10n, RegionWithParents regionWithParents) {
        W3RegionInfo regionInfo = w3regionsTreeService.getExactRegionInfo(regionWithParents.getId());
        String name = regionInfo.getNames().get(l10n);
        if (name == null || name.isEmpty()) {
            return null;
        }
        StringBuilder sb = new StringBuilder(name);
        for (int parent : regionWithParents.getParents()) {
            W3RegionInfo parentInfo = w3regionsTreeService.getExactRegionInfo(parent);
            String parentName = parentInfo.getNames().get(l10n);
            if (parentName == null || parentName.isEmpty()) {
                return null;
            }
            sb.append("\n").append(parentName);
        }
        return sb.toString();
    }

    private void buildIndex() throws InternalException {
        long buildIndexStartedAt = System.currentTimeMillis();
        log.info("Started regions building process");

        Map<Integer, Integer> region2TopLevelRegion = new HashMap<>();

        Map<L10nEnum, Map<L10nEnum, MultiprefixTree<RegionWithParents>>> country2Lang2Index = new EnumMap<>(L10nEnum.class);
        Map<L10nEnum, MultiprefixTree<RegionWithParents>> noCountryLang2Index = new EnumMap<>(L10nEnum.class);

        Map<Integer, RegionWithParents> region2RegionWithParents = new HashMap<>();

        for (L10nEnum countryLocale : L10nEnum.values()) {
            localizedExactMatch.put(countryLocale, new HashMap<>());
            noCountryLang2Index.put(countryLocale, new MultiprefixTree<>("No country, language: " + countryLocale));
            Map<L10nEnum, MultiprefixTree<RegionWithParents>> lang2Index = new EnumMap<>(L10nEnum.class);
            country2Lang2Index.put(countryLocale, lang2Index);
            for (L10nEnum langLocale : L10nEnum.values()) {
                MultiprefixTree<RegionWithParents> index = new MultiprefixTree<>("Country: " + countryLocale + ", language: " + langLocale);
                lang2Index.put(langLocale, index);
            }
        }

        log.info("Building prefix trees");
        w3regionsTreeService.getAllVisibleRegions(VISIBILITY_PREDICATE).forEach(node -> {
            region2RegionWithParents.put(node.getId(), new RegionWithParents(node.getId(), new ArrayList<>(0)));

            int tlRegion = setTopLevelRegion(region2TopLevelRegion, node);
            L10nEnum countryLocale = localeForCountry(tlRegion);

            Map<L10nEnum, MultiprefixTree<RegionWithParents>> lang2Index = countryLocale == null ? noCountryLang2Index : country2Lang2Index.get(countryLocale);
            RegionWithParents regionWithParents = region2RegionWithParents.get(node.getId());
            for (L10nEnum lang : ENUMS_PRIORITY_ORDER) {
                String localizedName = node.getNames().get(lang);
                if (localizedName == null || localizedName.isEmpty()) {
                    continue;
                }

                localizedName = localizedName.toLowerCase(lang.getLocale());
                localizedExactMatch.get(lang).computeIfAbsent(localizedName, s -> new ArrayList<>()).add(regionWithParents);
                exactMatch.computeIfAbsent(localizedName, s -> new ArrayList<>()).add(regionWithParents);

                lang2Index.get(lang).addResult(localizedName, regionWithParents);
            }
        });
        log.info("Finished building prefix trees");
        log.info("Searching and fixing non-distinct region names");

        Map<String, Set<Integer>> regionName2Id = new HashMap<>();
        Set<Integer> nonDistinctRegions = new HashSet<>();
        for (int regionId : region2RegionWithParents.keySet()) {
            W3RegionInfo regionInfo = w3regionsTreeService.getExactRegionInfo(regionId);
            for (L10nEnum l10n : L10nEnum.values()) {
                String name = regionInfo.getNames().get(l10n);
                if (name == null || name.isEmpty()) {
                    continue;
                }
                Set<Integer> regionsWithName = regionName2Id.computeIfAbsent(name, x -> new TreeSet<>(Comparator.<Integer>naturalOrder()));
                regionsWithName.add(regionId);
                if (regionsWithName.size() > 1) {
                    nonDistinctRegions.addAll(regionsWithName);
                }
            }
        }
        boolean hasChanges = true;
        while (!nonDistinctRegions.isEmpty() && hasChanges) {
            hasChanges = false;
            Set<Integer> newNonDistinct = new HashSet<>();
            Map<String, Set<Integer>> fullName2Regions = new HashMap<>();

            for (int regionId : nonDistinctRegions) {
                int lastUsedParent = regionId;
                RegionWithParents regionWithParents = region2RegionWithParents.get(regionId);
                List<Integer> parents = regionWithParents.getParents();
                if (!parents.isEmpty()) {
                    lastUsedParent = parents.get(parents.size() - 1);
                }
                W3RegionInfo nextParent = w3regionsTreeService.getVisibleParent(lastUsedParent, VISIBILITY_PREDICATE);
                if (nextParent != null && nextParent != RegionUtils.WHOLE_WORLD_REGION) {
                    hasChanges = true;
                    parents.add(nextParent.getId());
                    for (L10nEnum l10n : L10nEnum.values()) {
                        String fullName = regionWithParents2Name(l10n, regionWithParents);
                        if (fullName != null) {
                            Set<Integer> regionsWithSameFullName = fullName2Regions.computeIfAbsent(fullName, x -> new TreeSet<>(Comparator.<Integer>naturalOrder()));
                            regionsWithSameFullName.add(regionId);
                            if (regionsWithSameFullName.size() > 1) {
                                newNonDistinct.addAll(regionsWithSameFullName);
                            }
                        }
                    }
                }
            }
            nonDistinctRegions = newNonDistinct;
        }

        if (!nonDistinctRegions.isEmpty()) {
            log.warn("Have regions with non-distinct full names: {}", nonDistinctRegions.stream()
                    .map(Object::toString).collect(Collectors.joining(",")));
        }
        log.info("Finished fixing non-distinct region names");

        for (L10nEnum l10n : L10nEnum.values()) {
            LinkedHashSet<SuggestIndex<RegionWithParents>> indexesSeq = new LinkedHashSet<>();
            indexesSeq.add(country2Lang2Index.get(l10n).get(l10n));
            for (L10nEnum x : ENUMS_PRIORITY_ORDER) {
                indexesSeq.add(country2Lang2Index.get(l10n).get(x));
            }

            for (L10nEnum x : ENUMS_PRIORITY_ORDER) {
                indexesSeq.add(country2Lang2Index.get(x).get(l10n));
            }
            indexesSeq.add(noCountryLang2Index.get(l10n));

            for (L10nEnum x : ENUMS_PRIORITY_ORDER) {
                indexesSeq.add(country2Lang2Index.get(x).get(x));
            }

            for (L10nEnum x : ENUMS_PRIORITY_ORDER) {
                for (L10nEnum y : ENUMS_PRIORITY_ORDER) {
                    indexesSeq.add(country2Lang2Index.get(x).get(y));
                }
                indexesSeq.add(noCountryLang2Index.get(x));
            }
            indexMap.put(l10n, new SuggestMetaIndex<>(Cf.immutableList(indexesSeq)));
        }
        log.info("Regions index built in {} ms", System.currentTimeMillis() - buildIndexStartedAt);
    }

    @Required
    public void setW3regionsTreeService(W3RegionsTreeService w3regionsTreeService) {
        this.w3regionsTreeService = w3regionsTreeService;
    }

}
