/* Post-login home. After the operator signs in and a vendor is selected (SpoonityAuthGate), this
   hub lets them choose what to manage: their Brand or their wallet Passes. Styled with the
   macondo design tokens (surface/typography/border CSS vars + the DS Button) so it matches the
   studios. (The Enrollment Page card -- the customer-facing signup microsite at
   wallets.spoonity.com -- was removed from here; that app itself is untouched, just no longer
   linked from this hub.) */
const HUB = window.SpoonityWalletPassDesignSystem_de20b8;
const HUB_AUTH = window.SpoonityAuth;

/* An 11x11-ish arrow glyph reused by both card types below. */
function ArrowIcon({ size = 15, style }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" style={style}>
      <path d="M5 12h14M12 5l7 7-7 7" />
    </svg>
  );
}

/* Setup-sequence card (Brand, Wallet Passes) — vertical layout, step number
   baked into the title itself ("1 · Brand"), and a single dynamic badge: "Start here" on
   whichever step is the first one not yet done, a green "Done" on ones that are, and nothing at
   all on steps further out in the sequence that aren't next yet. That's deliberately different
   from badging every unfinished card the same way — it points at ONE next action instead of
   listing what's outstanding. Keeps a bottom cta-text + arrow (unlike ToolCard below); the whole
   card is still the only click target, this is just its label. */
function SetupCard({ icon, title, desc, cta, onClick, accent, badge }) {
  const [hover, setHover] = React.useState(false);
  const isNext = badge === "next";
  return (
    <button type="button" onClick={onClick}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{
        textAlign: "left", cursor: "pointer", display: "flex", flexDirection: "column",
        padding: 20, borderRadius: 14, background: "var(--surface-base)",
        // A single border string, not a separate borderColor override: a shorthand containing
        // var() can't be split into independent longhands up front (it stays one "pending" value
        // until paint), so a sibling borderColor:undefined would rip just the color back out to
        // its CSS default (currentColor, i.e. black) instead of leaving var(--border-primary) in
        // place — exactly the black-border bug this replaced.
        border: isNext ? `2px solid ${accent}` : `1px solid ${hover ? accent : "var(--border-primary)"}`,
        fontFamily: "var(--font-sans)",
        boxShadow: hover ? "0 16px 36px -18px rgba(17,24,39,.26)" : "0 1px 2px rgba(17,24,39,.04)",
        transform: hover ? "translateY(-3px)" : "none", transition: "transform .16s ease, box-shadow .16s ease, border-color .16s ease",
        outline: "none",
      }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 }}>
        <span style={{ width: 34, height: 34, borderRadius: 9, background: isNext ? accent + "1a" : "var(--surface-secondary, #f4f2ef)", color: isNext ? accent : "var(--typography-secondary)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
          {icon}
        </span>
        {isNext ? (
          <span style={{ fontSize: 11, fontWeight: 600, background: "rgba(217,138,15,.14)", color: "#b6790b", padding: "3px 8px", borderRadius: 7, whiteSpace: "nowrap" }}>Start here</span>
        ) : badge === "done" ? (
          <span style={{ fontSize: 11, fontWeight: 600, background: "rgba(63,125,88,.14)", color: "#3f7d58", padding: "3px 8px", borderRadius: 7, whiteSpace: "nowrap", display: "flex", alignItems: "center", gap: 4 }}>
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6L9 17l-5-5" /></svg>
            Done
          </span>
        ) : null}
      </div>
      <div style={{ fontWeight: 700, fontSize: 15, color: "var(--typography-primary)", marginBottom: 4 }}>{title}</div>
      <div style={{ fontSize: 12.5, color: "var(--typography-primary)", opacity: 0.72, lineHeight: 1.5, marginBottom: 12, flex: 1 }}>{desc}</div>
      {/* Orange (the accent) is reserved for the one "Start here" card — everywhere else it'd
          make all three look equally urgent, which is exactly the "5 equal doors" problem this
          redesign was meant to fix, just recreated one level down inside the setup section. */}
      <span style={{ fontSize: 13, fontWeight: 600, color: isNext ? accent : "var(--typography-secondary)", display: "inline-flex", alignItems: "center", gap: 4 }}>
        {cta} <ArrowIcon size={15} style={{ transform: hover ? "translateX(2px)" : "none", transition: "transform .16s ease" }} />
      </span>
    </button>
  );
}

/* Ongoing-tool card (Integrations, Analytics) — deliberately a different shape from SetupCard
   (horizontal, no step number, no bottom cta text — just a trailing arrow) so these read as a
   different KIND of thing at a glance, not two more steps in the same sequence. */
function ToolCard({ icon, title, desc, onClick, accent }) {
  const [hover, setHover] = React.useState(false);
  return (
    <button type="button" onClick={onClick}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{
        textAlign: "left", cursor: "pointer", display: "flex", alignItems: "center", gap: 12,
        padding: 16, borderRadius: 14, background: "var(--surface-base)",
        border: `1px solid ${hover ? accent : "var(--border-primary)"}`, fontFamily: "var(--font-sans)",
        boxShadow: hover ? "0 16px 36px -18px rgba(17,24,39,.26)" : "0 1px 2px rgba(17,24,39,.04)",
        transform: hover ? "translateY(-3px)" : "none", transition: "transform .16s ease, box-shadow .16s ease, border-color .16s ease",
      }}>
      <span style={{ width: 34, height: 34, flexShrink: 0, borderRadius: 9, background: "var(--surface-secondary, #f4f2ef)", color: "var(--typography-secondary)", display: "flex", alignItems: "center", justifyContent: "center" }}>
        {icon}
      </span>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontWeight: 700, fontSize: 15, color: "var(--typography-primary)", marginBottom: 2 }}>{title}</div>
        <div style={{ fontSize: 12.5, color: "var(--typography-primary)", opacity: 0.72, lineHeight: 1.5 }}>{desc}</div>
      </div>
      <ArrowIcon size={16} style={{ color: accent, flexShrink: 0, transform: hover ? "translateX(2px)" : "none", transition: "transform .16s ease" }} />
    </button>
  );
}

const ICON_BTN = {
  display: "flex", alignItems: "center", justifyContent: "center",
  width: 28, height: 28, borderRadius: 8, border: "1px solid var(--border-primary)",
  background: "var(--surface-base)", color: "var(--typography-tertiary)", cursor: "pointer", padding: 0,
};

function PassMiniCard({ draft, onClick, onAnalytics, accent, onArchive, onRestore, onPublish }) {
  const p = draft.displayPassJson || draft.pass_json || {};
  const isGoogle = draft.platform === "google";
  // Apple and Google passes use different field names for the same concepts.
  const bg = p.backgroundColor || "#4b2d6e";
  const fg = isGoogle ? "#ffffff" : (p.foregroundColor || "#ffffff");
  const label = isGoogle ? fg : (p.labelColor || fg);
  const name = (isGoogle ? p.cardTitle : p.name) || p.draft_name || "Untitled pass";
  const company = (isGoogle ? p.programName : p.company) || "";
  const logoSrc = p.logoSrc;
  const [hover, setHover] = React.useState(false);
  const updated = draft.updated_at ? new Date(draft.updated_at).toLocaleDateString(undefined, { month: "short", day: "numeric" }) : "";
  // Only a truly published pass has a real program for Analytics to scope to -- an unpublished
  // draft (even one that reuses a name/design) has no programId at all.
  const canViewAnalytics = draft.published && !!draft.programId;
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 6, minWidth: 0 }}>
      <button type="button" onClick={onClick}
        onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
        style={{
          textAlign: "left", cursor: "pointer", display: "flex", flexDirection: "column", gap: 0,
          borderRadius: 12, overflow: "hidden", border: hover ? `2px solid ${accent}` : "2px solid transparent",
          boxShadow: hover ? "0 12px 32px -10px rgba(17,24,39,.28)" : "0 2px 8px rgba(17,24,39,.10)",
          transform: hover ? "translateY(-2px)" : "none", transition: "transform .15s ease, box-shadow .15s ease, border-color .15s ease",
          background: "transparent", padding: 0, fontFamily: "var(--font-sans)", minWidth: 0,
        }}>
        {/* Pass face */}
        <div style={{ background: bg, padding: "16px 14px 14px", display: "flex", flexDirection: "column", gap: 8 }}>
          {logoSrc
            ? <img src={logoSrc} alt="" style={{ height: 20, maxWidth: 72, objectFit: "contain", objectPosition: "left" }} />
            : <div style={{ fontSize: 11, fontWeight: 700, color: fg, opacity: 0.85, letterSpacing: ".04em", textTransform: "uppercase", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{company || name}</div>
          }
          <div style={{ fontSize: 18, fontWeight: 700, color: fg, letterSpacing: "-.01em", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>-</div>
          <div style={{ fontSize: 9, fontWeight: 600, color: label, opacity: 0.7, textTransform: "uppercase", letterSpacing: ".08em" }}>LOYALTY</div>
        </div>
        {/* Footer */}
        <div style={{ background: "var(--surface-base)", padding: "10px 12px", borderTop: "1px solid var(--border-primary)" }}>
          <div style={{ fontSize: 12, fontWeight: 600, color: "var(--typography-primary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{name}</div>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginTop: 3 }}>
            <span style={{ fontSize: 10.5, color: "var(--typography-tertiary)" }}>{updated}</span>
            <span style={{
              fontSize: 10, fontWeight: 600, padding: "2px 7px", borderRadius: 99,
              background: draft.published ? "rgba(63,125,88,.12)" : "rgba(17,24,39,.08)",
              color: draft.published ? "#3f7d58" : "var(--typography-secondary)",
            }}>{draft.published ? "Published" : "Draft"}</span>
          </div>
        </div>
      </button>
      <div style={{ display: "flex", gap: 6 }}>
        {canViewAnalytics && (
          <button type="button" title="View analytics for this pass"
            onClick={(e) => { e.stopPropagation(); onAnalytics(draft.programId, name); }}
            style={ICON_BTN}
            onMouseEnter={(e) => { e.currentTarget.style.color = accent; e.currentTarget.style.borderColor = accent; }}
            onMouseLeave={(e) => { e.currentTarget.style.color = "var(--typography-tertiary)"; e.currentTarget.style.borderColor = "var(--border-primary)"; }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3v18h18" /><path d="M18 17V9" /><path d="M13 17V5" /><path d="M8 17v-3" /></svg>
          </button>
        )}
        {onArchive && (
          <button type="button" title={draft.programId ? "Archive this pass" : "Archive this draft"}
            onClick={(e) => { e.stopPropagation(); onArchive(draft); }}
            style={ICON_BTN}
            onMouseEnter={(e) => { e.currentTarget.style.color = accent; e.currentTarget.style.borderColor = accent; }}
            onMouseLeave={(e) => { e.currentTarget.style.color = "var(--typography-tertiary)"; e.currentTarget.style.borderColor = "var(--border-primary)"; }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="4" width="18" height="4" rx="1" /><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8" /><path d="M10 12h4" /></svg>
          </button>
        )}
        {/* Only on a card that has never been published: an archived pass with a programId is
            already live, so Restore alone is what "back to published" means for it — sending it
            through the publish modal again would needlessly re-upload its images and re-register
            its site domain. */}
        {onPublish && !draft.programId && (
          <button type="button" title="Publish this draft"
            onClick={(e) => { e.stopPropagation(); onPublish(draft); }}
            style={ICON_BTN}
            onMouseEnter={(e) => { e.currentTarget.style.color = accent; e.currentTarget.style.borderColor = accent; }}
            onMouseLeave={(e) => { e.currentTarget.style.color = "var(--typography-tertiary)"; e.currentTarget.style.borderColor = "var(--border-primary)"; }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 19V5" /><path d="M5 12l7-7 7 7" /></svg>
          </button>
        )}
        {onRestore && (
          <button type="button" title={draft.programId ? "Restore this pass" : "Restore this draft"}
            onClick={(e) => { e.stopPropagation(); onRestore(draft); }}
            style={ICON_BTN}
            onMouseEnter={(e) => { e.currentTarget.style.color = accent; e.currentTarget.style.borderColor = accent; }}
            onMouseLeave={(e) => { e.currentTarget.style.color = "var(--typography-tertiary)"; e.currentTarget.style.borderColor = "var(--border-primary)"; }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7" /><path d="M3 4v5h5" /></svg>
          </button>
        )}
      </div>
    </div>
  );
}

// Two-letter initials for the account-chip avatar (e.g. "Buckhead Life Restaurant Group" -> "BL").
function initials(name) {
  if (!name) return "";
  return name.trim().split(/\s+/).slice(0, 2).map((w) => w[0].toUpperCase()).join("");
}

function SpoonityHub({ onPasses, onIntegrations, onAnalytics, accent = "#F47920" }) {
  const session = (HUB_AUTH && HUB_AUTH.getSession && HUB_AUTH.getSession()) || {};
  const vendor = session.vendor || {};
  const vendors = session.vendors || [];
  const [brand, setBrand] = React.useState((window.SpoonityBrand && window.SpoonityBrand.get()) || null);
  const [createdPasses, setCreatedPasses] = React.useState(null); // null = loading
  const [showArchived, setShowArchived] = React.useState(false);
  const activePasses = (createdPasses || []).filter((d) => !d.archived);
  const archivedPasses = (createdPasses || []).filter((d) => d.archived);

  // Reflect brand creation/edits made from the Brand screen without a reload.
  React.useEffect(() => {
    if (!window.SpoonityBrand || !window.SpoonityBrand.onChange) return;
    return window.SpoonityBrand.onChange((b) => setBrand(b));
  }, []);

  // Show every saved pass — draft or published — as its own card, badged accordingly. Sourced
  // from two places: the editor's drafts (full-fidelity pass_json, includes ones never published)
  // and the vendor's real published programs (wallet_programs — catches anything published through
  // a path that never left a draft doc behind, e.g. an older publish). Where a draft and a program
  // both exist for the same programId, prefer the draft's pass_json: it's the exact state the
  // studio last saved, not a reconstruction, so clicking back in reopens it faithfully (including
  // fields the reconstruction can't round-trip, like custom field placement or the barcode toggle).
  React.useEffect(() => {
    const endpoint = window.SpoonityAuth ? window.SpoonityAuth.getEndpoint() : "";
    const db = window.SpoonityDrafts;
    Promise.all([
      fetch(endpoint + "/v1/programs?includeArchived=1", { credentials: "include" }).then((r) => r.json()).catch(() => ({ programs: [] })),
      db ? db.list({ includeArchived: true }).catch(() => []) : Promise.resolve([]),
    ]).then(([programsData, drafts]) => {
      const programs = programsData.programs || [];
      // A draft's pass_json is now { draft_name, apple, google } — one doc covers both platforms.
      // Older drafts saved before that change are still flat (a single platform's fields directly
      // on pass_json, tagged via the doc's own `platform`). Normalize to the flat shape
      // PassMiniCard already expects — preferring Apple's fields for display when both exist,
      // same convention used elsewhere (Analytics' program picker, etc.) — so nothing downstream
      // needs to know about the two possible shapes.
      const normalize = (pj, taggedPlatform) => {
        if (!pj) return { display: {}, programId: null, isGoogle: taggedPlatform === "google" };
        if (pj.apple || pj.google) {
          const display = pj.apple || pj.google || {};
          const programId = (pj.apple && pj.apple.programId) || (pj.google && pj.google.programId) || null;
          return { display: { ...display, draft_name: pj.draft_name || display.draft_name }, programId, isGoogle: !pj.apple && !!pj.google };
        }
        return { display: pj, programId: pj.programId || null, isGoogle: taggedPlatform === "google" };
      };
      const draftProgramIds = new Set(
        drafts.map((d) => normalize(d.pass_json, d.platform).programId).filter(Boolean).map(String)
      );
      // A draft carries its own copy of an archived pass's design, so it has to be badged from the
      // program's archived state too — otherwise archiving would leave the draft card behind.
      const archivedIds = new Set((programsData.archivedProgramIds || []).map(String));
      const draftEntries = drafts.map((d) => {
        const { display, programId, isGoogle } = normalize(d.pass_json, d.platform);
        return {
          // pass_json stays the RAW doc value (still { draft_name, apple, google } for unified
          // drafts) — reopening this card passes it straight through as initialDraft, and
          // PassStudio needs the full shape to seed BOTH platforms, not just whichever one this
          // card happens to display. displayPassJson is the flattened view PassMiniCard renders.
          // programId was previously computed here (for the published/dedup logic below) but
          // never attached to the entry itself, so a draft-originated published card had no
          // programId to deep-link its own "view analytics" icon to (see PassMiniCard).
          id: d.id, share_id: d.share_id, updated_at: d.updated_at, pass_json: d.pass_json,
          displayPassJson: display, platform: isGoogle ? "google" : "apple", published: !!programId, programId,
          // Two independent sources: a published pass is archived on its program, an
          // unpublished draft on its own doc (POST /drafts/:id/archive) — it has no program to
          // carry the flag. Either one shelves the card.
          archived: d.archived === true || archivedIds.has(String(programId)),
        };
      });
      const programOnlyEntries = programs
        .filter((p) => !draftProgramIds.has(String(p.programId)))
        .map((p) => ({ id: p.programId, programId: p.programId, updated_at: p.updatedAt, pass_json: p, displayPassJson: p, platform: "apple", published: true, archived: archivedIds.has(String(p.programId)) }));
      const merged = [...draftEntries, ...programOnlyEntries]
        .sort((a, b) => new Date(b.updated_at || 0) - new Date(a.updated_at || 0));
      setCreatedPasses(merged);
    }).catch(() => setCreatedPasses([]));
  }, []);

  // Archiving retires a pass from this grid without deleting it — anyone already holding it keeps
  // a working pass, and Restore brings it back. Which store holds the flag depends on the card:
  // a published pass is shelved on its program (so it also drops out of Analytics and the vendor
  // site's auto-pick), while a draft that was never published has no program and is shelved on
  // its own draft doc. Before, only the former had a control at all, so an abandoned draft could
  // only be hard-deleted from the studio's Load dialog — losing the design.
  // The studio keeps a per-vendor "draft I'm currently editing" pointer in localStorage, and
  // restores it on mount / autosaves back into it. Shelving that draft has to drop the pointer,
  // or the next visit to the studio reseeds the preset defaults (the archived draft is filtered
  // out of the restore list) and then autosaves those defaults straight over the design that was
  // just archived. Clearing it starts the studio fresh instead, which is what retiring a pass
  // means anyway.
  const forgetActiveDraft = (matches) => {
    if (!window.SpoonityAuth || !window.SpoonityAuth.draftKey) return;
    try {
      const key = window.SpoonityAuth.draftKey("spoonity_draft_id");
      const activeId = localStorage.getItem(key);
      if (!activeId) return;
      if (!(createdPasses || []).some((d) => d.id === activeId && matches(d))) return;
      localStorage.removeItem(key);
      localStorage.removeItem(window.SpoonityAuth.draftKey("spoonity_share_id"));
    } catch (e) {}
  };

  const setArchived = (entry, archived) => {
    const programId = entry && entry.programId;
    const endpoint = window.SpoonityAuth ? window.SpoonityAuth.getEndpoint() : "";
    const request = programId
      ? fetch(endpoint + "/v1/programs/" + encodeURIComponent(programId) + "/archive", {
          method: "POST", credentials: "include",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ archived }),
        }).then((r) => { if (!r.ok) throw new Error("request failed"); return r.json(); })
      // Guarded on the METHOD, not just the object: every script here is cache-busted by hand
      // (index.html's ?v=N), so a browser holding an older _firestore.js alongside a fresh
      // _hub.jsx — .jsx is served no-cache, .js isn't — would otherwise throw an uncaught
      // TypeError on click instead of surfacing anything to the operator.
      : (window.SpoonityDrafts && typeof window.SpoonityDrafts.setArchived === "function")
        ? window.SpoonityDrafts.setArchived(entry.id, archived)
        : Promise.reject(new Error("draft store out of date"));
    // Match on the same key the request used: programId groups a draft card with its published
    // program (both must move together), while a draft-only card is identified by its doc id.
    const matches = (d) => (programId
      ? String(d.programId) === String(programId)
      : d.id === entry.id);
    request
      .then(() => {
        if (archived) forgetActiveDraft(matches);
        setCreatedPasses((cur) => (cur || []).map((d) => (matches(d) ? { ...d, archived } : d)));
      })
      .catch(() => window.alert(archived ? "Could not archive that pass." : "Could not restore that pass."));
  };

  // "Publish" on an archived draft: lift the archive first, then hand it to the studio with the
  // publish modal already open. Publishing itself has to happen there — it needs the studio's
  // assembled two-platform state, and its own validation (icon, currency, company) reports what's
  // missing if the design was abandoned half-finished. Un-archiving first also means a cancelled
  // publish leaves the draft back in the main grid rather than hidden away again, which is what
  // an operator who just asked to publish it expects to find.
  const publishDraft = (entry) => {
    if (!entry) return;
    const done = () => onPasses(entry, { publish: true });
    if (window.SpoonityDrafts && typeof window.SpoonityDrafts.setArchived === "function") {
      window.SpoonityDrafts.setArchived(entry.id, false).then(done, done);
    } else {
      done();
    }
  };

  const signOut = () => { try { HUB_AUTH.logout(); } catch (e) {} location.reload(); };
  const switchVendor = (id) => { if (!id || id === vendor.id) return; Promise.resolve(HUB_AUTH.switchVendor(id)).then(() => location.reload()).catch(() => location.reload()); };

  const hasBrand = !!brand;
  // Passes step is "done" once at least one pass has been saved (draft or published) — but only
  // once createdPasses has actually loaded (null = still loading); don't flash "not done" first.
  const hasPasses = Array.isArray(createdPasses) && activePasses.length > 0;

  const ICONS = {
    // Distinct silhouettes per card — a palette (Brand), a card (Passes), a globe (Enrollment
    // Page), a plug (Integrations), a chart (Analytics). Previously Brand used a 5-point star,
    // which at a glance in the peach icon well read as just another generic sparkle like the
    // others — a real palette reads as "brand/color" even before the label loads.
    brand: <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2C6.5 2 2 6.5 2 12c0 2.8 1.8 4 3.5 4H7a2 2 0 0 1 2 2v.5c0 1.9 1.6 3.5 3.5 3.5C18 22 22 17.5 22 12 22 6.5 17.5 2 12 2z" /><circle cx="7.5" cy="10.5" r="1.15" fill="currentColor" stroke="none" /><circle cx="12" cy="7.3" r="1.15" fill="currentColor" stroke="none" /><circle cx="16.5" cy="10.5" r="1.15" fill="currentColor" stroke="none" /></svg>,
    passes: <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="5" width="20" height="14" rx="3" /><path d="M2 10h20M6 15h5" /></svg>,
    integrations: <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"><path d="M9 8V3M15 8V3M7 8h10v3a5 5 0 0 1-5 5 5 5 0 0 1-5-5V8z" /><path d="M12 16v6" /></svg>,
    analytics: <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3v18h18" /><path d="M7 14l4-4 3 3 5-6" /></svg>,
  };

  return (
    <div style={{ minHeight: "100vh", background: "var(--surface-secondary, #f8f6f4)", fontFamily: "var(--font-sans)", display: "flex", flexDirection: "column" }}>
      {/* 100vh, not 100% — #root (the mount point in index.html) has no explicit height of its
          own, so a percentage here has nothing to resolve against and silently falls back to
          auto, i.e. exactly as tall as the content. On any page shorter than the viewport that
          left a hard edge where the gray just stopped, with plain white below it down to the
          bottom of the screen. 100vh is viewport-relative regardless of ancestor heights, and
          still only a MINIMUM — taller content (e.g. a long "Created passes" grid) still grows
          the page past one viewport height exactly as before. */}
      {/* Header */}
      <header style={{ height: 64, flexShrink: 0, display: "flex", alignItems: "center", gap: 16, padding: "0 28px", background: "var(--surface-base)", borderBottom: "1px solid var(--border-primary)" }}>
        <img src="/ui_kits/pass-studio/assets/signin-logo.png" alt="Spoonity" style={{ height: 28 }} />
        <div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 12 }}>
          {/* Account chip — a real pill with an initials avatar and a chevron (matching a proper
              account switcher), max-width + ellipsis + title tooltip so a long vendor name
              truncates cleanly instead of just getting cut off against the sign-out button. */}
          {vendor.name && (
            <div style={{ display: "flex", alignItems: "center", gap: 8, background: "var(--surface-secondary, #f4f2ef)", border: "1px solid var(--border-primary)", borderRadius: 99, padding: "6px 10px", position: "relative" }}>
              <span style={{ width: 22, height: 22, borderRadius: "50%", background: accent + "1a", color: accent, fontSize: 11, fontWeight: 700, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
                {initials(vendor.name)}
              </span>
              <span title={vendor.name} style={{ fontSize: 13, color: "var(--typography-primary)", maxWidth: 200, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                {vendor.name}
              </span>
              {vendors.length > 1 && (
                <React.Fragment>
                  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" style={{ color: "var(--typography-tertiary)", flexShrink: 0 }}>
                    <path d="M6 9l6 6 6-6" />
                  </svg>
                  {/* A transparent native <select>, sized to cover the whole chip, keeps real
                      dropdown/keyboard behavior for switching vendors without having to
                      hand-roll a listbox just to get the avatar+chevron visual. */}
                  <select value={vendor.id || ""} onChange={(e) => switchVendor(e.target.value)}
                    title="Switch vendor" aria-label="Switch vendor"
                    style={{ position: "absolute", inset: 0, opacity: 0, cursor: "pointer", fontSize: 13 }}>
                    {vendors.map((v) => <option key={v.id} value={v.id}>{v.name || v.id}</option>)}
                  </select>
                </React.Fragment>
              )}
            </div>
          )}
          <button type="button" onClick={signOut}
            style={{ padding: "7px 12px", borderRadius: 9, border: "1px solid var(--border-primary)", background: "var(--surface-base)", cursor: "pointer", fontFamily: "var(--font-sans)", fontSize: 12.5, fontWeight: 600, color: "var(--typography-secondary)" }}>
            Sign out
          </button>
        </div>
      </header>

      {/* Content */}
      <main style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", padding: "56px 24px" }}>
        <div style={{ width: "100%", maxWidth: 1080 }}>
          <h1 style={{ fontSize: 26, fontWeight: 800, color: "var(--typography-primary)", margin: 0, letterSpacing: "-.01em" }}>
            What would you like to manage?
          </h1>
          {/* Vendor name already lives in the header chip now, so it doesn't need repeating here too. */}
          <p style={{ fontSize: 14, color: "var(--typography-primary)", opacity: 0.72, marginTop: 6, marginBottom: 32, lineHeight: 1.6 }}>
            Your brand powers both your wallet passes and your enrollment website.
          </p>

          {/* Setup sequence — Brand, then Passes, roughly in the order a new operator needs them.
              A dedicated section (rather than equal cards mixed with the ongoing tools below)
              gives first-timers an actual path, and an exact 2-item grid never leaves a dangling
              empty cell the way more items in one fluid grid could. Exactly one card gets
              "Start here": the first step in order that isn't done yet -- once both are done,
              no card is highlighted, which is honest rather than guessing at a next step.
              (A third "Enrollment page" card used to link out to the separate wallets.spoonity.com
              app here -- removed along with siteUrl/WALLET_DESIGNER_URL; that app itself is
              untouched, just no longer linked from this hub.) */}
          <p style={{ fontSize: 13, color: "var(--typography-secondary)", letterSpacing: ".02em", margin: "0 0 10px" }}>
            Set up your program
          </p>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: 14, marginBottom: 28 }}>
            <SetupCard accent={accent} icon={ICONS.brand} title="1 · Brand" cta="Manage"
              badge={!hasBrand ? "next" : "done"}
              desc="Logo, colors, identity. Set once, shared everywhere."
              onClick={() => window.SpoonityBrand && window.SpoonityBrand.openEditor()} />

            <SetupCard accent={accent} icon={ICONS.passes} title="2 · Wallet passes" cta="Design"
              badge={hasBrand && !hasPasses ? "next" : hasPasses ? "done" : null}
              desc="Design your Apple and Google Wallet cards."
              onClick={() => onPasses(null)} />
          </div>

          {/* Ongoing tools — not a sequence, so no "Start here"/step badges; these get used
              indefinitely after setup, not once and done. */}
          <p style={{ fontSize: 13, color: "var(--typography-secondary)", letterSpacing: ".02em", margin: "0 0 10px" }}>
            Tools
          </p>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: 14, marginBottom: 40 }}>
            <ToolCard accent={accent} icon={ICONS.integrations} title="Integrations"
              desc="API keys to auto-enroll customers."
              onClick={onIntegrations} />

            <ToolCard accent={accent} icon={ICONS.analytics} title="Analytics"
              desc="See how your passes are performing."
              onClick={() => onAnalytics()} />
          </div>

          {/* Created passes */}
          <div style={{ marginTop: 48 }}>
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 4 }}>
              <h2 style={{ fontSize: 15, fontWeight: 600, color: "var(--typography-primary)", margin: 0 }}>Created passes</h2>
              {createdPasses && createdPasses.length > 0 && (
                <button type="button" onClick={() => onPasses(null)}
                  style={{ display: "inline-flex", alignItems: "center", gap: 4, border: "1px solid var(--border-primary)", background: "var(--surface-base)", borderRadius: 9, cursor: "pointer", fontFamily: "var(--font-sans)", fontSize: 13, fontWeight: 600, color: "var(--typography-primary)", padding: "6px 12px" }}>
                  <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 5v14M5 12h14" /></svg>
                  New pass
                </button>
              )}
            </div>
            <p style={{ fontSize: 12.5, color: "var(--typography-primary)", opacity: 0.72, marginTop: 0, marginBottom: 18 }}>Every saved pass, published or still a draft — click one to reopen it in the studio.</p>

            {createdPasses === null ? (
              <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "28px 0", color: "var(--typography-tertiary)", fontSize: 13 }}>
                <span className="spty-spin-gravity" style={{ width: 20, height: 20 }} />
                Loading passes…
              </div>
            ) : activePasses.length === 0 && archivedPasses.length === 0 ? (
              <div style={{ padding: "32px 24px", borderRadius: 14, border: "1.5px dashed var(--border-primary)", textAlign: "center" }}>
                <div style={{ fontSize: 13, color: "var(--typography-tertiary)", lineHeight: 1.6 }}>
                  No saved passes yet.<br />
                  <button type="button" onClick={() => onPasses(null)}
                    style={{ border: "none", background: "transparent", cursor: "pointer", fontFamily: "var(--font-sans)", fontSize: 13, fontWeight: 600, color: accent, padding: "2px 0", marginTop: 6, display: "inline-block" }}>
                    Design your first pass →
                  </button>
                </div>
              </div>
            ) : (
              <React.Fragment>
                <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(160px, 1fr))", gap: 14 }}>
                  {activePasses.map((draft) => (
                    <PassMiniCard key={draft.id} draft={draft} accent={accent} onClick={() => onPasses(draft)} onAnalytics={onAnalytics}
                      onArchive={(entry) => setArchived(entry, true)} />
                  ))}
                </div>
                {archivedPasses.length > 0 && (
                  <div style={{ marginTop: 20 }}>
                    <button type="button" onClick={() => setShowArchived((v) => !v)}
                      style={{ border: "none", background: "transparent", cursor: "pointer", fontFamily: "var(--font-sans)", fontSize: 12.5, fontWeight: 600, color: "var(--typography-tertiary)", padding: "4px 0" }}>
                      {showArchived ? "Hide" : "Show"} archived ({archivedPasses.length})
                    </button>
                    {showArchived && (
                      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(160px, 1fr))", gap: 14, marginTop: 10, opacity: 0.6 }}>
                        {archivedPasses.map((draft) => (
                          <PassMiniCard key={draft.id} draft={draft} accent={accent} onClick={() => onPasses(draft)} onAnalytics={onAnalytics}
                            onRestore={(entry) => setArchived(entry, false)} onPublish={publishDraft} />
                        ))}
                      </div>
                    )}
                  </div>
                )}
              </React.Fragment>
            )}
          </div>
        </div>
      </main>
    </div>
  );
}

/* Top-level view switch: hub <-> pass studio <-> integrations <-> analytics. Brand is a modal
   overlay (from the gate); the wallet website designer is a separate app opened in a new tab. */
function SpoonityApp() {
  const [view, setView] = React.useState("hub");
  const [initialDraft, setInitialDraft] = React.useState(null);
  // Mirrors initialDraft/setInitialDraft above -- lets a specific pass's "view analytics" icon
  // (PassMiniCard) open straight into that pass's own dashboard instead of always landing on
  // Analytics' generic "pick a pass" picker.
  const [initialAnalyticsProgram, setInitialAnalyticsProgram] = React.useState(null);
  // opts.publish comes from the Hub's "Publish" control on an archived draft — it opens the studio
  // with the publish modal already up, so one click from the archived section lands on the
  // standard publish confirmation instead of the editor.
  const [autoPublish, setAutoPublish] = React.useState(false);
  const openPasses = (draft, opts) => { setInitialDraft(draft || null); setAutoPublish(!!(opts && opts.publish)); setView("passes"); };
  const openAnalytics = (programId, name) => { setInitialAnalyticsProgram(programId ? { programId, name } : null); setView("analytics"); };
  if (view === "passes") return <PassStudio onHome={() => { setInitialDraft(null); setAutoPublish(false); setView("hub"); }} initialDraft={initialDraft} autoPublish={autoPublish} />;
  if (view === "integrations") return <IntegrationsView onHome={() => setView("hub")} />;
  if (view === "analytics") return <AnalyticsView onHome={() => { setInitialAnalyticsProgram(null); setView("hub"); }} initialProgramId={initialAnalyticsProgram && initialAnalyticsProgram.programId} initialProgramName={initialAnalyticsProgram && initialAnalyticsProgram.name} />;
  return <SpoonityHub onPasses={openPasses} onIntegrations={() => setView("integrations")} onAnalytics={openAnalytics} />;
}

window.SpoonityHub = SpoonityHub;
window.SpoonityApp = SpoonityApp;
