<?php

namespace App\Services\Features;

use HTMLPurifier;
use HTMLPurifier_Config;

/**
 * Saneado del HTML enriquecido de las funcionalidades antes de guardarlo.
 *
 * Criterio (PRODSECU-193, decisión de producto del 2026-08-27): el autor maqueta
 * con **componentes Bootstrap por clase**, no con estilos propios. Bootstrap ya
 * está cargado en la página del Moodle del cliente, así que no viaja ni una línea
 * de CSS desde el Manager y el contenido hereda el tema del cliente.
 *
 * Por eso NO se admiten `<style>`, `<link>` ni `style=""` libre. No es una
 * limitación técnica —se comprobó que el `style` inline sobrevive al purificador de
 * Moodle— sino una decisión: un estilo fijo rompe en móvil, choca con los temas de
 * cliente, congela el diseño en cada registro, y un `<style>` es global y puede
 * alcanzar elementos del propio Moodle del cliente.
 *
 * El `style` de `span` se mantiene con seis propiedades porque es lo que generan
 * los botones de color y resaltado del propio editor; quitarlo rompería esa función.
 */
class HtmlSanitizer
{
    /**
     * Etiquetas que este saneador quita, y que **el Moodle también quitaría**.
     *
     * Estaban escritas dentro del constructor. Salen aquí porque hay un segundo
     * interesado: la regla que avisa al autor antes de guardar
     * (`App\Rules\SinEtiquetasQueElMoodleBorra`, MGR-002). Con la lista en dos sitios,
     * el día que se añada una etiqueta el aviso dejaría de mencionarla y el autor
     * volvería a perder contenido en silencio.
     *
     * @var list<string>
     */
    public const PROHIBIDAS = ['iframe', 'script', 'object', 'embed', 'form', 'input', 'button'];

    /**
     * Cuáles de las prohibidas trae este HTML.
     *
     * Se busca sobre el texto **antes** de purificar, que es el único momento en el que
     * se sabe qué había: después ya no queda rastro. La expresión acepta atributos y
     * mayúsculas (`<IFrame src=…`), y no pretende ser un analizador de HTML: para avisar
     * al autor basta con encontrar la etiqueta de apertura.
     *
     * @return list<string> en el orden de `PROHIBIDAS`, sin repetidos
     */
    public static function prohibidasPresentes(?string $html): array
    {
        if ($html === null || trim($html) === '') {
            return [];
        }

        $encontradas = [];

        foreach (self::PROHIBIDAS as $etiqueta) {
            if (preg_match('/<\s*' . $etiqueta . '(\s|>|\/)/i', $html) === 1) {
                $encontradas[] = $etiqueta;
            }
        }

        return $encontradas;
    }

    private HTMLPurifier $purifier;

    public function __construct()
    {
        $config = HTMLPurifier_Config::createDefault();

        // Lista blanca de etiquetas. Respecto a la versión anterior solo se añade
        // `class`: no se admite ninguna etiqueta nueva.
        $config->set('HTML.Allowed', implode(',', [
            'p[class]', 'br', 'strong', 'em', 'u', 'strike', 'sub', 'sup',
            'h1[class]', 'h2[class]', 'h3[class]', 'h4[class]', 'h5[class]', 'h6[class]',
            'ul[class]', 'ol[class]', 'li[class]',
            // Listas de definiciones: comprobado que sobreviven al purificador de
            // Moodle, y son el componente natural para un glosario de campos o un
            // "término / qué significa" en un manual. Ver features/PRODSECU-193.
            'dl[class]', 'dt[class]', 'dd[class]',
            'blockquote[class]', 'pre[class]', 'code[class]',
            'a[href|target|class]',
            'img[src|alt|width|height|title|class]',
            'table[class]', 'thead[class]', 'tbody[class]', 'tr[class]',
            'td[class|colspan|rowspan]', 'th[class|colspan|rowspan]',
            'div[class]', 'span[class|style]', 'hr[class]',
        ]));

        // Vocabulario cerrado de clases: solo las de Bootstrap que se comportan
        // IGUAL en Bootstrap 4 y 5. El Manager sirve el mismo contenido a entornos
        // Moodle 4.1 y 4.5 (Bootstrap 4) y 5.x (Bootstrap 5), y hay clases que
        // cambiaron de nombre entre versiones. Cerrar el vocabulario evita además
        // que el contenido se acople a clases del tema del cliente o que alguien
        // maquete encima de su página con utilidades de posicionamiento.
        $config->set('Attr.AllowedClasses', $this->allowedClasses());

        // Estilos inline: solo en `span` y solo estas propiedades (color, resaltado
        // y énfasis que produce el propio editor). Nada de maquetación.
        $config->set('CSS.AllowedProperties', 'text-align,color,background-color,font-weight,font-style,text-decoration');

        $config->set('HTML.TargetBlank', true);
        $config->set('Attr.AllowedFrameTargets', ['_blank', '_self']);

        $config->set('URI.AllowedSchemes', [
            'http' => true,
            'https' => true,
        ]);

        $config->set('HTML.ForbiddenElements', self::PROHIBIDAS);

        $cacheDir = storage_path('app/cache/htmlpurifier');
        if (!is_dir($cacheDir)) {
            mkdir($cacheDir, 0755, true);
        }
        $config->set('Cache.SerializerPath', $cacheDir);

        $this->purifier = new HTMLPurifier($config);
    }

    /**
     * Clases admitidas, todas idénticas en Bootstrap 4 y Bootstrap 5.
     *
     * Deliberadamente FUERA, porque cambiaron de nombre entre las dos versiones y
     * se degradarían en silencio en los Moodle 4.x:
     *   `ml-*`/`mr-*` (BS4) vs `ms-*`/`me-*` (BS5); `text-left`/`text-right` vs
     *   `text-start`/`text-end`; `float-left`/`right` vs `float-start`/`end`;
     *   `font-weight-bold` vs `fw-bold`; `badge-primary` vs `bg-primary`;
     *   `sr-only` vs `visually-hidden`; `gap-*` (solo BS5).
     *
     * Y fuera por seguridad, para que un contenido no pueda maquetar encima de la
     * página del cliente: `position-*`, `fixed-top`, `sticky-top`, `vh-100`,
     * `navbar*`, `modal*`, `offcanvas*`.
     *
     * @return array<int, string>
     */
    private function allowedClasses(): array
    {
        $variants = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'];

        $classes = [
            // Estructura
            'container', 'container-fluid', 'row', 'col', 'col-auto',
            // Tipografía (solo centrado: izquierda y derecha cambian de nombre entre BS4 y BS5)
            'text-center', 'text-muted', 'text-white', 'text-uppercase', 'text-lowercase',
            'text-capitalize', 'text-nowrap', 'text-truncate', 'lead', 'small',
            'blockquote', 'blockquote-footer',
            // Componentes
            'alert', 'alert-heading', 'alert-link',
            'card', 'card-body', 'card-header', 'card-footer',
            'card-title', 'card-subtitle', 'card-text', 'card-img-top',
            'badge', 'btn', 'btn-sm', 'btn-lg',
            'table', 'table-striped', 'table-bordered', 'table-borderless',
            'table-hover', 'table-sm', 'table-responsive',
            'list-group', 'list-group-item', 'list-group-item-action',
            // `figure-img` se queda porque va sobre el <img>, que sí sobrevive. Las
            // clases `figure` y `figure-caption` NO están: sus elementos (<figure>,
            // <figcaption>) son HTML5 y el purificador de Moodle los elimina, porque
            // usa doctype XHTML 1.0 Transitional. Comprobado el 2026-08-27.
            'figure-img',
            // Utilidades seguras
            'border', 'border-0', 'rounded', 'rounded-circle',
            'shadow', 'shadow-sm', 'shadow-lg',
            'd-none', 'd-block', 'd-inline', 'd-inline-block', 'd-flex',
            'flex-column', 'flex-wrap',
            'justify-content-start', 'justify-content-center', 'justify-content-end',
            'justify-content-between', 'justify-content-around',
            'align-items-start', 'align-items-center', 'align-items-end',
            'w-100', 'h-100', 'img-fluid', 'img-thumbnail', 'mx-auto',
            // Componentes interactivos que monta `local_tresipunt` con su propio
            // JavaScript a partir de estas clases. No usan `data-bs-*` a propósito:
            // el purificador de Moodle borra cualquier `data-*` (comprobado), y los
            // nombres de atributo de Bootstrap cambian entre Moodle 4.x y 5.x.
            //
            // Los nombres son los que espera `amd/src/remotecomponents.js` del plugin
            // (BEM con doble guión bajo). Verificado contra su código, no supuesto:
            // si aquí se escriben con guión simple, el plugin no los reconoce y el
            // contenido llega al cliente como divs sueltos.
            //
            //   <div class="tip-accordion">
            //     <div class="tip-accordion__item">
            //       <h5 class="tip-accordion__title">Título</h5>
            //       <div class="tip-accordion__body"><p>Contenido</p></div>
            //     </div>
            //   </div>
            //
            //   <div class="tip-tabs">
            //     <ul class="tip-tabs__nav">
            //       <li class="tip-tabs__tab">Primera</li>
            //     </ul>
            //     <div class="tip-tabs__panel"><p>Contenido</p></div>
            //   </div>
            //
            // Las clases que añade el JavaScript en cliente (`--ready`, `__toggle`,
            // `__btn`, `__item--open`) NO van aquí: nunca vienen en el contenido
            // guardado. Ver features/PRODSECU-193.
            'tip-accordion', 'tip-accordion__item', 'tip-accordion__title', 'tip-accordion__body',
            'tip-tabs', 'tip-tabs__nav', 'tip-tabs__tab', 'tip-tabs__panel',
        ];

        // Colores de texto, fondo, alertas, botones y items de lista
        foreach ($variants as $variant) {
            $classes[] = 'text-' . $variant;
            $classes[] = 'bg-' . $variant;
            $classes[] = 'alert-' . $variant;
            $classes[] = 'btn-' . $variant;
            $classes[] = 'btn-outline-' . $variant;
            $classes[] = 'list-group-item-' . $variant;
        }

        $classes[] = 'bg-white';
        $classes[] = 'bg-transparent';

        // Rejilla: col-1..12 y col-{sm,md,lg,xl}-1..12 (+ -auto)
        foreach (['', 'sm-', 'md-', 'lg-', 'xl-'] as $breakpoint) {
            for ($i = 1; $i <= 12; $i++) {
                $classes[] = 'col-' . $breakpoint . $i;
            }
            if ($breakpoint !== '') {
                $classes[] = 'col-' . $breakpoint . 'auto';
            }
        }

        // Espaciado: solo las variantes cuyo nombre no cambió entre BS4 y BS5
        foreach (['m', 'mt', 'mb', 'mx', 'my', 'p', 'pt', 'pb', 'px', 'py'] as $prefix) {
            for ($i = 0; $i <= 5; $i++) {
                $classes[] = $prefix . '-' . $i;
            }
        }

        return $classes;
    }

    /**
     * Sanitiza contenido HTML
     */
    public function sanitize(?string $html): string
    {
        if (empty($html)) {
            return '';
        }

        return $this->purifier->purify($html);
    }
}
