// Vanday DAM — app shell + route switcher
function App() {
  // Check session on mount. If not logged in, force the login view; if we
  // are, skip past it to the library.
  const [authChecked, setAuthChecked] = React.useState(false);
  const [view, setView] = React.useState("login"); // login | upload | library | preview | settings | portal
  // Set when /api/me reports the beta is full and this brand-new, uninvited
  // signup was turned away. We show a dedicated "beta is full" screen instead
  // of the app, and sign them out so they don't sit in a half-provisioned state.
  const [betaFull, setBetaFull] = React.useState(false);
  // Set when /api/me reports the signed-in person used an email that doesn't
  // match the invite they clicked. We show a dedicated screen letting them
  // either retry with the invited email or create a brand-new workspace.
  // Shape: { workspaceName, invitedEmail (masked), yourEmail }
  const [inviteMismatch, setInviteMismatch] = React.useState(null);

  // Welcome modal for first-time beta sign-up. Triggers when /api/me reports
  // a user with no onboardedAt timestamp on the free_beta plan.
  const [showWelcome, setShowWelcome] = React.useState(false);
  const dismissWelcome = React.useCallback(async () => {
    setShowWelcome(false);
    try { if (window.VandayMe) await window.VandayMe.markOnboarded(); } catch {}
  }, []);

  React.useEffect(() => {
    let stop = false;

    // Wait for Clerk to finish loading before firing /api/me, so the very
    // first request carries the Clerk session token. Without this, an old
    // legacy session cookie can sneak the user through as "legacy" and the
    // server never sees them as a real Clerk user (no demos, no user record).
    const waitForClerk = () => new Promise((resolve) => {
      if (window.__clerkReady) return resolve();
      const onReady = () => { window.removeEventListener("clerk-ready", onReady); resolve(); };
      window.addEventListener("clerk-ready", onReady);
      // Safety net: give up waiting after 4s and proceed without Clerk.
      setTimeout(() => { window.removeEventListener("clerk-ready", onReady); resolve(); }, 4000);
    });

    // Wait until window.Clerk.session is established AND getToken() returns
    // a non-null token. Without this, our very first /api/me call can race
    // ahead of Clerk's session setup, arrive at the server without an Auth
    // header, fail to provision the user, and break demo seeding + the
    // welcome modal for brand-new sign-ups.
    const waitForClerkSession = async () => {
      const deadline = Date.now() + 5000;
      while (Date.now() < deadline) {
        try {
          if (window.Clerk?.session) {
            const t = await window.Clerk.session.getToken();
            if (t) return t;
          }
        } catch {}
        // If there's no Clerk user at all, we're an anonymous visitor — bail.
        if (window.Clerk && !window.Clerk.user && !window.Clerk.session) return null;
        await new Promise((r) => setTimeout(r, 100));
      }
      return null;
    };

    const fetchMe = async () => {
      const token = await waitForClerkSession();
      const headers = token ? { Authorization: `Bearer ${token}` } : {};
      // Pass the signed-in person's name so the server can store it for the
      // team/members list. Clerk's JWT doesn't always carry name claims, so
      // sourcing it from the client SDK here makes names reliable. Encoded to
      // stay header-safe for non-ASCII names.
      try {
        const cu = window.Clerk?.user;
        const clientName = cu
          ? ([cu.firstName, cu.lastName].filter(Boolean).join(" ") || cu.fullName || "")
          : "";
        if (clientName) headers["X-Vanday-Name"] = encodeURIComponent(clientName);
      } catch {}
      // Forward the invite id (if they arrived via an invite link) so the
      // server can check the signed-in email matches the invite. This /api/me
      // call doesn't go through jfetch, so attach the header explicitly here.
      try {
        const inv = window.sessionStorage.getItem("vanday_invite_id");
        if (inv) headers["X-Vanday-Invite"] = inv;
      } catch {}
      return fetch("/api/me", { headers });
    };

    const loadMe = async () => {
      try {
        await waitForClerk();

        // Populate window.USERS with a single entry for the signed-in user
        // so member chips, the bottom-left avatar, the comment author label,
        // etc. all show "you" rather than blank or mock identities.
        try {
          const cu = window.Clerk?.user;
          if (cu) {
            const name = [cu.firstName, cu.lastName].filter(Boolean).join(" ") || cu.primaryEmailAddress?.emailAddress || "You";
            const initials = (name.split(/\s+/).map((s) => s[0]).filter(Boolean).slice(0, 2).join("") || "Y").toUpperCase();
            window.USERS.length = 0;
            window.USERS.push({
              id: cu.id,
              name,
              email: cu.primaryEmailAddress?.emailAddress || null,
              role: "Owner",
              status: "Active",
              lastActive: "Just now",
              joined: new Date(cu.createdAt || Date.now()).toLocaleDateString(),
              color: "oklch(0.55 0.14 32)",
              initials,
            });
            try { localStorage.setItem("vanday_active_user_id", cu.id); } catch {}
          }
        } catch {}

        let r = await fetchMe();
        if (stop) return;
        setAuthChecked(true);
        if (!r.ok) return;
        let d = await r.json();

        // Resilience: if Clerk reports a signed-in user but /api/me didn't
        // see one (race during sign-up before the session token attaches),
        // retry up to three times so the server-side provisioning + demo
        // seeding actually runs for brand-new accounts.
        let retries = 0;
        while (window.Clerk?.user && !d.user && retries < 3) {
          await new Promise((res) => setTimeout(res, 400));
          r = await fetchMe();
          if (stop) return;
          if (!r.ok) break;
          d = await r.json();
          retries += 1;
        }

        // Beta is full: this is a brand-new, uninvited signup we can't admit.
        // Show the "beta is full" screen and sign them out (their account was
        // never created server-side, but Clerk still has a session).
        if (d.betaFull) {
          setBetaFull(true);
          try { if (window.Clerk && window.Clerk.user) await window.Clerk.signOut(); } catch {}
          return;
        }

        // Invite email mismatch: the person signed in/up with an email that
        // doesn't match the workspace invite they clicked. The server didn't
        // provision them (no user/org footprint). Show the choice screen.
        if (d.inviteMismatch) {
          setInviteMismatch(d.inviteMismatch);
          return;
        }

        // Successfully provisioned (no mismatch, no beta-full) — drop the
        // stashed invite id so we stop sending the X-Vanday-Invite header.
        if (d.user) { try { window.sessionStorage.removeItem("vanday_invite_id"); } catch {} }

        if (d.loggedIn) setView((v) => (v === "login" ? "library" : v));
        // First-time free-beta user — show the welcome modal. But ONLY for the
        // person who owns/created the workspace (their own personal workspace).
        // Someone who joined via invite lands in an existing team workspace they
        // don't administer: the workspace is already named, demos already
        // seeded, and social accounts are the admin's to set up — so the
        // onboarding modal (name your workspace, connect socials) doesn't apply.
        const isInvitedMember = d.org && d.org.isPersonal === false;
        if (d.user && !d.user.onboardedAt && (d.plan?.name || "free_beta") === "free_beta" && !isInvitedMember) {
          setShowWelcome(true);
        }
        // Make the response available to other components via the global cache.
        if (window.VandayMe) {
          try { await window.VandayMe.fetchMe(); } catch {}
        }
      } catch { if (!stop) setAuthChecked(true); }
    };
    loadMe();
    return () => { stop = true; };
  }, []);

  // Server bootstrap: load real assets from the local Node service, then
  // re-render whenever the server-side list changes (uploads, AI completion).
  // If the server isn't reachable, the mocked sample data still shows.
  VandayServer.useServerRev();
  VandayServer.useServerAssets({ pollMs: 2500 });

  const handleLogout = React.useCallback(async () => {
    try { await fetch("/api/logout", { method: "POST" }); } catch {}
    // Also sign out of Clerk so the next visit doesn't auto-log in.
    try { if (window.Clerk && window.Clerk.user) await window.Clerk.signOut(); } catch {}
    setView("login");
  }, []);
  // Expose globally so each page's Rail "Logout" button hits the real API
  // without prop-drilling through every screen.
  React.useEffect(() => { window.__vandayLogout = handleLogout; }, [handleLogout]);
  // Tiny global nav helper — lets the welcome modal (and other floating UI)
  // send the user to a different view without prop-drilling.
  React.useEffect(() => { window.__vandayGoto = (v) => setView(v); }, []);
  const [activeFolder, setActiveFolder] = React.useState("all");
  const [previewAsset, setPreviewAsset] = React.useState(null);
  const [publishAsset, setPublishAsset] = React.useState(null);
  const [permissionsFolder, setPermissionsFolder] = React.useState(null);
  const [shareFolder, setShareFolder] = React.useState(null);
  const [activePortal, setActivePortal] = React.useState(null);
  const [similarSource, setSimilarSource] = React.useState(null);
  // Preview navigation stack: when user jumps from one asset's Related tab
  // to another, we push the previous asset here so they can Back out of it.
  const [previewStack, setPreviewStack] = React.useState([]);

  // Quick-upload flow: destination picker + persistent progress dock.
  // Hooked into Rail / topbar buttons everywhere. If user wants the full
  // batch experience they can still open the dedicated UploadPage.
  const [showUploadPicker, setShowUploadPicker] = React.useState(false);
  const [uploadQueue, setUploadQueue] = React.useState([]);
  const queueIdRef = React.useRef(1);
  const openQuickUpload = () => setShowUploadPicker(true);

  // Real upload: hand the user's selected files to the server API and let
  // the upload dock track real progress + the real server response.
  const queueFiles = async ({ folderId, files }) => {
    setShowUploadPicker(false);
    if (!files || files.length === 0) return;

    // Stage each file as an "uploading" row in the dock right away, so the
    // user sees activity while the network call is in flight.
    const rows = files.map((f) => ({
      id: `u-${queueIdRef.current++}`,
      name: f.name,
      size: f.size,
      thumb: f.thumb,
      folder: folderId,
      progress: 30,           // arbitrary — we don't get true progress events from fetch
      status: "uploading",
      asset: null,
      _file: f._file,         // underlying browser File
    }));
    setUploadQueue((q) => [...q, ...rows]);

    // Fire one POST /api/upload with every file in this batch.
    const realFiles = files.map((f) => f._file).filter(Boolean);
    try {
      const resp = await window.VandayAPI.uploadFiles(realFiles, { folder: folderId });
      const created = Array.isArray(resp?.assets) ? resp.assets : [];

      // Reconcile staged rows with what the server actually created.
      //
      // Three cases:
      //   1. created.length === rows.length — normal images, 1-to-1 by index
      //      (handles HEIC where the asset's name changes to .jpg)
      //   2. created.length > rows.length — at least one file was a zip the
      //      server expanded into many assets, or HEIC at the zip root added
      //      to the count. Replace our staged rows with one per server asset.
      //   3. created.length < rows.length — some files were rejected silently
      //      (rare). Show the unmatched ones as failed.
      setUploadQueue((qs) => {
        const others = qs.filter((q) => !rows.some((r) => r.id === q.id));
        if (created.length === rows.length) {
          return [
            ...others,
            ...rows.map((q, i) => {
              const a = created[i];
              return { ...q, status: "analyzing", progress: 100, asset: a, name: a.name, thumb: a.url, _file: undefined };
            }),
          ];
        }
        if (created.length > 0) {
          // Replace staged rows with one row per actually-created asset.
          return [
            ...others,
            ...created.map((a) => ({
              id: `u-${queueIdRef.current++}`,
              name: a.name,
              size: (a.sizeBytes || 0) / (1024 * 1024),
              thumb: a.url,
              folder: a.folder,
              progress: 100,
              status: "analyzing",
              asset: a,
            })),
          ];
        }
        // Nothing created — show all staged rows as failed.
        return [
          ...others,
          ...rows.map((q) => ({ ...q, status: "failed", error: "Server did not accept these files", _file: undefined })),
        ];
      });

      // Poll each uploaded asset until AI tagging finishes (or we time out).
      for (const a of created) {
        const start = Date.now();
        const tick = async () => {
          if (Date.now() - start > 120_000) return;
          try {
            const fresh = await window.VandayAPI.getAsset(a.id);
            if (fresh.aiState === "done" || fresh.aiState === "failed") {
              setUploadQueue((qs) => qs.map((q) =>
                q.asset?.id === a.id
                  ? { ...q, status: "ready", asset: fresh }
                  : q
              ));
              return;
            }
          } catch {/* keep polling */}
          setTimeout(tick, 700);
        };
        setTimeout(tick, 700);
      }
    } catch (err) {
      // Parse server error message if it's a structured 4xx.
      let msg = String(err?.message || err || "Upload failed");
      try {
        const m = msg.match(/: (\{.+\})$/);
        if (m) {
          const body = JSON.parse(m[1]);
          if (body.message) msg = body.message;
          else if (body.error) msg = body.error.replace(/_/g, " ");
        }
      } catch {}
      setUploadQueue((qs) => qs.map((q) => (
        rows.some((r) => r.id === q.id)
          ? { ...q, status: "failed", error: msg, _file: undefined }
          : q
      )));
    }
  };

  const retryUpload = (id) => setUploadQueue((qs) => qs.map((q) => (
    q.id === id ? { ...q, status: "uploading", progress: 0, willFail: false, error: null } : q
  )));
  const clearCompleted = () => setUploadQueue((qs) => qs.filter((q) => (
    q.status === "uploading" || q.status === "analyzing"
  )));

  const openSimilar = (a) => {
    setSimilarSource(a);
    setView("library");
  };
  const clearSimilar = () => setSimilarSource(null);

  const isPublished = activeFolder === "__published";
  // Preview navigation context — include variants so prev/next works
  // whether the user opened an original or a crop.
  const baseList = ALL_ASSETS;
  const visibleAssets =
    activeFolder === "all" || isPublished
      ? baseList
      : baseList.filter((a) => a.folder === activeFolder);
  const previewIndex = previewAsset
    ? visibleAssets.findIndex((a) => a.id === previewAsset.id)
    : -1;

  const openAsset = (a) => {
    // If we're already viewing a different asset and the user is opening
    // another from inside preview (e.g. via Related / Find similar / crops),
    // push the current asset so they can navigate back.
    if (view === "preview" && previewAsset && previewAsset.id !== a.id) {
      setPreviewStack((s) => [...s, previewAsset]);
    }
    setPreviewAsset(a);
    setView("preview");
  };
  const goBackInPreview = () => {
    setPreviewStack((s) => {
      if (s.length === 0) return s;
      const next = s.slice(0, -1);
      setPreviewAsset(s[s.length - 1]);
      return next;
    });
  };
  const closePreview = () => {
    setPreviewStack([]);
    setView("library");
  };
  const goPrev = () => {
    if (previewIndex > 0) setPreviewAsset(visibleAssets[previewIndex - 1]);
  };
  const goNext = () => {
    if (previewIndex < visibleAssets.length - 1) setPreviewAsset(visibleAssets[previewIndex + 1]);
  };
  const openPublish = (a) => setPublishAsset(a);
  const closePublish = () => setPublishAsset(null);

  // Reopen the Publish modal after the social-connect flow. The modal
  // navigates the whole tab to Upload-Post and back to /?back_from=connect;
  // here we restore it for the asset the user was publishing. We retry until
  // the server asset list has loaded (assets stream in asynchronously).
  React.useEffect(() => {
    const p = new URLSearchParams(window.location.search);
    if (p.get("back_from") !== "connect") return;
    let pendingId = null;
    try { pendingId = sessionStorage.getItem("vanday_publish_return"); } catch {}
    if (!pendingId) return; // came from Settings, not the publish modal

    const cleanUrl = () => {
      const q = new URLSearchParams(window.location.search);
      q.delete("back_from");
      const qs = q.toString();
      window.history.replaceState({}, "", window.location.pathname + (qs ? "?" + qs : "") + window.location.hash);
    };
    const tryOpen = () => {
      const a = getAsset(pendingId);
      if (!a) return false;
      setPublishAsset(a);
      try { sessionStorage.removeItem("vanday_publish_return"); } catch {}
      cleanUrl();
      return true;
    };
    if (tryOpen()) return;
    let tries = 0;
    const iv = setInterval(() => {
      tries += 1;
      if (tryOpen() || tries > 40) { // ~10s ceiling
        clearInterval(iv);
        if (tries > 40) { try { sessionStorage.removeItem("vanday_publish_return"); } catch {} cleanUrl(); }
      }
    }, 250);
    return () => clearInterval(iv);
  }, []);

  // Keyboard navigation on preview
  React.useEffect(() => {
    if (view !== "preview" || publishAsset) return;
    const onKey = (e) => {
      if (e.key === "Escape") closePreview();
      if (e.key === "ArrowLeft") goPrev();
      if (e.key === "ArrowRight") goNext();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [view, previewIndex, publishAsset]);

  // Beta is full — brand-new, uninvited signups can't get in. Show a friendly
  // dead-end instead of the app. (All hooks above run unconditionally; this
  // early return is safe.)
  if (betaFull) {
    return (
      <div style={{
        position: "fixed", inset: 0,
        display: "grid", placeItems: "center", padding: 24,
        background: "var(--bg, #0b0f1a)", color: "var(--text, #f3f5f9)",
        fontFamily: "inherit",
      }}>
        <div style={{
          width: "100%", maxWidth: 460, textAlign: "center",
          background: "var(--surface, #fff)", color: "var(--text, #111)",
          borderRadius: 16, boxShadow: "0 20px 60px rgba(0,0,0,0.25)",
          padding: "40px 34px",
        }}>
          <div style={{
            width: 52, height: 52, borderRadius: 13, margin: "0 auto 18px",
            background: "var(--brand, #5b6cff)", color: "white",
            display: "grid", placeItems: "center", fontWeight: 700, fontSize: 22,
          }}>v.</div>
          <h1 style={{ fontSize: 20, fontWeight: 700, margin: "0 0 10px" }}>
            The Vanday beta is full
          </h1>
          <p style={{ fontSize: 14, lineHeight: 1.6, color: "var(--text-muted)", margin: 0 }}>
            Thanks so much for your interest! We've reached the limit for this
            round of the free beta. We'll reach out by email as soon as we
            re-open sign-ups — no need to do anything in the meantime.
          </p>
        </div>
      </div>
    );
  }

  // Invite email mismatch — the person authenticated with an email that
  // doesn't match the workspace invite. Offer a clear choice: retry with the
  // invited email, or abandon the invite and start their own workspace.
  if (inviteMismatch) {
    const useInvitedEmail = async () => {
      // Keep the invite id in sessionStorage and bounce them back through
      // sign-up. Signing out of Clerk + reloading re-opens the sign-up popup
      // (the URL still carries ?join=invite), where they can use the right email.
      try { if (window.Clerk && window.Clerk.user) await window.Clerk.signOut(); } catch {}
      location.reload();
    };
    const createNewInstead = () => {
      // Drop the invite link so jfetch stops sending X-Vanday-Invite; the
      // server then provisions a normal personal workspace for this email.
      try { window.sessionStorage.removeItem("vanday_invite_id"); } catch {}
      location.reload();
    };
    return (
      <div style={{
        position: "fixed", inset: 0,
        display: "grid", placeItems: "center", padding: 24,
        background: "var(--bg, #0b0f1a)", color: "var(--text, #f3f5f9)",
        fontFamily: "inherit",
      }}>
        <div style={{
          width: "100%", maxWidth: 480, textAlign: "center",
          background: "var(--surface, #fff)", color: "var(--text, #111)",
          borderRadius: 16, boxShadow: "0 20px 60px rgba(0,0,0,0.25)",
          padding: "40px 34px",
        }}>
          <div style={{
            width: 52, height: 52, borderRadius: 13, margin: "0 auto 18px",
            background: "var(--brand, #5b6cff)", color: "white",
            display: "grid", placeItems: "center", fontWeight: 700, fontSize: 22,
          }}>v.</div>
          <h1 style={{ fontSize: 20, fontWeight: 700, margin: "0 0 10px" }}>
            Your email doesn't match the invite
          </h1>
          <p style={{ fontSize: 14, lineHeight: 1.6, color: "var(--text-muted)", margin: "0 0 8px" }}>
            To join <strong>{inviteMismatch.workspaceName}</strong>, sign in with the
            email the invite was sent to{inviteMismatch.invitedEmail ? <> (<strong>{inviteMismatch.invitedEmail}</strong>)</> : null}.
          </p>
          {inviteMismatch.yourEmail && (
            <p style={{ fontSize: 13, lineHeight: 1.6, color: "var(--text-faint)", margin: "0 0 22px" }}>
              You signed in as <strong>{inviteMismatch.yourEmail}</strong>.
            </p>
          )}
          <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
            <button type="button" className="btn primary btn-block" onClick={useInvitedEmail}>
              Use the invited email
            </button>
            <button type="button" className="btn btn-block" onClick={createNewInstead}>
              Create a new account instead
            </button>
          </div>
        </div>
      </div>
    );
  }

  let screen;
  if (view === "login") screen = <LoginPage onLogin={() => setView("library")} />;
  else if (view === "upload") screen = <UploadPage onNav={setView} onOpenAsset={openAsset} onQuickUpload={openQuickUpload} />;
  else if (view === "admin") screen = <AdminDashboard onClose={() => setView("library")} />;
  else if (view === "settings") screen = <SettingsPage onNav={setView} onQuickUpload={openQuickUpload} />;
  else if (view === "portal") screen = <SharePortalPage portal={activePortal} onClose={() => { setActivePortal(null); setView("library"); }} />;
  else if (view === "preview")
    screen = (
      <PreviewPage
        asset={previewAsset}
        onClose={closePreview}
        onNav={setView}
        onPrev={goPrev}
        onNext={goNext}
        index={previewIndex >= 0 ? previewIndex : 0}
        total={visibleAssets.length}
        onPublish={openPublish}
        onOpenAsset={openAsset}
        onFindSimilar={openSimilar}
        onShare={(ctx) => setShareFolder(ctx)}
        backStack={previewStack}
        onBack={goBackInPreview}
        onQuickUpload={openQuickUpload}
      />
    );
  else
    screen = (
      <LibraryPage
        onOpenAsset={openAsset}
        onNav={setView}
        activeFolder={activeFolder}
        setActiveFolder={setActiveFolder}
        onPublish={openPublish}
        onFindSimilar={openSimilar}
        similarSource={similarSource}
        onClearSimilar={clearSimilar}
        onPermissions={setPermissionsFolder}
        onShare={setShareFolder}
        onQuickUpload={openQuickUpload}
      />
    );

  return (
    <>
      {screen}
      <WelcomeModal open={showWelcome} onClose={dismissWelcome} />
      {publishAsset && (
        <PublishModal asset={publishAsset} onClose={closePublish} />
      )}
      {permissionsFolder && (
        <PermissionsModal
          folder={permissionsFolder}
          onClose={() => setPermissionsFolder(null)}
        />
      )}
      {shareFolder && (
        <ShareModal
          folder={shareFolder.asset ? null : shareFolder}
          asset={shareFolder.asset || null}
          onClose={() => setShareFolder(null)}
          onOpenPortal={(p) => {
            setActivePortal({ ...p, folder: shareFolder.asset ? null : shareFolder.id, assetIds: p.assetIds });
            setShareFolder(null);
            setView("portal");
          }}
        />
      )}

      {showUploadPicker && (
        <UploadDestinationModal
          contextFolder={activeFolder}
          onCancel={() => setShowUploadPicker(false)}
          onConfirm={queueFiles}
        />
      )}

      {view !== "login" && view !== "portal" && (
        <UploadDock
          queue={uploadQueue}
          onDismiss={clearCompleted}
          onRetry={retryUpload}
          onOpenAsset={openAsset}
        />
      )}
    </>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
