// In-app browser-history integration so Back/Forward step through a module's sub-views and
// overlays before leaving the page — not straight back to the previous module.
//
// Two primitives share the existing page-history (App stamps `sfPage` in history.state):
//   - useHistorySubview: a module's active sub-view rides in history.state._sub, so Back inside
//     a module (e.g. Messages thread → list) returns to the parent view first.
//   - useBackToClose: while an overlay/modal is mounted it occupies one history entry; Back
//     closes the topmost one. A module-level stack keeps nesting correct — closing one modal by
//     its own button pops exactly its entry without tripping the modals underneath.
//
// setPage() pushes a clean { sfPage } entry (dropping _sub/_ov), so each page starts at its root.
import { useEffect, useRef, useState } from "react";

// --- Sub-views (value carried in history.state._sub) ---------------------------------------
export function useHistorySubview<T>(root: T): {
  view: T;
  open: (next: T) => void;
  back: () => void;
} {
  const [view, setView] = useState<T>(() => (window.history.state?._sub as T) ?? root);
  useEffect(() => {
    const onPop = () => setView((window.history.state?._sub as T) ?? root);
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  return {
    view,
    open: (next: T) => {
      setView(next);
      window.history.pushState({ ...window.history.state, _sub: next }, "");
    },
    back: () => window.history.back(),
  };
}

// --- Overlays / modals (one history entry each, nesting-safe) -------------------------------
// A SINGLE module-level popstate handler owns the whole overlay stack, and history is kept in
// sync LAZILY via a coalesced microtask rather than eagerly per mount/unmount. This matters for
// modal→modal handoffs (e.g. About → Release notes): the leaving modal unmounts and the entering
// one mounts in the SAME React commit, so within one tick the desired overlay-entry count drops
// by one then rises by one — a net zero. Reconciling once, after the tick settles, sees that net
// zero and touches history NOT AT ALL: the new modal simply reuses the old one's entry.
//
// The previous eager design pushed on mount and called history.back() on unmount independently.
// Interleaved during a handoff that produced (a) a flash-and-vanish where the new modal's popstate
// listener mistook the leaving modal's back() for a genuine user Back and closed itself, and
// (b) a mangled history where the pointer ended up BEHIND the surviving entry, so a later browser
// Back walked off the app entirely instead of closing the modal. Coalescing removes both.
type Overlay = { token: number; cb: () => void };
const overlayStack: Overlay[] = [];
let overlayCounter = 0;
// Overlay history entries we've actually pushed. Reconciled toward overlayStack.length.
let historyDepth = 0;
// Programmatic history.back() calls still awaiting their popstate — each must be ignored.
let pendingProgrammatic = 0;
let reconcileScheduled = false;
let listenerInstalled = false;

// Push/pop history entries until their count matches the live overlay count. Runs once per tick.
function reconcileHistory(): void {
  reconcileScheduled = false;
  const want = overlayStack.length;
  while (historyDepth < want) {
    historyDepth++;
    window.history.pushState({ ...window.history.state, _ov: ++overlayCounter }, "");
  }
  while (historyDepth > want) {
    historyDepth--;
    pendingProgrammatic++;
    window.history.back();
  }
}

function scheduleReconcile(): void {
  if (reconcileScheduled) return;
  reconcileScheduled = true;
  queueMicrotask(reconcileHistory);
}

function ensureOverlayListener(): void {
  if (listenerInstalled) return;
  listenerInstalled = true;
  window.addEventListener("popstate", () => {
    if (pendingProgrammatic > 0) {
      pendingProgrammatic--; // our own back(): consume and ignore
      return;
    }
    // Genuine user Back: the browser already dropped one overlay entry, so close the topmost
    // overlay to match. Its unmount cleanup finds itself already off the stack and only re-syncs.
    if (historyDepth > 0) historyDepth--;
    const top = overlayStack.pop();
    if (top) top.cb();
    scheduleReconcile();
  });
}

/** While the calling component is mounted, Back (or the browser/mouse back button) closes it via
 * `onClose` instead of navigating away. Mount the component only while the overlay is open. */
export function useBackToClose(onClose: () => void): void {
  const cb = useRef(onClose);
  cb.current = onClose;
  useEffect(() => {
    ensureOverlayListener();
    const entry: Overlay = { token: ++overlayCounter, cb: () => cb.current() };
    overlayStack.push(entry);
    scheduleReconcile();
    return () => {
      const idx = overlayStack.indexOf(entry);
      // idx === -1 means the shared handler already removed us on a real user Back; either way,
      // just re-sync history to the (possibly unchanged) overlay count on the next tick.
      if (idx !== -1) overlayStack.splice(idx, 1);
      scheduleReconcile();
    };
  }, []);
}

/** A Back-closable layer that exists only WHILE `active`. Stacks on top of `useBackToClose` and any
 * other active `useBackToExit` (LIFO) so nested views peel off one Back at a time — e.g. a player's
 * fullscreen sits above its maximise, which sits above the player itself: Back exits fullscreen,
 * then maximise, then closes the player. Leaving a layer by its own control (button/Esc) flips
 * `active` false, whose cleanup drops the entry and its history state on the next tick — exactly the
 * useBackToClose lifecycle, just gated on a flag. `onExit` runs on a genuine Back while active. */
export function useBackToExit(active: boolean, onExit: () => void): void {
  const cb = useRef(onExit);
  cb.current = onExit;
  useEffect(() => {
    if (!active) return;
    ensureOverlayListener();
    const entry: Overlay = { token: ++overlayCounter, cb: () => cb.current() };
    overlayStack.push(entry);
    scheduleReconcile();
    return () => {
      const idx = overlayStack.indexOf(entry);
      if (idx !== -1) overlayStack.splice(idx, 1);
      scheduleReconcile();
    };
  }, [active]);
}
