import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useAtom } from 'jotai';
import { curtainPinnedByTabAtom } from '../../state/mobilePagerHeader';
import type { MobilePagerTab } from '../mobile-tabs-pager/MobilePagerPaneContext';
import { CURTAIN_SNAP_MS, MASCOT_CARD_PX, SEARCH_FORM_BASE_PX, TABS_ROW_PX } from './geometry';

// Discrete snap stops for the curtain. The curtain's resting `top`
// is derived from this value plus the live finger drag delta. There
// is no separate «active vs reveal» mode flag — the snap encodes both.
export type CurtainSnap =
  | 'closed' // curtain flush under tabs row; nothing revealed
  | 'refresh' // dancing-mascot pull-to-refresh card revealed (momentary)
  | 'form-search'; // full search form revealed

export const isFormSnap = (snap: CurtainSnap): snap is 'form-search' => snap === 'form-search';

export const isRefreshSnap = (snap: CurtainSnap): snap is 'refresh' => snap === 'refresh';

// Whether a form is currently rendered in the header. Stays set during
// the curtain's close transition so the form has content to slide
// behind; cleared by `acknowledgeClosed` after the snap settles at
// `closed`. There is only one form (search) — new-chat was folded
// into search's People section — so this is a simple on/off flag.
export type ActiveForm = 'search' | null;

export type CurtainState = {
  snap: CurtainSnap;
  // Per-tab pin overlay. Stored in `curtainPinnedByTabAtom` keyed by
  // the consumer-supplied `pinKey` so the lock survives the route-
  // driven listing-pane unmount when the user taps into a Room and
  // back. Each tab keeps its own pin (Direct/Channels/Bots are
  // independent).
  pinned: boolean;
  // Setter for the pinned overlay. Called by the gesture hook on
  // commit (drag-up-from-closed past threshold sets true; drag-down-
  // while-pinned past threshold sets false).
  setPinned: (next: boolean) => void;
  activeForm: ActiveForm;
  // Live finger delta in px. Added to the snap-derived resting top to
  // compute the curtain's visible top. Stays at 0 when no gesture is
  // in flight. Positive = finger pulled down (refresh reveal); negative
  // = finger pulled up (close gesture).
  liveDragPx: number;
  // True while a touch gesture is in flight. Controls the curtain's
  // `top` transition: disabled while dragging (curtain tracks finger
  // 1:1), restored on release so the snap commit animates smoothly.
  isDragging: boolean;
  // Live measured height of the active form's outer; used to compute
  // the curtain's resting `top` when `snap === 'form-search'`. `null`
  // while no form is mounted.
  formHeightPx: number | null;
  // Ref pointing at the rendered form's outer — a ResizeObserver
  // watches this to feed `formHeightPx`. Consumer attaches it to the
  // form's wrapping element.
  formMeasureRef: React.RefObject<HTMLDivElement>;
  // Open the search form. Sets `snap` and `activeForm` synchronously.
  open: () => void;
  // Close the curtain (raise it back to `closed`). Keeps `activeForm`
  // set until the snap transition lands so the form stays mounted
  // during the slide-up.
  close: () => void;
  // Commit a snap stop directly. Used by the touch gesture on release.
  // Also resets `liveDragPx` and `isDragging` in one batched update.
  // Narrowed to the two non-form destinations the gesture hooks ever
  // reach — refresh-reveal and close. Form snaps are entered through
  // `open()` which sets `activeForm` synchronously alongside the snap.
  commit: (next: 'refresh' | 'closed') => void;
  // Setter for the live drag delta — called from the touch gesture on
  // every touchmove. Updates are batched by React inside event handlers.
  setLiveDrag: (px: number, dragging: boolean) => void;
  // Notify the hook that the curtain reached `closed` so any lingering
  // form can be unmounted (called from the curtain element's
  // `onTransitionEnd`).
  acknowledgeClosed: () => void;
};

// Resting `top` (px) of the curtain for a given snap stop and the
// currently measured form height (null falls back to the base).
//
// The refresh snap rests exactly `MASCOT_CARD_PX` below the tabs row so
// the dancing-mascot card is fully revealed below the boundary.
//
// The form rests at exactly `TABS_ROW_PX + formHeight` — the search
// results are a scroll list, and any strip between the last visible
// result and the curtain's top reads as «content cut off early» rather
// than as breathing room. So the form's measured height already reaches
// the curtain top: the list clips flush at the curtain's edge.
// `SEARCH_FORM_BASE_PX` is the pre-measure fallback and is sized (= the
// form's full outer height) so it equals the measured `formH`, avoiding
// any open-time top jump.
export function snapTopPx(snap: CurtainSnap, formH: number | null): number {
  switch (snap) {
    case 'closed':
      return TABS_ROW_PX;
    case 'refresh':
      return TABS_ROW_PX + MASCOT_CARD_PX;
    case 'form-search':
      return TABS_ROW_PX + (formH ?? SEARCH_FORM_BASE_PX);
    default:
      return TABS_ROW_PX;
  }
}

// `pinKey` is typed as the pager tab name (not a free-form string):
// the pin and reveal maps it keys into are read back by the pager and
// its static header via `[activeTab]` / `[tab]`, so the coupling is a
// hard contract, compiler-enforced since Jun 2026.
export function useCurtainState(pinKey: MobilePagerTab): CurtainState {
  const [snap, setSnap] = useState<CurtainSnap>('closed');
  const [activeForm, setActiveForm] = useState<ActiveForm>(null);
  const [formHeightPx, setFormHeightPx] = useState<number | null>(null);
  const [liveDragPx, setLiveDragPx] = useState(0);
  const [isDragging, setIsDragging] = useState(false);
  // Per-tab pin lives in `curtainPinnedByTabAtom` so the lock 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.
  const [pinnedMap, setPinnedMap] = useAtom(curtainPinnedByTabAtom);
  const pinned = !!pinnedMap[pinKey];

  const formMeasureRef = useRef<HTMLDivElement>(null);

  const setPinned = useCallback(
    (next: boolean) => {
      setPinnedMap((prev) => {
        // Compare-and-skip so we don't allocate a fresh object (and
        // re-render every other subscriber of the atom) when nothing
        // actually changes.
        if (!!prev[pinKey] === next) return prev;
        return { ...prev, [pinKey]: next };
      });
      // Drop any in-flight live drag on commit so the curtain renders
      // at the new pinned-derived top without a residual finger offset.
      setLiveDragPx(0);
      setIsDragging(false);
    },
    [pinKey, setPinnedMap]
  );

  const open = useCallback(() => {
    setActiveForm('search');
    setSnap('form-search');
    setLiveDragPx(0);
    setIsDragging(false);
    // Safety net: clear pin so the form is visible. In practice the
    // visible opener (the tabs-row Search icon, via the per-pane header
    // or the static pager header) is covered by the curtain when pinned,
    // so the user can't trigger this directly — but a future
    // programmatic open() would otherwise mount the form behind the
    // still-pinned curtain at curtainTop=0 and present an invisible
    // form.
    setPinned(false);
  }, [setPinned]);

  const close = useCallback(() => {
    setSnap('closed');
    setLiveDragPx(0);
    setIsDragging(false);
  }, []);

  const commit = useCallback((next: 'refresh' | 'closed') => {
    setSnap(next);
    setLiveDragPx(0);
    setIsDragging(false);
    // A 'refresh' commit reveals the mascot, so drop any lingering form.
    // Without this, a fast close→pull-to-refresh landed inside the form's
    // ~280–480ms close-slide window (before `acknowledgeClosed`/the safety
    // timer clear `activeForm`) would leave `activeForm` set, and the
    // header's `activeForm ? form : mascot` gate would render the closing
    // search form in place of the mascot during the dance.
    //
    // The 'closed' destination intentionally KEEPS `activeForm` set so the
    // closing transition has form content beneath the curtain as it slides
    // up — `acknowledgeClosed` clears it once the snap settles at `closed`.
    if (next === 'refresh') setActiveForm(null);
  }, []);

  const setLiveDrag = useCallback((px: number, dragging: boolean) => {
    setLiveDragPx(px);
    setIsDragging(dragging);
  }, []);

  const acknowledgeClosed = useCallback(() => {
    if (snap === 'closed') setActiveForm(null);
  }, [snap]);

  // Measure the form's outer height while it's mounted. We do NOT
  // overwrite the measured height to null on unmount — keeping the
  // last-known height stable lets the close transition target the
  // same `top` value as the open transition (no jump at SNAP_MS-end).
  useLayoutEffect(() => {
    if (!activeForm) return undefined;
    const el = formMeasureRef.current;
    if (!el) return undefined;
    const measure = () => setFormHeightPx(el.offsetHeight);
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, [activeForm]);

  // Safety-net for missed `transitionend` (route unmount mid-anim,
  // browser quirks). Once snap settles at `closed`, force-drop the
  // form after a generous window past the snap duration.
  const timerRef = useRef<number | null>(null);
  useEffect(() => {
    if (snap !== 'closed') {
      if (timerRef.current !== null) {
        window.clearTimeout(timerRef.current);
        timerRef.current = null;
      }
      return undefined;
    }
    timerRef.current = window.setTimeout(() => {
      setActiveForm(null);
    }, CURTAIN_SNAP_MS + 200);
    return () => {
      if (timerRef.current !== null) {
        window.clearTimeout(timerRef.current);
        timerRef.current = null;
      }
    };
  }, [snap]);

  return useMemo(
    () => ({
      snap,
      pinned,
      setPinned,
      activeForm,
      liveDragPx,
      isDragging,
      formHeightPx,
      formMeasureRef,
      open,
      close,
      commit,
      setLiveDrag,
      acknowledgeClosed,
    }),
    [
      snap,
      pinned,
      setPinned,
      activeForm,
      liveDragPx,
      isDragging,
      formHeightPx,
      open,
      close,
      commit,
      setLiveDrag,
      acknowledgeClosed,
    ]
  );
}
