// 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 pins which version is shared, and — via
// ctx.preview() → the CP's owner-only ?ver= param — lets the owner
// re-mount a past version in their own iframe.

import { Button, CdsRoot, type CdsFont, type CdsPlatform } from "@ant/cds";
import {
  StrictMode,
  useCallback,
  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 { installDomReconcileGuard } from "./chrome/dom-reconcile-guard";
import { Edit } from "./chrome/Edit";
import { Ico } from "./chrome/icons";
import { Report } from "./chrome/Report";
import { Share } from "./chrome/Share";
import { ShowHeader } from "./chrome/ShowHeader";
import { TitleMenu } from "./chrome/TitleMenu";
import {
  type ChromeCtx,
  type ChromeHandle,
  type ChromeMod,
} from "./chrome/types";
import { KNOWN_MODES, useMobile } from "./chrome/util";
import { parseFrameChromeCmd } from "./protocol.chrome";

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

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

// Translator tolerance for the shell document's React tree: browser
// translators (Chrome/Google Translate, Edge) reparent text nodes under
// the header, and React's commit-phase removeChild/insertBefore then throw
// NotFoundError (facebook/react#11538), crashing the chrome into the
// degraded static header (Sentry issue 7585726464). Module eval runs
// before the first createRoot commit — the same timing as the SPA's
// install in apps/claude-ai/src/main.tsx — and the guard is lazy inside:
// Node.prototype is only patched once a translator signal appears, so
// untranslated sessions run native DOM.
installDomReconcileGuard();

const ds = document.documentElement.dataset;

export const oneOf = <T extends string>(
  v: string | undefined,
  ok: readonly T[],
): T | undefined => (ok.includes(v as T) ? (v as T) : undefined);

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();
  // User-toggled "Hide header" (AccountMenu): collapses --bar to 0 via
  // html[data-header-hidden] so the artifact fills the viewport. Ephemeral
  // by design — plain component state, never persisted. No keyboard
  // shortcut: the corner ShowHeader button is the one exit path, which
  // keeps the PR frame-shell-only and avoids the "whose Escape is it"
  // question for artifacts with their own modals.
  const [headerHidden, setHeaderHidden] = useState(false);
  const hideHeader = useCallback(() => setHeaderHidden(true), []);
  const showHeader = useCallback(() => setHeaderHidden(false), []);
  useEffect(() => {
    if (!headerHidden) return undefined;
    ds.headerHidden = "";
    return () => {
      delete ds.headerHidden;
      // Return focus to the trigger that opened it. Runs after the
      // ds.headerHidden delete so #hdr is no longer display:none.
      document
        .querySelector<HTMLButtonElement>("button[data-account-trigger]")
        ?.focus();
    };
  }, [headerHidden]);
  // 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 = parseFrameChromeCmd(e.data);
      if (!d) 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 role = ctx.boot.perm?.role;
  const isOwner = role === "owner";
  // Report affordance: non-owner viewers only, behind the CP-resolved
  // `frame_report_button` GB flag. Client gate is visibility-only; the
  // route 404s flag-off and answers oracle-safe 202.
  const showReport = ctx.boot.reportEnabled === true && !isOwner;
  // Share needs org context (the CP rejects org-less writes anyway) and
  // a role that may PATCH perm — owner always, writer when the CP says so
  // (writersEnabled only arrives on writer/owner boots; role==="writer"
  // without it is a contract bug we fail closed on). Delete stays
  // owner-only. Unknown read modes render no share UI — see KNOWN_MODES.
  const mode = ctx.boot.perm?.mode ?? "";
  const writeMode = ctx.boot.perm?.write?.mode ?? "owner";
  const canShare =
    (isOwner ||
      (role === "writer" && ctx.boot.perm?.writersEnabled === true)) &&
    org !== undefined &&
    KNOWN_MODES.has(mode) &&
    // Fail-closed on the write axis too: an unknown write tier this build
    // can't represent must not be editable (a single role gesture would
    // clobber it). Absent write → "owner" → known. Gate-on only: a
    // gate-off session never asserts the write axis (no role dropdown,
    // PATCH bodies omit `write`), so an unknown tier there is
    // unclobberable and must not cost the owner the whole sheet.
    (ctx.boot.perm?.writersEnabled !== true || KNOWN_MODES.has(writeMode)) &&
    // writersEnabled===true is itself the post-upgrade-CP signal — the
    // owner-boot-only `live`/`users` disjunct below doesn't hold for a
    // writer whose read allowlist is empty, and gating them on it would
    // hide the sheet from exactly the users this PR exists to serve.
    (ctx.boot.perm?.writersEnabled === true ||
      // 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");
  // mayWiden is the CP's Phase-1 prosumer hint (perm.mayWiden): false means
  // this caller's widen would 403 at setReadMode. The share sheet has
  // nothing useful for an owner-only-forever caller, so render the trigger
  // disabled with an upsell tooltip instead. No stored-mode guard: Pro/Max
  // never had Artifacts access, and orgs don't cross the consumer/non-
  // consumer boundary (see the CP's mayWidenReadMode), so a consumer-tier
  // owner's frame is always already private — there are no grandfathered
  // shares to narrow. The one transient exception is a Team/Enterprise
  // owner whose OrgType degrades to "" during a userauth brownout: their
  // shared frame reads mayWiden=false until refresh; the CP's requireAuth
  // retry makes this rare, and a refresh recovers. `=== false`
  // specifically — undefined is a CP that predates the field, and the
  // sheet stays interactive (server still enforces). The locked button
  // mirrors Share.tsx's Popover.Trigger (secondary; iconOnly on mobile).
  const shareLocked = canShare && ctx.boot.perm?.mayWiden === false;
  const lockedShareBtn = (
    <Button
      data-share-btn=""
      variant="secondary"
      disabled
      disabledReason="Sharing artifacts is available on Team or Enterprise plans"
      disabledReasonSide="bottom"
      {...(mobile
        ? {
            iconOnly: true as const,
            icon: Ico.shareApple,
            "aria-label": "Share",
          }
        : { children: "Share" })}
    />
  );
  // 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"}
        >
          {shareLocked
            ? lockedShareBtn
            : canShare && (
                <Share
                  gen={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>
      {/* 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} mobile={mobile} />}
      <div
        className="r"
        data-chrome-root=""
        data-size={mobile ? undefined : "xs"}
      >
        {/* `gen` is a PROP (not a React key): a sigOf-changing reboot
            re-seeds useShareSaves' optimistic baselines in-chain (serialized
            after any pending write settles) instead of remounting. An open
            sheet stays open and in-flight writes' success handlers apply to
            this same instance. */}
        {shareLocked
          ? lockedShareBtn
          : canShare && (
              <Share
                gen={gen}
                ctx={ctx}
                org={org}
                account={account}
                hostChrome={false}
                hostOpenGen={hostShareGen}
                hostDismissGen={hostDismissGen}
              />
            )}
        {showReport && <Report ctx={ctx} />}
        <AccountMenu
          account={account}
          mobile={mobile}
          onHideHeader={hideHeader}
        />
      </div>
      {headerHidden && <ShowHeader onExit={showHeader} mobile={mobile} />}
    </>
  );
}

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={oneOf<CdsPlatform>(ds.platform, ["web", "desktop"]) ?? "web"}
      font={oneOf<CdsFont>(ds.font, ["anthropic", "system"])}
    >
      {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 });
  // `gen` bumps per share-relevant boot delta; useShareSaves enqueues an
  // in-chain re-seed on each bump so the sheet's optimistic baselines
  // adopt server truth without a remount (the sheet stays open). Only
  // bump when the share-relevant fields actually changed; a no-change
  // reboot (e.g. token refresh) re-renders
  // in place. `versions` is excluded: Share reads it via useMemo (not
  // useState-seeded), so shell.ts's lazy /versions fetch resolving is an
  // in-place label re-render with no reseed needed. perm.version IS part
  // of the signature: it's the catch-all for any perm write (rotate, or a
  // change whose mode/users delta isn't observable from this client's
  // last snapshot).
  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
    // reseed).
    b
      ? JSON.stringify([
          b.perm?.role,
          b.perm?.mode,
          b.perm?.users,
          b.perm?.write?.mode,
          b.perm?.write?.users,
          b.perm?.writersEnabled,
          b.perm?.version,
          b.shared,
          b.live,
          b.history,
        ])
      : "";
  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();
    },
  };
}
mount satisfies ChromeMod["mount"];
