<?php

use Illuminate\Support\Str;

if (! function_exists('clean_html')) {
    /**
     * Sanitises rich-text and pasted markup before it reaches a public page.
     *
     * Builder content is authored by academy staff, not developers, and the
     * custom-HTML block accepts arbitrary paste. Without this, one pasted
     * snippet could run script on every visitor of a tenant's site.
     * Allow-list based: anything not explicitly permitted is dropped.
     */
    function clean_html(?string $html): string
    {
        if (blank($html)) {
            return '';
        }

        $allowedTags = [
            'p', 'br', 'strong', 'b', 'em', 'i', 'u', 's', 'span', 'div',
            'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
            'ul', 'ol', 'li', 'blockquote', 'hr',
            'a', 'img', 'figure', 'figcaption',
            'table', 'thead', 'tbody', 'tr', 'th', 'td',
            'sup', 'sub', 'small', 'code', 'pre',
        ];

        $allowedAttributes = [
            'a' => ['href', 'title', 'target', 'rel'],
            'img' => ['src', 'alt', 'width', 'height', 'loading'],
            'td' => ['colspan', 'rowspan'],
            'th' => ['colspan', 'rowspan', 'scope'],
            '*' => ['class', 'dir', 'id'],
        ];

        $previous = libxml_use_internal_errors(true);

        $doc = new DOMDocument('1.0', 'UTF-8');
        $doc->loadHTML(
            '<?xml encoding="UTF-8"><div id="ec-clean-root">'.$html.'</div>',
            LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NONET,
        );

        libxml_clear_errors();
        libxml_use_internal_errors($previous);

        $root = $doc->getElementById('ec-clean-root');

        if (! $root) {
            return e($html);
        }

        $xpath = new DOMXPath($doc);

        // Strip disallowed elements, keeping their text where sensible.
        foreach (iterator_to_array($xpath->query('//*', $root)) as $node) {
            if (! $node instanceof DOMElement || $node === $root) {
                continue;
            }

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

            if (! in_array($tag, $allowedTags, true)) {
                // script/style contents are discarded entirely; other unknown
                // wrappers are unwrapped so their text survives.
                if (in_array($tag, ['script', 'style', 'iframe', 'object', 'embed', 'form'], true)) {
                    $node->parentNode?->removeChild($node);
                } else {
                    while ($node->firstChild) {
                        $node->parentNode?->insertBefore($node->firstChild, $node);
                    }
                    $node->parentNode?->removeChild($node);
                }

                continue;
            }

            $permitted = array_merge($allowedAttributes['*'], $allowedAttributes[$tag] ?? []);

            foreach (iterator_to_array($node->attributes ?? []) as $attr) {
                $name = strtolower($attr->nodeName);
                $value = trim($attr->nodeValue ?? '');

                // Drop every event handler and anything not allow-listed.
                if (str_starts_with($name, 'on') || ! in_array($name, $permitted, true)) {
                    $node->removeAttribute($attr->nodeName);

                    continue;
                }

                // Block javascript:, data: and other executable URL schemes.
                if (in_array($name, ['href', 'src'], true)) {
                    $scheme = Str::of($value)->lower()->replace([' ', "\t", "\n", "\0"], '')->value();

                    $safe = $scheme === ''
                        || str_starts_with($scheme, 'http://')
                        || str_starts_with($scheme, 'https://')
                        || str_starts_with($scheme, 'mailto:')
                        || str_starts_with($scheme, 'tel:')
                        || str_starts_with($scheme, '/')
                        || str_starts_with($scheme, '#')
                        || ($name === 'src' && str_starts_with($scheme, 'data:image/'));

                    if (! $safe) {
                        $node->removeAttribute($attr->nodeName);
                    }
                }
            }

            // Any link leaving the site opens safely.
            if ($tag === 'a' && $node->getAttribute('target') === '_blank') {
                $node->setAttribute('rel', 'noopener noreferrer');
            }
        }

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

        return $out;
    }
}

if (! function_exists('website_video_embed_url')) {
    /** Builds a player URL with playback flags for YouTube/Vimeo. */
    function website_video_embed_url(?string $url, array $params = []): ?string
    {
        if (blank($url)) {
            return null;
        }

        $query = array_filter($params, fn ($v) => $v !== null && $v !== '' && $v !== 0);

        if (preg_match('~(?:youtube\.com/(?:watch\?v=|embed/|shorts/)|youtu\.be/)([\w-]{6,})~i', $url, $m)) {
            if (! empty($params['loop'])) {
                $query['playlist'] = $m[1];
            }

            return 'https://www.youtube-nocookie.com/embed/'.$m[1]
                .($query ? '?'.http_build_query($query) : '');
        }

        if (preg_match('~vimeo\.com/(?:video/)?(\d+)~i', $url, $m)) {
            $vimeo = array_filter([
                'autoplay' => $params['autoplay'] ?? null,
                'muted' => $params['mute'] ?? null,
                'loop' => $params['loop'] ?? null,
            ]);

            return 'https://player.vimeo.com/video/'.$m[1]
                .($vimeo ? '?'.http_build_query($vimeo) : '');
        }

        return null;
    }
}

if (! function_exists('website_embed_url')) {
    /**
     * Resolves an embed URL for an allow-listed provider.
     * Returning null for anything unrecognised keeps arbitrary third-party
     * iframes off tenant sites.
     */
    function website_embed_url(?string $provider, ?string $url): ?string
    {
        if (blank($provider) || blank($url)) {
            return null;
        }

        return match ($provider) {
            'youtube', 'vimeo' => website_video_embed_url($url),
            'google_maps' => 'https://www.google.com/maps?q='.urlencode($url).'&output=embed',
            'google_form' => str_contains($url, 'docs.google.com/forms') ? $url : null,
            'instagram' => preg_match('~instagram\.com/(p|reel)/([\w-]+)~i', $url, $m)
                ? "https://www.instagram.com/{$m[1]}/{$m[2]}/embed"
                : null,
            'facebook' => 'https://www.facebook.com/plugins/page.php?href='.urlencode($url),
            default => null,
        };
    }
}

if (! function_exists('safe_url')) {
    /**
     * Returns a link target only when its scheme is safe to place in an href.
     *
     * Builder content is authored by staff, and menu items, button links and
     * blueprint imports all feed straight into href attributes. Without this a
     * stored `javascript:` URL would execute for every visitor of a tenant's
     * public site. Returns null for anything not allow-listed so callers can
     * skip rendering the link entirely rather than emitting a dead or unsafe one.
     */
    function safe_url(?string $url): ?string
    {
        if (blank($url)) {
            return null;
        }

        $raw = trim($url);

        // Strip characters browsers ignore when resolving a scheme, so that
        // "java\tscript:" and "java&#115;cript:" cannot slip past the check.
        $probe = html_entity_decode($raw, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        $probe = preg_replace('/[\x00-\x20\x7F]+/u', '', (string) $probe) ?? '';
        $probe = mb_strtolower($probe);

        // Protocol-relative URLs (//evil.test) inherit the page scheme and are
        // an easy way to leave the site unnoticed — treat them as absolute.
        if (str_starts_with($probe, '//')) {
            return null;
        }

        // Site-relative paths and fragments are always fine.
        if (str_starts_with($probe, '/') || str_starts_with($probe, '#') || str_starts_with($probe, '?')) {
            return $raw;
        }

        if (preg_match('/^([a-z][a-z0-9+.\-]*):/', $probe, $m)) {
            return in_array($m[1], ['http', 'https', 'mailto', 'tel', 'whatsapp'], true) ? $raw : null;
        }

        // No scheme at all — a bare path or domain fragment.
        return $raw;
    }
}
