<?php

/**
 * DOMDocument-based allowlist HTML sanitizer for article content produced by Quill.
 */
class HtmlSanitizer
{
    private const ALLOWED_TAGS = [
        'p', 'h2', 'h3', 'h4', 'h5', 'h6',
        'a', 'strong', 'em', 'b', 'i', 'u', 's',
        'ul', 'ol', 'li',
        'blockquote', 'pre', 'code',
        'br', 'hr',
        'img', 'figure', 'figcaption',
        'table', 'thead', 'tbody', 'tr', 'td', 'th',
        'span', 'div',
    ];

    private const ALLOWED_ATTRS = [
        'a' => ['href', 'title', 'rel', 'target'],
        'img' => ['src', 'alt', 'title', 'width', 'height', 'loading', 'decoding'],
        'li' => ['data-list'],
        '*' => ['class'],
    ];

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

        $html = '<?xml encoding="UTF-8"?><div id="__root__">' . $html . '</div>';

        libxml_use_internal_errors(true);
        $dom = new DOMDocument('1.0', 'UTF-8');
        $dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NONET);
        libxml_clear_errors();

        $root = $dom->getElementById('__root__');
        if (!$root) {
            return '';
        }

        self::cleanNode($root);

        $out = '';
        foreach ($root->childNodes as $child) {
            $out .= $dom->saveHTML($child);
        }
        return trim($out);
    }

    private static function cleanNode(DOMNode $node): void
    {
        $toRemove = [];
        foreach ($node->childNodes as $child) {
            if ($child instanceof DOMElement) {
                $tag = strtolower($child->nodeName);
                if (!in_array($tag, self::ALLOWED_TAGS, true)) {
                    // unwrap (keep children) for structural cleanups instead of nuking
                    while ($child->firstChild) {
                        $node->insertBefore($child->firstChild, $child);
                    }
                    $toRemove[] = $child;
                    continue;
                }

                // sanitize attributes
                $attrs = [];
                foreach ($child->attributes as $a) {
                    $attrs[] = $a->name;
                }
                $allowedForTag = self::ALLOWED_ATTRS[$tag] ?? [];
                $allowedForAll = self::ALLOWED_ATTRS['*'] ?? [];
                $allowed = array_merge($allowedForTag, $allowedForAll);
                foreach ($attrs as $name) {
                    if (!in_array(strtolower($name), $allowed, true)) {
                        $child->removeAttribute($name);
                        continue;
                    }
                    // sanitize href/src to avoid javascript:
                    if (in_array(strtolower($name), ['href', 'src'], true)) {
                        $val = $child->getAttribute($name);
                        $val = trim($val);
                        if (preg_match('#^\s*javascript:#i', $val) || preg_match('#^\s*data:(?!image/)#i', $val)) {
                            $child->removeAttribute($name);
                            continue;
                        }
                    }
                }

                // External link hardening
                if ($tag === 'a' && $child->hasAttribute('href')) {
                    $href = $child->getAttribute('href');
                    if (preg_match('#^https?://#i', $href) && parse_url($href, PHP_URL_HOST) !== parse_url(APP_URL, PHP_URL_HOST)) {
                        $child->setAttribute('rel', 'noopener noreferrer nofollow ugc');
                        if (!$child->hasAttribute('target')) {
                            $child->setAttribute('target', '_blank');
                        }
                    }
                }

                // lazy load images by default
                if ($tag === 'img') {
                    if (!$child->hasAttribute('loading')) $child->setAttribute('loading', 'lazy');
                    if (!$child->hasAttribute('decoding')) $child->setAttribute('decoding', 'async');
                }

                self::cleanNode($child);
            }
        }
        foreach ($toRemove as $r) {
            if ($r->parentNode) $r->parentNode->removeChild($r);
        }
    }

    public static function wordCount(string $html): int
    {
        $text = trim(preg_replace('/\s+/', ' ', strip_tags($html)));
        if ($text === '') return 0;
        return str_word_count($text);
    }

    public static function excerpt(string $html, int $length = 200): string
    {
        $text = trim(preg_replace('/\s+/', ' ', strip_tags($html)));
        if (mb_strlen($text) <= $length) return $text;
        return rtrim(mb_substr($text, 0, $length)) . '…';
    }
}
