<?php

namespace App\Support;

use DOMDocument;
use DOMElement;
use DOMNode;

class LegalDocumentHtml
{
    /**
     * Clean admin-authored legal HTML before storing or rendering it.
     */
    public static function sanitize(?string $html): string
    {
        $html = trim((string) $html);

        if ($html === '') {
            return '';
        }

        $previous = libxml_use_internal_errors(true);
        $document = new DOMDocument('1.0', 'UTF-8');
        $document->loadHTML(
            '<!DOCTYPE html><html><body><div id="legal-document-root">'.$html.'</div></body></html>',
            LIBXML_NOERROR | LIBXML_NOWARNING,
        );
        libxml_clear_errors();
        libxml_use_internal_errors($previous);

        $root = $document->getElementById('legal-document-root');

        if ($root === null) {
            return '';
        }

        self::sanitizeChildren($root);

        $clean = '';
        foreach ($root->childNodes as $child) {
            $clean .= $document->saveHTML($child);
        }

        return trim($clean);
    }

    private static function sanitizeChildren(DOMNode $node): void
    {
        foreach (iterator_to_array($node->childNodes) as $child) {
            if (! $child instanceof DOMElement) {
                continue;
            }

            if (in_array(strtolower($child->tagName), ['script', 'style', 'iframe', 'object', 'embed'], true)) {
                $child->parentNode?->removeChild($child);

                continue;
            }

            if (! in_array(strtolower($child->tagName), self::allowedTags(), true)) {
                self::sanitizeChildren($child);
                self::unwrapNode($child);

                continue;
            }

            self::cleanAttributes($child);
            self::sanitizeChildren($child);
        }
    }

    private static function unwrapNode(DOMElement $node): void
    {
        $parent = $node->parentNode;

        if ($parent === null) {
            return;
        }

        while ($node->firstChild !== null) {
            $parent->insertBefore($node->firstChild, $node);
        }

        $parent->removeChild($node);
    }

    private static function cleanAttributes(DOMElement $node): void
    {
        $href = strtolower($node->tagName) === 'a'
            ? trim((string) $node->getAttribute('href'))
            : '';

        foreach (iterator_to_array($node->attributes) as $attribute) {
            $node->removeAttribute($attribute->name);
        }

        if (strtolower($node->tagName) !== 'a') {
            return;
        }

        if ($href === '' || ! self::isSafeUrl($href)) {
            return;
        }

        $node->setAttribute('href', $href);
        $node->setAttribute('rel', 'noopener noreferrer');
    }

    private static function isSafeUrl(string $url): bool
    {
        if (str_starts_with($url, '/') || str_starts_with($url, '#')) {
            return true;
        }

        $scheme = parse_url($url, PHP_URL_SCHEME);

        return in_array(is_string($scheme) ? strtolower($scheme) : null, ['http', 'https', 'mailto'], true);
    }

    /**
     * @return list<string>
     */
    private static function allowedTags(): array
    {
        return [
            'a',
            'blockquote',
            'br',
            'div',
            'em',
            'h2',
            'h3',
            'i',
            'li',
            'ol',
            'p',
            's',
            'span',
            'strong',
            'u',
            'ul',
        ];
    }
}
