import React, { useEffect } from 'react';
import { openHttpsInNewBrowserWindow, openQortalLinkInVisitorContext, normalizeQortalWebsiteHref } from './lib/openQortal';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AppErrorBoundary } from './ui/AppErrorBoundary';
import { WebBuilderCanvas } from './ui/WebBuilderCanvas';
import { SiteViewContext } from './context/SiteViewContext';
import { useBuilderStore } from './store/builderStore';
import { mergeSite } from './lib/mergeSite';
import { buildVisitorRetainSnapshots } from './lib/visitorRetainSnapshots';
import { isQortalRuntime } from './lib/qortalEnv';
import { primeAndWarmQAppEmbedsFromSite } from './lib/primeQAppEmbeds';
import { enableVisitorPublishedSite } from './lib/visitorPublishedSite';
import { loadPublishedSiteRaw } from './lib/publishedSiteConfig';
import { readVisitorThemeForPublisher } from './lib/visitorThemeStorage';
import {
  deliverDiscoverCaptureToParent,
  isDiscoverCaptureRuntime,
  registerDiscoverCaptureConfigBridge,
} from './lib/discoverCaptureProtocol';
import {
  runDiscoverCaptureInDocument,
  waitForCaptureBlocksInDocument,
} from './lib/discoverPreviewCapture';
import { VisitorAuthBar } from './ui/VisitorAuthBar';
import { VisitorSoftEditBar } from './ui/VisitorSoftEditBar';
import { PublishWebsiteModal } from './ui/PublishWebsiteModal';
import { VisitorGroupAccessGate } from './ui/VisitorGroupAccessGate';
import { RteMediaLinkOverlay } from './ui/RteMediaLinkOverlay';
import { useQortalApiBridge } from './hooks/useQortalApiBridge';
import {
  navigateToSameSiteSlug as sameSiteNavigate,
  parseSameSiteSlug as sameSiteParse,
} from './lib/sameSiteNav';
import { scrollBlockIntoView, blockSectionDomId } from './lib/visitorPeerNavigate';
import {
  QORTAL_WEB_EMBED_ARTICLE_OPEN,
  QORTAL_WEB_EMBED_AUTH_SESSION,
  QORTAL_WEB_EMBED_BLOCK_FOCUS,
  QORTAL_WEB_EMBED_READY,
  QORTAL_WEB_EMBED_SCROLL,
  QORTAL_WEB_EMBED_THEME,
  type QortalWebEmbedAuthMessage,
} from './lib/qortalWebEmbed';
import type { BuilderTheme } from './types/site';
import '../style.css';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: { retry: 1, refetchOnWindowFocus: false },
  },
});

/**
 * Extracts the page slug from the published WEBSITE URL. Two routing modes are supported on the
 * visitor side:
 *
 *   1. **Hash routing** (preferred for SPA navigation between pages — no Qortal Core round-trip
 *      and no need for per-slug `index.html` in the published archive):
 *        qortal://WEBSITE/<name>/#<slug>
 *      Internal nav clicks (Header → "Page" or same-publisher "Qortal site" buttons) flip
 *      `location.hash` and the SPA re-renders the matching page WITHOUT reloading the document.
 *
 *   2. **Pathname routing** (deep-link friendly — used when the visitor lands on the URL directly,
 *      e.g. shared bookmarks). Requires `<slug>/index.html` to exist in the published bundle so
 *      Qortal Core can serve a static file at that path. We bundle these copies in
 *      `buildWebsiteFileBundle` so the deep-link case works.
 *
 * Hash takes precedence over pathname — if both are set, the hash wins (clean URL after a copy /
 * paste during in-app nav). The pinning script in `site.html` keeps the iframe URL canonical and
 * strips duplicated `<name>` segments that the Hub used to inject.
 */
function readPageSlugFromUrl(): string {
  if (typeof location === 'undefined') return '';
  try {
    const rawHash = (location.hash || '').replace(/^#+/, '').trim();
    if (rawHash) {
      const head = rawHash.split('/')[0] || '';
      return decodeURIComponent(head).toLowerCase();
    }
    const m = /^\/render\/(?:WEBSITE|APP)\/[^/]+\/(.*)$/i.exec(location.pathname || '');
    if (!m) return '';
    const tail = (m[1] || '').replace(/^\/+/, '').replace(/\/+$/, '');
    if (!tail) return '';
    /** First segment of the remaining path is the slug. */
    const slug = tail.split('/')[0] || '';
    return decodeURIComponent(slug).toLowerCase();
  } catch {
    return '';
  }
}

/**
 * Cached merged site config (parsed once at startup). Used by the hashchange / popstate listeners
 * to swap pages without re-parsing the JSON / re-running mergeSite each navigation.
 */
let cachedMergedSite: ReturnType<typeof mergeSite> | null = null;

/**
 * Same-site nav: thin wrappers that pair the URL update from `lib/sameSiteNav.ts` with the
 * embed-driven `setStoreToSlug` swap so a single click both navigates the URL and re-renders
 * the SPA. Exported here so the click router can use them via `parseSameSiteSlug` /
 * `navigateToSameSiteSlug` aliases imported above.
 */
function parseSameSiteSlug(href: string): string | null {
  return sameSiteParse(href);
}
function navigateToSameSiteSlug(slug: string) {
  if (sameSiteNavigate(slug)) {
    /** `history.pushState` does not fire `hashchange` automatically when the hash changes;
     * `sameSiteNavigate` dispatches the event so our listener picks it up. We also trigger an
     * immediate render so the "click → page renders" feedback is instant rather than waiting
     * for the synthetic event to roundtrip through the listener. */
    setStoreToSlug(slug);
  }
}

function setStoreToSlug(slug: string) {
  if (!cachedMergedSite) return;
  const merged = cachedMergedSite;
  /** Rebuilt `site` from cached merge each navigation — keep the visitor's theme from the store. */
  const visitorTheme = useBuilderStore.getState().site.theme;
  const retain = useBuilderStore.getState().visitorRetainSnapshots;
  let site: ReturnType<typeof mergeSite>;

  if (retain && retain.length > 1) {
    const homeSnap = retain.find((r) => r.pageId === '__home__');
    if (!homeSnap) {
      site = { ...merged, theme: visitorTheme };
    } else if (slug) {
      const match = (merged.pages || []).find(
        (p) => p.id !== '__home__' && p.slug.toLowerCase() === slug,
      );
      if (match) {
        const snap = retain.find((r) => r.pageId === match.id);
        if (snap) {
          site = { ...merged, blocks: snap.blocks, activePageId: match.id, pages: merged.pages };
          if (match.title) {
            site = { ...site, docTitle: match.title };
          }
        } else {
          site = {
            ...merged,
            blocks: homeSnap.blocks,
            activePageId: null,
            pages: merged.pages,
            docTitle: merged.docTitle,
          };
        }
      } else {
        site = {
          ...merged,
          blocks: homeSnap.blocks,
          activePageId: null,
          pages: merged.pages,
          docTitle: merged.docTitle,
        };
      }
    } else {
      site = {
        ...merged,
        blocks: homeSnap.blocks,
        activePageId: null,
        pages: merged.pages,
        docTitle: merged.docTitle,
      };
    }
    site = { ...site, theme: visitorTheme };
  } else {
    let s = merged;
    if (slug) {
      const match = (merged.pages || []).find(
        (p) => p.id !== '__home__' && p.slug.toLowerCase() === slug,
      );
      if (match) {
        s = { ...merged, blocks: match.blocks, activePageId: match.id };
        if (match.title) {
          s = { ...s, docTitle: match.title };
        }
      }
    } else {
      /** Hash cleared / pathname home — canonical home snapshot + predictable `activePageId` for Q-App persist keys. */
      s = { ...merged, blocks: merged.blocks, activePageId: null };
    }
    site = { ...s, theme: visitorTheme };
  }

  useBuilderStore.setState({ site, selectedBlockId: null });
  /** Reflect the page title on the browser tab so the Hub displays the active page. */
  if (typeof document !== 'undefined') {
    const t = (site.docTitle || '').trim();
    if (t) document.title = t;
  }
  /** Maintain a `<link rel="canonical">` whose href matches the current canonical Qortal URL.
   * Hubs that prefer canonical metadata over the raw iframe URL (some builds do, some don't)
   * can copy that link instead of the address-bar memory of the tab's entry URL. Cheap, no-op
   * for hubs that ignore it. */
  updateCanonicalLink(site.publisherName, slug);
  /** Tell the Hub what resource is now displayed so its address bar / "Copy link" tracks our
   * WEBSITE rather than any embedded Q-App (e.g. an embedded Q-Blog that fires its own
   * `QDN_RESOURCE_DISPLAYED` — without this the embed wins the race and Hub's copy-link
   * returns the embed's URL). */
  notifyHubResourceDisplayed(site.publisherName, slug);
  /** Visitors expect to land at the top of the next page (not stay scrolled on the previous one's
   * footer). Skip when no scrollport exists yet (initial mount). */
  try {
    const sp = document.querySelector('.qwb-visitor');
    if (sp instanceof HTMLElement) sp.scrollTo({ top: 0, behavior: 'auto' });
  } catch {
    /* */
  }
}

function updateCanonicalLink(publisherName: string | undefined, slug: string) {
  if (typeof document === 'undefined') return;
  const name = String(publisherName || '').trim();
  if (!name) return;
  const base = 'qortal://WEBSITE/' + encodeURIComponent(name);
  const href = slug ? base + '/' + encodeURIComponent(slug) : base;
  let link = document.querySelector('link[rel="canonical"]') as HTMLLinkElement | null;
  if (!link) {
    link = document.createElement('link');
    link.setAttribute('rel', 'canonical');
    document.head.appendChild(link);
  }
  if (link.href !== href) {
    link.setAttribute('href', href);
  }
}

/**
 * Tell the Hub which WEBSITE resource (and which sub-path) the visitor is currently looking at.
 * This is the official Qortal-Hub mechanism for "set the address bar / copy-link target" — see
 * `QDN_RESOURCE_DISPLAYED` in the Q-Apps doc. We need to fire this whenever the SPA swaps pages
 * AND on initial mount so an embedded Q-App child (e.g. a Q-Blog iframe inside the WEBSITE)
 * cannot win the race and leave Hub's address bar pointing at the embed instead of at our
 * WEBSITE — which is exactly the symptom that produced the "Copy link returns the embed's path
 * (/<name>/<embed-slug>)" report.
 *
 * The published-site bridge (`site.html`) ALSO blocks `QDN_RESOURCE_DISPLAYED` from the child
 * iframe; this call here is the "tell Hub the right value" half of the same fix.
 */
function notifyHubResourceDisplayed(publisherName: string | undefined, slug: string) {
  if (typeof window === 'undefined') return;
  const name = String(publisherName || '').trim();
  if (!name) return;
  const w = window as Window & {
    qortalRequest?: (req: Record<string, unknown>) => Promise<unknown>;
  };
  if (typeof w.qortalRequest !== 'function') return;
  try {
    w.qortalRequest({
      action: 'QDN_RESOURCE_DISPLAYED',
      service: 'WEBSITE',
      name,
      identifier: 'default',
      ...(slug ? { path: '/' + slug.replace(/^\/+/, '') } : {}),
    }).catch(() => {
      /* Non-fatal — older Hub builds may not expose this action; the canonical link + URL pin
       * still keep things internally consistent. */
    });
  } catch {
    /* */
  }
}

function applyLoadedSite(raw: unknown) {
  const merged = mergeSite(raw);
  cachedMergedSite = merged;
    /**
     * Multi-page: the published bundle contains the home page (`merged.blocks`) plus all
     * sub-pages in `merged.pages[]`. The visitor's URL determines which one renders. We mutate
     * `site.blocks` to point at the matching page's blocks before storing in zustand so the rest
     * of the visitor app code (which reads `state.site.blocks`) doesn't need to know about pages.
     */
    const slug = readPageSlugFromUrl();
    const visitorRetainSnapshots = buildVisitorRetainSnapshots(merged);
    let site: ReturnType<typeof mergeSite>;
    if (visitorRetainSnapshots && visitorRetainSnapshots.length > 1) {
      const homeSnap = visitorRetainSnapshots.find((r) => r.pageId === '__home__')!;
      if (slug) {
        const match = (merged.pages || []).find(
          (p) => p.id !== '__home__' && p.slug.toLowerCase() === slug,
        );
        if (match) {
          const snap = visitorRetainSnapshots.find((r) => r.pageId === match.id);
          if (snap) {
            site = { ...merged, blocks: snap.blocks, activePageId: match.id, pages: merged.pages };
            if (match.title) {
              site = { ...site, docTitle: match.title };
            }
          } else {
            site = {
              ...merged,
              blocks: homeSnap.blocks,
              activePageId: null,
              pages: merged.pages,
              docTitle: merged.docTitle,
            };
          }
        } else {
          site = {
            ...merged,
            blocks: homeSnap.blocks,
            activePageId: null,
            pages: merged.pages,
            docTitle: merged.docTitle,
          };
        }
      } else {
        site = {
          ...merged,
          blocks: homeSnap.blocks,
          activePageId: null,
          pages: merged.pages,
          docTitle: merged.docTitle,
        };
      }
    } else {
      let s = merged;
      if (slug) {
        const match = (merged.pages || []).find(
          (p) => p.id !== '__home__' && p.slug.toLowerCase() === slug,
        );
        if (match) {
          s = { ...merged, blocks: match.blocks, activePageId: match.id };
          if (match.title) {
            s = { ...s, docTitle: match.title };
          }
        }
      } else {
        s = { ...merged, blocks: merged.blocks, activePageId: null };
      }
      site = s;
    }
    const pub = String(merged.publisherName || '').trim();
    const persistedTheme = pub ? readVisitorThemeForPublisher(pub) : null;
    if (persistedTheme) {
      site = { ...site, theme: persistedTheme };
    }
    useBuilderStore.setState({
      site,
      visitorRetainSnapshots,
      selectedBlockId: null,
      inQortal: isQortalRuntime(),
    });
    /** Set the initial canonical link before React mounts so Hubs that read it on tab-open get
     * the right value from the very first render. */
    updateCanonicalLink(site.publisherName, slug);
    /** Initial QDN_RESOURCE_DISPLAYED notification — fired with a small delay so Hub's bridge
     * (`qortalRequest`) is definitely installed first. We re-fire on every SPA navigation via
     * `setStoreToSlug`, but the FIRST one ensures Hub's address bar shows our WEBSITE before any
     * child Q-App embed's own QDN_RESOURCE_DISPLAYED race-condition kicks in. */
    if (!isDiscoverCaptureRuntime()) {
      window.setTimeout(() => notifyHubResourceDisplayed(site.publisherName, slug), 0);
    }
    if (!isDiscoverCaptureRuntime() && isQortalRuntime()) {
      void primeAndWarmQAppEmbedsFromSite(site).catch(() => {
        /* non-fatal */
      });
    }
    useBuilderStore.getState().setToastHandler((m, k) => {
      try {
        if (k === 'err') {
          console.error('[visitor]', m);
        } else if (k === 'warn') {
          console.warn('[visitor]', m);
        } else {
          console.log('[visitor]', m);
        }
      } catch {
        /* */
      }
    });
}

const discoverCaptureConfigPromise = registerDiscoverCaptureConfigBridge();
/** StrictMode remount must not cancel an in-flight capture run. */
let discoverCaptureRunnerStarted = false;

async function bootVisitorSite(): Promise<void> {
  enableVisitorPublishedSite();
  const capturePromise = discoverCaptureConfigPromise ?? registerDiscoverCaptureConfigBridge();
  if (capturePromise) {
    const raw = await capturePromise;
    if (!raw) return;
    try {
      applyLoadedSite(raw);
    } catch (e) {
      try {
        console.error('published site config error', e);
      } catch {
        /* */
      }
    }
    return;
  }
  const raw = await loadPublishedSiteRaw();
  if (!raw) return;
  try {
    applyLoadedSite(raw);
  } catch (e) {
    try {
      console.error('published site config error', e);
    } catch {
      /* */
    }
  }
}

function VisitorBootError() {
  return (
    <div className="qwb-visitor qwb-main" data-qwb-visitor="1">
      <p className="qwb-visitor-boot-error">
        This website could not load its configuration. Republish from Qortal Web Builder.
      </p>
    </div>
  );
}

function DiscoverCaptureRunner() {
  useEffect(() => {
    if (!isDiscoverCaptureRuntime() || discoverCaptureRunnerStarted) return;
    discoverCaptureRunnerStarted = true;

    void (async () => {
      try {
        await waitForCaptureBlocksInDocument(document, 1, 24_000);
        const file = await runDiscoverCaptureInDocument(document);
        if (!file || file.size <= 512) {
          deliverDiscoverCaptureToParent({ ok: false, error: 'empty capture' });
          return;
        }
        const png = new Uint8Array(await file.arrayBuffer());
        deliverDiscoverCaptureToParent({ ok: true, png });
      } catch (e) {
        deliverDiscoverCaptureToParent({
          ok: false,
          error: e instanceof Error ? e.message : String(e),
        });
      }
    })();
  }, []);
  return null;
}

function applyEmbedBlockFocus(blockDomId: string, articleId?: string) {
  const secId = blockSectionDomId(blockDomId);
  const artId = String(articleId || '').trim();
  try {
    document.documentElement.setAttribute('data-qwb-qw-block-focus', '1');
    if (artId) document.documentElement.setAttribute('data-qwb-qw-article-id', artId);
    else document.documentElement.removeAttribute('data-qwb-qw-article-id');
  } catch {
    /* */
  }
  let css = `
[data-qwb-qw-block-focus="1"] .qwb-visitor-auth,
[data-qwb-qw-block-focus="1"] .qwb-visitor-soft-edit { display:none!important; }
[data-qwb-qw-block-focus="1"] [data-qwb-visitor] .qwb-block--public { display:none!important; }
[data-qwb-qw-block-focus="1"] [data-qwb-visitor] #${secId} { display:block!important; }
[data-qwb-qw-block-focus="1"] [data-qwb-visitor] .qwb-main { padding-top:0!important; }
`.trim();
  if (artId) {
    const cardId = 'qwb-article-' + artId.replace(/[^a-zA-Z0-9_-]/g, '-');
    css += `
[data-qwb-qw-block-focus="1"] #${secId} .qwb-articles__card:not(#${cardId}) { display:none!important; }
[data-qwb-qw-block-focus="1"] #${secId} .qwb-articles__pager { display:none!important; }
[data-qwb-qw-block-focus="1"] #${secId} .qwb-articles__adminbar { display:none!important; }
`.trim();
  }
  try {
    const id = 'qwb-qortal-web-block-focus';
    let el = document.getElementById(id);
    if (!el) {
      el = document.createElement('style');
      el.id = id;
      (document.head || document.documentElement).appendChild(el);
    }
    el.textContent = css;
  } catch {
    /* */
  }
  scrollBlockIntoView(blockDomId);
}

function applyEmbedArticleOpen(blockDomId: string, articleId: string) {
  const block = String(blockDomId || '').trim();
  const art = String(articleId || '').trim();
  if (!block || !art) return;
  applyEmbedBlockFocus(block, art);
  useBuilderStore.getState().setVisitorPeerHighlight({
    blockDomId: block,
    kind: 'article',
    resourceId: art,
    articleId: art,
  });
}

function postEmbedReadyToParent() {
  try {
    if (!(window as Window & { __QWB_QORTAL_WEB_EMBED__?: boolean }).__QWB_QORTAL_WEB_EMBED__) return;
    window.parent?.postMessage({ type: QORTAL_WEB_EMBED_READY }, '*');
  } catch {
    /* cross-origin parent */
  }
}

function applyEmbedShellTheme(siteTheme: BuilderTheme) {
  useBuilderStore.setState((s) => ({
    site: { ...s.site, theme: siteTheme },
  }));
  try {
    document.querySelectorAll('[data-site-theme]').forEach((el) => {
      el.setAttribute('data-site-theme', siteTheme);
    });
  } catch {
    /* */
  }
}

function VisitorRoot() {
  const captureMode = isDiscoverCaptureRuntime();
  const docTitle = useBuilderStore((s) => s.site.docTitle);
  const theme = useBuilderStore((s) => s.site.theme);
  const pageWidth = useBuilderStore((s) => s.site.pageWidth);
  const resolveOwner = useBuilderStore((s) => s.resolveOwner);
  const toast = useBuilderStore((s) => s.toast);
  useQortalApiBridge();
  useEffect(() => {
    const onMsg = (ev: MessageEvent) => {
      const d = ev.data as { type?: string; blockId?: string; blockDomId?: string; siteTheme?: string } | null;
      if (!d) return;
      if (d.type === QORTAL_WEB_EMBED_SCROLL) {
        const blockId = String(d.blockId || '').trim();
        if (blockId) scrollBlockIntoView(blockId);
        return;
      }
      if (d.type === QORTAL_WEB_EMBED_BLOCK_FOCUS) {
        const blockDomId = String(d.blockDomId || d.blockId || '').trim();
        const articleId = typeof d.articleId === 'string' ? d.articleId.trim() : '';
        if (blockDomId) applyEmbedBlockFocus(blockDomId, articleId || undefined);
        return;
      }
      if (d.type === QORTAL_WEB_EMBED_ARTICLE_OPEN) {
        const blockDomId = String(d.blockDomId || d.blockId || '').trim();
        const articleId = String(d.articleId || '').trim();
        if (blockDomId && articleId) applyEmbedArticleOpen(blockDomId, articleId);
        return;
      }
      if (d.type === QORTAL_WEB_EMBED_THEME) {
        const t = d.siteTheme === 'paper' || d.siteTheme === 'slate' ? d.siteTheme : null;
        if (t) applyEmbedShellTheme(t);
      }
    };
    window.addEventListener('message', onMsg);
    return () => window.removeEventListener('message', onMsg);
  }, []);

  /** Qortal Web Q-App embed: inherit masthead authentication from parent Qortal Web. */
  useEffect(() => {
    try {
      (window as Window & { __QWB_QORTAL_WEB_EMBED__?: boolean }).__QWB_QORTAL_WEB_EMBED__ = true;
    } catch {
      /* */
    }
  }, []);

  useEffect(() => {
    const onMsg = (ev: MessageEvent) => {
      const d = ev.data as QortalWebEmbedAuthMessage | null;
      if (!d || d.type !== QORTAL_WEB_EMBED_AUTH_SESSION) return;
      const u = d.user;
      const store = useBuilderStore.getState();
      if (!u?.address) {
        store.setUserAccount(null);
        store.setSessionTag(Date.now());
        return;
      }
      store.setUserAccount({
        address: String(u.address).trim(),
        publicKey: typeof u.publicKey === 'string' ? u.publicKey : undefined,
        primaryName: typeof u.primaryName === 'string' ? u.primaryName : undefined,
      });
      store.setSessionTag(Date.now());
      if (!store.inQortal) {
        useBuilderStore.setState({ inQortal: true });
      }
      void store.resolveOwner().finally(postEmbedReadyToParent);
      return;
    };
    window.addEventListener('message', onMsg);
    return () => window.removeEventListener('message', onMsg);
  }, []);
  useEffect(() => {
    const t = (docTitle || '').trim();
    if (t) {
      document.title = t;
    }
  }, [docTitle]);
  useEffect(() => {
    if (captureMode) return;
    void resolveOwner().finally(postEmbedReadyToParent);
  }, [captureMode, resolveOwner]);
  /** Lock page scroll to the visitor root so `position:sticky` on the site header uses this scrollport (not the window). */
  useEffect(() => {
    document.documentElement.classList.add('qwb-pub');
    return () => document.documentElement.classList.remove('qwb-pub');
  }, []);
  /**
   * SPA-style internal navigation: when the URL hash or path changes, swap to the matching
   * sub-page without reloading the document. Triggered by clicks on Header "Page" / same-site
   * "Qortal site" buttons (which do `location.hash = '<slug>'`) and by browser back / forward.
   */
  useEffect(() => {
    const onUrlChange = () => {
      const slug = readPageSlugFromUrl();
      setStoreToSlug(slug);
    };
    window.addEventListener('hashchange', onUrlChange);
    window.addEventListener('popstate', onUrlChange);
    return () => {
      window.removeEventListener('hashchange', onUrlChange);
      window.removeEventListener('popstate', onUrlChange);
    };
  }, []);
  /** Rich text and other raw HTML may render `<a href>` without React onClick — route qortal and same-tab web URLs like block links. */
  useEffect(() => {
    const onClick = (e: MouseEvent) => {
      if (e.defaultPrevented) return;
      const el = (e.target as Element | null)?.closest?.('a[href]');
      if (!el || !document.querySelector('[data-qwb-visitor]')?.contains(el)) return;
      const href = el.getAttribute('href');
      if (!href) return;
      const h = href.trim();
      if (/^mailto:/i.test(h) || /^tel:/i.test(h)) return;
      if (/^qortal:/i.test(h)) {
        const qHref = /^qortal:\/\/WEBSITE\//i.test(h) ? normalizeQortalWebsiteHref(h) : h;
        /**
         * If the qortal:// URL points at THIS website (same registered name), keep the user on
         * the page and switch via hash routing instead of round-tripping through the Hub.
         */
        const sameSiteSlug = parseSameSiteSlug(qHref);
        if (sameSiteSlug !== null) {
          e.preventDefault();
          e.stopPropagation();
          navigateToSameSiteSlug(sameSiteSlug);
          return;
        }
        e.preventDefault();
        e.stopPropagation();
        openQortalLinkInVisitorContext(qHref, toast);
        return;
      }
      if (/^https?:\/\//i.test(h)) {
        e.preventDefault();
        e.stopPropagation();
        openHttpsInNewBrowserWindow(h);
      }
    };
    document.addEventListener('click', onClick, true);
    return () => document.removeEventListener('click', onClick, true);
  }, [toast]);
  return (
    <SiteViewContext.Provider value={{ readOnly: true }}>
      {captureMode ? (
        <div
          className="qwb-visitor qwb-main"
          data-site-theme={theme}
          data-qwb-visitor="1"
          data-qwb-page-width={pageWidth || 'narrow'}
        >
          <WebBuilderCanvas />
          <DiscoverCaptureRunner />
        </div>
      ) : (
        <VisitorGroupAccessGate>
          <div
            className="qwb-visitor qwb-main"
            data-site-theme={theme}
            data-qwb-visitor="1"
            data-qwb-page-width={pageWidth || 'narrow'}
          >
            <VisitorAuthBar />
            <VisitorSoftEditBar />
            <PublishWebsiteModal />
            <WebBuilderCanvas />
            <RteMediaLinkOverlay />
          </div>
        </VisitorGroupAccessGate>
      )}
    </SiteViewContext.Provider>
  );
}

const rootEl = document.getElementById('root');
if (!rootEl) {
  throw new Error('Missing #root');
}
void bootVisitorSite().then(() => {
  const app = (
    <AppErrorBoundary>
      <QueryClientProvider client={queryClient}>
        {cachedMergedSite ? <VisitorRoot /> : <VisitorBootError />}
      </QueryClientProvider>
    </AppErrorBoundary>
  );
  ReactDOM.createRoot(rootEl).render(
    isDiscoverCaptureRuntime() ? app : <React.StrictMode>{app}</React.StrictMode>,
  );
});
