<?php

declare(strict_types=1);

namespace App\Services;

use DOMDocument;
use DOMElement;
use DOMNode;

class HomepageAnnouncementSanitizer
{
    private const ALLOWED_TAGS = [
        'p', 'br', 'strong', 'b', 'em', 'i', 'a', 'ul', 'ol', 'li', 'h2', 'h3', 'blockquote',
    ];

    private const ALLOWED_ATTRS = [
        'a' => ['href', 'title', 'target', 'rel'],
    ];

    public function sanitizeHtml(?string $html): string
    {
        if ($html === null || trim($html) === '') {
            return '';
        }

        $encodedHtml = mb_encode_numericentity(
            $html,
            [0x80, 0x10FFFF, 0, 0xFFFFFF],
            'UTF-8'
        );

        $document = new DOMDocument('1.0', 'UTF-8');

        libxml_use_internal_errors(true);
        $document->loadHTML(
            '<?xml encoding="UTF-8"><html><body>' . $encodedHtml . '</body></html>',
            LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
        );
        libxml_clear_errors();

        $body = $document->getElementsByTagName('body')->item(0);
        if (! $body instanceof DOMNode) {
            return '';
        }

        $this->cleanNode($body);

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

        return trim(html_entity_decode($innerHtml, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
    }

    public function sanitizeCustomUrl(?string $url): ?string
    {
        $url = trim((string) $url);

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

        if (! $this->isSafeCustomUrl($url)) {
            return null;
        }

        return $url;
    }

    public function isSafeCustomUrl(?string $url): bool
    {
        $url = trim((string) $url);
        if ($url === '') {
            return true;
        }

        $lower = strtolower($url);
        if (str_starts_with($lower, 'javascript:') || str_contains($lower, 'onerror=') || str_contains($lower, 'onclick=')) {
            return false;
        }

        if (str_starts_with($url, '/')) {
            return true;
        }

        return str_starts_with($lower, 'https://');
    }

    private function cleanNode(DOMNode $node): void
    {
        $toRemove = [];
        $toUnwrap = [];

        foreach ($node->childNodes as $child) {
            if ($child->nodeType !== XML_ELEMENT_NODE) {
                continue;
            }

            if (! $child instanceof DOMElement) {
                continue;
            }

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

            if (in_array($tag, ['script', 'style', 'iframe'], true)) {
                $toRemove[] = $child;
                continue;
            }

            if (! in_array($tag, self::ALLOWED_TAGS, true)) {
                $toUnwrap[] = $child;
                continue;
            }

            $allowedAttrs = self::ALLOWED_ATTRS[$tag] ?? [];
            $attrsToRemove = [];
            foreach ($child->attributes as $attribute) {
                if (! in_array($attribute->nodeName, $allowedAttrs, true)) {
                    $attrsToRemove[] = $attribute->nodeName;
                }
            }

            foreach ($attrsToRemove as $attributeName) {
                $child->removeAttribute($attributeName);
            }

            if ($tag === 'a') {
                $href = trim($child->getAttribute('href'));
                if ($href === '' || ! $this->isSafeAnchorHref($href)) {
                    $toUnwrap[] = $child;
                    continue;
                }

                if (str_starts_with(strtolower($href), 'https://')) {
                    $child->setAttribute('rel', 'noopener noreferrer');
                    $child->setAttribute('target', '_blank');
                } else {
                    $child->removeAttribute('target');
                    $child->removeAttribute('rel');
                }
            }

            $this->cleanNode($child);
        }

        foreach ($toRemove as $element) {
            $node->removeChild($element);
        }

        foreach ($toUnwrap as $element) {
            while ($element->firstChild) {
                $node->insertBefore($element->firstChild, $element);
            }

            $node->removeChild($element);
        }
    }

    private function isSafeAnchorHref(string $href): bool
    {
        $lower = strtolower(trim($href));

        if (str_starts_with($lower, 'javascript:') || str_starts_with($lower, 'data:')) {
            return false;
        }

        if (str_starts_with($href, '/') || str_starts_with($href, '#')) {
            return true;
        }

        return str_starts_with($lower, 'https://');
    }
}