// The root. Sign-in, or a workspace.

const { useState, useEffect, useCallback } = React;

// THE TAB IS THE FIRST SEGMENT OF THE ADDRESS, AND ONLY THAT.
// `#/my-deals/AG-26-001` is the my-deals tab with a deal open — common.jsx's
// useAddressedRecord owns the second half. Compared whole, the string matches
// no tab in any role's set and every deep link renders the refusal state.
//
// DEFINED HERE, OUTSIDE THE COMPONENT, because three separate places turn an
// address into a tab — the initial state, the hashchange listener, and go() —
// and the first pass at this fixed two of them. A cold load of
// `#/my-deals/AG-26-001` signed in, called go() with the whole string, and
// refused the requester their own deal list. One definition, three callers.
//
// What this does NOT change is whether a tab is allowed: Workspace still
// checks this first segment against the role's own tab set, so
// `#/library/anything` as an administrator refuses exactly as before, having
// fetched nothing.
const tabOf = (hash) => String(hash).replace(/^#\/?/, '').split('/')[0] || null;
const plainRack = { pinned: [], hidden: [], opens_on: null };

function App() {
  const [identity, setIdentity] = useState(null);
  const [restoring, setRestoring] = useState(true);
  const [signingOut, setSigningOut] = useState(false);
  const [signOutFailure, setSignOutFailure] = useState(null);
  // ── This person's own rack (0108) ────────────────────────────────────────
  // Read ONCE, here, and handed down — the navigation rack needs the pins and
  // what is put away, and sign-in needs to know where to land. Two reads of
  // one row is two copies of one fact, and the copies drift.
  //
  // NO ROW IS A REAL ANSWER: it means this person has chosen nothing, and the
  // plain rack is what they get. A default row written on their behalf would
  // be a record of a decision nobody took, which is this product's own
  // anti-pattern.
  const [rack, setRack] = useState(null);
  // The address bar is the router. Deep links matter here for a reason beyond
  // convenience: the acceptance test types another role's address directly, and
  // a shell with no addressable routes could not be tested for the thing that
  // matters most about it.
  const [tab, setTab] = useState(() => tabOf(window.location.hash));

  useEffect(() => {
    const onHash = () => setTab(tabOf(window.location.hash));
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);

  // `key` may carry a record — go('my-deals/AG-26-001') is how a cold load
  // keeps the address it arrived on. The hash takes it whole; the TAB takes
  // its first segment only.
  const go = useCallback((key) => {
    window.location.hash = `#/${key}`;
    setTab(tabOf(key));
  }, []);

  const loadRack = useCallback(async () => {
    const r = await API.myWorkspace();
    const mine = (r.ok && r.rows && r.rows[0]) ? r.rows[0] : null;
    // A REFUSED OR FAILED READ LEAVES THE PLAIN RACK, never a broken one. This
    // is a convenience; nothing about the workspace depends on it, and a
    // workspace that would not open because somebody's shortcuts could not be
    // fetched would be a much worse product than one with no shortcuts.
    setRack(mine || plainRack);
    return mine;
  }, []);

  const onSignedIn = useCallback(async (who) => {
    setIdentity(who);
    // Land on the first tab of this role's workspace — "what is waiting on you"
    // — unless the address already names one. Somebody following a link to a
    // pane they may not open still gets the refusal state rather than a
    // silent redirect, because being told is more use than being moved.
    const current = window.location.hash.replace(/^#\/?/, '');
    if (current) { go(current); return; }
    // WHERE THIS PERSON CHOSE TO LAND, if they chose — and only if their role
    // holds it. A preference naming an area somebody does not hold would land
    // them on a refusal, and `opens_on` is not a grant: the tab set is.
    const tabs = WORKSPACES[who.role].tabs;
    // A Viewer changes nothing, including personal workspace state. The shell
    // therefore draws no customiser and 0108 deliberately grants this role no
    // preference read. Give it the plain rack directly instead of asking the
    // database a question whose only honest answer is a 403.
    if (who.role === 'viewer') {
      setRack(plainRack);
      go(tabs[0].key);
      return;
    }
    const mine = await loadRack();
    const chosen = mine && mine.opens_on
      && tabs.some((t) => t.key === mine.opens_on) ? mine.opens_on : null;
    go(chosen || tabs[0].key);
  }, [go, loadRack]);

  // The production credential is an HTTP-only cookie, so a reload restores
  // the visible identity by asking the server. JavaScript never receives or
  // persists the credential itself.
  useEffect(() => {
    let active = true;
    const restore = async () => {
      const result = await API.restoreSession();
      if (!active) return;
      if (result.ok) await onSignedIn(result.identity);
      if (active) setRestoring(false);
    };
    restore();
    return () => { active = false; };
  }, [onSignedIn]);

  const onSignOut = useCallback(async () => {
    if (signingOut) return;
    setSigningOut(true);
    setSignOutFailure(null);
    // Once the server confirms the HTTP-only session is gone, forget every
    // half-written paper and personal view with the visible identity. A failed
    // sign-out does none of those things because this person is still signed in.
    const result = await API.signOut();
    if (!result.ok) {
      setSignOutFailure(result.reason || 'sign-out could not be completed');
      setSigningOut(false);
      return;
    }
    forgetDrafts(); forgetSavedViews();
    setIdentity(null);
    // AND FORGET THE RACK. It is the last person's, and the next one to sign
    // in during this same page load must not inherit their shortcuts — the
    // same reason the drafts go, one line up.
    setRack(null);
    window.location.hash = '';
    setSigningOut(false);
  }, [signingOut]);

  // A session that has expired, or a person who has been revoked, comes back as
  // a 401 from whatever they next touch. The honest response is the front door:
  // leaving a workspace on screen that can no longer load anything would be a
  // surface claiming access that is gone.
  //
  // THE REFUSAL ITSELF, first. Until 2026-08-22 this rule was kept only by the
  // poll below, so a 401 already on screen was discarded and the workspace
  // stayed up — masthead, grant and all — until the next tick, up to thirty
  // seconds later. api.jsx computed `expired` at five sites and nothing read
  // it. Now the transport raises the front door the moment any reply is a 401.
  useEffect(() => {
    API.onExpired(() => {
      API.forget(); forgetDrafts(); forgetSavedViews(); setIdentity(null); setRack(null);
      window.location.hash = '';
    });
    return () => API.onExpired(null);
  }, []);

  // AND THE POLL STAYS, for the case the line above cannot see: somebody who is
  // READING rather than clicking makes no requests, so nothing would carry a
  // 401 to them. Revocation while idle is what this is for — it is no longer
  // the only path, just the one that needs no act from the person.
  useEffect(() => {
    if (!identity) return;
    let active = true;
    let checking = false;
    const check = async () => {
      if (checking) return;
      checking = true;
      try {
        const r = await API.me();
        // THE THIRD SITE, and it was missed until the guard counted them. The
      // other two are the deliberate sign-out and the 401 raised by a click;
      // this is the one nobody clicks — revocation while idle. It forgot the
      // visible identity and left the drafts, so the next person to sign in during this
      // page load would have inherited them.
      if (active && !r.ok && r.expired) {
        API.forget(); forgetDrafts(); forgetSavedViews(); setIdentity(null); setRack(null);
      }
      } finally {
        checking = false;
      }
    };
    const t = setInterval(check, 30000);
    return () => { active = false; clearInterval(t); };
  }, [identity]);

  if (restoring) return (
    <div className="h-full flex items-center justify-center" role="status"
         style={{ background: 'var(--bg)', color: 'var(--mute)' }}>
      Checking your session…
    </div>
  );
  if (!identity) return <SignIn onSignedIn={onSignedIn} />;

  const active = tab || WORKSPACES[identity.role].tabs[0].key;

  // ONE LAYOUT, EVERY ROLE (Mike, 2026-08-10): the navigation rack sits under
  // the masthead on every screen. What varies by role is which pigeonholes
  // the rack offers, never where the rack is.
  return (
    <div className="h-full flex flex-col" style={{ background: 'var(--bg)' }}>
      <Masthead identity={identity} onSignOut={onSignOut}
                signingOut={signingOut} signOutFailure={signOutFailure} />
      <Nav me={identity} active={active} onSelect={go}
           rack={rack} onRackChange={loadRack} />
      <div className="flex-1 overflow-auto px-6 py-6" data-testid="workspace">
        <Workspace me={identity} tab={active} />
      </div>
      <Footer
        identity={identity}
        note="Every pane here is fed by an endpoint your role can actually read."
      />
    </div>
  );
}

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