// Lazy React header chrome — shell.ts import()s this by URL lazily, in
// parallel with the iframe mount, so React arrives as a separate chunk
// that never blocks first content.
// This file is the rollup entry ("frame-shell-chrome" in vite.config.ts);
// the implementation lives in ./chrome/.
//
// The share popover (audience select, people list, per-version share pinning,
// copy link), the title caret menu (delete), the account menu, and the
// first-open coachmark. People are added via an org-member typeahead
// (members_v2 search) because the prod ACL stores account UUIDs, never
// emails. The history view exists to pin which version is shared (boot has
// no version param, so there's no view-old-version navigation).

import { CdsRoot, type CdsFont, type CdsPlatform } from "@ant/cds";
import { StrictMode, useEffect, useState, type ReactNode } from "react";
import { createRoot } from "react-dom/client";

import { AccountMenu } from "./chrome/AccountMenu";
import { useAccount } from "./chrome/api";
import { Byline } from "./chrome/Byline";
import { Edit } from "./chrome/Edit";
import { Share } from "./chrome/Share";
import { TitleMenu } from "./chrome/TitleMenu";
import { type ChromeCtx } from "./chrome/types";
import { KNOWN_MODES, useMobile } from "./chrome/util";

import chromeCss from "./chrome.css?inline";
import chromeTwCss from "./chrome.tw.css?inline";

export type { ChromeCtx, FrameEventName, FrameTrackOpts } from "./chrome/types";

export interface ChromeHandle {
  /** Re-render the chrome from a later boot (live-reload reboots). The
   * share sheet re-seeds from the fresh boot — its optimistic baselines
   * must come from server truth as of the reboot, not the original load. */
  update(ctx: ChromeCtx): void;
  /** Tests only — in production the page outlives the chrome. */
  unmount(): void;
}

const ds = document.documentElement.dataset;

function Chrome({ ctx, gen }: { ctx: ChromeCtx; gen: number }): ReactNode {
  // Read per render, not at module init: frame-shell.html stamps the
  // attributes pre-paint, and the dynamic read keeps the flags testable
  // without a module reset.
  const embedded = "embedded" in ds;
  // Host-chrome embed: header hidden (--bar:0); the host owns Share +
  // attribution and opens the sheet via the postMessage bridge.
  const hostChrome = embedded && ds.chrome === "none";
  const org = ctx.org;
  // shell.ts prefetches /api/account at module eval (~500ms before this
  // chunk mounts), so account is typically resolved by first render. The
  // createRoot on #hdr replaces the static skeleton in one commit — no
  // per-slot swap dance.
  const account = useAccount(ctx.accountPrefetch, org);
  const mobile = useMobile();
  // Host-command bridge: when embedded the header is hidden, so the host
  // opens the share sheet by posting here. Only the SAME-ORIGIN embedding
  // window may drive chrome: the artifact content iframe is cross-origin
  // (fails the origin check) and any other window fails the source pin.
  // e.data is untrusted input — strict shape check, no other fields read,
  // no reply posted. Commands arriving before this chunk mounts (or for a
  // non-owner, who has no Share) are dropped — the command only opens UI;
  // it grants nothing the viewer's own session couldn't already do.
  const [hostShareGen, setHostShareGen] = useState(0);
  const [hostDismissGen, setHostDismissGen] = useState(0);
  useEffect(() => {
    if (!hostChrome) return;
    const onMsg = (e: MessageEvent): void => {
      if (e.origin !== location.origin || e.source !== window.parent) return;
      const d = e.data as { __frame_chrome?: unknown; cmd?: unknown } | null;
      if (!d || d.__frame_chrome !== true) return;
      if (d.cmd === "share") setHostShareGen((g) => g + 1);
      else if (d.cmd === "dismiss") setHostDismissGen((g) => g + 1);
    };
    window.addEventListener("message", onMsg);
    return () => window.removeEventListener("message", onMsg);
  }, [hostChrome]);
  const isOwner = ctx.boot.perm?.role === "owner";
  // Share/delete are owner-only and need org context (the CP rejects
  // org-less writes anyway). Unknown read modes render no share UI —
  // see KNOWN_MODES.
  const mode = ctx.boot.perm?.mode ?? "";
  const canShare =
    isOwner &&
    org !== undefined &&
    KNOWN_MODES.has(mode) &&
    // The companion-CP protocol signal is `live` (an owner-only field
    // that's never omitempty'd). `perm.users` is `omitempty` on the wire,
    // so an owner with an empty allowlist arrives without the key — that's
    // [] (useShareSaves seeds it as such), not "CP doesn't support it". A
    // boot with NEITHER is a pre-upgrade CP → degraded mode (every PATCH
    // replaces the stored list wholesale, so editing from a boot that
    // never carried it would silently wipe whatever the server has).
    (Array.isArray(ctx.boot.perm?.users) || typeof ctx.boot.live === "string");
  // Embedded slim header: byline left, Share right. Host-chrome embed:
  // header is CSS-hidden; render only Share so the host's postMessage
  // command can open the (portaled) sheet.
  if (embedded) {
    return (
      <>
        {!hostChrome && (
          <div className="l">
            <Byline ctx={ctx} account={account} />
          </div>
        )}
        <div
          className="r"
          data-chrome-root=""
          data-size={mobile ? undefined : "xs"}
        >
          {canShare && (
            <Share
              key={gen}
              ctx={ctx}
              org={org}
              account={account}
              hostChrome={hostChrome}
              hostOpenGen={hostShareGen}
              hostDismissGen={hostDismissGen}
            />
          )}
        </div>
      </>
    );
  }
  // The CdsRoot is comfortable (base = 32px) so popovers/menus get touch-
  // sized rows everywhere. Pin the in-header controls back to step 2
  // (24px) on desktop via data-size="xs" so the bar matches the static
  // shell.css geometry; mobile takes the 32px base.
  return (
    <>
      <div className="l">
        <a className="logo" href="/code/artifacts" aria-label="Your artifacts">
          <span className="ico" aria-hidden>
            {"\u{E017}"}
          </span>
        </a>
        <TitleMenu
          ctx={ctx}
          org={org}
          account={account}
          interactive={isOwner && org !== undefined}
          mobile={mobile}
        />
        <Byline ctx={ctx} account={account} />
      </div>
      <div
        className="r"
        data-chrome-root=""
        data-size={mobile ? undefined : "xs"}
      >
        {/* Edit reopens the owner's last-editing session with this frame in
            its drawer. last_edit is owner-boot-only, so the isOwner guard is
            defense-in-depth. */}
        {isOwner && <Edit ctx={ctx} />}
        {/* Keyed per share-relevant boot delta (see sigOf below): a reboot's
            fresh perm/history must re-seed the sheet's optimistic baselines.
            Known gap: when sigOf DOES change mid-save, the remount drops the
            old useShareSaves instance — its in-flight PATCH still completes
            (keepalive) but the success handler's apply() is a no-op on the
            unmounted tree, and the new instance seeds from the reboot's perm
            (which may predate that PATCH). Narrow window (live-reload during
            an active share write); the proper fix (useShareSaves adopts a
            new boot via reconcile-style logic instead of remounting) is the
            first deferred follow-up in the PR body. */}
        {canShare && (
          <Share
            key={gen}
            ctx={ctx}
            org={org}
            account={account}
            hostChrome={false}
            hostOpenGen={hostShareGen}
            hostDismissGen={hostDismissGen}
          />
        )}
        <AccountMenu account={account} mobile={mobile} />
      </div>
    </>
  );
}

function injectCss(): void {
  if (!document.getElementById("frame-chrome-css")) {
    const s = document.createElement("style");
    s.id = "frame-chrome-css";
    // The Tailwind sheet first — its preflight reset must come before
    // chrome.css's selectors.
    s.textContent = chromeTwCss + chromeCss;
    document.head.appendChild(s);
  }
}

// CdsRoot owns .cds-root + data-mode stamping now; scope() wraps each
// tree so CDS-portaled content (CdsPortal) lands inside it. mode is
// omitted so CdsRoot's [data-mode] DOM fallback reads <html> (stamped
// pre-stylesheet from the embedder) and live-tracks it.
//
// platform defaults to "web", never CdsRoot's UA sniff: the chrome (share
// sheet, menus) is a web-page surface, and inside an Electron-hosted
// embed the sniff would silently flip --cds-cursor-interactive to the
// native arrow convention — the same sheet at the standalone URL shows a
// pointer. An embedder that wants the native convention opts in
// explicitly via the platform axis (?p= / the desktop pane preload).
function scope(children: ReactNode): ReactNode {
  return (
    <CdsRoot
      // display: contents so .l/.r are #hdr's direct flex children.
      className="contents"
      // Comfortable everywhere so portaled surfaces (share sheet, menus,
      // confirm dialog) get the 32px base step at every viewport. The
      // in-header controls are pinned back to step 2 on desktop via
      // data-size="xs" (Chrome's [data-chrome-root] + TitleMenu's trigger)
      // so they align with the static shell.css geometry, which is fixed-px
      // and ships no CDS runtime.
      density="comfortable"
      platform={(ds.platform as CdsPlatform | undefined) ?? "web"}
      font={ds.font as CdsFont | undefined}
    >
      {children}
    </CdsRoot>
  );
}

/** Mounts the chrome; returns an unmount handle (the shell never calls it —
 * the page lives as long as the chrome — but tests must, or document-level
 * listeners and timers leak across cases). */
export function mount(
  el: HTMLElement,
  ctx: ChromeCtx,
  onError?: (e: unknown) => void,
): ChromeHandle {
  // Chrome styles are ?inline-bundled into this JS chunk — the
  // @vite-ignore import in the entries has no mechanism to auto-load a
  // separate CSS asset.
  injectCss();
  // onUncaughtError surfaces throws from the chrome during concurrent
  // render — those never propagate out of createRoot().render() and so
  // are invisible to the caller's try/catch.
  const root = createRoot(el, { onUncaughtError: onError });
  // The gen key remounts Share so its optimistic baselines re-seed from
  // server truth — but a remount also closes an open sheet and drops
  // in-progress state. Only bump when the share-relevant boot fields
  // actually changed; a no-change reboot (e.g. token refresh) re-renders
  // in place. perm.version is part of the signature: baselines and the
  // If-Match token must re-seed TOGETHER — adopting a fresh token without
  // fresh baselines would let a stale list save cleanly over a concurrent
  // change (the exact overwrite If-Match exists to reject).
  const sigOf = (b: ChromeCtx["boot"]): string =>
    // Null-safe (a malformed boot must throw inside React's render where
    // onUncaughtError catches it, not here) and key-order-stable (perm is
    // destructured into a fixed-order array so two boots with identical
    // content but differently-ordered JSON can't trigger a gratuitous
    // remount).
    b
      ? JSON.stringify([
          b.perm?.role,
          b.perm?.mode,
          b.perm?.users,
          b.perm?.version,
          b.shared,
          b.live,
          b.history,
          b.versions,
        ])
      : "";
  let gen = 0;
  let lastSig = sigOf(ctx.boot);
  const render = (c: ChromeCtx): void =>
    root.render(<StrictMode>{scope(<Chrome ctx={c} gen={gen} />)}</StrictMode>);
  render(ctx);
  return {
    update(next) {
      const sig = sigOf(next.boot);
      if (sig !== lastSig) {
        gen++;
        lastSig = sig;
      }
      render(next);
    },
    unmount() {
      root.unmount();
    },
  };
}
