<?php

declare(strict_types=1);

namespace App\Core;

use DOMDocument;
use DOMElement;
use DOMNode;

/**
 * Nettoyage du HTML riche (liste blanche), aligné sur la barre TinyMCE (D-18 / EDIT-02).
 * L'éditeur côté client n'est PAS une barrière de sécurité : on assainit à l'enregistrement.
 */
final class Html
{
    /** Balises autorisées => attributs autorisés. */
    private const ALLOWED = [
        'p' => ['class', 'style'], 'br' => [], 'hr' => ['class', 'style'],
        'strong' => [], 'b' => [], 'em' => [], 'i' => [], 'u' => [], 'sup' => [], 'sub' => [],
        'h1' => ['class', 'style'], 'h2' => ['class', 'style'], 'h3' => ['class', 'style'], 'h4' => ['class', 'style'],
        'ul' => ['class'], 'ol' => ['class'], 'li' => ['class'], 'blockquote' => ['class'],
        'a' => ['href', 'class', 'style'],
        'img' => ['src', 'alt', 'title', 'width', 'height', 'class', 'style'],
        'div' => ['class', 'style'], 'span' => ['class', 'style'],
    ];

    /** Balises supprimées AVEC leur contenu (jamais déballées). */
    private const DROP = ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input', 'button', 'svg', 'math', 'link', 'meta'];

    public static function sanitize(string $html): string
    {
        if (trim($html) === '') {
            return '';
        }

        if (!class_exists(DOMDocument::class)) {
            // Repli si l'extension DOM est absente (attributs perdus).
            return trim(strip_tags($html, '<p><br><strong><b><em><i><sup><sub><u><h1><h2><h3><h4><ul><ol><li><blockquote><a>'));
        }

        $doc = new DOMDocument();
        libxml_use_internal_errors(true);
        $doc->loadHTML(
            '<?xml encoding="UTF-8"><div id="root">' . $html . '</div>',
            LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
        );
        libxml_clear_errors();

        $root = $doc->documentElement;
        if (!$root instanceof DOMElement) {
            return '';
        }

        self::cleanChildren($root);

        $out = '';
        foreach (iterator_to_array($root->childNodes) as $child) {
            $out .= $doc->saveHTML($child);
        }

        return trim($out);
    }

    private static function cleanChildren(DOMNode $node): void
    {
        foreach (iterator_to_array($node->childNodes) as $child) {
            if ($child->nodeType === XML_TEXT_NODE) {
                continue;
            }

            if (!$child instanceof DOMElement) {
                // Commentaires, instructions… : supprimés.
                $node->removeChild($child);
                continue;
            }

            $tag = strtolower($child->nodeName);

            if (!array_key_exists($tag, self::ALLOWED)) {
                if (in_array($tag, self::DROP, true)) {
                    // Balise dangereuse : on supprime l'élément ET son contenu.
                    $node->removeChild($child);
                    continue;
                }
                // Autre balise non autorisée : on conserve le contenu, on retire la balise.
                self::cleanChildren($child);
                while ($child->firstChild !== null) {
                    $node->insertBefore($child->firstChild, $child);
                }
                $node->removeChild($child);
                continue;
            }

            $allowedAttrs = self::ALLOWED[$tag];
            foreach (iterator_to_array($child->attributes) as $attr) {
                $name = strtolower($attr->nodeName);
                if (!in_array($name, $allowedAttrs, true)) {
                    $child->removeAttribute($attr->nodeName);
                    continue;
                }
                if ($name === 'href' && !self::safeHref((string) $attr->nodeValue)) {
                    $child->removeAttribute('href');
                } elseif ($name === 'src' && !self::safeSrc((string) $attr->nodeValue)) {
                    $child->removeAttribute('src');
                } elseif ($name === 'style') {
                    $clean = self::safeStyle((string) $attr->nodeValue);
                    $clean === '' ? $child->removeAttribute('style') : $child->setAttribute('style', $clean);
                }
            }

            // <img> sans source valide : élément retiré.
            if ($tag === 'img' && !$child->hasAttribute('src')) {
                $node->removeChild($child);
                continue;
            }

            self::cleanChildren($child);
        }
    }

    private static function safeHref(string $href): bool
    {
        $href = trim($href);
        if ($href === '') {
            return false;
        }
        if (str_starts_with($href, '#') || str_starts_with($href, '/')) {
            return true;
        }

        return (bool) preg_match('#^(https?:|mailto:)#i', $href);
    }

    private static function safeSrc(string $src): bool
    {
        $src = trim($src);
        if ($src === '') {
            return false;
        }
        return str_starts_with($src, '/') || (bool) preg_match('#^https?://#i', $src);
    }

    /** Conserve un style inline simple en bloquant les vecteurs CSS dangereux (url(), expression…). */
    private static function safeStyle(string $style): string
    {
        $style = trim($style);
        if ($style === '' || preg_match('#url\s*\(|expression|javascript:|@import|<#i', $style)) {
            return '';
        }
        return $style;
    }
}
