package ru.yandex.webmaster3.core.util;

import com.google.common.base.Strings;
import com.ibm.icu.text.IDNA;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.wmtools.common.SupportedProtocols;
import ru.yandex.wmtools.common.util.uri.URI2;
import ru.yandex.wmtools.common.util.uri.UriUtils;
import ru.yandex.wmtools.common.util.uri.WebmasterUriUtils;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author aherman
 */
public class IdUtils {
    /**
     * WMCSUPPORT-2494: стандартные методы из класса IDN почему то кидают исключение
     * для эмодзи внутри доменного имени, поэтому вместо них используем методы из ICU.
     */
    public static final Idn IDN = new Idn();

    private static final WebmasterHostId.Schema DEFAULT_SCHEMA = WebmasterHostId.Schema.HTTP;

    public static String hostIdToUrl(WebmasterHostId hostId) {
        StringBuilder sb = new StringBuilder();
        sb.append(hostId.getSchema().getSchemaPrefix());
        sb.append(hostId.getPunycodeHostname());
        if (!hostId.isDefaultPort()) {
            sb.append(':').append(hostId.getPort());
        }
        return sb.toString();
    }

    public static String hostIdToReadableUrl(WebmasterHostId hostId) {
        StringBuilder sb = new StringBuilder();
        sb.append(hostId.getSchema().getSchemaPrefix());
        sb.append(hostId.getReadableHostname());
        if (!hostId.isDefaultPort()) {
            sb.append(':').append(hostId.getPort());
        }
        return sb.toString();
    }

    public static WebmasterHostId urlToHostId(String urlString) {
        try {
            return urlToHostId(SupportedProtocols.getURL(urlString.trim()));
        } catch (MalformedURLException | URISyntaxException | SupportedProtocols.UnsupportedProtocolException e) {
            throw new WebmasterException("Unable to parse url: " + urlString, new WebmasterErrorResponse.InternalUnknownErrorResponse(
                    IdUtils.class, "Unable to parse url: " + urlString
            ));
        }
    }

    /***
     * Сейчас метод вызывается только из ручек AddHostAction
     */
    public static WebmasterHostId urlToHostIdWithUpgradedValidation(String urlString) {
        try {
            return urlToHostIdWithUpgradedValidation(SupportedProtocols.getURL(urlString.trim()));
        } catch (MalformedURLException | URISyntaxException | SupportedProtocols.UnsupportedProtocolException e) {
            throw new WebmasterException("Unable to parse url: " + urlString, new WebmasterErrorResponse.InternalUnknownErrorResponse(
                    IdUtils.class, "Unable to parse url: " + urlString
            ));
        }
    }

    /***
     * Убирает у хоста лишние точки на конце перед проверкой. Сейчас метод вызывается только из ручек AddHostAction
     */
    public static WebmasterHostId urlToHostIdWithUpgradedValidation(URL url) {
        String protocol = url.getProtocol();
        String host = url.getHost();
        int port = url.getPort();
        WebmasterHostId.Schema schema;
        if (WebmasterHostId.Schema.HTTP.getSchemaName().equalsIgnoreCase(protocol)) {
            schema = WebmasterHostId.Schema.HTTP;
        } else if (WebmasterHostId.Schema.HTTPS.getSchemaName().equalsIgnoreCase(protocol)) {
            schema = WebmasterHostId.Schema.HTTPS;
        } else {
            throw new WebmasterException("Unknown schema: " + url, new WebmasterErrorResponse.InternalUnknownErrorResponse(
                    IdUtils.class, "Unknown schema: " + url
            ));
        }
        if (port < 0) {
            port = schema.getDefaultPort();
        }
        host = IDN.toASCII(host);
        host = cutLastDots(host);
        UriUtils.verifyASCIIDomain(host);
        return new WebmasterHostId(schema, host, port);
    }

    public static WebmasterHostId urlToHostId(URL url) {
        String protocol = url.getProtocol();
        String host = url.getHost();
        int port = url.getPort();
        WebmasterHostId.Schema schema;
        if (WebmasterHostId.Schema.HTTP.getSchemaName().equalsIgnoreCase(protocol)) {
            schema = WebmasterHostId.Schema.HTTP;
        } else if (WebmasterHostId.Schema.HTTPS.getSchemaName().equalsIgnoreCase(protocol)) {
            schema = WebmasterHostId.Schema.HTTPS;
        } else {
            throw new WebmasterException("Unknown schema: " + url, new WebmasterErrorResponse.InternalUnknownErrorResponse(
                    IdUtils.class, "Unknown schema: " + url
            ));
        }
        if (port < 0) {
            port = schema.getDefaultPort();
        }
        host = IDN.toASCII(host);
        UriUtils.verifyASCIIDomain(host);
        return new WebmasterHostId(schema, host, port);
    }

    public static WebmasterHostId urlToHostId(URI uri) {
        String rawSchema = uri.getScheme();
        String host = uri.getHost();
        int port = uri.getPort();
        WebmasterHostId.Schema schema;
        if (WebmasterHostId.Schema.HTTP.getSchemaName().equalsIgnoreCase(rawSchema)) {
            schema = WebmasterHostId.Schema.HTTP;
        } else if (WebmasterHostId.Schema.HTTPS.getSchemaName().equalsIgnoreCase(rawSchema)) {
            schema = WebmasterHostId.Schema.HTTPS;
        } else {
            throw new WebmasterException("Unknown schema: " + uri, new WebmasterErrorResponse.InternalUnknownErrorResponse(
                    IdUtils.class, "Unknown schema: " + uri
            ));
        }
        if (port < 0) {
            port = schema.getDefaultPort();
        }
        host = IDN.toASCII(host);
        UriUtils.verifyASCIIDomain(host);
        return new WebmasterHostId(schema, host, port);
    }

    /**
     * Checks scheme and www prefix of url and converts according to given hostId.
     * <p/>
     * Example: hostId = https:www.site.ru:443, url = site.ru, result = https://www.site.ru
     *
     * @param hostId webmaster host id
     * @param url    normalizing url
     * @return
     */
    public static URL normalizeUrl(WebmasterHostId hostId, URL url) throws MalformedURLException {
        String newHostName = url.getHost();
        if (url.getHost() != null) {
            boolean isWwwUrl = url.getHost().toLowerCase().startsWith("www.") || "www".equalsIgnoreCase(url.getHost());
            if (isWwwUrl ^ hostId.getPunycodeHostname().startsWith("www.")) {
                if (isWwwUrl) {
                    newHostName = newHostName.substring("www.".length());
                } else {
                    newHostName = "www." + url.getHost();
                }
            }
        }
        String protocol = hostId.getSchema().getSchemaName();
        return new URL(protocol, newHostName, url.getPort(), url.getFile());
    }

    /**
     * Аналог YQL-ного Url::Normalize
     * @param url
     * @return
     */
    public static String normalizeUrl(String url) {
        if (Strings.isNullOrEmpty(url)) {
            return url;
        }
        URI2 uri2 = UriUtils.toUri(url,
                UriUtils.UriFeature.DEFAULT_SCHEME_HTTP,
                UriUtils.UriFeature.USE_PUNYCODED_HOSTNAME
        );
        return uri2.toUriString();
    }

    public static String hostIdToWebIdString(WebmasterHostId hostId) {
        WebmasterHostId.Schema schema = hostId.getSchema();
        String readableHostname = hostId.getReadableHostname();
        int port = hostId.getPort();

        return schema.getSchemaName() + ":" + readableHostname + ":" + port;
    }

    /**
     * <p>
     * Format: <code>schema:punycodeHostname:port</code>
     * </p>
     *
     * @param hostIdStr
     * @return
     */
    public static WebmasterHostId stringToHostId(String hostIdStr) {
        hostIdStr = hostIdStr.trim();

        int schemaEnd = hostIdStr.indexOf(':');
        if (schemaEnd < 0) {
            throw new IllegalArgumentException("Unknown hostId: " + hostIdStr);
        }
        WebmasterHostId.Schema schema = null;
        String schemaStr = hostIdStr.substring(0, schemaEnd);
        for (WebmasterHostId.Schema sch : WebmasterHostId.Schema.values()) {
            if (sch.getSchemaName().equals(schemaStr)) {
                schema = sch;
                break;
            }
        }
        if (schema == null) {
            throw new IllegalArgumentException("Unknown hostId: " + hostIdStr);
        }

        int hostnameEnd = hostIdStr.indexOf(':', schemaEnd + 1);
        if (hostnameEnd < 0) {
            throw new IllegalArgumentException("Unknown hostId: " + hostIdStr);
        }
        String punycodeHostname = hostIdStr.substring(schemaEnd + 1, hostnameEnd);
        String port = hostIdStr.substring(hostnameEnd + 1);
        if (StringUtils.isEmpty(punycodeHostname) || StringUtils.isEmpty(port)) {
            throw new IllegalArgumentException("Unknown hostId: " + hostIdStr);
        }

        return new WebmasterHostId(schema, punycodeHostname, Integer.parseInt(port));
    }

    public static WebmasterHostId webIdStringToHostId(String hostIdStr, boolean verifyDomainName) {
        hostIdStr = hostIdStr.trim();

        int schemaEnd = hostIdStr.indexOf(':');
        if (schemaEnd < 0) {
            throw new IllegalArgumentException("Unknown hostId: " + hostIdStr);
        }
        WebmasterHostId.Schema schema = null;
        String schemaStr = hostIdStr.substring(0, schemaEnd);
        for (WebmasterHostId.Schema sch : WebmasterHostId.Schema.values()) {
            if (sch.getSchemaName().equals(schemaStr)) {
                schema = sch;
                break;
            }
        }
        if (schema == null) {
            throw new IllegalArgumentException("Unknown hostId: " + hostIdStr);
        }

        int hostnameEnd = hostIdStr.indexOf(':', schemaEnd + 1);
        if (hostnameEnd < 0) {
            throw new IllegalArgumentException("Unknown hostId: " + hostIdStr);
        }
        String punycodeHostname = IDN.toASCII(hostIdStr.substring(schemaEnd + 1, hostnameEnd));
        if (verifyDomainName) {
            UriUtils.verifyASCIIDomain(punycodeHostname);
        }
        String port = hostIdStr.substring(hostnameEnd + 1);
        if (StringUtils.isEmpty(punycodeHostname) || StringUtils.isEmpty(port)) {
            throw new IllegalArgumentException("Unknown hostId: " + hostIdStr);
        }

        return new WebmasterHostId(schema, punycodeHostname, Integer.parseInt(port));
    }

    public static String toHostString(WebmasterHostId hostId, boolean showDefaultSchema, boolean unicodeHost, boolean showDefaultPort) {
        StringBuilder sb = new StringBuilder();
        if (showDefaultSchema || hostId.getSchema() != DEFAULT_SCHEMA) {
            sb.append(hostId.getSchema().getSchemaPrefix());
        }
        if (unicodeHost) {
            sb.append(hostId.getReadableHostname());
        } else {
            sb.append(hostId.getPunycodeHostname());
        }

        if (showDefaultPort || !hostId.isDefaultPort()) {
            sb.append(":").append(hostId.getPort());
        }
        return sb.toString();
    }

    public static String toRobotHostString(WebmasterHostId hostId) {
        return toHostString(hostId, false, false, false);
    }

    public static WebmasterHostId fromUri2(URI2 uri2) throws IllegalArgumentException {
        final WebmasterHostId.Schema schema;
        final int defaultPort;
        if (WebmasterHostId.Schema.HTTP.getSchemaName().equals(uri2.getScheme())) {
            schema = WebmasterHostId.Schema.HTTP;
            defaultPort = WebmasterHostId.DEFAULT_HTTP_PORT;
        } else if (WebmasterHostId.Schema.HTTPS.getSchemaName().equals(uri2.getScheme())) {
            schema = WebmasterHostId.Schema.HTTPS;
            defaultPort = WebmasterHostId.DEFAULT_HTTPS_PORT;
        } else {
            throw new IllegalArgumentException("unsupported scheme " + uri2.getScheme());
        }
        int explicitPort = uri2.getPort() != -1 ? uri2.getPort() : defaultPort;
        String hostName = uri2.getHost();
        if (!(hostName.startsWith("xn--") || hostName.contains(".xn--"))) {
            hostName = IDN.toASCII(hostName);
            UriUtils.verifyASCIIDomain(hostName);
        }
        return new WebmasterHostId(schema, hostName, explicitPort);
    }

    public static WebmasterHostId cutWWW(WebmasterHostId hostId) {
        WebmasterHostId.Schema s = hostId.getSchema();
        String punycodeHostName = hostId.getPunycodeHostname();
        int port = hostId.getPort();
        if (WwwUtil.isWWW(punycodeHostName)) {
            return new WebmasterHostId(s, punycodeHostName.substring(WwwUtil.WWW_PREFIX.length()), port);
        } else {
            return hostId;
        }
    }

    public static WebmasterHostId switchWWW(@NotNull final WebmasterHostId hostId) {
        WebmasterHostId.Schema s = hostId.getSchema();
        String punycodeHostName = hostId.getPunycodeHostname();
        int port = hostId.getPort();
        if (WwwUtil.isWWW(punycodeHostName)) {
            return new WebmasterHostId(s, punycodeHostName.substring(WwwUtil.WWW_PREFIX.length()), port);
        } else {
            return new WebmasterHostId(s, WwwUtil.WWW_PREFIX + punycodeHostName, port);
        }
    }

    public static WebmasterHostId switchHttps(@NotNull final WebmasterHostId hostId) {
        final WebmasterHostId.Schema s = hostId.getSchema();
        final WebmasterHostId.Schema newSchema =
                (s == WebmasterHostId.Schema.HTTP) ? WebmasterHostId.Schema.HTTPS : WebmasterHostId.Schema.HTTP;
        final int newPort;
        if (!hostId.isDefaultPort()) {
            newPort = hostId.getPort();
        } else {
            newPort = newSchema.getDefaultPort();
        }
        return new WebmasterHostId(newSchema, hostId.getPunycodeHostname(), newPort);
    }

    public static WebmasterHostId withSchema(@NotNull WebmasterHostId hostId, WebmasterHostId.Schema desiredSchema) {
        if (hostId.getSchema() == desiredSchema) {
            return hostId;
        }
        int newPort = hostId.getPort();
        if (hostId.isDefaultPort()) {
            newPort = desiredSchema.getDefaultPort();
        }
        return new WebmasterHostId(desiredSchema, hostId.getPunycodeHostname(), newPort);
    }

    @Nullable
    public static String toRelativeUrl(@NotNull WebmasterHostId hostId, String url) throws Exception {
        return toRelativeUrl(hostId, url, true);
    }

    @Nullable
    public static String toRelativeUrl(@NotNull WebmasterHostId hostId, String url, boolean decode) throws Exception {
        url = StringUtils.trimToEmpty(url);
        if (url.length() > 1024) {
            return null;
        }
        if (url.startsWith("/")) {
            url = hostIdToUrl(hostId) + url;
        }

        URI uri = WebmasterUriUtils.toOldUri(url);
        WebmasterHostId hostIdFromUrl = urlToHostId(uri.toString());

        if (!hostId.equals(hostIdFromUrl)) {
            return null;
        }
        StringBuilder sb = new StringBuilder();
        if (StringUtils.isEmpty(uri.getPath())) {
            sb.append('/');
        } else {
            sb.append(decode ? uri.getPath() : uri.getRawPath());
        }

        if (uri.getQuery() != null) {
            sb.append('?').append(decode ? uri.getQuery() : uri.getRawQuery());
        }

        return sb.toString();
    }

    public static WebmasterHostId toDomainHostId(WebmasterHostId hostId) {
        return urlToHostId(WwwUtil.cutWWWAndM(hostId.getPunycodeHostname()));
    }

    public static List<WebmasterHostId> allHostsForDomain(String domain) {
        return allHostsForDomain(urlToHostId(domain));
    }

    public static List<WebmasterHostId> allHttpsHostsForDomain(String domain) {
        WebmasterHostId hostId = urlToHostId(WebmasterHostId.Schema.HTTPS.getSchemaPrefix() + domain);

        return Arrays.asList(hostId, switchWWW(hostId));
    }

    public static List<WebmasterHostId> allHostsForDomain(WebmasterHostId hostId) {
        return Stream.of(hostId)
                .flatMap(h -> Stream.of(h, switchWWW(h)))
                .flatMap(h -> Stream.of(h, switchHttps(h)))
                .collect(Collectors.toList());
    }

    public static List<WebmasterHostId> allHostsForDomainWithM(WebmasterHostId hostId) {
        var allHosts = allHostsForDomain(hostId);
        var h = IdUtils.urlToHostId("m." + hostId.getPunycodeHostname());
        allHosts.add(h);
        allHosts.add(switchHttps(h));

        return allHosts;
    }

    private static String cutLastDots(String hostName) {
        int index = hostName.length() - 1;
        while (index >= 0 && hostName.charAt(index) == '.')
            index--;
        return hostName.substring(0, index + 1);
    }

    public static class Idn {
        private final static IDNA UTS64 = IDNA.getUTS46Instance(IDNA.DEFAULT);

        // Стандартный класс IDN на такое не ругается
        private final static Set<IDNA.Error> HARMLESS_ERRORS = Set.of(
                IDNA.Error.LEADING_HYPHEN,
                IDNA.Error.TRAILING_HYPHEN,
                IDNA.Error.HYPHEN_3_4
        );

        // Возвращаем input без изменений
        private final static Set<IDNA.Error> RETURN_AS_IS_ERRORS = Set.of(
                // Имя выглядит как punicode, но таковым на самом деле не является
                IDNA.Error.PUNYCODE,
                IDNA.Error.INVALID_ACE_LABEL
        );

        public String toASCII(String input) {
            if (StringUtils.isEmpty(input)) {
                return input;
            }

            var idnaInfo = new IDNA.Info();
            var output = new StringBuilder();
            UTS64.nameToASCII(input, output, idnaInfo);

            List<IDNA.Error> errors = idnaInfo.hasErrors()? filterHarmlessErrors(idnaInfo.getErrors()) : Collections.emptyList();
            if (!errors.isEmpty()) {
                if (errors.size() == 1 && RETURN_AS_IS_ERRORS.contains(errors.get(0))) {
                    return input;
                } else {
                    var message = "Errors converting name " + input + " to ASCII: " + Arrays.toString(idnaInfo.getErrors().toArray());
                    throw new WebmasterException(message, new WebmasterErrorResponse.InternalUnknownErrorResponse(IdUtils.class, message));
                }
            }

            return output.toString();
        }

        public String toUnicode(String input) {
            if (StringUtils.isEmpty(input)) {
                return input;
            }

            var idnaInfo = new IDNA.Info();
            var output = new StringBuilder();
            UTS64.nameToUnicode(input, output, idnaInfo);

            List<IDNA.Error> errors = idnaInfo.hasErrors()? filterHarmlessErrors(idnaInfo.getErrors()) : Collections.emptyList();
            if (!errors.isEmpty()) {
                if (errors.size() == 1 && RETURN_AS_IS_ERRORS.contains(errors.get(0))) {
                    return input;
                } else {
                    var message = "Errors converting name " + input +  " to Unicode: " + Arrays.toString(idnaInfo.getErrors().toArray());
                    throw new WebmasterException(message, new WebmasterErrorResponse.InternalUnknownErrorResponse(IdUtils.class, message));
                }
            }

            return output.toString();
        }

        private List<IDNA.Error> filterHarmlessErrors(Collection<IDNA.Error> errors) {
            return errors.stream().filter(e -> !HARMLESS_ERRORS.contains(e)).collect(Collectors.toList());
        }
    }
}
