package ru.yandex.canvas.service.html5;

import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.canvas.service.SessionParams;
import ru.yandex.canvas.service.TankerKeySet;

import static ru.yandex.canvas.Html5Constants.IS_INLINE_EVENT_HANDLERS_FORBIDDEN;
import static ru.yandex.canvas.service.SessionParams.Html5Tag.CPM_PRICE;

public class AdHtmlParser {
    private static final Logger logger = LoggerFactory.getLogger(AdHtmlParser.class);

    private int width = 300;
    private int height = 250;
    public static final int STRETCHABLE_WIDTH = 1900; // ширина для перетяжек (см. DIRECT-149661)
    public static final int STRETCHABLE_HEIGHT = 1900; // высота для перетяжек (см. DIRECT-164947)
    private String bgrcolor;
    private final List<String> errors;
    private final List<String> patchesForUpload;
    private final List<String> relativePaths;
    private final List<String> absolutePaths;
    private final List<HtmlFileParsed> filesParsed;
    private final SessionParams.Html5Tag productType;
    public static final Set<String> NOT_NEED_IN_UPLOAD_HOSTS = ImmutableSet.of("yastatic.net", "awaps.yandex.net");

    private final Map<String, List<Pattern>> whitelistExternalLibs = ImmutableMap.<String, List<Pattern>>builder()
            .put("animate.adobe.com",
                    ImmutableList.of(Pattern.compile("^/runtime/[\\d|\\.]+/edge\\.[\\d|\\.]+\\.min\\.js'")))

            .put("cdnjs.cloudflare.com",
                    ImmutableList.of(Pattern.compile("^/ajax/libs/gsap/[\\d|\\.]+[/a-zA-Z]+\\.min\\.js"),
                            Pattern.compile("^/ajax/libs/gsap/latest[/a-zA-Z]+\\.min\\.js$")))
            .put("code.createjs.com", ImmutableList.of(Pattern.compile("^/createjs-[\\d|\\.]+\\.min\\.js$"),
                    Pattern.compile("^/[\\d|\\.]+/createjs\\.min\\.js$"),
                    Pattern.compile("^/easeljs-[\\d|\\.]+\\.min\\.js$"),
                    Pattern.compile("^/movieclip-[\\d|\\.]+\\.min\\.js$"),
                    Pattern.compile("^/preloadjs-[\\d|\\.]+\\.min\\.js$"),
                    Pattern.compile("^/tweenjs-[\\d|\\.]+\\.min\\.js$")))
            .put("code.jquery.com", ImmutableList.of(Pattern.compile("^/jquery-[\\d|\\.]+\\.min\\.js$")))
            .put("yastatic.net", ImmutableList.of(Pattern.compile(
                    "^/(bem-components|bem-core|backbone|bootstrap|d3|jquery|lodash|raphael|react|swfobject" +
                            "|underscore)/.*")))
            .put("www.gstatic.com", ImmutableList.of(Pattern.compile("^/swiffy/[v|\\-|\\d|\\.]+/runtime\\.js")))
            .put("awaps.yandex.net", ImmutableList.of(Pattern.compile("/data/lib/adsdk\\.js")))
            .build();

    private final Pattern adSizePatternA = Pattern.compile("^width=(?<width>\\d+|100%),\\s?height=(?<height>\\d+|100%)$");
    private final Pattern adSizePatternB = Pattern.compile("^height=(?<height>\\d+|100%),\\s?width=(?<width>\\d+|100%)$");

    public AdHtmlParser(List<HtmlFile> files, SessionParams.Html5Tag productType) {
        errors = new ArrayList<>();
        patchesForUpload = new ArrayList<>();
        relativePaths = new ArrayList<>();
        absolutePaths = new ArrayList<>();
        //снаружи проверили что в коллекции есть минимум один элемент, максимум 2
        Preconditions.checkNotNull(files);
        Preconditions.checkArgument(files.size() > 0);
        filesParsed = files.stream()
                .map(it -> {
                    Document document;
                    // Кидает org.jsoup.UncheckedIOException: java.io.IOException: Input is binary and unsupported
                    // Вроде бы пофикшено в версии jsoup 1.12.2 (на момент написания используется 1.12.1)
                    // Bugfix: Removed binary input detection as it was causing too many false positives.
                    // https://github.com/jhy/jsoup/issues/1250
                    try {
                        document = Jsoup.parse(it.getContent());
                        return new HtmlFileParsed(it.getFileName(), document);
                    } catch (UncheckedIOException ex) {
                        errors.add(TankerKeySet.HTML5.key("unsupported_html_binary_file"));
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        this.productType = productType;
    }

    public void check() {
        checkAdSize();
        checkAdBgrcolor();
        for (HtmlFileParsed file : filesParsed) {
            checkSrc(file.doc);
        }
    }

    private void checkSrc(Document doc) {
        Elements media = doc.select("[src]");

        for (Element el : media) {
            String host;
            String src = el.attr("src").replace("\\", "/"); //Заменим windows слэш на unix

            try {
                host = new URL(src).getHost();
            } catch (MalformedURLException e) {
                host = null;
            }

            if (host != null && isLibWhitelisted(src)) {
                if (!NOT_NEED_IN_UPLOAD_HOSTS.contains(host)) {
                    patchesForUpload.add(src);
                }
            } else if (!src.startsWith("/") && !src.matches("^https?://.+")) {
                if (!src.startsWith("data:") && !src.startsWith("base64,")) {
                    String srcNormalized = normalizePaths(src);
                    relativePaths.add(srcNormalized);
                }
            } else {
                //TODO: check iframe, video, audio, link, background: url(, ...
                absolutePaths.add(src); //It could be ever garbage instead of an url
            }
        }

        if (absolutePaths.size() > 0) {
            String paths = String.join(", ", absolutePaths);
            errors.add(TankerKeySet.HTML5.formattedKey("has_not_allowed_external_paths", paths));
        }

        checkInlineEventHandlers(doc);
    }

    private static String normalizePaths(String src) {
        try {
            return Paths.get(src).normalize().toString().replaceAll("//", "\\");
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
        }
        return src;
    }

    /**
     * валидация на наличие inline-событий в html5-баннере морды
     */
    private void checkInlineEventHandlers(Document doc) {
        if (!IS_INLINE_EVENT_HANDLERS_FORBIDDEN.contains(this.productType)) {
            return;
        }
        Elements nodeInlineEvent = doc.getElementsByAttributeStarting("on");
        if (nodeInlineEvent.stream()
                .anyMatch(
                        element -> this.productType != SessionParams.Html5Tag.CPM_PRICE
                        || !element.nodeName().equalsIgnoreCase("body")
                        || !element.hasAttr("onload")
                        || element.getElementsByAttributeStarting("on").size() > 1
                )) {
            errors.add(TankerKeySet.HTML5.key("not_valid_html_file"));
        }
    }

    private boolean isLibWhitelisted(String src) {
        URL srcUrl;

        try {
            srcUrl = new URL(src);
        } catch (MalformedURLException e) {
            return false;
        }

        if (whitelistExternalLibs.containsKey(srcUrl.getHost())) {
            for (Pattern re : whitelistExternalLibs.get(srcUrl.getHost())) {
                if (re.matcher(srcUrl.getPath()).matches()) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * проверка в раскрывающемся блоке элемента <meta name=\"ad.bgrcolor\">
     * пример <meta name="ad.bgrcolor" content="FFFFFF">
     */
    private void checkAdBgrcolor() {
        if (filesParsed.size() < 2 || productType != CPM_PRICE) {
            return;
        }
        Document doc = filesParsed.stream()
                .filter(it -> !it.isMain())
                .map(HtmlFileParsed::getDoc)
                .findFirst().orElse(null);
        if (doc == null) {
            return;//нечего проверять
        }
        Elements meta = doc.head().select("meta[name=ad.bgrcolor][content]");
        if (meta.isEmpty()) {
            errors.add(TankerKeySet.HTML5.key("no_ad_bgrcolor_found"));
            return;
        }
        for (Element el : meta) {
            bgrcolor = el.attr("content");
        }
    }

    private void checkAdSize() {
        //должен быть один главный файл и возможно один расхлоп
        filesParsed.forEach(it -> it.main = hasSize(it.doc));
        long cntWithAdSize = filesParsed.stream()
                .filter(HtmlFileParsed::isMain)
                .count();
        if (cntWithAdSize == 0) {
            errors.add(TankerKeySet.HTML5.key("no_ad_size_found"));
        }
        if (cntWithAdSize > 1) {
            errors.add(TankerKeySet.HTML5.key("multi_ad_size_found"));
        }
    }

    /**
     * Проверяет наличие тега размера и устанавливает поля width и height
     */
    private boolean hasSize(Document doc) {
        Elements meta = doc.head().select("meta[name=ad.size][content]");

        boolean found = false;

        for (Element el : meta) {
            String content = el.attr("content");
            if (content != null) {
                Matcher matcher = Stream.of(adSizePatternA, adSizePatternB)
                        .map(p -> p.matcher(content))
                        .filter(m -> m.matches())
                        .findFirst()
                        .orElse(null);

                if (matcher != null) {
                    found = true;
                    String widthStr = matcher.group("width");
                    String heightStr = matcher.group("height");
                    boolean isStretchableWidth = "100%".equals(widthStr);
                    boolean isStretchableHeight = "100%".equals(heightStr);
                    width = isStretchableWidth ? STRETCHABLE_WIDTH : Integer.parseInt(widthStr);
                    height = isStretchableHeight ? STRETCHABLE_HEIGHT : Integer.parseInt(heightStr);
                    break;
                }
            }
        }
        return found;
    }

    public List<String> getErrors() {
        return errors;
    }

    public List<String> getPatchesForUpload() {
        return patchesForUpload;
    }

    public List<String> getRelativePaths() {
        return relativePaths;
    }

    public List<String> getAbsolutePaths() {
        return absolutePaths;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    public String getHtmlFileName() {
        return filesParsed.stream()
                .filter(HtmlFileParsed::isMain)
                .map(HtmlFileParsed::getFileName)
                .findFirst().orElse(filesParsed.get(0).getFileName());
    }

    public String getBgrcolor() {
        return bgrcolor;
    }

    public String getExpandedHtmlFileName() {
        return filesParsed.stream()
                .filter(it -> !it.isMain())
                .map(HtmlFileParsed::getFileName)
                .findFirst().orElse(null);
    }

    private static class HtmlFileParsed {
        public String fileName;
        public Document doc;
        public boolean main = true;

        public HtmlFileParsed(String fileName, Document doc) {
            this.fileName = fileName;
            this.doc = doc;
        }

        public boolean isMain() {
            return main;
        }

        public String getFileName() {
            return fileName;
        }

        public Document getDoc() {
            return doc;
        }
    }

    public static class HtmlFile {
        private final String fileName;
        private final String content;

        public HtmlFile(String fileName, String content) {
            this.fileName = fileName;
            this.content = content;
        }

        public String getFileName() {
            return fileName;
        }

        public String getContent() {
            return content;
        }
    }
}
