/* ELEV8 ATS - shell hooks and cross-cutting state.
   Split out of app/shell.jsx (R144). Every declaration below is UNCHANGED - this file is a move,
   not a rewrite. Loaded BEFORE app/shell.jsx in "ELEV8 ATS.html": shell.jsx's Object.assign at the
   bottom exports these names, so the bindings must already exist when that line runs.
   Contents: the two shell contexts, the bus/workspace re-render hooks, the data-mode / platform
   signals / sync plumbing, the dialog-a11y hook, and the mobile gesture primitives. */

const E8Ctx = React.createContext({});
/* Lets the active screen register a contextual action for the mobile top bar. */
const MobileActionCtx = React.createContext({ setAction: () => {} });
/* Re-render subscribers when pipeline stages are edited + saved in the Stages config. */
function useStagesVersion() {
  const [v, setV] = React.useState(0);
  React.useEffect(() => {
    const fn = () => setV((n) => n + 1);
    window.addEventListener('e8-stages-changed', fn);
    return () => window.removeEventListener('e8-stages-changed', fn);
  }, []);
  return v;
}

/* R78: re-render subscribers when the event bus reports queue/comms/store changes -
   this is what keeps nav badges honest as approvals clear and messages are read. */
function useBusVersion() {
  const [v, setV] = React.useState(0);
  React.useEffect(() => (
    window.E8Events
      ? window.E8Events.subscribe(['queue:changed', 'comms:changed', 'store:changed'], () => setV((n) => n + 1))
      : undefined
  ), []);
  return v;
}

function useWorkspaceVersion() {
  const [version, setVersion] = React.useState(0);
  React.useEffect(() => (
    window.E8Events
      ? window.E8Events.subscribe(['workspace:changed', 'store:changed', 'persona:changed'], () => setVersion((v) => v + 1))
      : undefined
  ), []);
  return version;
}
/* ---------- Data mode (Demo / Empty / Scale) - R98 Task 6 ----------
   The mode is applied pre-store by app/data-mode.js; switching is confirm + write key + reload
   (module-level captures across the app make a hot swap unsafe). On switch we clear the
   id-coupled overlay keys whose record ids no longer exist in the target dataset, but we keep
   the per-mode patch key and the per-mode dashboard layout (records + layout created in a mode
   survive a round-trip through other modes) and user prefs (appearance, accent, columns, views). */
var E8_DATA_MODES = [
  { id: 'demo', label: 'Demo', icon: 'dataset', sub: 'The full sample dataset' },
  { id: 'empty', label: 'Empty', icon: 'inbox', sub: 'First-time setup, no data' },
  { id: 'scale', label: 'Scale', icon: 'stacked_bar_chart', sub: 'About 100 rows per list' },
];
function e8CurrentDataMode() { return window.E8_DATA_MODE || 'demo'; }
function e8ClearModeScopedKeys() {
  /* e8-dash-layout-v1 is NOT cleared: it is scoped per mode at the read site (screens-core), so
     each mode keeps its own dashboard and empty mode still gets the first-run default. */
  var EXACT = ['e8-approvals-v1', 'e8-watch-v1', 'e8-watch-sweep', 'e8-match-v1', 'e8-match-feedback-v1',
    'e8-ap-screen-v1', 'e8-notify-v1', 'e8-cand-segments-v1', 'e8-tp-migrated-v1', 'e8-audit-v1', 'e8-events-v1'];
  var PREFIX = ['e8-ts-', 'e8-match-decisions-', 'e8-match-weights-', 'e8-composer-draft-'];
  try {
    EXACT.forEach(function (k) { localStorage.removeItem(k); });
    var kill = [];
    for (var i = 0; i < localStorage.length; i++) {
      var k = localStorage.key(i);
      if (k && PREFIX.some(function (p) { return k.indexOf(p) === 0; })) kill.push(k);
    }
    kill.forEach(function (k) { localStorage.removeItem(k); });
  } catch (e) {}
}
function e8SwitchDataMode(next) {
  if (next === e8CurrentDataMode()) return;
  e8ClearModeScopedKeys();
  try { localStorage.setItem('e8-data-mode', next); } catch (e) {}
  window.location.reload();
}
/* ---------- Platform signals (ELEV8 provenance marks) kill switch ----------
   localStorage 'e8-signals' ('on' default); when off, .e8-signals-off on the app root
   (body carries .elev8) hides every .e8-pm mark - the CSS rule ships from Badge.jsx. */
function e8SignalsOn() {
  try { return window.localStorage.getItem('e8-signals') !== 'off'; } catch (e) { return true; }
}
function e8SetSignals(on) {
  try { window.localStorage.setItem('e8-signals', on ? 'on' : 'off'); } catch (e) {}
  document.body.classList.toggle('e8-signals-off', !on);
  window.dispatchEvent(new Event('e8-signals-changed'));
}
/* Apply the persisted state at load so marks never flash before React mounts. */
e8SetSignals(e8SignalsOn());

function useSignals() {
  const [on, setOn] = React.useState(e8SignalsOn);
  React.useEffect(() => {
    const fn = () => setOn(e8SignalsOn());
    window.addEventListener('e8-signals-changed', fn);
    return () => window.removeEventListener('e8-signals-changed', fn);
  }, []);
  return [on, e8SetSignals];
}
/* ---------- Simulated ELEV8 platform sync (sidebar card + ⌘K action) ---------- */
const E8_SYNC_SEED_STAMP = '8:40 AM'; // the fiction's "last synced" time before any manual sync
let e8SyncStamp = null; // set by e8SyncNow; SyncCard reads it on the 'e8-synced' event
function e8SyncNow(showToast) {
  e8SyncStamp = new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
  window.dispatchEvent(new Event('e8-synced'));
  if (showToast) showToast('Synced from ELEV8 - 3 records updated');
}
/* ---------- Shared mobile primitives ---------- */

/* True when the viewport is at/under the mobile breakpoint (mirrors app.css 840px). */
function useIsMobile(bp = 840) {
  const q = '(max-width: ' + bp + 'px)';
  const [m, setM] = React.useState(() => !!(window.matchMedia && window.matchMedia(q).matches));
  React.useEffect(() => {
    if (!window.matchMedia) return;
    const mq = window.matchMedia(q);
    const fn = (e) => setM(e.matches);
    mq.addEventListener ? mq.addEventListener('change', fn) : mq.addListener(fn);
    return () => { mq.removeEventListener ? mq.removeEventListener('change', fn) : mq.removeListener(fn); };
  }, [q]);
  return m;
}

/* Drag-down-to-dismiss for bottom sheets. Touch-only - zero desktop impact.
   Spread `bind` on the grab handle + sheet head; apply `style` to the sheet element. */
function useSheetDrag(onClose) {
  const [dragY, setDragY] = React.useState(0);
  const start = React.useRef(null);
  const cur = React.useRef(0);
  const set = (y) => { cur.current = y; setDragY(y); };
  const onTouchStart = (e) => { start.current = e.touches[0].clientY; };
  const onTouchMove = (e) => {
    if (start.current == null) return;
    set(Math.max(0, e.touches[0].clientY - start.current));
  };
  const onTouchEnd = () => {
    const d = cur.current;
    start.current = null;
    set(0);
    if (d > 110) onClose();
  };
  const style = dragY ? { transform: 'translateY(' + dragY + 'px)', transition: 'none' } : null;
  return { dragY, style, bind: { onTouchStart, onTouchMove, onTouchEnd } };
}

/* Swipe-to-triage. Drag right past the threshold commits `left` (advance), drag left
   commits `right` (reject). Reveal pads ramp with distance; past threshold the row "arms"
   (saturates + label flips to Release) with elastic resistance. Release before threshold
   springs back. Tap (move < 8px) -> onTap. Touch-only, axis-locked so vertical scroll is
   never hijacked. The caller removes the row on commit (auto-advance) + offers Undo. */
function SwipeTriage({ left, right, onCommit, onTap, onLongPress, children }) {
  const [tx, setTx] = React.useState(0);
  const txRef = React.useRef(0);
  const setBoth = (v) => { txRef.current = v; setTx(v); };
  const [dragging, setDragging] = React.useState(false);
  const st = React.useRef(null);
  const moved = React.useRef(0);
  const wrapRef = React.useRef(null);
  const lpTimer = React.useRef(null);
  const lpFired = React.useRef(false);
  const clearLP = () => { if (lpTimer.current) { clearTimeout(lpTimer.current); lpTimer.current = null; } };
  React.useEffect(() => clearLP, []);
  const widthOf = () => (wrapRef.current ? wrapRef.current.offsetWidth : 320);
  const onTouchStart = (e) => {
    const t = e.touches[0];
    st.current = { x: t.clientX, y: t.clientY, axis: null, w: widthOf() };
    moved.current = 0;
    setDragging(true);
    lpFired.current = false;
    if (onLongPress) { clearLP(); lpTimer.current = setTimeout(() => { lpFired.current = true; if (navigator.vibrate) { try { navigator.vibrate(14); } catch (x) {} } onLongPress(); }, 480); }
  };
  const onTouchMove = (e) => {
    if (!st.current) return;
    const t = e.touches[0];
    const dx = t.clientX - st.current.x;
    const dy = t.clientY - st.current.y;
    moved.current = Math.max(moved.current, Math.abs(dx) + Math.abs(dy));
    if (moved.current > 10) clearLP();
    if (!st.current.axis) st.current.axis = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y';
    if (st.current.axis !== 'x') return;
    e.preventDefault();
    const th = st.current.w * 0.42;
    let v = dx;
    if (v > 0 && !left) v = 0;
    if (v < 0 && !right) v = 0;
    if (Math.abs(v) > th) v = Math.sign(v) * (th + (Math.abs(v) - th) * 0.38);
    setBoth(v);
  };
  const onTouchEnd = () => {
    clearLP();
    if (!st.current) return;
    const th = st.current.w * 0.42;
    const v = txRef.current;
    st.current = null;
    setDragging(false);
    const dir = (v >= th && left) ? 1 : (v <= -th && right) ? -1 : 0;
    if (dir !== 0) {
      if (navigator.vibrate) { try { navigator.vibrate(12); } catch (x) {} }
      setBoth(dir * (widthOf() + 40));
      window.setTimeout(() => { if (onCommit) onCommit(dir === 1 ? 'advance' : 'reject'); }, 200);
    } else {
      setBoth(0);
    }
  };
  const onClick = () => { if (lpFired.current) { lpFired.current = false; return; } if (moved.current < 8 && onTap) onTap(); };
  const th = widthOf() * 0.42;
  const armed = Math.abs(tx) >= th;
  const pad = (side, cfg) => cfg ? (
    <div className={'e8-triage-pad ' + side + (armed ? ' armed' : '')} style={{ width: Math.max(0, side === 'left' ? tx : -tx), background: cfg.bg }}>
      <span className="material-symbols-outlined">{cfg.icon}</span>
      <span className="e8-triage-lbl">{armed ? 'Release' : cfg.label}</span>
    </div>
  ) : null;
  return (
    <div className="e8-triage" ref={wrapRef}>
      {pad('left', left)}
      {pad('right', right)}
      <div className={'e8-triage-card' + (dragging ? ' dragging' : '')} style={{ transform: 'translateX(' + tx + 'px)' }}
        onTouchStart={onTouchStart} onTouchMove={onTouchMove} onTouchEnd={onTouchEnd}
        {...(onTap ? window.clickableProps(onClick) : { onClick })}>
        {children}
      </div>
    </div>
  );
}

/* Detent bottom sheet (mobile). Snaps between half and full; drag the handle/header to
   move it; fling down or drag below the half line to dismiss. Scrim opacity ramps with
   openness so the list behind stays bright at half. Touch-drag + reduced-motion aware. */
function BottomSheet({ open, onClose, title, children, footer, initial }) {
  const [detent, setDetent] = React.useState(initial || 'half');
  const [dragY, setDragY] = React.useState(null);
  const drag = React.useRef(null);
  const dlgRef = React.useRef(null);
  useDialog(open, dlgRef);
  const H = () => window.innerHeight || 800;
  const detentY = (d) => d === 'full' ? 0 : Math.round(H() * 0.42);
  const closedY = () => Math.round(H() * 0.92) + 48;
  React.useEffect(() => { if (open) { setDetent(initial || 'half'); setDragY(null); } }, [open]);
  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [open, onClose]);
  const baseY = open ? detentY(detent) : closedY();
  const curY = (dragY != null) ? dragY : baseY;
  const onStart = (e) => { const t = e.touches[0]; const now = window.performance ? performance.now() : 0; drag.current = { y: t.clientY, base: baseY, ly: t.clientY, lt: now, v: 0 }; };
  const onMove = (e) => {
    if (!drag.current) return;
    const t = e.touches[0];
    const now = window.performance ? performance.now() : 0;
    const ny = drag.current.base + (t.clientY - drag.current.y);
    const dt = now - drag.current.lt;
    if (dt > 0) drag.current.v = (t.clientY - drag.current.ly) / dt;
    drag.current.ly = t.clientY; drag.current.lt = now;
    setDragY(ny < 0 ? ny * 0.4 : ny);
  };
  const onEnd = () => {
    if (!drag.current) return;
    const v = drag.current.v;
    const y = (dragY != null) ? dragY : baseY;
    drag.current = null;
    const half = detentY('half');
    if (v > 0.6 || y > half + H() * 0.16) { setDragY(null); onClose(); return; }
    let target = 'half';
    if (v < -0.4) target = 'full';
    else if (v > 0.4) target = 'half';
    else target = (y < half / 2) ? 'full' : 'half';
    setDetent(target); setDragY(null);
  };
  const prog = Math.max(0, Math.min(1, (closedY() - curY) / closedY()));
  /* Element opacity 0->1; the scrim's darkness (alpha) lives in the --ui-scrim token. */
  const scrim = open ? Number((prog * prog).toFixed(3)) : 0;
  const bind = { onTouchStart: onStart, onTouchMove: onMove, onTouchEnd: onEnd };
  return (
    <React.Fragment>
      <div className={'e8-bsheet-backdrop' + (open ? ' is-open' : '')} style={{ opacity: scrim }} onClick={onClose} aria-hidden={!open}></div>
      <div ref={dlgRef} className={'e8-bsheet' + (dragY != null ? ' dragging' : '')} role="dialog" aria-modal="true" aria-label={title || 'Details'} aria-hidden={!open}
        style={{ transform: 'translateY(' + curY + 'px)' }}>
        <div className="e8-bsheet-grab" {...bind}></div>
        {title ? (
          <div className="e8-bsheet-head" {...bind}>
            <span className="e8-bsheet-title">{title}</span>
            <button type="button" className="e8-bsheet-close" aria-label="Close" onClick={onClose}><span className="material-symbols-outlined">close</span></button>
          </div>
        ) : null}
        <div className="e8-bsheet-body">{children}</div>
        {footer ? <div className="e8-bsheet-foot">{footer}</div> : null}
      </div>
    </React.Fragment>
  );
}

/* Swipe horizontally to move between record tabs (mobile). Spread `bind` on the tab-panel
   wrapper and merge `style`; no-ops on desktop. Axis-locked so vertical scroll stays free. */
function useSwipeTabs(tabs, active, onChange) {
  const isMobile = useIsMobile();
  const [dx, setDx] = React.useState(0);
  const dxRef = React.useRef(0);
  const st = React.useRef(null);
  const idx = tabs.indexOf(active);
  const set = (v) => { dxRef.current = v; setDx(v); };
  const onTouchStart = (e) => { const t = e.touches[0]; st.current = { x: t.clientX, y: t.clientY, axis: null }; };
  const onTouchMove = (e) => {
    if (!st.current) return;
    const t = e.touches[0];
    const ddx = t.clientX - st.current.x;
    const ddy = t.clientY - st.current.y;
    if (!st.current.axis) st.current.axis = Math.abs(ddx) > Math.abs(ddy) + 6 ? 'x' : 'y';
    if (st.current.axis !== 'x') return;
    e.preventDefault();
    let v = ddx;
    if ((idx <= 0 && v > 0) || (idx >= tabs.length - 1 && v < 0)) v *= 0.3;
    set(v);
  };
  const onTouchEnd = () => {
    if (!st.current) return;
    const v = dxRef.current; const ax = st.current.axis; st.current = null;
    if (ax === 'x') {
      let changed = false;
      if (v <= -56 && idx < tabs.length - 1) { onChange(tabs[idx + 1]); changed = true; }
      else if (v >= 56 && idx > 0) { onChange(tabs[idx - 1]); changed = true; }
      if (changed && navigator.vibrate) { try { navigator.vibrate(8); } catch (e) {} }
    }
    set(0);
  };
  if (!isMobile) return { bind: {}, style: null };
  return { bind: { onTouchStart, onTouchMove, onTouchEnd }, style: { touchAction: 'pan-y', transform: dx ? 'translateX(' + dx + 'px)' : undefined, transition: dx ? 'none' : 'transform .2s ease' } };
}

/* Pull down at the top of a list to sync (mobile). Hooks the main scroll container; the
   screen renders the fixed indicator from { pull, refreshing }. Simulated - no live fetch. */
function usePullToRefresh(onRefresh) {
  const isMobile = useIsMobile();
  const cbRef = React.useRef(onRefresh);
  cbRef.current = onRefresh;
  const [pull, setPull] = React.useState(0);
  const [refreshing, setRefreshing] = React.useState(false);
  const pullRef = React.useRef(0);
  const refRef = React.useRef(false);
  const st = React.useRef(null);
  const setPullBoth = (v) => { pullRef.current = v; setPull(v); };
  const setRefBoth = (v) => { refRef.current = v; setRefreshing(v); };
  React.useEffect(() => {
    if (!isMobile) return;
    const sc = document.querySelector('.e8-content');
    if (!sc) return;
    const TH = 72;
    const onStart = (e) => { st.current = (sc.scrollTop <= 0 && !refRef.current) ? { y: e.touches[0].clientY } : null; };
    const onMove = (e) => {
      if (!st.current || refRef.current) return;
      const dy = e.touches[0].clientY - st.current.y;
      if (dy <= 0 || sc.scrollTop > 0) { if (pullRef.current) setPullBoth(0); return; }
      e.preventDefault();
      setPullBoth(Math.min(110, dy * 0.5));
    };
    const finish = () => {
      if (!st.current) return;
      st.current = null;
      if (pullRef.current >= TH) {
        if (navigator.vibrate) { try { navigator.vibrate(10); } catch (e) {} }
        setRefBoth(true); setPullBoth(64);
        Promise.resolve(cbRef.current && cbRef.current()).then(() => { setRefBoth(false); setPullBoth(0); });
      } else { setPullBoth(0); }
    };
    sc.addEventListener('touchstart', onStart, { passive: true });
    sc.addEventListener('touchmove', onMove, { passive: false });
    sc.addEventListener('touchend', finish, { passive: true });
    sc.addEventListener('touchcancel', finish, { passive: true });
    return () => { sc.removeEventListener('touchstart', onStart); sc.removeEventListener('touchmove', onMove); sc.removeEventListener('touchend', finish); sc.removeEventListener('touchcancel', finish); };
  }, [isMobile]);
  return { pull, refreshing };
}
/* Dialog a11y for an always-mounted overlay: move focus in on open, trap Tab within,
   restore focus to the trigger on close, and mark the container `inert` while closed so
   its controls leave the tab order + a11y tree. Spread nothing - pass (open, ref). */
function useDialog(open, ref) {
  /* Capture the opener SYNCHRONOUSLY during the render that opens the dialog - before React's
     autoFocus (commit phase) or the move-in below pulls focus into the dialog. Capturing in the
     effect below is too late: an autoFocus field is already document.activeElement by then, so
     close would "restore" to that unmounting field and drop focus to <body>. Guarded to the first
     open render (prevFocus stays null until then) and reset on close so a re-open re-captures. */
  const prevFocus = React.useRef(null);
  if (open && prevFocus.current === null && typeof document !== 'undefined') {
    const ae = document.activeElement;
    prevFocus.current = (ae && ae !== document.body) ? ae : null;
  }
  React.useEffect(() => {
    const node = ref.current;
    if (!node) return;
    try { node.inert = !open; } catch (e) {}
    if (!open) { prevFocus.current = null; return; }
    const SEL = 'a[href],button:not([disabled]),input:not([disabled]),textarea:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
    const list = () => Array.prototype.slice.call(node.querySelectorAll(SEL)).filter((el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement);
    const f = list();
    /* R98 fix: if an autoFocus already landed inside the dialog, keep it - refocusing the first
       focusable (usually the header close button) blurred the input and tripped its required
       error. Otherwise prefer the first FORM CONTROL over buttons/links so create dialogs open
       on their first field rather than on close. */
    if (!node.contains(document.activeElement)) {
      const ctrl = f.find((el) => /^(INPUT|SELECT|TEXTAREA)$/.test(el.tagName)) || f.find((el) => el.tagName === 'BUTTON') || f[0];
      if (ctrl) { try { ctrl.focus(); } catch (e) {} } else { try { node.focus(); } catch (e) {} }
    }
    const onKey = (e) => {
      if (e.key !== 'Tab') return;
      const items = list();
      if (!items.length) return;
      const a = items[0], b = items[items.length - 1];
      if (e.shiftKey && document.activeElement === a) { e.preventDefault(); b.focus(); }
      else if (!e.shiftKey && document.activeElement === b) { e.preventDefault(); a.focus(); }
    };
    node.addEventListener('keydown', onKey);
    /* Restore focus to the opener on close/unmount. Guard if it's gone (isConnected): a dialog
       opened from a menu item whose trigger unmounted has no live opener to return to. */
    return () => {
      node.removeEventListener('keydown', onKey);
      const p = prevFocus.current;
      prevFocus.current = null;
      if (p && p.isConnected && p.focus) { try { p.focus(); } catch (e) {} }
    };
  }, [open]);
}
