package ru.yandex.webmaster3.core.domain;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.core.io.Resource;
import ru.yandex.webmaster3.core.util.IdUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

/**
 * Created by ifilippov5 on 09.11.17.
 * https://publicsuffix.org/list/
 */
public class PublicSuffixListCacheService {
    private static final Logger log = LoggerFactory.getLogger(PublicSuffixListCacheService.class);

    private static final String WILDCARD = "*";
    private static final String EXCLUSION = "!";
    private static final String COMMENT = "//";

    private Resource publicSuffixListData;
    private Resource webmasterSuffixList;
    private List<SpecialDomain> rules;
    private List<SpecialDomain> exclusionRules;

    private final Lock updateLock = new ReentrantLock();

    private void lazyInit() throws IOException {
        if (rules != null && exclusionRules != null) {
            return;
        }
        updateLock.lock();
        try {
            if (rules != null && exclusionRules != null) {
                return;
            }
            log.info("Download rules...");
            resolveRules();
        } finally {
            updateLock.unlock();
        }
    }

    public void resolveRules() throws IOException {
        rules = new ArrayList<>();
        exclusionRules = new ArrayList<>();
        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(publicSuffixListData.getInputStream(), StandardCharsets.UTF_8))) {
            readRules(br);
        }

        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(webmasterSuffixList.getInputStream(), StandardCharsets.UTF_8))) {
            readRules(br);
        }
    }

    private void readRules(BufferedReader br) throws IOException {
        String line;

        while ((line = br.readLine()) != null) {
            line = StringUtils.trimToEmpty(line);
            if (line.startsWith(COMMENT)) {
                continue;
            }

            boolean exclusion = line.startsWith(EXCLUSION);
            if (exclusion) {
                exclusionRules.add(SpecialDomain.create(line.substring(1)));
                continue;
            }

            SpecialDomain specialDomain = SpecialDomain.create(line);
            if (specialDomain == null) {
                continue;
            }
            if (specialDomain.size() > 1) { //skip TLD
                rules.add(specialDomain);
            }
        }
    }

    public String getOwner(String fullDomain) throws IOException {
        lazyInit();
        SpecialDomain specialDomain = SpecialDomain.create(fullDomain);
        if (specialDomain == null) {
            return null;
        }
        int ownerStart = getOwnerStart(specialDomain);
        if (ownerStart < 0) {
            return null;
        }
        return specialDomain.createOwner(ownerStart);
    }

    private int getOwnerStart(SpecialDomain domain) {
        int ownerLabelStart = Integer.MAX_VALUE;
        for (SpecialDomain exclusionRule : exclusionRules) { //prevailing rule is a exception rule
            if (matches(domain, exclusionRule)) {
                ownerLabelStart = domain.size() - exclusionRule.size();//without additional label at front
                break;
            }
        }
        if (ownerLabelStart == Integer.MAX_VALUE) { //если не матчит какое-то из exception rules
            for (SpecialDomain rule : rules) {
                if (matches(domain, rule)) {
                    ownerLabelStart = Math.min(ownerLabelStart, domain.size() - rule.size() - 1); //with additional label at front
                }
            }
        }

        if (ownerLabelStart == Integer.MAX_VALUE) { // If no rules match, the prevailing rule is "*".
            // return TLD
            ownerLabelStart = domain.size() - 2;
        }
        if (ownerLabelStart < 0) {
            return -1;
        }
        return ownerLabelStart;
    }

    private boolean matches(SpecialDomain domain, SpecialDomain rule) {
        if (rule.size() > domain.size()) {
            return false;
        }
        for (int i = 0;i < rule.size();i++) {
            int ruleGroupIndex = rule.size() - 1 - i;
            int domainGroupIndex = domain.size() - 1 - i;
            if (!rule.getGroup(ruleGroupIndex).equals(domain.getGroup(domainGroupIndex))
                    && !rule.getGroup(ruleGroupIndex).equals(WILDCARD)) {
                return false;
            }
        }
        return true;
    }

    @Required
    public void setPublicSuffixListData(Resource publicSuffixListData) {
        this.publicSuffixListData = publicSuffixListData;
    }

    @Required
    public void setWebmasterSuffixList(Resource webmasterSuffixList) {
        this.webmasterSuffixList = webmasterSuffixList;
    }

    /**
     * Описывает два типа объектов:
     *  example.example.example - обычный домен
     *  example.*.*.example - правило
     * Хранит группы символов между точками
     */
    static class SpecialDomain {
        private static String DELIMITER = "\\.";
        private final String[] groups;

        private SpecialDomain(String[] groups) {
            this.groups = groups;
        }

        public String getGroup(int index) {
            return groups[index];
        }

        public String[] getGroups() {
            return groups;
        }

        public int size() {
            return groups.length;
        }

        public String createOwner(int ownerStart) {
            return Arrays
                    .stream(groups)
                    .skip(ownerStart)
                    .collect(Collectors.joining("."));
        }

        public static String normalize(String str) {
            return IdUtils.IDN.toASCII(str).toLowerCase();
        }

        private static boolean invalid(String str) {
            return Arrays.stream(str.split(DELIMITER)).anyMatch(String::isEmpty);
        }

        public static SpecialDomain create(String str) {
            if (invalid(str)) {
                return null;
            }
            return new SpecialDomain(normalize(str).split(DELIMITER));
        }
    }
}
