import { atom } from 'jotai';
import { IconSrc } from 'folds';
import type { MobilePagerTab } from '../components/mobile-tabs-pager/MobilePagerPaneContext';

// Per-tab «primary action» published by a pane's StreamHeader. When
// present it replaces the default Plus / «new chat» button in the
// static header (and renders the matching Plus in the per-pane
// StreamHeader's tabs row).
// Channels uses this to surface «create channel» (inside a workspace)
// or «create community» (on the landing) instead of the DM-creation
// path that Direct keeps as default.
export type StreamHeaderPrimaryAction = {
  iconSrc: IconSrc;
  label: string;
  onClick: () => void;
};

// Controls exposed by the active pane's curtain to the shared static
// tabs row at the top of MobileTabsPager. The pager hoists the tabs +
// action icons OUT of each pane's StreamHeader so the header stays
// visually static while the listing content slides horizontally — but
// the action icons (Plus / Search / X) still need to drive the active
// pane's local curtain, so we proxy them through this atom.
//
// Lifecycle: each pane's StreamHeader subscribes itself to a
// MobilePagerPaneContext that flags whether it's the active pane. When
// active, the StreamHeader's effect writes its curtain controls here;
// on deactivation (or unmount) the effect clears them. Only one pane
// is active at any moment, so writes don't race.
export type MobilePagerCurtainControls = {
  openSearch: () => void;
  closeForm: () => void;
  isFormActive: boolean;
  // True while the inline form is MOUNTED (`activeForm !== null`) —
  // outlives the snap-based `isFormActive` through the 280–480ms close
  // window until `acknowledgeClosed` / the safety timer clears the form
  // (useCurtainState.ts). The pager's refresh singleton gates on THIS:
  // gating on `isFormActive` would pop a mascot kept alive by another
  // dwelling tab in behind the still-visible closing form, whose band
  // has no opaque backdrop in pager mode. `isFormActive` stays
  // untouched — the static header's Search↔X swap must still flip at
  // close-start.
  formMounted: boolean;
  // Optional tab-specific create action (the Plus button). When null
  // (Direct) the static header renders NO Plus — new-chat is gone and
  // people are found through search. Channels sets it to «create
  // channel» / «create community».
  primaryAction: StreamHeaderPrimaryAction | null;
};

export const mobilePagerCurtainAtom = atom<MobilePagerCurtainControls | null>(null);

// Per-tab pull-to-refresh reveal state, published by EVERY pane's
// StreamHeader (keyed by its `pinKey`, like `curtainPinnedByTabAtom`):
// true while that pane's curtain is at the `refresh` snap or a non-form
// drag is in flight — i.e. while the pane's refresh band is (or is
// being pulled) on screen.
//
// Consumed (ORed via `mobilePagerAnyRevealAtom`) by
// `PagerRefreshSingleton` — the strip-hosted, screen-static refresh
// chrome (one mascot card + ring field for the whole app) shows while
// ANY tab reveals. Why a per-tab map and not an active-pane boolean:
// an INACTIVE pane legitimately rests at `refresh` through its
// auto-close dwell after a swipe-away, and keeping the singleton alive
// for it makes a return swipe seamless — no dance restart.
//
// Per-key writes need no compare-and-clear choreography: each pane owns
// its key, so writers never race.
//
// Keyed by `MobilePagerTab` (not a free-form string): the pager-side
// readers index these maps by the pager's tab name, so a pane's
// `pinKey` MUST be the tab name — the type makes the compiler enforce
// what previously held only by convention.
export const curtainRefreshRevealByTabAtom = atom<Partial<Record<MobilePagerTab, boolean>>>({});

// OR over the per-tab reveal map. Derived so the singleton re-renders
// only on actual flips — jotai skips notifying when the derived value
// is Object.is-equal — not on map identity churn
// ({A:true} → {A:true,B:false}).
export const mobilePagerAnyRevealAtom = atom((get) =>
  Object.values(get(curtainRefreshRevealByTabAtom)).some(Boolean)
);

// ACTIVE pane's form-MOUNTED boolean, derived from the controls atom so
// the singleton does not subscribe to the whole controls object (it is
// rewritten on activation/primaryAction changes); the boolean only
// notifies on real flips. See MobilePagerCurtainControls.formMounted
// for why MOUNTED (not the snap-based isFormActive).
export const mobilePagerFormMountedAtom = atom(
  (get) => get(mobilePagerCurtainAtom)?.formMounted ?? false
);

// Imperative channel published the OTHER way — by the strip-hosted
// refresh singleton (PagerRefreshSingleton), consumed by the active
// pane's StreamHeader:
//
//   * `getVideo` — the live dance <video> (null while the poster is
//     shown). StreamHeader's pull-to-refresh auto-close reads the loop
//     position so the curtain only retracts at a dance-loop boundary.
//   * `spawnEcho` — one-shot echo pulse ring. The singleton is
//     `pointer-events: none` behind the panes, so it can never receive
//     the user's tap itself; the active pane's StreamHeader keeps an
//     invisible tap-catcher in the mascot band and proxies pokes here.
//
// Both members are stable (the singleton publishes one object on
// mount), so consumers can safely list the api object in effect deps.
export type MobilePagerMascotApi = {
  getVideo: () => HTMLVideoElement | null;
  spawnEcho: () => void;
};

export const mobilePagerMascotApiAtom = atom<MobilePagerMascotApi | null>(null);

// Per-tab pinned state for the StreamHeader curtain. Each listing
// surface (Direct / Channels / Bots) reads/writes by its own stable
// `pinKey` so the pin survives the route-driven listing-pane unmount
// that happens when the user taps into a Room and back: the atom
// outlives any individual StreamHeader instance.
//
// In-memory only (no localStorage). The pinned state is ephemeral UI
// state — user expects a fresh «header visible» default on cold start;
// surviving app restarts would be surprising.
//
// Map shape (keys are the pager tab names — the type enforces the
// pinKey↔tab-name coupling that `MobileTabsPagerHeader`'s elevation
// gate relies on when it reads `pinnedByTab[activeTab]`):
//   { direct: true, channels: false, bots: false }
// Missing key ⇒ false. Each tab's pin lives independently — pinning
// Direct doesn't pin Channels.
export const curtainPinnedByTabAtom = atom<Partial<Record<MobilePagerTab, boolean>>>({});

// True while a horseshoe bottom sheet (settings on Direct, workspace
// switcher on Channels) is geometrically active — i.e. `expandedPx > 0`,
// which covers both the in-flight drag and the committed-open state.
//
// Source of truth for `MobileTabsPagerHeader`'s z-elevation: when this
// is true, the static pager header bumps to a positive z-index so it
// paints above the strip's stacking context and covers the safe-top +
// tabsRow band with `SurfaceVariant.Container`. Without this, the
// container's `VOJO_HORSESHOE_VOID_COLOR` paint that drives the carve
// would bleed up through the transparent strip stack into the system-
// tray strip.
//
// Tracking the GEOMETRIC signal (not the sheet-open atoms) is load-
// bearing: container bg flips to the void colour the moment a drag-
// up on `DirectSelfRow` crosses 0 px, but the sheet atom only commits
// after 80 px past the threshold. Driving elevation off the geometry
// keeps the static header z-above the void from the first frame of
// drag — no «pane goes black then header re-paints light blue» flash.
//
// Mount exclusivity: in pager mode all three listing surfaces stay
// mounted, but only one's horseshoe can have `horseshoeActive=true`
// because the pager's own swipe gesture is gated off whenever a sheet
// is open and the per-pane drag origin (DirectSelfRow / WorkspaceFooter)
// only receives touches on the active pane. Two horseshoes writing
// `true` at the same time is therefore unreachable through any user
// flow.
export const mobileHorseshoeActiveAtom = atom<boolean>(false);
