import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Deterministische controles op een HTML-pagina &mdash; mijlpaal M8.
 *
 * <p>Dit is de poortwachter van de agent-lus: na elke schrijfactie van het model
 * draait het harnas deze controles en voert de uitkomst terug. Het model mag
 * onbetrouwbaar zijn; de controle is dat niet. Geen volledige HTML5-validator
 * (dat is een vak apart), wel de fouten die een gegenereerde pagina echt
 * onbruikbaar maken: scheve tag-nesting, ontbrekende basisstructuur, en het
 * ontbreken van inhoud die de opdracht expliciet eiste.
 */
final class HtmlControles {

    /** Elementen die geen sluittag hebben (void elements uit de HTML-standaard). */
    private static final Set<String> LEEG = Set.of(
            "area", "base", "br", "col", "embed", "hr", "img", "input",
            "link", "meta", "param", "source", "track", "wbr");

    private static final Pattern TAG = Pattern.compile(
            "<(/?)([a-zA-Z][a-zA-Z0-9-]*)((?:[^>\"']|\"[^\"]*\"|'[^']*')*?)(/?)>");

    /**
     * Controleert de pagina en geeft de gevonden problemen terug, leesbaar voor
     * mens &eacute;n model. Een lege lijst betekent: door de poort.
     *
     * @param html   de volledige paginatekst
     * @param eisen  letterlijke tekstfragmenten die in de pagina moeten voorkomen
     *               (de aanvullende eisen van de opdracht, bv. {@code "<h1>"})
     */
    /** Commentaar telt niet mee: een eis die alleen in {@code <!-- -->} staat, staat er niet. */
    static String zonderCommentaar(String html) {
        return html.replaceAll("(?s)<!--.*?-->", "");
    }

    /** Dezelfde eis-toets als in {@link #controleer}, ook bruikbaar voor de hint in de agent. */
    static boolean bevatEis(String html, String eis) {
        return zonderCommentaar(html).contains(eis);
    }

    static List<String> controleer(String html, List<String> eisen) {
        List<String> problemen = new ArrayList<>();

        String zonderCommentaar = zonderCommentaar(html);
        String lower = zonderCommentaar.toLowerCase(Locale.ROOT);

        if (!lower.stripLeading().startsWith("<!doctype html")) {
            problemen.add("de pagina begint niet met <!DOCTYPE html>");
        }
        for (String verplicht : new String[]{"<html", "<head", "<body"}) {
            if (!lower.contains(verplicht)) {
                problemen.add("het element " + verplicht + "> ontbreekt");
            }
        }
        Matcher titel = Pattern.compile("<title[^>]*>(.*?)</title>", Pattern.DOTALL | Pattern.CASE_INSENSITIVE)
                .matcher(zonderCommentaar);
        if (!titel.find()) {
            problemen.add("er is geen <title>");
        } else if (titel.group(1).isBlank()) {
            problemen.add("de <title> is leeg");
        }

        // -- tag-balans: een stapel, met de void-elementen uitgezonderd --
        // script/style kunnen '<' in hun inhoud hebben; die inhoud slaan we over
        String zonderScripts = zonderCommentaar
                .replaceAll("(?is)<script\\b[^>]*>.*?</script>", "<script></script>")
                .replaceAll("(?is)<style\\b[^>]*>.*?</style>", "<style></style>");
        // de hele pagina wordt gescand; alleen het áántal nestingmeldingen is begrensd,
        // zodat de niet-gesloten-samenvatting onderaan nooit kan wegvallen
        Deque<String> stapel = new ArrayDeque<>();
        Matcher m = TAG.matcher(zonderScripts);
        int nestingMeldingen = 0;
        while (m.find()) {
            String naam = m.group(2).toLowerCase(Locale.ROOT);
            boolean sluit = !m.group(1).isEmpty();
            boolean zelfSluitend = !m.group(4).isEmpty() || LEEG.contains(naam);
            if (!sluit && !zelfSluitend) {
                stapel.push(naam);
            } else if (sluit) {
                if (stapel.isEmpty()) {
                    if (nestingMeldingen++ < 8) problemen.add("sluittag </" + naam + "> zonder openingstag");
                } else if (!stapel.peek().equals(naam)) {
                    if (nestingMeldingen++ < 8) {
                        problemen.add("scheve nesting: </" + naam + "> terwijl <"
                                + stapel.peek() + "> nog open staat");
                    }
                    stapel.pop();                       // beperk vervolgschade in de melding
                } else {
                    stapel.pop();
                }
            }
        }
        if (!stapel.isEmpty()) {
            problemen.add("niet gesloten: <" + String.join(">, <", stapel) + ">");
        }

        // -- de aanvullende eisen van de opdracht --
        for (String eis : eisen) {
            if (!zonderCommentaar.contains(eis)) {
                problemen.add("de opdracht eist ‘" + eis + "’ en dat staat er niet in");
            }
        }
        return problemen;
    }
}
