/* Ledger root — auth gate + routing. Mounts React into #root. */
(function () {
  // Nobody, until the server says otherwise. Every failure path resolves to this
  // rather than to a partially-filled object, so a thrown RPC can never leave a
  // capability reading as true by omission.
  const NO_ACCESS = { authorized: false, isAdmin: false, isWorkspace: false,
                      isAnalytics: false, isAnalyticsRevenue: false,
                      isContentCenter: false, isContentOps: false,
                      isEngineering: false, isEditorial: false, isEditorialReviewer: false,
                      isEditorialSponsors: false };

  // Two gates, one screen. Either they signed in with a valid @lawnewz.com account
  // and aren't on the ledger_access allow-list at all, or (since 0097) they ARE on
  // it but hold no app yet — being on the list only means you may sign in. The
  // second is a real state, not an error: an admin adds someone before deciding
  // which app they get, and the DB defaults every capability to false.
  function AccessPending({ email, onSignOut, listed }) {
    const { Button } = window.LawCrimeDesignSystem_e18d68;
    React.useEffect(() => { setTimeout(() => window.lucide && window.lucide.createIcons(), 30); });
    return (
      <div className="login">
        <div className="login__card">
          <div className="login__top">
            <img src="vendor/design/assets/logos/lawandcrime-wordmark-white.png" alt="Law and Crime" />
            <span className="login__tape">LEDGER</span>
          </div>
          <div className="login__body">
            <h1>Access pending</h1>
            <p>
              You're signed in as <b>{email}</b>, but {listed
                ? "no Ledger app has been assigned to your account yet"
                : "your account hasn't been granted access to Ledger yet"}.
              Ask an administrator to {listed ? "assign one" : "add you"}, then sign in again.
            </p>
            <Button variant="primary" icon="log-out" onClick={onSignOut} style={{ width: "100%" }}>
              Sign out
            </Button>
          </div>
        </div>
      </div>
    );
  }

  function App() {
    // session: undefined = checking, null = signed out, object = signed in
    const [session, setSession] = React.useState(undefined);
    const [authError, setAuthError] = React.useState(null);
    // access: undefined = checking, object = the resolved capability set (NO_ACCESS shape)
    const [access, setAccess] = React.useState(undefined);
    // null = "wherever home is for this person", resolved below once we know which
    // apps they hold. A Performance link carries its cut in the hash
    // (`#perf:g=week&p=12`), so a pasted link has to land on that tab rather than
    // home. The prefix is namespaced precisely so Supabase's own `#access_token` on
    // the OAuth return can never be mistaken for one. The route guard below still
    // applies: someone without the capability follows the link and lands on their
    // own home instead.
    // github-oauth sends the browser back to `#eng-overview?linked=<login>` after a
    // GitHub link, so that hash has to land on the Engineering app rather than on
    // this person's home — otherwise linking your account drops you on the
    // Newsroom and the confirmation is never seen.
    const [route, setRoute] = React.useState(() => {
      const h = window.location.hash || "";
      if (h.startsWith("#perf")) return "performance";
      if (h.startsWith("#hov")) return "viewership";
      if (h.startsWith("#eng-")) return h.slice(1).split("?")[0];
      // Editorial deep links: #ed-search?q=…, #ed-review, #ed-day?d=…, and #ed, #ed/show/<key>, #ed/ep/<EP-…>.
      if (h.startsWith("#ed-search")) return "ed-search";
      if (h.startsWith("#ed-review")) return "ed-review";
      if (h.startsWith("#ed-day")) return "ed-day";
      if (h.startsWith("#ed-sponsors")) return "ed-sponsors";
      if (h === "#ed" || h.startsWith("#ed/")) return "editorial";
      if (h.startsWith("#agencies")) return "agencies"; // #agencies?view=place&state=AZ&county=Maricopa (G2)
      return null;
    });
    const [searchQuery, setSearchQuery] = React.useState(null);
    // Tags have two entry points, and they are different tools rather than two
    // answers to one question. The topbar type-ahead is a JUMP: you know the tag,
    // you want the matters, and the Matters TABLE is the more capable destination —
    // sortable columns, the stage / state / has-media filters, 50 a page. Search's
    // own tag control (Tags.jsx) is for BUILDING a query in place: combine two
    // tags, flip all/any, add text, without leaving the results. Routing the
    // type-ahead into Search instead was tried and reverted — it made `Matters`'
    // tag chip unreachable, which is dead code, and gave up the sortable table for
    // the commonest case to gain nothing.
    const [mattersIntent, setMattersIntent] = React.useState(null);
    // Open one agency's profile from anywhere (Lacey 2026-10-08: clicking an agency on
    // a matter went nowhere). An object, not a bare id, so opening the same agency a
    // second time after Back still fires.
    const [agencyIntent, setAgencyIntent] = React.useState(null);
    // Lane is a scope, and a scope has to survive a reload or it is just a filter
    // you re-pick every morning. Keyed by user so a shared machine doesn't inherit
    // someone else's desk. v1 §3.3 also wants the FIRST value seeded from role —
    // that needs `lnc_staff.default_lane` and a populated staff table, neither of
    // which exists, so the seeded default is `all` (today's behaviour unchanged).
    const [lane, setLane] = React.useState(window.LEDGER_LANE_DEFAULT || "all");
    const [laneCounts, setLaneCounts] = React.useState(null);

    // Keep the Performance hashes in step with the tab on screen. They are only
    // READ at load (above), so a stale one decides where a reload lands: leaving
    // CTV Revenue kept `#perf`, and a reload on Viewership (or anywhere else) went
    // back to the revenue desk. Viewership claims `#hov`; any other tab drops a
    // leftover `#perf` / `#hov`. Performance writes its own `#perf:<cut>` (child
    // effects run first), so on that tab only a missing prefix is filled in.
    // Other hashes (`#eng-…`, Supabase's `#access_token`) are never touched.
    // Best-effort: replaceState throws on an opaque origin.
    React.useEffect(() => {
      try {
        const h = window.location.hash || "";
        const bare = window.location.pathname + window.location.search;
        if (route === "viewership") {
          if (!h.startsWith("#hov")) history.replaceState(null, "", bare + "#hov");
        } else if (route === "performance") {
          if (!h.startsWith("#perf")) history.replaceState(null, "", bare + "#perf");
        } else if (h.startsWith("#perf") || h.startsWith("#hov")) {
          history.replaceState(null, "", bare);
        } else if (route !== "agencies" && h.startsWith("#agencies")) {
          // Leaving Agencies: its place link is only meaningful there.
          history.replaceState(null, "", bare);
        } else if (route !== "editorial" && route !== "ed-search" && route !== "ed-review" && route !== "ed-day" && route !== "ed-sponsors" &&
                   (h === "#ed" || h.startsWith("#ed/") || h.startsWith("#ed-search") || h.startsWith("#ed-review") || h.startsWith("#ed-day") || h.startsWith("#ed-sponsors"))) {
          // Leaving Editorial: drop its hash so a reload lands where the person is.
          history.replaceState(null, "", bare);
        }
      } catch (e) { /* link state is a convenience */ }
    }, [route]);

    function openMatters(intent) { setMattersIntent(intent); setRoute("matters"); }
    function openAgency(id) { if (id) { setAgencyIntent({ id, at: Date.now() }); setRoute("agencies"); } }
    // Leaf components (the matter page's Agencies tab) call this rather than having
    // the opener threaded through every level between here and them.
    window.LedgerOpenAgency = openAgency;
    // G4: a courthouse page lives inside Agencies (no nav item of its own, spec D8). Content Ops only.
    function openCourthouse(id) { if (id) { setAgencyIntent({ courthouse: id, at: Date.now() }); setRoute("agencies"); } }
    window.LedgerOpenCourthouse = openCourthouse;
    function navTo(id) {
      if (id === "matters") setMattersIntent(null);
      if (id === "agencies") setAgencyIntent(null);
      // The Editorial sub-tabs open at their top level, not wherever the hash last was.
      if (id === "editorial") { try { history.pushState(null, "", window.location.pathname + window.location.search + "#ed"); } catch (e) { /* noop */ } window.dispatchEvent(new HashChangeEvent("hashchange")); }
      if (id === "ed-search") { try { history.pushState(null, "", window.location.pathname + window.location.search + "#ed-search"); } catch (e) { /* noop */ } }
      if (id === "ed-review") { try { history.pushState(null, "", window.location.pathname + window.location.search + "#ed-review"); } catch (e) { /* noop */ } }
      if (id === "ed-sponsors") { try { history.pushState(null, "", window.location.pathname + window.location.search + "#ed-sponsors"); } catch (e) { /* noop */ } }
      if (id === "ed-day") { try { history.pushState(null, "", window.location.pathname + window.location.search + "#ed-day"); } catch (e) { /* noop */ } window.dispatchEvent(new HashChangeEvent("hashchange")); }
      setRoute(id);
    }

    // Editorial navigates by hash (episode links on the matter page, person links,
    // the show → episode drill-down), so follow an #ed hash to its route. Anything
    // else is left alone: the rest of the app does not route on hashchange.
    React.useEffect(() => {
      const on = () => {
        const h = window.location.hash || "";
        if (h.startsWith("#ed-search")) setRoute("ed-search");
        else if (h.startsWith("#ed-review")) setRoute("ed-review");
        else if (h.startsWith("#ed-day")) setRoute("ed-day");
        else if (h.startsWith("#ed-sponsors")) setRoute("ed-sponsors");
        else if (h === "#ed" || h.startsWith("#ed/")) setRoute("editorial");
      };
      window.addEventListener("hashchange", on);
      return () => window.removeEventListener("hashchange", on);
    }, []);

    // Evaluate the current session (applies the @lawnewz.com domain guard), then
    // resolve the capability set. ONE round trip since 0097: ledger_capabilities() (0124; was ledger_access_self())
    // returns allow-list membership plus every per-app flag, so there is a single
    // server-side answer to "who is this" rather than three calls the client has to
    // agree with each other. RLS is still the real gate — this decides what to draw.
    const evaluate = React.useCallback(async () => {
      const s = await window.LedgerAuth.getSession();
      if (s && s.error) { setAuthError(s.error); setSession(null); setAccess(NO_ACCESS); return; }
      setAuthError(null); setSession(s);
      if (!s) { setAccess(NO_ACCESS); return; }
      // window.LedgerCaps: a read-only copy for leaf components that draw a control
      // only for some people (the Feed rail's Accept/Dismiss). RLS/RPCs still gate.
      try { const a = await window.LedgerAPI.accessSelf(); window.LedgerCaps = a; setAccess(a); }
      catch { window.LedgerCaps = NO_ACCESS; setAccess(NO_ACCESS); }
    }, []);

    React.useEffect(() => {
      evaluate();
      const off = window.LedgerAuth.onAuthChange(() => evaluate());
      return off;
    }, [evaluate]);

    // Restore this user's lane once we know who they are, and keep it stored.
    const userId = session && session.user ? session.user.id : null;
    const laneKey = userId ? `ledger.lane.${userId}` : null;
    React.useEffect(() => {
      if (!laneKey) return;
      try {
        const saved = window.localStorage.getItem(laneKey);
        const known = (window.LEDGER_LANES || []).some((l) => l.id === saved);
        if (known) setLane(saved);
      } catch { /* private mode / storage disabled — the default still works */ }
    }, [laneKey]);

    function pickLane(next) {
      setLane(next);
      if (laneKey) { try { window.localStorage.setItem(laneKey, next); } catch { /* non-fatal */ } }
    }

    // Switcher badges. Reloaded when the lane changes so an override or a promotion
    // is reflected without a page refresh.
    // Lanes scope MATTERS, so this is a workspace-only fetch. Without the guard a
    // Performance-only user fires a guaranteed-denied request on every load and on
    // every lane change — swallowed by the catch, but still noise in the console
    // and a round trip that can never succeed.
    // DEPEND ON THE IDENTITY, NOT ON THE OBJECTS CARRYING IT. `session` and `access`
    // are both rebuilt by evaluate(), and evaluate() runs on every onAuthStateChange:
    // INITIAL_SESSION, the hourly TOKEN_REFRESHED, and a re-emitted SIGNED_IN each
    // time the tab regains focus. Keyed on the objects, every one of those fired a
    // fresh laneCounts() for an answer that had not changed — 1,874 calls against a
    // matter_lane_counts() that cost 1.3s before `0106`, which is most of how one
    // dashboard left open in a tab saturated the instance. A user id, a boolean and
    // the lane are the only things that can change the answer, so they are the deps.
    const isWorkspace = !!(access && access.isWorkspace);
    React.useEffect(() => {
      if (!userId || !isWorkspace) return;
      let live = true;
      window.LedgerAPI.laneCounts().then((c) => { if (live) setLaneCounts(c); }).catch(() => {});
      return () => { live = false; };
    }, [userId, isWorkspace, lane]);

    async function signOut() { await window.LedgerAuth.signOut(); setSession(null); setAccess(NO_ACCESS); }
    function goSearch(q) { setSearchQuery(q || null); setRoute("search"); }

    if (session === undefined || (session && access === undefined)) {
      return <div style={{ height: "100vh", display: "grid", placeItems: "center",
        color: "var(--text-muted)", fontSize: 14 }}>Checking session…</div>;
    }
    if (!session) return <window.LedgerLogin error={authError} />;

    // HOME is the first app this person actually holds, in LEDGER_APPS order — not
    // a hardcoded "newsroom". That constant was the whole reason a Performance-only
    // account was impossible to serve: they'd sign in, and every path (initial load,
    // a refused route, a deep link they can't follow) put them on a Newsroom whose
    // every query RLS denies. A blank page that looks like a broken app.
    //
    // Within that app, home is its `home` page if this person may open it, else the
    // app's first page they can (0124: a dashboards-only Performance account can't
    // open the revenue board, so it lands on Viewership). Same rule the sidebar
    // heading uses.
    const openable = (n) => !n.capability || !!access[n.capability];
    const home = (window.LEDGER_APPS || [])
      .filter((a) => access[a.capability])
      .map((a) => {
        const pages = (window.LEDGER_NAV || []).filter((n) => n.app === a.id && openable(n));
        const own = pages.find((n) => n.id === a.home);
        return own ? own.id : (pages[0] ? pages[0].id : null);
      })
      .filter(Boolean)[0] || null;

    // On the allow-list but holding no app. Being listed only means you may sign in.
    if (!access.authorized || !home) {
      return <AccessPending email={session.user?.email} onSignOut={signOut}
                            listed={!!access.authorized} />;
    }

    // Route guard. One rule, read off the capability each tab declares in
    // LEDGER_NAV (config.js), so a new app never needs a new clause here. Forcing
    // `route` by hand gets you home instead; the DB refuses the data either way.
    function mayOpen(id) {
      const n = (window.LEDGER_NAV || []).find((x) => x.id === id);
      return !n || openable(n);
    }
    // Tags folded into Search (session 11) — the standalone route is retired; anything
    // still pointing at "tags" lands on the Search browse (which is the tag taxonomy).
    const wanted = route || home;
    // Title Review (admin) became the Metadata desk's Titles filter in 0130.
    const normRoute = wanted === "tags" ? "search" : wanted === "titles" ? "metadata" : wanted;
    const safeRoute = mayOpen(normRoute) ? normRoute : home;

    // DEMO-ONLY: page label for the per-section comment instrument (window.LedgerComment
    // reads this). MatterDetail refines it to the open matter. Removed at go-live.
    window.LedgerFeedbackCtx = { page: window.titleize(safeRoute), context: null };

    let view;
    switch (safeRoute) {
      case "matters": view = <window.LedgerMatters intent={mattersIntent} lane={lane} onLane={pickLane} />; break;
      case "calendar": view = <window.LedgerTrialCalendar
                                onOpenMatter={(id) => openMatters({ type: "open", id })} />; break;
      case "coverage": view = <window.LedgerCoverage lane={lane}
                                onOpenMatter={(id) => openMatters({ type: "open", id })} />; break;
      case "agencies": view = <window.LedgerAgencies openIntent={agencyIntent}
                                onOpenMatter={(id) => openMatters({ type: "open", id })} />; break;
      case "search":  view = <window.LedgerSearch initialQuery={searchQuery} lane={lane}
                                onOpenMatter={(id) => openMatters({ type: "open", id })} />; break;
      case "requests": view = <window.LedgerRequestCalendar
                                onOpenMatter={(id) => openMatters({ type: "open", id })} />; break;
      case "freelancers": view = <window.LedgerFreelancers
                                onOpenMatter={(id) => openMatters({ type: "open", id })} />; break;
      case "doctypes": view = <window.LedgerDocTypes />; break;
      case "metadata": view = <window.LedgerMetadataDesk
                                onOpenMatter={(id) => openMatters({ type: "open", id })} />; break;
      case "editorial":
      case "ed-search":
      case "ed-review":
      case "ed-day":
      case "ed-sponsors": view = <window.LedgerEditorial view={safeRoute}
                                onOpenMatter={(id) => openMatters({ type: "open", id })} />; break;
      case "performance": view = <window.LedgerPerformance />; break;
      case "viewership": view = <window.LedgerViewership />; break;
      // Three routes, one component: the views share the board, the identity and
      // the ingest banner, and splitting them into three files would mean three
      // copies of the load.
      case "eng-overview":
      case "eng-approvals":
      case "eng-repos": view = <window.LedgerEngineering view={safeRoute} />; break;
      case "access":  view = <window.LedgerAccess />; break;
      case "admin":   view = <window.LedgerAdmin />; break;
      case "feedback": view = <window.LedgerFeedbackAdmin />; break; // DEMO-ONLY

      // ONE HOME, AT EVERY LANE (2026-09-11, reversing part of v1 §3.3). This used
      // to branch: `lane === "police_video"` mounted LedgerWorklist and config.js
      // renamed the sidebar tab to match. So the lane switcher — which the chrome
      // presents as a scope — was also a navigation control, and the only one in
      // the app that moved you somewhere without your clicking a tab. It now
      // scopes the Newsroom instead, which is what a filter does. The worklist's
      // due-date rail moved to the Asset Requests desk, where those rows live.
      default:
        view = <window.LedgerNewsroom
                 onNew={() => openMatters({ type: "create" })}
                 onNav={navTo}
                 lane={lane}
                 onOpenMatter={(id) => openMatters(id ? { type: "open", id } : null)} />;
    }

    return (
      <window.LedgerShell
        active={safeRoute}
        onNav={navTo}
        user={session.user}
        onSignOut={signOut}
        onSearch={goSearch}
        onOpenMatter={(id) => openMatters({ type: "open", id })}
        onOpenTag={(k) => openMatters({ type: "tag", tag: k })}
        home={home}
        caps={access}
        lane={lane}
        onLane={pickLane}
        laneCounts={laneCounts}
      >
        {view}
      </window.LedgerShell>
    );
  }

  function mount() {
    const ready = window.LedgerLogin && window.LedgerShell && window.LedgerNewsroom &&
                  window.LedgerMatters && window.LedgerTagFilter && window.LedgerTagBrowse &&
                  window.LedgerTagTaxonomy && window.LedgerTrialCalendar && window.LedgerCoverage && window.LedgerAgencies && window.LedgerSearch &&
                  window.LedgerOpsQueue && window.LedgerRequestCalendar && window.LedgerPerformance && window.LedgerViewership &&
                  window.LedgerEngineering &&
                  window.LedgerAdmin && window.LedgerAccess && window.LedgerAuth &&
                  window.LedgerMetadataDesk && window.LedgerFreelancers && window.LedgerDocumentsBrowse && window.LedgerEditorial && window.LedgerEditorialReview &&
                  window.LedgerComment && window.LedgerFeedbackAdmin; // DEMO-ONLY feedback
    if (!ready) { setTimeout(mount, 40); return; }
    ReactDOM.createRoot(document.getElementById("root")).render(<App />);
    setTimeout(() => window.lucide && window.lucide.createIcons(), 40);
  }
  mount();
})();
