/* =========================================================================
   Shared Studio header controls: the widget dock slot and the account menu.

   Lives in its own file, loaded by BOTH studios' index.html, for the same
   reason _auth.jsx does: Apple Pass Studio (PassStudio.jsx) and Google Pass
   Studio (google-pass-studio/index.html, now a redirect into the hub) were separate entry points that would otherwise
   each need their own copy, and this repo has no build step to keep copies
   honest. These are classic <script type="text/babel"> files sharing one
   global scope, so a top-level `function` declaration here is reachable from
   both — the same mechanism _auth.jsx's SpoonityAuthGate already relies on.
   ========================================================================= */

// Publishes this element as the dock target for the floating admin widgets, and takes it back on
// unmount so leaving the Studio returns them to floating rather than destroying them along with
// the header they were living in.
function ChromeSlot() {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const chrome = window.SpoonityChrome;
    if (!chrome) return undefined;
    // Captured, not read back off the ref in the cleanup: by the time React runs the cleanup
    // the ref has usually already been detached, so ref.current is null and there would be
    // nothing left to compare against.
    const el = ref.current;
    chrome.setSlot(el);
    // Hand the slot back only if it is still ours.
    //
    // Every signed-in screen now mounts a ChromeSlot, so moving between them (hub -> editor ->
    // hub) tears one down while building another. React commits that swap in a single pass and
    // runs the outgoing tree's effect cleanups before the incoming tree's effects, so the calls
    // land as clear-then-set and an unconditional setSlot(null) would in fact be correct today
    // -- the harness that drives these four screens confirms it.
    //
    // It is guarded anyway because that ordering is a React implementation detail we neither
    // control nor assert in CI, and the failure it guards against is silent and total: if the
    // new slot's setSlot ever lands BEFORE the old one's cleanup (a startTransition around the
    // view switch, a Suspense boundary between the two screens, a second React root, or simply
    // a future React), the cleanup would null out the live slot and both widgets would snap
    // back to floating over a header with a visible empty hole in it -- and nothing on screen
    // would say why. One property read makes the cleanup idempotent under either order.
    //
    // getSlot is feature-detected rather than assumed: index.html cache-busts every script by
    // hand, so a browser can hold a stale _chrome-dock.js (no getSlot) next to a fresh copy of
    // this file. Without it we fall through to the old unconditional clear, which is exactly
    // the behaviour that stale pairing had before.
    return () => {
      if (typeof chrome.getSlot === "function" && chrome.getSlot() !== el) return;
      chrome.setSlot(null);
    };
  }, []);
  return <div ref={ref} style={{ display: "flex", alignItems: "center", gap: 2 }} />;
}

// Above this many vendors the switcher grows a filter box; at or below it the whole list already
// fits the maxHeight:240 scroll box below, and a search field over three rows is chrome for
// nothing. One constant rather than a literal at the use site so the threshold can be changed --
// or removed, by setting it to 0 -- in one place.
const VENDOR_FILTER_THRESHOLD = 8;

// Two-letter initials for the vendor chip's avatar ("Buckhead Life Restaurant Group" -> "BL").
// Moved here from _hub.jsx along with the chip itself; the hub was the only caller.
function vendorInitials(name) {
  if (!name) return "";
  return name.trim().split(/\s+/).slice(0, 2).map((w) => w[0].toUpperCase()).join("");
}

// The active vendor, named in the header rather than hidden one click deep behind the account
// icon. A deliberate departure from "the profile menu holds the account details": operators who
// carry several merchants need the at-a-glance "which one am I in" before they touch anything,
// and getting that wrong means editing the wrong brand's live pass.
//
// Display-only. This started life as the hub's chip with a transparent <select> stretched over
// it, which was the only vendor switcher the Studio had. Now that AccountMenu carries one on
// every screen, keeping the <select> would put two switchers in the same header, eight pixels
// apart, with different affordances -- so the chip keeps the name and the menu keeps the verb.
function VendorChip() {
  const AUTH = window.SpoonityAuth;
  const I18N = window.SpoonityI18n;
  const session = (AUTH && AUTH.getSession && AUTH.getSession()) || {};
  const vendor = session.vendor || {};
  // Nothing useful to show without a resolved vendor (a standalone/prototype mount, or a session
  // that only carries an id). Same call as AccountMenu's: an empty pill is worse than no pill.
  if (!vendor.name) return null;
  // The avatar tint is the Spoonity brand orange, not the caller's page accent. The chip used to
  // take the hub's `accent` prop, but it now sits inside the account cluster on four different
  // screens -- one of them the pass editor, whose accent is whatever colour the operator picked
  // for the pass they are designing. Tying an account control to that would repaint it per draft.
  // The tint behind the initials is that same orange at 12%. Written out as rgba() rather than
  // derived from the var: the old chip built it by concatenating an alpha onto a hex accent
  // ("#F47920" + "1a"), which a var() cannot do, and color-mix() has no fallback path in an
  // inline style if a browser does not support it. --spoonity-orange is a fixed token
  // (tokens/colors.css, not redefined per theme), so the literal cannot drift away from it.
  const brandOrange = "var(--spoonity-orange, #ff7e3d)";
  return (
    // The class is here only for the sub-640px hide in _studio-polish.css: inline styles cannot
    // carry a media query, and below that width the three icons alone are what has to fit.
    <div className="spoonity-vendor-chip"
      title={I18N ? I18N.T("acct_activeVendorTitle", { name: vendor.name }) : vendor.name}
      style={{ display: "flex", alignItems: "center", gap: 8, background: "var(--surface-secondary, #f4f2ef)", border: "1px solid var(--border-primary)", borderRadius: 99, padding: "6px 10px" }}>
      <span aria-hidden="true" style={{ width: 22, height: 22, borderRadius: "50%", background: "rgba(255, 126, 61, .12)", color: brandOrange, fontSize: 11, fontWeight: 700, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
        {vendorInitials(vendor.name)}
      </span>
      {/* max-width + ellipsis + the title above, so a long merchant name truncates cleanly
          instead of shoving the three icons off the right edge of the bar. */}
      <span style={{ fontSize: 13, color: "var(--typography-primary)", maxWidth: 200, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
        {vendor.name}
      </span>
    </div>
  );
}

// The Studio's one breakpoint, as the JSX files can see it.
//
// It has to exist twice. _studio-polish.css owns the CSS half of `(max-width: 640px)` and is the
// right home for anything that can be expressed as a class rule; this is the other half, for the
// things that cannot. PassStudio.jsx in particular styles its whole layout inline -- a React
// style object has nowhere to put a media query -- so "is this a phone" has to be a value the
// render can branch on, not a rule the browser applies afterwards.
//
// It lives in this file rather than in PassStudio.jsx because these files share one global scope
// (Babel standalone, no build step, no imports) and this one is already the shared header's home
// and loads before every screen that needs it -- see index.html's script order.
//
// Keep the 640 here and the 640 in _studio-polish.css in step. Two numbers, one breakpoint: a
// layout that stacks at one width while its stylesheet hides things at another is worse than
// either alone.
const STUDIO_PHONE_QUERY = "(max-width: 640px)";
function useIsPhone() {
  const query = () => window.matchMedia(STUDIO_PHONE_QUERY);
  const [isPhone, setIsPhone] = React.useState(() => query().matches);
  React.useEffect(() => {
    const mq = query();
    const onChange = (e) => setIsPhone(e.matches);
    // Re-read on subscribe as well as on change: a rotation or a devtools resize between the
    // initial useState and this effect would otherwise leave the value one render stale, and
    // nothing would ever correct it because no further change event is coming.
    setIsPhone(mq.matches);
    // addListener is the deprecated spelling, still the only one Safari understood before 14.
    if (mq.addEventListener) { mq.addEventListener("change", onChange); return () => mq.removeEventListener("change", onChange); }
    mq.addListener(onChange); return () => mq.removeListener(onChange);
  }, []);
  return isPhone;
}

// The whole right-hand end of a Studio header: vendor chip, then the docked feedback and
// announcements widgets, then the account icon. Every signed-in screen renders this and nothing
// else in that corner, which is the point -- the hub, the pass editor, Analytics and Integrations
// each used to end their header differently (a sign-out button, a save indicator, a member count,
// nothing at all), so "where is my account" depended on which screen you happened to be on.
//
// One component rather than four copies of the same three tags: this file exists precisely
// because there is no build step here to keep copies honest, and the spacing (marginLeft:auto,
// gap 14) is the kind of detail that drifts silently between copies.
//
// Screen-specific header items -- the editor's brand button and save indicator, the Google
// studio's autosave badge, Analytics' member count -- come in as children and render at the
// LEFT of the cluster, keeping the three controls at the far right on every screen. That is the
// only prop, and it is the one thing that genuinely differs per screen; everything else
// (session, vendor, language) the cluster reads for itself, so the common case is <HeaderControls />.
function HeaderControls({ children }) {
  return (
    <div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 14 }}>
      {children}
      <VendorChip />
      {/* Where the feedback and news widgets dock. Both mount onto document.body long before
          any header exists (they are plain scripts, not part of the React tree), so they are
          handed this element once it is on screen rather than looking for it themselves --
          see _chrome-dock.js. Empty and zero-width until they arrive. */}
      <ChromeSlot />
      <AccountMenu />
    </div>
  );
}

// The account control, replacing a hardcoded <PS.Avatar initials="DL" /> that was never wired to
// anything — "DL" is the sample member Dana Lee used throughout the previews, not the signed-in
// user, so the header was showing a mock to real operators. The hub already had the working
// version (_hub.jsx's account chip); this puts vendor switching and sign-out on the builder
// surface too, which is where operators actually spend their time.
//
// The icon is Lucide's circle-user-round. Macondo's own set is Iconsax, which has no reliable CDN
// (readme.md §Icon system), and Lucide is the substitute that file names — flagged here so it is a
// recorded decision rather than a hand-rolled glyph.
function AccountMenu() {
  const AUTH = window.SpoonityAuth;
  // The language picker used to live inside the pass editor, several clicks from anywhere else.
  // The account menu is on every Studio screen, so it is where a global preference belongs.
  const I18N = window.SpoonityI18n;
  const lang = (I18N && I18N.useStudioLang) ? I18N.useStudioLang() : (I18N ? I18N.getLang() : "en");
  const session = (AUTH && AUTH.getSession && AUTH.getSession()) || {};
  const vendor = session.vendor || {};
  const vendors = session.vendors || [];
  const [open, setOpen] = React.useState(false);
  const [filter, setFilter] = React.useState("");
  // Which row the keyboard is pointing at, as an index into the FILTERED list. -1 is "none yet":
  // the menu opens with nothing highlighted, so an immediate Enter does nothing rather than
  // switching to whichever vendor happened to sort first.
  const [highlight, setHighlight] = React.useState(-1);
  const wrapRef = React.useRef(null);
  const filterRef = React.useRef(null);
  // Ids for aria-activedescendant: the input has focus while the arrow keys move a highlight that
  // lives on a different element, and that attribute is the only thing that tells a screen reader
  // which row is current. useId keeps them unique if two menus ever mount at once.
  const uid = React.useId();
  const listId = uid + "-vendors";
  const rowId = (id) => uid + "-v-" + id;

  // The same predicate as the login-time picker (_auth.jsx:308) -- substring, case-insensitive,
  // over the name and the id -- so the two vendor searches in this product behave identically.
  // Matching on id matters: operators are given numeric vendor ids in support threads far more
  // often than exact merchant names.
  const q = filter.trim().toLowerCase();
  const filteredVendors = vendors.filter((v) => !q || (v.name || "").toLowerCase().includes(q) || String(v.id).toLowerCase().includes(q));
  const showFilter = vendors.length > VENDOR_FILTER_THRESHOLD;

  // A reopened menu must never show a stale filter. The operator closed it on "cut", and coming
  // back to a two-row list with no memory of why is exactly how someone edits the wrong brand's
  // live pass. Reset on close rather than on open so the state dies at the teardown.
  React.useEffect(() => {
    if (open) return;
    setFilter("");
    setHighlight(-1);
  }, [open]);

  // Focus the box on open so a long list is typeable without a second click -- the same courtesy
  // the login picker already does (_auth.jsx:166). Only when it is actually rendered.
  React.useEffect(() => {
    if (open && showFilter && filterRef.current) filterRef.current.focus();
  }, [open, showFilter]);

  React.useEffect(() => {
    if (!open) return undefined;
    const onDown = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    // Escape clears before it closes. A filtered list is a mode, and the first Escape should leave
    // the mode, not the menu -- closing on a typo would throw away the menu the operator is still
    // using. Only an already-empty filter closes, which is the behaviour this had before.
    const onKey = (e) => {
      if (e.key !== "Escape") return;
      if (filter) { setFilter(""); setHighlight(-1); return; }
      setOpen(false);
    };
    document.addEventListener("mousedown", onDown);
    document.addEventListener("keydown", onKey);
    return () => { document.removeEventListener("mousedown", onDown); document.removeEventListener("keydown", onKey); };
  }, [open, filter]);

  // No auth on this surface (a standalone/prototype mount) means no account to show. Rendering
  // nothing is right: a dead account button is worse than none.
  if (!AUTH) return null;

  const signOut = () => { try { AUTH.logout(); } catch (e) {} location.reload(); };
  // Reload rather than re-fetch: the whole editor is scoped to the active vendor (catalog, drafts,
  // brand), so a switch changes everything under it. Same approach as the hub's switcher, and the
  // failure path reloads too — landing back on a coherent session beats a half-switched one.
  const switchVendor = (id) => {
    if (!id || id === vendor.id) { setOpen(false); return; }
    Promise.resolve(AUTH.switchVendor(id)).then(() => location.reload()).catch(() => location.reload());
  };

  // Arrow keys and Enter are handled on the menu container rather than on the document, because
  // React events bubble: one handler on the dropdown catches keys typed in the filter box and on
  // the rows alike, and it always sees the current filteredVendors without re-subscribing a
  // document listener on every keystroke. Escape stays at document level (above) so it still
  // closes a menu whose focus has wandered outside it.
  const onMenuKeyDown = (e) => {
    if (!filteredVendors.length) return;
    if (e.key === "ArrowDown" || e.key === "ArrowUp") {
      e.preventDefault();
      const next = highlight + (e.key === "ArrowDown" ? 1 : -1);
      setHighlight(next < 0 ? filteredVendors.length - 1 : next >= filteredVendors.length ? 0 : next);
      return;
    }
    // Enter only from the filter box. Every row is a real <button>, so Enter on a focused row
    // already fires its onClick -- handling it here as well would call switchVendor twice.
    if (e.key === "Enter" && e.target === filterRef.current && highlight >= 0 && highlight < filteredVendors.length) {
      e.preventDefault();
      switchVendor(filteredVendors[highlight].id);
    }
  };

  const item = {
    display: "flex", alignItems: "center", gap: 8, width: "100%", padding: "8px 10px",
    border: "none", background: "none", borderRadius: 7, cursor: "pointer", textAlign: "left",
    fontFamily: "var(--font-sans)", fontSize: 13, color: "var(--typography-primary)",
  };

  return (
    <div ref={wrapRef} style={{ position: "relative", display: "flex", alignItems: "center" }}>
      <button type="button" onClick={() => setOpen((v) => !v)}
        aria-haspopup="menu" aria-expanded={open}
        title={vendor.name ? I18N.T("acct_accountTitle", { name: vendor.name }) : I18N.T("acct_account")}
        aria-label={I18N.T("acct_account")}
        style={{
          display: "flex", alignItems: "center", justifyContent: "center", width: 32, height: 32,
          borderRadius: "50%", border: "1px solid transparent", cursor: "pointer",
          background: open ? "var(--surface-secondary, #f4f2ef)" : "none",
          color: open ? "var(--typography-primary)" : "var(--typography-secondary)",
        }}>
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
          <path d="M18 20a6 6 0 0 0-12 0" /><circle cx="12" cy="10" r="4" /><circle cx="12" cy="12" r="10" />
        </svg>
      </button>

      {open && (
        <div role="menu" onKeyDown={onMenuKeyDown} style={{
          position: "absolute", top: "calc(100% + 8px)", right: 0, zIndex: 3000,
          minWidth: 230, maxWidth: 300, padding: 6,
          background: "var(--surface-base)", border: "1px solid var(--border-primary)",
          borderRadius: 12, boxShadow: "var(--shadow-popup, 0 4px 24px 0 rgba(0,0,0,.078))",
        }}>
          {/* Which account you are in, stated before anything you can click. A long vendor name
              truncates with the full one on hover rather than stretching the menu. */}
          <div style={{ padding: "6px 10px 8px" }}>
            <div style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: "var(--typography-tertiary)" }}>
              {I18N.T("acct_signedIn")}
            </div>
            <div title={vendor.name || ""} style={{ marginTop: 2, fontSize: 13, fontWeight: 600, color: "var(--typography-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
              {vendor.name || vendor.id || I18N.T("acct_unknownVendor")}
            </div>
          </div>

          {vendors.length > 1 && (
            <React.Fragment>
              <div style={{ height: 1, background: "var(--border-primary)", margin: "4px 0" }} />
              <div style={{ padding: "6px 10px 4px", fontSize: 10.5, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: "var(--typography-tertiary)" }}>
                {I18N.T("acct_switchVendor")}
              </div>
              {/* Only past the threshold: below it the list already fits the scroll box, and a
                  search field over three rows is a control that costs more than it saves. The
                  wrapper is role="none" because an <input> is not a valid child of role="menu" --
                  without it the container's semantics are a lie to a screen reader, which is the
                  kind of wrong that never shows up in a screenshot. */}
              {showFilter && (
                <div role="none" style={{ padding: "2px 10px 6px" }}>
                  <input ref={filterRef} type="text" value={filter}
                    onChange={(e) => { setFilter(e.target.value); setHighlight(-1); }}
                    placeholder={I18N.T("auth_filterVendors")}
                    aria-label={I18N.T("auth_filterVendors")}
                    aria-controls={listId}
                    aria-activedescendant={highlight >= 0 && filteredVendors[highlight] ? rowId(filteredVendors[highlight].id) : undefined}
                    onFocus={(e) => { e.currentTarget.style.borderColor = "var(--border-focused)"; }}
                    onBlur={(e) => { e.currentTarget.style.borderColor = "var(--border-primary)"; }}
                    style={{
                      width: "100%", boxSizing: "border-box", padding: "6px 8px", outline: "none",
                      fontFamily: "var(--font-sans)", fontSize: 13,
                      color: "var(--typography-primary)", background: "var(--surface-base)",
                      border: "1px solid var(--border-primary)", borderRadius: "var(--radius-input, 7px)",
                    }} />
                </div>
              )}
              {/* Scrolls rather than growing without limit — some logins carry a long list. */}
              <div id={listId} style={{ maxHeight: 240, overflowY: "auto" }}>
                {filteredVendors.length === 0 && (
                  <div style={{ padding: "8px 10px", fontSize: 12.5, color: "var(--typography-tertiary)" }}>
                    {I18N.T("auth_noMatchingVendors")}
                  </div>
                )}
                {filteredVendors.map((v, i) => {
                  const active = v.id === vendor.id;
                  // One highlight for both pointer and keyboard, rather than the hover-only inline
                  // style this had before: with two sources of "current row" the arrow keys and the
                  // mouse would paint different rows at the same time.
                  const highlighted = i === highlight;
                  return (
                    <button key={v.id} id={rowId(v.id)} type="button" role="menuitem" onClick={() => switchVendor(v.id)}
                      title={v.name || v.id}
                      onMouseEnter={() => setHighlight(i)}
                      onMouseLeave={() => setHighlight((cur) => (cur === i ? -1 : cur))}
                      style={{ ...item, fontWeight: active ? 600 : 400, background: highlighted ? "var(--surface-secondary, #f4f2ef)" : "none" }}>
                      <span style={{ width: 14, flexShrink: 0, color: "var(--spoonity-orange, #ff7e3d)" }}>
                        {active && (
                          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6L9 17l-5-5" /></svg>
                        )}
                      </span>
                      <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{v.name || v.id}</span>
                    </button>
                  );
                })}
              </div>
              {/* Gated on the same threshold as the box itself: "3 of 3 vendors" under a list you
                  can see in full is the same chrome-for-nothing the threshold exists to avoid. */}
              {showFilter && (
                <div style={{ padding: "4px 10px 2px", fontSize: 11, color: "var(--typography-tertiary)" }}>
                  {I18N.T("auth_vendorCount", { matches: filteredVendors.length, total: vendors.length })}
                </div>
              )}
            </React.Fragment>
          )}

          {/* Omitted where _i18n.js was never loaded (a standalone mount), for the same reason the
              whole menu is omitted without auth: a picker that cannot pick is worse than none.
              These rows are role="menuitemradio" rather than plain menuitems because they are one
              mutually-exclusive choice and aria-checked is what tells a screen reader which. */}
          {I18N && (
            <React.Fragment>
              <div style={{ height: 1, background: "var(--border-primary)", margin: "4px 0" }} />
              <div style={{ padding: "6px 10px 4px", fontSize: 10.5, fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase", color: "var(--typography-tertiary)" }}>
                {I18N.T("language")}
              </div>
              {I18N.LANGS.map((l) => {
                const active = l.code === lang;
                return (
                  <button key={l.code} type="button" role="menuitemradio" aria-checked={active}
                    onClick={() => { I18N.setLang(l.code); setOpen(false); }}
                    onMouseEnter={(e) => { e.currentTarget.style.background = "var(--surface-secondary, #f4f2ef)"; }}
                    onMouseLeave={(e) => { e.currentTarget.style.background = "none"; }}
                    style={{ ...item, fontWeight: active ? 600 : 400 }}>
                    <span style={{ width: 14, flexShrink: 0, color: "var(--spoonity-orange, #ff7e3d)" }}>
                      {active && (
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6L9 17l-5-5" /></svg>
                      )}
                    </span>
                    <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{l.label}</span>
                  </button>
                );
              })}
            </React.Fragment>
          )}

          <div style={{ height: 1, background: "var(--border-primary)", margin: "4px 0" }} />
          <button type="button" role="menuitem" onClick={signOut}
            onMouseEnter={(e) => { e.currentTarget.style.background = "var(--surface-secondary, #f4f2ef)"; }}
            onMouseLeave={(e) => { e.currentTarget.style.background = "none"; }}
            style={item}>
            <span style={{ width: 14, flexShrink: 0, display: "flex", color: "var(--typography-tertiary)" }}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><path d="M16 17l5-5-5-5" /><path d="M21 12H9" /></svg>
            </span>
            {I18N.T("acct_signOut")}
          </button>
        </div>
      )}
    </div>
  );
}
