package nu.marginalia.converting.processor.logic;

import nu.marginalia.converting.model.DocumentHeaders;
import nu.marginalia.converting.model.GeneratorType;
import nu.marginalia.model.EdgeUrl;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.nodes.Document;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/** Extract keywords for the document meta generator tag */
public class DocumentGeneratorExtractor {
    private static final String defaultValue = "unset";

    public DocumentGenerator detectGenerator(EdgeUrl url, Document doc, DocumentHeaders responseHeaders) {

        // Fextralife leaves no known tech fingerprint, but we know it's a wiki software of some sort
        if (url.domain.toString().endsWith(".wiki.fextralife.com")) {
            return DocumentGenerator.of("wiki");
        }

        var tags = doc.select("meta[name=generator]");

        if (tags.isEmpty()) {
            // Some sites have a comment in the head instead of a meta tag
            return fingerprintServerTech(doc, responseHeaders);
        }
        if (tags.size() > 1) {
            return DocumentGenerator.multiple();
        }
        String generator = tags.attr("content");

        // Remove leading or trailing junk from the generator string, "powered by" etc.
        generator = removePrefixOrSuffix(generator);

        if (generator.isBlank())
            return fingerprintServerTech(doc, responseHeaders);

        if (generator.startsWith("AMP by WP"))
            return DocumentGenerator.of("wordpress", "wordpress-amp");

        String[] parts = StringUtils.split(generator, " ,:!");
        if (parts.length == 0)
            return fingerprintServerTech(doc, responseHeaders);

        int slashIdx = parts[0].indexOf('/');
        if (slashIdx >= 0) {
            // mozilla and staroffice has a really weird format
            return DocumentGenerator.of(parts[0].substring(0, slashIdx));
        }

        if (parts.length > 3) {
            return fingerprintServerTech(doc, responseHeaders); // if it's still very long after trim(), it's probably a custom hand written message
        }

        switch (parts[0]) {
            case "joomla!":
                return DocumentGenerator.of("joomla");
            case "plone":
            case "claris":
            case "one.com":
            case "wix.com":
            case "wpbakery":
            case "FluxGarden":
                return DocumentGenerator.of(parts[0]);
            case "adobe":
            case "microsoft":
                if (parts.length > 1) {
                    return DocumentGenerator.of(parts[1]);
                }
                else {
                    return DocumentGenerator.of(parts[0]);
                }
        }


        if (parts.length > 1) {
            return DocumentGenerator.of(parts[0], parts[0] + "_" + truncVersion(parts[1]));
        }
        else {
            return DocumentGenerator.of(parts[0]);
        }
    }

    // Fallback logic when there is no meta tag
    private DocumentGenerator fingerprintServerTech(Document doc, DocumentHeaders responseHeaders) {

        for (var comment : doc.getElementsByTag("head").comments()) {
            String data = comment.getData();

            if (data.contains("Generated by javadoc")) {
                return DocumentGenerator.of("javadoc");
            }
            if (data.contains("Squarespace")) {
                return DocumentGenerator.of("squarespace");
            }
            if (data.contains("phpBB")) {
                return DocumentGenerator.of("phpbb");
            }
        }

        for (var tag : doc.head().getElementsByTag("script")) {
            String scriptSrc = tag.attr("src");

            if (scriptSrc.contains("wp-content") || scriptSrc.contains("wp-includes")) {
                return DocumentGenerator.of("wordpress", "wordpress-sneaky");
            }
            if (scriptSrc.contains("squarespace.com")) {
                return DocumentGenerator.of("squarespace");
            }
            if (scriptSrc.contains("cdn.cloversites.com")) {
                return DocumentGenerator.of("cloversites");
            }
            if (scriptSrc.contains("bndzgl.com")) {
                return DocumentGenerator.of("bndzgl");
            }
            if (scriptSrc.contains("editmysite.com")) {
                return DocumentGenerator.of("editmysite");
            }
            if (scriptSrc.contains("website-editor.net")) {
                return DocumentGenerator.of("website-editor.net");
            }
            String scriptHtml = tag.html();
            if (scriptHtml.contains("window.lemmyConfig")) {
                return DocumentGenerator.of("lemmy");
            }
            if (scriptHtml.contains("URL_DOMAIN = 'wikidot.com'")) {
                return DocumentGenerator.of("wikidot");
            }

        }

        for (var tag : doc.head().getElementsByTag("link")) {
            if (tag.attr("href").contains("wp-content")) {
                return DocumentGenerator.of("wordpress", "wordpress-sneaky");
            }
        }

        if (doc.getElementById("flarum-json-payload") != null) {
            return DocumentGenerator.of("flarum");
        }

        if (doc.getElementById("tracpowered") != null) {
            return DocumentGenerator.of("trac");
        }

        if (doc.getElementById("_xfClientLoadTime") != null) {
            return DocumentGenerator.of("xenforo");
        }

        if (!doc.getElementsByClass("ipsApp").isEmpty()) {
            return DocumentGenerator.of("invision");
        }

        if (doc.getElementById("___gatsby") != null) {
            return DocumentGenerator.of("gatsby");
        }

        for (var header : responseHeaders.eachLineLowercase()) {
            if (header.contains("x-drupal-cache")) {
                return DocumentGenerator.of("drupal");
            }
            if (header.contains("x-powered-by: asp.net")) {
                return DocumentGenerator.of("asp.net");
            }
            if (header.contains("x-powered-by: php")) {
                return DocumentGenerator.of("php");
            }
            if (header.contains("x-powered-by: wp engine")) {
                return DocumentGenerator.of("wordpress", "wp-engine", "wordpress-sneaky");
            }
            if (header.contains("x-powered-by: statamic")) {
                return DocumentGenerator.of("laravel", "statamic");
            }
        }

        // These should be all the way down as they are the most generic
        for (var header : responseHeaders.eachLineLowercase()) {
            if (header.contains("server: mastodon")) {
                return DocumentGenerator.of("mastodon");
            }
            if (header.contains("server: gunicorn")) {
                return DocumentGenerator.of("gunicorn");
            }
            if (header.contains("server: nginx")) {
                return DocumentGenerator.of("nginx");
            }
            if (header.contains("server: apache")) {
                return DocumentGenerator.of("apache");
            }
            if (header.contains("server: cowboy")) {
                return DocumentGenerator.of("cowboy"); // erlang, apparently
            }
        }

        return DocumentGenerator.unset();
    }

    private String removePrefixOrSuffix(String generator) {

        generator = generator.toLowerCase().trim();

        // strip common prefixes
        for (String prefix : Arrays.asList("powered by ", "generated by ")) {

            if (generator.startsWith(prefix)) {
                generator = generator.substring(prefix.length());
                break;
            }
        }

        int dashIdx = generator.indexOf('-'); // Some strings have values like 'foobar 2.3 - the free online generator!'
        if (dashIdx >= 0) {
            generator = generator.substring(0, dashIdx);
        }

        int parenIdx = generator.indexOf('('); // Some strings have values like 'Drupal 9 (https://www.drupal.org)'
        if (parenIdx >= 0) {
            generator = generator.substring(0, parenIdx);
        }

        if (!StringUtils.isAsciiPrintable(generator))
            return "";

        return generator;
    }

    // Censor exact version strings, being able to search by major version is enough
    // for any non-blackhat purpose; creating a directory with exact version string
    // is a security risk for the site owner.
    private String truncVersion(String part) {
        int periodIdx = part.indexOf('.', part.startsWith("0.") ? 2 : 0);

        if (periodIdx < 0)
            return part;

        return part.substring(0, periodIdx);
    }

    public record DocumentGenerator(GeneratorType type, List<String> keywords) {
        public static DocumentGenerator unset() {
            return new DocumentGenerator(GeneratorType.UNKNOWN, List.of(defaultValue));
        }

        public static DocumentGenerator of(String... parts) {
            if (parts.length == 0)
                return unset();

            List<String> keywords = new ArrayList<>(List.of(parts));

            final GeneratorType type = switch (parts[0]) {
                case "joomla", "wordpress", "drupal", "plone", "postnuke", "divi", "freeway", "unicity",
                     "modx", "sitemagic", "agility", "edlio", "blogger", "slider", "slider_revolution", "gravcms",
                     "typo3", "dotnetnuke", "cms", "coremedia", "dspace", "laravel", "trac", "bunnypress", "astro",
                     "ghost", "publii"
                     -> GeneratorType.CMS;
                case "wix.com", "one.com", "wpbakery", "claris", "wordpress.com", "hubspot",
                     "visual_composer", "mobirise", "everweb", "rapidweaver", "shorthand",
                     "visual", "nitropack", "squarespace", "editmysite", "websiteeditor.net",

                     "svbtle.com", "write.as", "montaigne.io", // blogging platforms, maybe should be in another category?

                     "cloversites", // clover is a church-oriented website builder, found that kinda neat
                     "bndzgl", // band websites ..?

                    /* these are not SAAS but close enough */
                    "redux", "bootply"
                     -> GeneratorType.SAAS;
                case "staroffice", "word", "frontpage", "dreamweaver", "mshtml",
                     "iweb", "excel", "wordperfect", "netscape", "corel", "powerpoint",
                     "openoffice.org", "openoffice", "latex2html", "lotus", "homesite",
                     "trellix", "yahoo", "libreoffice", "opera", "stone's_webwriter",
                     "pdf2htmlex", "nvu", "mozilla", "golive", "tenfingers", "publisher",
                     "allaire", "neooffice"
                     -> GeneratorType.BOOMER_STATIC;
                case "hugo", "jekyll", "hakyll", "nikola", "zola", "olivetti", "pelican", "sushy", "hexo", "eleventy",
                     "gridsome", "vuepress", "docusaurus", "docpad", "techou", "quarto", "soupault"
                     -> GeneratorType.ZOOMER_STATIC;
                case "vi", "vim", "emacs", "orgmode", "hand", "vscode", "atom", "bbedit", "nano",
                     "notepad.exe", "gedit", "me",
                     "geany", "sublime", "notepad++", "author",
                     "notepad", "namo", "arachnophilia", "scite",
                     "alleycode", "htmlkit", "acehtml", "bluefish", "htmled", "cutehtml", "fileedit", "cocoa"
                     -> GeneratorType.MANUAL;
                case "vbulletin", "phpbb", "mybb", "nodebb", "flarum", "tribe",
                     "discourse", "lemmy", "xenforo", "invision"
                     -> GeneratorType.FORUM;
                case "mediawiki", "dokuwiki", "wikidot", "sharepoint"
                     -> GeneratorType.WIKI;
                case "pandoc", "mkdocs", "doxygen", "javadoc", "asciidoc", "jsdoc", "FluxGarden", "wiki"
                     -> GeneratorType.DOCS;
                case "woocommerce", "shopfactory", "prestashop", "magento", "shopify", "sitedirect", "seomatic", "osclass"
                     -> GeneratorType.ECOMMERCE_AND_SPAM;
                default
                     -> GeneratorType.UNKNOWN;
            };

            if (type != GeneratorType.UNKNOWN) {
                keywords.add(type.name().toLowerCase());
            }

            return new DocumentGenerator(type, keywords);
        }

        public static DocumentGenerator multiple() {
            // It's *generally* WordPress or the like that injects multiple generator tags

            return new DocumentGenerator(GeneratorType.CMS, List.of("wordpress", "wp-best-guess"));
        }
    }


}
