import React, {
  MutableRefObject,
  ReactNode,
  TransitionEvent,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { useMatch, useNavigate } from 'react-router-dom';
import { useAtomValue, useSetAtom } from 'jotai';
import { Box, Icon, IconButton, Icons, toRem } from 'folds';
import { BOTS_PATH, CHANNELS_PATH, DIRECT_PATH } from '../../pages/paths';
import { isNativePlatform } from '../../utils/capacitor';
import { useBotPresets } from '../../features/bots/catalog';
import { useMobilePagerPane } from '../mobile-tabs-pager/MobilePagerPaneContext';
import {
  MobilePagerCurtainControls,
  StreamHeaderPrimaryAction,
  mobilePagerCurtainAtom,
} from '../../state/mobilePagerHeader';
import { TABS_ROW_PX, WEB_TABS_ROW_PX } from './geometry';
import { settingsSheetAtom } from '../../state/settingsSheet';
import { channelsWorkspaceSheetAtom } from '../../state/channelsWorkspaceSheet';
import * as css from './StreamHeader.css';
import { Segment } from './Segment';
import { Chip } from './Chip';
import { isFormSnap, snapTopPx, useCurtainState } from './useCurtainState';
import { useCurtainHandleGesture } from './useCurtainHandleGesture';
import { useCurtainBodyGesture } from './useCurtainBodyGesture';
import { InlineNewChatForm } from './forms/InlineNewChatForm';
import { InlineRoomSearch } from './forms/InlineRoomSearch';

const INLINE_FORM_ID = 'stream-header-inline-form';

type StreamHeaderProps = {
  // Scroll viewport that hosts the chat list under the curtain. The
  // curtain BODY gesture (`useCurtainBodyGesture`) reads this ref's
  // `scrollHeight`/`clientHeight` to decide whether to engage: long
  // lists keep native scroll, short / empty lists drive the curtain
  // via body drag. May be a ref whose `.current` is null on listing
  // surfaces that render their empty state directly as a curtain
  // child without wrapping it in `PageNavContent` (Direct's
  // `DirectEmpty`, ChannelsRootNav's `ChannelsLanding`) — the body
  // gesture treats null as «not scrollable» and engages.
  scrollRef: MutableRefObject<HTMLDivElement | null>;
  // Curtain contents — the chat list. The list is rendered inside an
  // `overflow: auto` div that the gesture hook listens to.
  children: ReactNode;
  // Optional row(s) pinned to the bottom of the curtain (DirectSelfRow,
  // WorkspaceFooter). Hidden while a form is active so the on-screen
  // keyboard's viewport resize doesn't push them up over the form
  // (see commit 14ed080).
  bottomPinned?: ReactNode;
  // Stable identifier used to persist the curtain's pinned overlay
  // across listing-pane remounts (the user taps into a Room and back,
  // which unmounts the listing pane). Pin state is stored in
  // `curtainPinnedByTabAtom[pinKey]` so it outlives any individual
  // StreamHeader instance. Each listing tab (Direct/Channels/Bots)
  // passes its own key; the Channels landing CTA and workspace
  // listing share `"channels"` so pin survives the toggle between
  // empty state and a chosen workspace.
  pinKey: string;
  // Optional override for the Plus button. When omitted the header
  // renders the default «new chat» action that opens InlineNewChatForm
  // via the curtain. Channels overrides this with «create channel» /
  // «create community» so the same Plus slot launches a contextual
  // action instead of the DM-creation form.
  primaryAction?: StreamHeaderPrimaryAction;
};

export function StreamHeader({
  scrollRef,
  children,
  bottomPinned,
  pinKey,
  primaryAction,
}: StreamHeaderProps) {
  const { t } = useTranslation();
  const navigate = useNavigate();
  const bots = useBotPresets();
  const navOpts = useMemo(() => ({ replace: isNativePlatform() }), []);

  const directMatch = useMatch({ path: DIRECT_PATH, caseSensitive: true, end: false });
  const botsMatch = useMatch({ path: BOTS_PATH, caseSensitive: true, end: false });
  const channelsMatch = useMatch({ path: CHANNELS_PATH, caseSensitive: true, end: false });
  const showBotsSegment = bots.length > 0 || !!botsMatch;

  // Pager mode wiring. When this StreamHeader is mounted inside
  // MobileTabsPager, the shared static tabs row at the pager root
  // owns the visible segments + action icons; our local tabs row is
  // kept in DOM (preserving the curtain's TABS_ROW_PX-based snap
  // geometry) but rendered with `opacity: 0` (still tap-able). Only
  // the currently active pane writes its curtain controls to
  // `mobilePagerCurtainAtom` so the shared icons drive THIS curtain.
  //
  // `selectTabInstant` is the pager's tap-commit entrypoint: when our
  // invisible per-pane Segments capture a tap (because the static
  // header sits behind the strip in z-stack at rest), routing through
  // this callback runs the same commit path as the static header's
  // taps and suppresses the swipe-finish slide animation so tab
  // switches feel snappy. Falls back to a plain `navigate(...)` on
  // surfaces outside pager mode (desktop, non-listing routes).
  const pagerPane = useMobilePagerPane();
  const inPagerMode = pagerPane !== null;
  const isActivePagerPane = pagerPane?.isActive ?? false;
  const selectTabInstant = pagerPane?.selectTabInstant ?? null;

  const onSegmentDirect = useCallback(() => {
    if (selectTabInstant) selectTabInstant('direct');
    else navigate(DIRECT_PATH, navOpts);
  }, [selectTabInstant, navigate, navOpts]);
  const onSegmentChannels = useCallback(() => {
    if (selectTabInstant) selectTabInstant('channels');
    else navigate(CHANNELS_PATH, navOpts);
  }, [selectTabInstant, navigate, navOpts]);
  const onSegmentBots = useCallback(() => {
    if (selectTabInstant) selectTabInstant('bots');
    else navigate(BOTS_PATH, navOpts);
  }, [selectTabInstant, navigate, navOpts]);

  const curtain = useCurtainState(pinKey);

  // Suppress every curtain gesture whenever the user is interacting
  // with something else that would otherwise race the pin path:
  //
  //   * Settings sheet open (DirectSelfRow-originated bottom sheet) —
  //     a drag-up on the still-visible list above the sheet would
  //     mutate the pin atom underneath the sheet and the user would
  //     see an unexpected pinned curtain on dismissal.
  //   * Workspace switcher sheet open — same shape, on the Channels
  //     workspace surface.
  //   * Inactive pager pane — the strip clips offscreen panes so they
  //     shouldn't receive touches in practice, but bind defense-in-
  //     depth so a stray pointer event on a translateX'd pane never
  //     pins someone else's tab.
  //
  // Mirrors `MobileTabsPager.gestureDisabled` which suppresses the
  // pager's OWN horizontal-swipe gesture under the same conditions.
  const settingsSheetOpen = !!useAtomValue(settingsSheetAtom);
  const workspaceSheetOpen = !!useAtomValue(channelsWorkspaceSheetAtom);
  const offscreenPagerPane = inPagerMode && !isActivePagerPane;
  const gestureDisabled = settingsSheetOpen || workspaceSheetOpen || offscreenPagerPane;

  // Two parallel curtain-gesture surfaces:
  //
  //   * `useCurtainHandleGesture` — the dedicated 32 px drag-handle
  //     at the top of the curtain. Crisp 1:1 finger ↔ curtain. From
  //     closed the gesture is a free-range drag spanning pin↔closed↔
  //     peek in one motion (`closed-free`); other snaps drive single-
  //     destination transitions (unpin / close-peek / form-close).
  //     Engages regardless of whether the chat list is scrollable —
  //     the handle is a distinct surface and never competes with list
  //     scroll. Only rendered on native (`isNativePlatform()`).
  //
  //   * `useCurtainBodyGesture` — anywhere on the curtain body
  //     OUTSIDE the handle (chat list, empty-state placeholder).
  //     Rubber-banded (0.65) for all transitions, so the body drag
  //     reads as physically «heavier» than the handle's crisp pull.
  //     Engages only when the chat list has no scrollable content;
  //     additionally bails on touches that start inside the bottom-
  //     pinned slot (DirectSelfRow / WorkspaceFooter have their own
  //     drag-to-open bottom sheets) and on touches that start while
  //     pinned (unpin is HANDLE-only — the user has to grab the
  //     dedicated affordance to release the lock).
  //
  // Both hooks share `handleVisual` (mirrors desktop
  // `PageNavResizeHandle`: `dragging` lights up the grabber pill;
  // `atCommit` stretches + brightens it once the user crosses the
  // per-transition commit threshold). The two surfaces are mutually
  // exclusive on each touch (handle's listener short-circuits when
  // the touch starts on the handle; body's listener does the same
  // when it ISN'T on the handle), so they never fight over the
  // visual.
  const handleRef = useRef<HTMLDivElement>(null);
  const curtainRef = useRef<HTMLDivElement>(null);
  const bottomPinnedRef = useRef<HTMLDivElement>(null);
  const [handleVisual, setHandleVisual] = useState<{ dragging: boolean; atCommit: boolean }>({
    dragging: false,
    atCommit: false,
  });
  useCurtainHandleGesture({
    handleRef,
    snap: curtain.snap,
    pinned: curtain.pinned,
    setPinned: curtain.setPinned,
    setLiveDrag: curtain.setLiveDrag,
    commit: curtain.commit,
    disabled: gestureDisabled,
    setHandleState: setHandleVisual,
  });
  useCurtainBodyGesture({
    curtainRef,
    handleRef,
    bottomPinnedRef,
    scrollRef,
    snap: curtain.snap,
    pinned: curtain.pinned,
    setPinned: curtain.setPinned,
    setLiveDrag: curtain.setLiveDrag,
    commit: curtain.commit,
    disabled: gestureDisabled,
    setHandleState: setHandleVisual,
  });

  const isActive = isFormSnap(curtain.snap);
  const openSearch = useCallback(() => curtain.open('search'), [curtain]);
  const openChat = useCallback(() => curtain.open('chat'), [curtain]);
  const { close } = curtain;

  // Memoised controls object so the cleanup's identity check (atom
  // compare-and-clear) is meaningful — without useMemo a fresh object
  // would be created on every render and the cleanup of an earlier
  // render would never match the atom's current contents.
  const pagerControls = useMemo<MobilePagerCurtainControls>(
    () => ({
      openSearch,
      openChat,
      closeForm: close,
      isFormActive: isActive,
      primaryAction: primaryAction ?? null,
    }),
    [openSearch, openChat, close, isActive, primaryAction]
  );

  const setPagerCurtain = useSetAtom(mobilePagerCurtainAtom);
  useEffect(() => {
    if (!isActivePagerPane) return undefined;
    setPagerCurtain(pagerControls);
    // Compare-and-clear cleanup: only wipe the atom if it still holds
    // OUR controls. If another pane became active between this render
    // and the cleanup (rapid tab switch), it has already overwritten
    // the atom with its own controls — we must not clobber that.
    return () => {
      setPagerCurtain((prev) => (prev === pagerControls ? null : prev));
    };
  }, [isActivePagerPane, pagerControls, setPagerCurtain]);

  // Curtain's `top` is the resting snap position plus the live drag
  // delta. React-driven (no inline DOM writes), so finger-tracking and
  // commit happen in the same render pipeline and there's no
  // intermediate "snap back, then animate" flash on release.
  //
  // When `pinned` is true the local snap (kept at {closed, peek,
  // form-*}) is overridden — the curtain rests at y = 0 inside the
  // stage (= y = safe-top in viewport), covering the tabs row. The
  // global pinned atom shares this state across every listing tab so
  // swiping between Direct / Channels / Bots preserves the lock.
  //
  // `platformOffset` is the web-only shift that lifts every non-pinned
  // snap by the delta between native and web tabs-row heights. Tabs
  // row on web is `WEB_TABS_ROW_PX` (= 54px, matching PageHeader on
  // the right pane); `snapTopPx` is computed against `TABS_ROW_PX`
  // (= 64px) which stays authoritative for native pin/peek geometry.
  // Subtracting the delta on web realigns the closed/form snaps with
  // the smaller tabs row without touching the snap-state machine.
  // Pinned (= 0) doesn't need the offset because the safe-top + native
  // contract owns that case and pinned is native-only.
  const platformOffset = isNativePlatform() ? 0 : WEB_TABS_ROW_PX - TABS_ROW_PX;
  const curtainTop = curtain.pinned
    ? 0 + curtain.liveDragPx
    : snapTopPx(curtain.snap, curtain.formHeightPx) + platformOffset + curtain.liveDragPx;

  // After the curtain settles at `closed`, unmount any lingering form.
  // Guarded so unrelated transitionend events (e.g. children's own
  // transitions bubbling up) don't drop the form mid-animation.
  const onCurtainTransitionEnd = useCallback(
    (evt: TransitionEvent<HTMLDivElement>) => {
      if (evt.target !== evt.currentTarget) return;
      if (evt.propertyName !== 'top') return;
      curtain.acknowledgeClosed();
    },
    [curtain]
  );

  // On-screen keyboard detection via VisualViewport API. Global
  // viewport-meta is `interactive-widget=resizes-content` (load-
  // bearing for the room composer's keyboard-follow behaviour), which
  // shrinks the layout viewport when a soft keyboard appears. Any
  // `bottom: 0` child — including DirectSelfRow inside the curtain —
  // rises with the shrunken viewport and ends up sitting RIGHT ABOVE
  // the keyboard, blocking the inline form the user is typing into.
  //
  // Fix: when the keyboard is up, collapse the `bottomPinned` slot
  // to zero height so it neither claims flex space at the curtain
  // bottom nor renders above the keyboard. The user perceives the
  // keyboard as overlaying everything below the form (matching their
  // mental model: "клавиатура рисуется поверх кнопок и чатов, кнопка
  // настройки остаётся прибитой снизу"). The row reappears the moment
  // the keyboard retracts.
  //
  // The reference-height tracking mirrors `AuthLayout.tsx`: bump the
  // reference upward on every grow (so rotation / keyboard-close
  // events stay self-correcting) and treat a meaningful shrink (>=
  // KEYBOARD_PROBE_PX) as «keyboard is up». The probe avoids
  // spurious flips on small browser-chrome animations.
  const [keyboardOpen, setKeyboardOpen] = useState(false);
  useEffect(() => {
    // Desktop browsers / Electron have no soft keyboard, but their
    // VisualViewport DOES shrink on aggressive page zoom (Ctrl+`+`).
    // That used to slip past as a hidden quirk while the curtain
    // rendered as a rounded card; once the web variant flattened the
    // curtain it became a visible regression — the DirectSelfRow at
    // the bottom would collapse to height: 0 under zoom and read as
    // broken layout. Gate the listener to native so the probe only
    // arms where a real soft keyboard can actually appear.
    if (!isNativePlatform()) return undefined;
    const vv = window.visualViewport;
    if (!vv) return undefined;
    const KEYBOARD_PROBE_PX = 100;
    let referenceH = vv.height;
    let rafId: number | null = null;
    const apply = () => {
      rafId = null;
      if (vv.height > referenceH) referenceH = vv.height;
      setKeyboardOpen(referenceH - vv.height >= KEYBOARD_PROBE_PX);
    };
    const onResize = () => {
      if (rafId === null) rafId = requestAnimationFrame(apply);
    };
    apply();
    vv.addEventListener('resize', onResize);
    return () => {
      if (rafId !== null) cancelAnimationFrame(rafId);
      vv.removeEventListener('resize', onResize);
    };
  }, []);

  return (
    <div className={css.stage} data-platform={isNativePlatform() ? undefined : 'web'}>
      <header className={css.header}>
        {/* ── Tabs row + action icons (always visible) ───────────
            In pager mode the row stays mounted (curtain snap math
            depends on its TABS_ROW_PX height) but is painted invisible
            via `opacity: 0` because the shared static tabs row at the
            pager root owns the visible chrome. Critically opacity (not
            `visibility: hidden`) keeps the row HIT-TESTABLE while
            invisible — the strip's stacking context paints ON TOP of
            the static pager header at rest (the curtain-rises-over-
            header contract), so without per-pane taps the segments
            would be unreachable. Both rows wire onClick to the same
            navigate() destination, so whichever row captures the tap
            the user-visible result is identical. The static header z-
            elevates only while a horseshoe sheet is active and the
            active pane isn't pinned — in that window the static header
            sits above the strip and captures taps directly; the rest
            of the time taps land on this opacity-0 row and resolve
            through its own per-pane onClick handlers.

            `aria-hidden` removes the duplicate (per-pane) segments and
            icons from the accessibility tree so screen readers don't
            announce three sets of "Direct / Channels / Bots" plus the
            single visible set from the shared static header. */}
        <div
          className={css.tabsRow}
          style={inPagerMode ? { opacity: 0 } : undefined}
          aria-hidden={inPagerMode || undefined}
        >
          <div className={css.tabsCluster}>
            <Segment
              active={!!directMatch}
              label={t('Direct.segment_dm')}
              onClick={onSegmentDirect}
            />
            <Segment
              active={!!channelsMatch}
              label={t('Direct.segment_channels')}
              onClick={onSegmentChannels}
            />
            {showBotsSegment && (
              <Segment
                active={!!botsMatch}
                label={t('Direct.segment_bots')}
                onClick={onSegmentBots}
              />
            )}
          </div>
          <Box grow="Yes" />
          {isActive ? (
            <IconButton
              variant="SurfaceVariant"
              fill="None"
              size="400"
              radii="Pill"
              onClick={close}
              aria-label={t('Direct.close')}
              aria-controls={INLINE_FORM_ID}
              aria-expanded
            >
              <Icon size="100" src={Icons.Cross} />
            </IconButton>
          ) : (
            <div className={css.iconsCluster}>
              <IconButton
                variant="SurfaceVariant"
                fill="None"
                size="400"
                radii="Pill"
                onClick={primaryAction ? primaryAction.onClick : openChat}
                aria-label={primaryAction ? primaryAction.label : t('Direct.create_chat')}
                // `aria-controls` points at the curtain-mounted form
                // region — drop it when `primaryAction` opens a portal
                // dialog (`Modal` lives outside this subtree, so there
                // is nothing to control here). `aria-haspopup="dialog"`
                // + `aria-expanded={false}` stay accurate for both
                // branches: the override opens a true Modal dialog.
                aria-controls={primaryAction ? undefined : INLINE_FORM_ID}
                aria-expanded={false}
                aria-haspopup="dialog"
              >
                <Icon size="100" src={primaryAction ? primaryAction.iconSrc : Icons.Plus} />
              </IconButton>
              <IconButton
                variant="SurfaceVariant"
                fill="None"
                size="400"
                radii="Pill"
                onClick={openSearch}
                aria-label={t('Search.search')}
                aria-controls={INLINE_FORM_ID}
                aria-expanded={false}
                aria-haspopup="dialog"
              >
                <Icon size="100" src={Icons.Search} />
              </IconButton>
            </div>
          )}
        </div>

        {/* ── Chips vs form ──────────────────────────────────────
            Mutually exclusive. While a form is mounted (including the
            curtain's close-snap window before `acknowledgeClosed`), the
            chips stay unrendered so the form doesn't visually jump
            from y = TABS_ROW_PX to y = TABS_ROW_PX + 2·CHIP_ROW_PX
            mid-animation.

            When chips are rendered: always present in their fixed
            header positions; the curtain occludes them by z-stacking.
            As the user drags the curtain down, the chips reveal from
            underneath naturally. `Chip.hidden` only controls keyboard
            focus (the chip paints normally; the curtain's z-index does
            the visual hiding). */}
        {curtain.activeForm ? (
          <div
            id={INLINE_FORM_ID}
            role="region"
            aria-label={
              curtain.activeForm === 'search'
                ? t('Search.search')
                : t('Direct.create_chat_subtitle')
            }
            className={css.formArea}
            style={{
              height: toRem(curtain.formHeightPx ?? 0),
            }}
          >
            <div ref={curtain.formMeasureRef} className={css.formInner}>
              {curtain.activeForm === 'search' && <InlineRoomSearch onClose={close} />}
              {curtain.activeForm === 'chat' && <InlineNewChatForm onClose={close} />}
            </div>
          </div>
        ) : (
          <>
            <div className={css.chipRow}>
              <Chip
                iconSrc={Icons.Search}
                label={t('Search.search')}
                onClick={openSearch}
                hidden={curtain.snap !== 'peek'}
              />
            </div>
            <div className={css.chipRow}>
              <Chip
                iconSrc={primaryAction ? primaryAction.iconSrc : Icons.Plus}
                label={primaryAction ? primaryAction.label : t('Direct.create_chat')}
                onClick={primaryAction ? primaryAction.onClick : openChat}
                hidden={curtain.snap !== 'peek'}
              />
            </div>
          </>
        )}
      </header>

      {/* ── Curtain layer ─────────────────────────────────────
          Renders ABOVE the header (z-index higher). `top` combines the
          snap-derived resting position with the live finger drag — one
          React-controlled inline style, no ref-based DOM writes. The
          transition is disabled during the drag and restored on commit
          so the snap commit animates smoothly without an intermediate
          "snap back then animate forward" flash. */}
      <div
        ref={curtainRef}
        className={css.curtain}
        style={{
          top: toRem(curtainTop),
          transition: curtain.isDragging ? 'none' : undefined,
        }}
        onTransitionEnd={onCurtainTransitionEnd}
      >
        {/* Drag handle — native-only. On web (desktop browsers,
            Electron) the curtain has no interactive snap states, so
            the handle would be pure decoration with no behaviour
            behind it; rendering it conditionally drops the 32 px
            grabber strip on those surfaces and lets the chat list
            sit flush against the curtain's rounded top.

            On native the handle hosts the authoritative curtain
            gesture (pin / unpin / peek / close-peek / form-close)
            and stays mounted across snap transitions so the gesture
            surface is always reachable when there is one to make.

            `data-dragging` / `data-at-commit` mirror the desktop
            `PageNavResizeHandle`: CSS selectors on `handleBar` light
            the pill up Primary-blue + stretch it when these flip.
            Both attrs are emitted/cleared only via React state set by
            the gesture hook (dedup'd), so the handle visual updates
            without slamming the DOM on every touchmove. */}
        {isNativePlatform() && (
          <div
            ref={handleRef}
            className={css.handle}
            data-dragging={handleVisual.dragging || undefined}
            data-at-commit={handleVisual.atCommit || undefined}
            aria-hidden
          >
            <div className={css.handleBar} />
          </div>
        )}
        {children}
        {/* `bottomPinned` (DirectSelfRow, WorkspaceFooter) is kept
            mounted across snaps so the curtain reads as a self-
            contained "screen" with its bottom row always pinned to
            the stage bottom. While the on-screen keyboard is up the
            slot collapses to `height: 0` so it neither paints nor
            claims flex space above the keyboard (see the
            `keyboardOpen` effect above for the rationale). */}
        {bottomPinned && (
          <div
            ref={bottomPinnedRef}
            className={css.bottomPinnedSlot}
            style={keyboardOpen ? { height: 0, overflow: 'hidden' } : undefined}
          >
            {bottomPinned}
          </div>
        )}
      </div>
    </div>
  );
}
