<?php

declare(strict_types=1);

/**
 * 富文本正文消毒與輸出（Summernote HTML；技師評論等前台模組共用）。
 */

/** 是否為 Summernote / HTML 內容（舊資料多為純文字）。 */
function fate_rich_body_looks_like_html(string $body): bool
{
    return (bool) preg_match('/<(?:p|br|strong|b|em|i|u|ul|ol|li|blockquote|a|img|table|thead|tbody|tr|th|td|h[1-6]|div|span)\b/i', $body);
}

/** 擷取可見文字長度（用於字數驗證）。 */
function fate_rich_body_text_length(string $body): int
{
    $plain = strip_tags($body);
    $plain = html_entity_decode($plain, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $plain = str_replace("\xc2\xa0", ' ', $plain);
    $plain = trim(preg_replace('/\s+/u', ' ', $plain) ?? $plain);

    return function_exists('mb_strlen') ? mb_strlen($plain) : strlen($plain);
}

/** 正文轉純文字（供預讀字數計算）。 */
function fate_rich_body_plain_text(string $body): string
{
    if (fate_rich_body_looks_like_html($body)) {
        $plain = strip_tags($body);
        $plain = html_entity_decode($plain, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $plain = str_replace("\xc2\xa0", ' ', $plain);

        return trim(preg_replace('/\s+/u', ' ', $plain) ?? $plain);
    }

    return trim($body);
}

/**
 * 消毒並正規化要存入資料庫的正文。
 * 純文字仍原樣保留；HTML 僅允許安全標籤。
 */
function fate_rich_body_prepare_for_storage(string $body): string
{
    $body = trim($body);
    if ($body === '') {
        return '';
    }

    if (!fate_rich_body_looks_like_html($body)) {
        return $body;
    }

    $body = fate_rich_body_sanitize_html($body);
    if ($body === '' || preg_match('/^<p>(?:\s|&nbsp;|<br\s*\/?>)*<\/p>$/iu', $body)) {
        return '';
    }

    return $body;
}

/**
 * @return list<string>
 */
function fate_rich_body_allowed_tags(): array
{
    return ['p', 'br', 'strong', 'b', 'em', 'i', 'u', 's', 'strike', 'ul', 'ol', 'li', 'blockquote', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'h3', 'h4', 'span', 'div'];
}

/** 允許嵌入正文中的圖片 src（本站 uploads/reviews/ 與 uploads/forum/）。 */
function fate_rich_body_is_allowed_image_src(string $src): bool
{
    $src = trim($src);
    if ($src === '' || preg_match('#^(data:|javascript:)#i', $src)) {
        return false;
    }

    $path = (string) parse_url($src, PHP_URL_PATH);
    if ($path === '') {
        $path = $src;
    }
    $path = str_replace('\\', '/', $path);

    return (bool) preg_match(
        '#(?:^|/)(?:\.\./)?uploads/(?:reviews|forum|blog)/[a-zA-Z0-9._-]+\.(?:jpe?g|png|webp|gif)$#i',
        $path
    );
}

function fate_rich_body_sanitize_img_dimension(string $value): ?string
{
    $value = trim($value);
    if ($value === '') {
        return null;
    }
    if (preg_match('/^(\d{1,4})(?:\.\d+)?$/', $value, $m) === 1) {
        $n = (int) $m[1];

        return ($n >= 1 && $n <= 1600) ? (string) $n : null;
    }
    if (preg_match('/^(\d{1,3})%$/', $value, $m) === 1) {
        $n = (int) $m[1];

        return ($n >= 1 && $n <= 100) ? ($n . '%') : null;
    }

    return null;
}

function fate_rich_body_sanitize_img_css_size(string $value): ?string
{
    $value = trim(strtolower($value));
    if ($value === 'auto') {
        return 'auto';
    }
    if (preg_match('/^(\d{1,4})(?:\.\d+)?px$/', $value, $m) === 1) {
        $n = (int) $m[1];

        return ($n >= 1 && $n <= 1600) ? ($n . 'px') : null;
    }
    if (preg_match('/^(\d{1,3})%$/', $value, $m) === 1) {
        $n = (int) $m[1];

        return ($n >= 1 && $n <= 100) ? ($n . '%') : null;
    }

    return null;
}

function fate_rich_body_sanitize_img_style(string $style): string
{
    $style = trim($style);
    if ($style === '') {
        return '';
    }
    $allowed = [];
    foreach (preg_split('/\s*;\s*/', $style) as $decl) {
        if ($decl === '') {
            continue;
        }
        if (preg_match('/^(width|height|max-width)\s*:\s*(.+)$/i', $decl, $m) !== 1) {
            continue;
        }
        $prop = strtolower($m[1]);
        $san = fate_rich_body_sanitize_img_css_size(trim($m[2]));
        if ($san === null) {
            continue;
        }
        if ($prop === 'height' && $san === 'auto') {
            $allowed[] = 'height:auto';
            continue;
        }
        if ($san === 'auto') {
            continue;
        }
        $allowed[] = $prop . ':' . $san;
    }

    return implode(';', $allowed);
}

function fate_rich_body_sanitize_links_in_html(string $html): string
{
    return (string) preg_replace_callback(
        '/<a\s+([^>]+)>/i',
        static function (array $m): string {
            if (!preg_match('/href=(["\'])([^"\']+)\1/i', $m[1], $href)) {
                return '';
            }
            $url = $href[2];
            if (!preg_match('#^(https?://|mailto:)#i', $url)) {
                return '';
            }
            $esc = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');

            return '<a href="' . $esc . '" target="_blank" rel="noopener noreferrer nofollow">';
        },
        $html
    );
}

function fate_rich_body_sanitize_html(string $html): string
{
    $html = preg_replace('/<(script|style|iframe|object|embed|form|input|textarea|button|link|meta|svg|math)[^>]*>.*?<\/\1>/is', '', $html) ?? $html;
    $html = preg_replace('/<(script|style|iframe|object|embed|form|input|textarea|button|link|meta|svg|math)[^>]*\/?>/i', '', $html) ?? $html;

    if (!class_exists(DOMDocument::class)) {
        $allowed = '<' . implode('><', fate_rich_body_allowed_tags()) . '>';
        $clean = strip_tags($html, $allowed);

        return fate_rich_body_sanitize_links_in_html($clean);
    }

    $prev = libxml_use_internal_errors(true);
    $doc = new DOMDocument('1.0', 'UTF-8');
    $wrapper = '<?xml encoding="UTF-8"><div id="fate-rich-sanitize-root">' . $html . '</div>';
    if (!$doc->loadHTML($wrapper, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD)) {
        libxml_clear_errors();
        libxml_use_internal_errors($prev);

        return fate_rich_body_sanitize_links_in_html(strip_tags($html, '<' . implode('><', fate_rich_body_allowed_tags()) . '>'));
    }
    libxml_clear_errors();

    $root = $doc->getElementById('fate-rich-sanitize-root');
    if ($root === null) {
        libxml_use_internal_errors($prev);

        return '';
    }

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

    return trim($out);
}

function fate_rich_body_sanitize_dom_node(DOMNode $node): void
{
    if (!$node->hasChildNodes()) {
        return;
    }

    $allowed = array_flip(fate_rich_body_allowed_tags());
    $remove = [];
    foreach ($node->childNodes as $child) {
        if ($child instanceof DOMElement) {
            $tag = strtolower($child->tagName);
            if (!isset($allowed[$tag])) {
                $remove[] = $child;
                continue;
            }
            if ($tag === 'a') {
                $href = trim((string) $child->getAttribute('href'));
                if ($href === '' || !preg_match('#^(https?://|mailto:)#i', $href)) {
                    $remove[] = $child;
                    continue;
                }
                while ($child->attributes->length > 0) {
                    $child->removeAttribute($child->attributes->item(0)->name);
                }
                $child->setAttribute('href', $href);
                $child->setAttribute('target', '_blank');
                $child->setAttribute('rel', 'noopener noreferrer nofollow');
            } elseif ($tag === 'img') {
                $src = trim((string) $child->getAttribute('src'));
                if (!fate_rich_body_is_allowed_image_src($src)) {
                    $remove[] = $child;
                    continue;
                }
                $alt = trim((string) $child->getAttribute('alt'));
                $width = fate_rich_body_sanitize_img_dimension((string) $child->getAttribute('width'));
                $height = fate_rich_body_sanitize_img_dimension((string) $child->getAttribute('height'));
                $style = fate_rich_body_sanitize_img_style((string) $child->getAttribute('style'));
                while ($child->attributes->length > 0) {
                    $child->removeAttribute($child->attributes->item(0)->name);
                }
                $child->setAttribute('src', $src);
                $child->setAttribute('alt', $alt);
                $child->setAttribute('loading', 'lazy');
                $child->setAttribute('decoding', 'async');
                if ($width !== null) {
                    $child->setAttribute('width', $width);
                }
                if ($height !== null) {
                    $child->setAttribute('height', $height);
                }
                if ($style !== '') {
                    $child->setAttribute('style', $style);
                }
            } elseif (in_array($tag, ['table', 'thead', 'tbody', 'tr', 'th', 'td'], true)) {
                $colspan = in_array($tag, ['th', 'td'], true) ? (int) $child->getAttribute('colspan') : 0;
                $rowspan = in_array($tag, ['th', 'td'], true) ? (int) $child->getAttribute('rowspan') : 0;
                while ($child->attributes->length > 0) {
                    $child->removeAttribute($child->attributes->item(0)->name);
                }
                if ($colspan > 1 && $colspan <= 12) {
                    $child->setAttribute('colspan', (string) $colspan);
                }
                if ($rowspan > 1 && $rowspan <= 12) {
                    $child->setAttribute('rowspan', (string) $rowspan);
                }
            } else {
                while ($child->attributes->length > 0) {
                    $child->removeAttribute($child->attributes->item(0)->name);
                }
            }
            fate_rich_body_sanitize_dom_node($child);
        } elseif ($child instanceof DOMComment) {
            $remove[] = $child;
        } else {
            fate_rich_body_sanitize_dom_node($child);
        }
    }
    foreach ($remove as $r) {
        if ($r->parentNode !== null) {
            $r->parentNode->removeChild($r);
        }
    }
}

/** 編輯器 textarea 初始內容（HTML 原樣、純文字跳脫）。 */
function fate_rich_editor_textarea_content(string $body): string
{
    if (fate_rich_body_looks_like_html($body)) {
        return fate_rich_body_sanitize_html($body);
    }

    return htmlspecialchars($body, ENT_QUOTES, 'UTF-8');
}

/** 將正文安全輸出為 HTML（富文本消毒或純文字跳脫 + 換行 + 連結）。 */
function fate_rich_body_html(string $body): string
{
    if (fate_rich_body_looks_like_html($body)) {
        return fate_rich_body_sanitize_html($body);
    }

    $safe = htmlspecialchars($body, ENT_QUOTES, 'UTF-8');
    $safe = preg_replace_callback(
        '~https?://[^\s<]+~u',
        static fn ($m) => '<a href="' . htmlspecialchars($m[0], ENT_QUOTES, 'UTF-8') . '" target="_blank" rel="noopener noreferrer nofollow">' . htmlspecialchars($m[0], ENT_QUOTES, 'UTF-8') . '</a>',
        $safe
    );

    return nl2br((string) $safe);
}
