<?php

use Illuminate\Support\HtmlString;
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;
    }
}

if (! function_exists('website_locales')) {
    /** The locales the public site can be served in, default first. */
    function website_locales(): array
    {
        return ['ar', 'en'];
    }
}

if (! function_exists('website_locale_from_path')) {
    /**
     * Reads a locale out of the first path segment, or null when there is none.
     *
     * The public site answers both `/about-us` and `/en/about-us`. The prefix is
     * what makes a page addressable per language — without it a shared link
     * lands the reader in whatever locale their own session happens to hold.
     */
    function website_locale_from_path(string $path): ?string
    {
        $first = strtok(trim($path, '/'), '/');

        return in_array($first, website_locales(), true) ? $first : null;
    }
}

if (! function_exists('website_strip_locale')) {
    /** Removes a leading locale segment from a path, leaving the page slug. */
    function website_strip_locale(string $path): string
    {
        $path = trim($path, '/');
        $locale = website_locale_from_path($path);

        if ($locale === null) {
            return $path;
        }

        return ltrim(substr($path, strlen($locale)), '/');
    }
}

if (! function_exists('website_url')) {
    /**
     * A public-site URL carrying its locale in the path.
     *
     * Every internal link on the public site goes through this, so following a
     * link never silently changes the language the reader chose.
     */
    function website_url(string $slug = '', ?string $locale = null): string
    {
        $locale = in_array($locale, website_locales(), true) ? $locale : app()->getLocale();
        $locale = in_array($locale, website_locales(), true) ? $locale : website_locales()[0];

        $slug = trim(website_strip_locale($slug), '/');

        return url($slug === '' ? '/'.$locale : '/'.$locale.'/'.$slug);
    }
}

if (! function_exists('website_current_url_in')) {
    /** The URL of the page being viewed, in another locale. */
    function website_current_url_in(string $locale): string
    {
        $query = request()->getQueryString();

        return website_url(website_strip_locale(request()->path()), $locale)
            .($query ? '?'.$query : '');
    }
}

if (! function_exists('website_highlight')) {
    /**
     * Renders `{braced}` fragments of a heading in the site's accent colour.
     *
     * "Welcome to {OC-Sport}" is one editable string with one emphasis rule, not
     * two fields the editor has to keep in sync. Everything outside the braces
     * is escaped, so a heading remains plain text as far as the author is
     * concerned — the braces are the only markup they can express.
     */
    function website_highlight(?string $text): HtmlString
    {
        if (blank($text)) {
            return new HtmlString('');
        }

        $html = preg_replace_callback(
            '/\{([^{}]*)\}/u',
            // $text is escaped before the match runs, so the captured fragment
            // is already safe — escaping it again would double-encode entities.
            fn (array $m) => '<span class="ec-accent-text">'.$m[1].'</span>',
            e($text, false),
        );

        return new HtmlString($html ?? e($text));
    }
}

if (! function_exists('website_text')) {
    /**
     * Resolves a possibly-bilingual value to the active locale.
     *
     * Block content stores translatable fields as ['ar' => …, 'en' => …].
     * WebsiteBlock::get() unwraps that for top-level fields, but a repeater row
     * read straight out of the data array does not go through it — so without
     * this, a translatable sub-field reaches the template as an array and
     * blows up the block at echo time.
     *
     * Falls back to the other locale rather than rendering an empty element,
     * matching WebsiteBlock::get().
     */
    function website_text(mixed $value, ?string $locale = null): mixed
    {
        if (! is_array($value) || ! array_key_exists('ar', $value)) {
            return $value;
        }

        $primary = ($locale ?? app()->getLocale()) === 'ar' ? 'ar' : 'en';
        $fallback = $primary === 'ar' ? 'en' : 'ar';

        return filled($value[$primary] ?? null) ? $value[$primary] : ($value[$fallback] ?? null);
    }
}
