import { useEffect, useState } from 'react';

/* Renders admin-authored HTML body copy from the WYSIWYG editor.
 *
 * The editor (TipTap) outputs HTML. Storage is HTML. The public renderer
 * sanitizes through DOMPurify to strip anything outside the allowlist —
 * mirrors the WYSIWYG's own toolbar so admins can't smuggle through markup
 * the editor doesn't expose (e.g., by directly editing Settings JSON or
 * via API). DOMPurify is lazy-loaded (~16 KB gzipped) so the initial
 * Welcome bundle stays untouched.
 *
 * Allowlisted tags/attrs match what TipTap's StarterKit + Link extension
 * emit: paragraphs, bold, italic, lists, blockquote, code, links. Anything
 * else (img/iframe/video/script/style/forms/headings/tables) is stripped.
 *
 * Pre-load fallback: render a TAG-STRIPPED plain-text version while the
 * chunk fetches — on slow networks the raw `<em>…</em> &amp;` markup was
 * visibly readable in headlines for seconds. The fallback is still rendered
 * as React TEXT (never innerHTML), so the cheap regex strip is purely
 * presentational — sanitization still happens before any HTML reaches the
 * DOM. */

/** Display-only: strip tags + decode the entities TipTap emits. Decode
 *  `&amp;` LAST so `&amp;lt;` correctly displays as `&lt;` (no double
 *  decode). Output is rendered as text content, never as HTML. */
function plainTextFallback(html: string): string {
    return html
        .replace(/<[^>]*>/g, '')
        .replace(/&lt;/g, '<')
        .replace(/&gt;/g, '>')
        .replace(/&quot;/g, '"')
        .replace(/&#0?39;|&apos;/g, "'")
        .replace(/&nbsp;/g, ' ')
        .replace(/&amp;/g, '&');
}

const ALLOWED_TAGS = [
    'p',
    'strong',
    'em',
    'b',
    'i',
    'a',
    'br',
    'ul',
    'ol',
    'li',
    'blockquote',
    'code',
    's',
    'u',
];

// `style` is allowlisted ONLY so TipTap's TextAlign extension can persist
// `style="text-align: center|left|right"` through round-trip. The
// `uponSanitizeAttribute` hook (installed by `loadSanitizer` /
// `loadInlineSanitizer`) inspects each style attribute and strips
// anything that isn't a `text-align` declaration with a known value.
// No other CSS properties survive — closes the arbitrary-CSS-injection
// surface that the bare `style` allowance would otherwise open.
const ALLOWED_ATTR = ['href', 'title', 'target', 'rel', 'style'];

// Whitelist for `style` attribute values. TipTap emits exactly one of:
//   text-align: left|center|right|justify
// Trailing semicolon optional. Any other CSS gets the attribute dropped.
const SAFE_TEXT_ALIGN_RE = /^\s*text-align\s*:\s*(left|center|right|justify)\s*;?\s*$/i;

/** True when an href points off-site: an explicit scheme (http/https/mailto/
 *  tel/…) or a protocol-relative `//host`. Relative paths (`/contact`,
 *  `pricing`) are first-party. Used to scope the new-tab + nofollow hardening
 *  to external links only. */
function isExternalHref(href: string | null): boolean {
    if (!href) return false;
    return /^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith('//');
}

type Sanitizer = (html: string) => string;

let sanitizerPromise: Promise<Sanitizer> | null = null;

function loadSanitizer(): Promise<Sanitizer> {
    if (sanitizerPromise) return sanitizerPromise;
    sanitizerPromise = import('dompurify').then((mod) => {
        const DOMPurify = mod.default;
        DOMPurify.addHook('afterSanitizeAttributes', (node) => {
            // Harden OFF-SITE anchors only: external links open in a new tab
            // and carry nofollow. Internal/relative links (/contact, /pricing)
            // keep same-tab navigation and no nofollow — they're first-party.
            if (
                node instanceof HTMLAnchorElement &&
                isExternalHref(node.getAttribute('href'))
            ) {
                node.setAttribute('target', '_blank');
                node.setAttribute('rel', 'noopener noreferrer nofollow');
            }
        });
        // Strip every `style` attribute that isn't a known-safe text-align
        // declaration. TipTap is the only legitimate source of `style` here
        // (via the TextAlign extension). Anything else (background-image:
        // url(javascript:...), expression(...), arbitrary CSS) gets dropped.
        DOMPurify.addHook('uponSanitizeAttribute', (_node, data) => {
            if (data.attrName === 'style' && !SAFE_TEXT_ALIGN_RE.test(data.attrValue)) {
                data.keepAttr = false;
            }
        });
        return (html: string) =>
            DOMPurify.sanitize(html, {
                ALLOWED_TAGS,
                ALLOWED_ATTR,
                // `\/(?!\/)` re-allows first-party path links (`/contact`)
                // that the bare protocol list dropped, while still blocking
                // protocol-relative `//evil.com` and `javascript:` URIs.
                ALLOWED_URI_REGEXP: /^(?:https?:|mailto:|tel:|#|\/(?!\/))/i,
            });
    });
    return sanitizerPromise;
}

const BASE_CLASSES =
    'leading-relaxed text-muted-foreground ' +
    '[&_p]:mt-4 [&_p:first-child]:mt-0 ' +
    '[&_strong]:font-semibold [&_strong]:text-foreground ' +
    '[&_em]:italic ' +
    '[&_a]:text-accent [&_a]:underline [&_a]:underline-offset-2 hover:[&_a]:no-underline ' +
    '[&_ul]:mt-4 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:mt-4 [&_ol]:list-decimal [&_ol]:pl-5 [&_li]:mt-1 ' +
    '[&_blockquote]:mt-4 [&_blockquote]:border-l-2 [&_blockquote]:border-accent [&_blockquote]:pl-4 [&_blockquote]:italic ' +
    '[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em]';

/* Inline-only sanitizer for contexts that already provide their own block
 * wrapper (headlines, pill labels, etc.). Strips `<p>`, lists, blockquote,
 * code blocks — anything that would close the parent's flow context — and
 * keeps only inline-allowed tags (em, strong, a, br, s, u). The result
 * renders cleanly inside an `<h1>` or a pill `<div>`.
 *
 * Implementation: reuses the same lazy DOMPurify load as SanitizedHtmlBody,
 * just with a stricter ALLOWED_TAGS list and `KEEP_CONTENT: true` so block
 * tags get unwrapped (their inner text/markup is preserved) instead of
 * being silently dropped along with their content. */
const INLINE_ALLOWED_TAGS = ['strong', 'em', 'b', 'i', 'a', 'br', 's', 'u'];

let inlineSanitizerPromise: Promise<Sanitizer> | null = null;

function loadInlineSanitizer(): Promise<Sanitizer> {
    if (inlineSanitizerPromise) return inlineSanitizerPromise;
    inlineSanitizerPromise = import('dompurify').then((mod) => {
        const DOMPurify = mod.default;
        DOMPurify.addHook('afterSanitizeAttributes', (node) => {
            // Harden OFF-SITE anchors only: external links open in a new tab
            // and carry nofollow. Internal/relative links (/contact, /pricing)
            // keep same-tab navigation and no nofollow — they're first-party.
            if (
                node instanceof HTMLAnchorElement &&
                isExternalHref(node.getAttribute('href'))
            ) {
                node.setAttribute('target', '_blank');
                node.setAttribute('rel', 'noopener noreferrer nofollow');
            }
        });
        // Strip every `style` attribute that isn't a known-safe text-align
        // declaration. TipTap is the only legitimate source of `style` here
        // (via the TextAlign extension). Anything else (background-image:
        // url(javascript:...), expression(...), arbitrary CSS) gets dropped.
        DOMPurify.addHook('uponSanitizeAttribute', (_node, data) => {
            if (data.attrName === 'style' && !SAFE_TEXT_ALIGN_RE.test(data.attrValue)) {
                data.keepAttr = false;
            }
        });
        return (html: string) =>
            DOMPurify.sanitize(html, {
                ALLOWED_TAGS: INLINE_ALLOWED_TAGS,
                ALLOWED_ATTR,
                // `\/(?!\/)` re-allows first-party path links (`/contact`)
                // that the bare protocol list dropped, while still blocking
                // protocol-relative `//evil.com` and `javascript:` URIs.
                ALLOWED_URI_REGEXP: /^(?:https?:|mailto:|tel:|#|\/(?!\/))/i,
                KEEP_CONTENT: true,
            });
    });
    return inlineSanitizerPromise;
}

/* Renders HTML inline (no wrapper element). Strips block tags so the
 * caller can place this inside an `<h1>` / `<span>` / etc without
 * breaking flow. The component itself emits no DOM wrapper — uses a
 * `<span>` so it can sit inside any block-level parent. */
export function SanitizedHtmlInline({
    html,
}: {
    html: string | undefined | null;
}) {
    const [safe, setSafe] = useState<string | null>(null);

    useEffect(() => {
        let cancelled = false;
        loadInlineSanitizer().then((sanitize) => {
            if (!cancelled) setSafe(sanitize(html ?? ''));
        });
        return () => {
            cancelled = true;
        };
    }, [html]);

    if (!html) return null;

    if (safe === null) {
        // Pre-sanitizer fallback: tag-stripped plain text (rendered as text,
        // not HTML) so slow networks read clean copy instead of raw markup.
        return <span>{plainTextFallback(html)}</span>;
    }

    return <span dangerouslySetInnerHTML={{ __html: safe }} />;
}

export function SanitizedHtmlBody({
    html,
    className,
}: {
    html: string | undefined | null;
    className?: string;
}) {
    const [safe, setSafe] = useState<string | null>(null);

    useEffect(() => {
        let cancelled = false;
        loadSanitizer().then((sanitize) => {
            if (!cancelled) setSafe(sanitize(html ?? ''));
        });
        return () => {
            cancelled = true;
        };
    }, [html]);

    if (!html) return null;
    const cls = className ? `${BASE_CLASSES} ${className}` : BASE_CLASSES;

    if (safe === null) {
        // Pre-sanitizer fallback: tag-stripped plain text (rendered as text,
        // not HTML) so slow networks read clean copy instead of raw markup.
        return (
            <div className={`${cls} whitespace-pre-line`}>
                {plainTextFallback(html)}
            </div>
        );
    }

    return <div className={cls} dangerouslySetInnerHTML={{ __html: safe }} />;
}
