import { useEffect, useRef, useState, type ReactNode } from 'react';
import { type Axis, lockAxis, canScrollFurther, resist, shouldCommit, neighborIndex } from '../../lib/tab-swipe';

interface SwipeableTabsProps<T extends string> {
  /** Visible tab ids, in order. */
  tabs: T[];
  activeTab: T;
  onTabChange: (tab: T) => void;
  /** Swiping right past the threshold while on the FIRST tab (e.g. close). */
  onSwipeBackFromFirst?: () => void;
  /**
   * Render a tab's content. `preview` is true for the neighbor mounted DURING a
   * drag — return a lightweight placeholder for side-effecting tabs (e.g. a
   * noVNC iframe or SSH WebSocket) so a partial/cancelled swipe doesn't open
   * connections; the real content mounts once the swipe commits (preview=false).
   */
  renderTab: (tab: T, preview: boolean) => ReactNode;
}

type Mode = 'idle' | 'pending' | 'vert' | 'scroll' | 'drag';

/**
 * Horizontal swipe-between-tabs with finger-following content and a peeking
 * neighbor (iOS-style). Native horizontal scrolling INSIDE the content (e.g. a
 * wide <pre>) wins until it reaches its edge, so scrolling a code block no
 * longer steals the tab gesture. The active layer's transform is mutated
 * imperatively during the drag so the (heavy) tab panels don't re-render per
 * frame; the neighbor is mounted only while a drag is in progress.
 */
export function SwipeableTabs<T extends string>({
  tabs,
  activeTab,
  onTabChange,
  onSwipeBackFromFirst,
  renderTab,
}: SwipeableTabsProps<T>) {
  const containerRef = useRef<HTMLDivElement>(null);
  const activeRef = useRef<HTMLDivElement>(null);
  const peekRef = useRef<HTMLDivElement>(null);
  // The neighbor tab to mount while dragging ('left' = next on the right edge,
  // 'right' = prev on the left edge). null = no drag in progress.
  const [peek, setPeek] = useState<{ dir: 'left' | 'right'; tab: T } | null>(null);

  const g = useRef({
    startX: 0,
    startY: 0,
    axis: null as Axis | null,
    mode: 'idle' as Mode,
    scroller: null as Element | null,
    dir: null as 'left' | 'right' | null,
    width: 0,
    dragX: 0,
    rawDx: 0,
  });

  // Latest props for the native (re-attached) handlers.
  const propsRef = useRef({ tabs, activeTab, onTabChange, onSwipeBackFromFirst });
  propsRef.current = { tabs, activeTab, onTabChange, onSwipeBackFromFirst };

  const baseOffset = (dir: 'left' | 'right') => (dir === 'left' ? '100%' : '-100%');

  const paint = (dx: number, animate: boolean) => {
    const a = activeRef.current;
    const p = peekRef.current;
    const t = animate ? 'transform 0.24s cubic-bezier(0.22,0.61,0.36,1)' : 'none';
    if (a) {
      a.style.transition = t;
      a.style.transform = `translate3d(${dx}px,0,0)`;
    }
    if (p && g.current.dir) {
      p.style.transition = t;
      p.style.transform = `translate3d(calc(${baseOffset(g.current.dir)} + ${dx}px),0,0)`;
    }
  };

  // Find the nearest horizontally-scrollable ancestor between `el` and the
  // container (inclusive of el, exclusive of container).
  const findScroller = (el: Element | null): Element | null => {
    let node = el;
    const stop = containerRef.current;
    while (node && node !== stop) {
      if (node instanceof HTMLElement) {
        const ox = getComputedStyle(node).overflowX;
        if ((ox === 'auto' || ox === 'scroll') && node.scrollWidth - node.clientWidth > 1) {
          return node;
        }
      }
      node = node.parentElement;
    }
    return null;
  };

  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;

    const reset = () => {
      g.current.mode = 'idle';
      g.current.axis = null;
      g.current.scroller = null;
      g.current.dir = null;
      g.current.dragX = 0;
    };

    const onStart = (e: TouchEvent) => {
      if (e.touches.length !== 1) return;
      g.current.mode = 'idle';
      // Don't fight cursor placement, button presses, or link taps: ignore
      // gestures that begin on a form control / button / anchor / editable.
      const target = e.target as Element | null;
      if (target?.closest('input, textarea, select, button, a, [contenteditable="true"], [data-no-swipe]')) {
        return;
      }
      const t = e.touches[0];
      g.current.startX = t.clientX;
      g.current.startY = t.clientY;
      g.current.axis = null;
      g.current.mode = 'pending';
      g.current.scroller = findScroller(e.target as Element | null);
      g.current.width = el.clientWidth;
      g.current.dir = null;
      g.current.dragX = 0;
    };

    const onMove = (e: TouchEvent) => {
      if (g.current.mode === 'idle' || g.current.mode === 'vert' || g.current.mode === 'scroll') return;
      const t = e.touches[0];
      if (!t) return;
      const dx = t.clientX - g.current.startX;
      const dy = t.clientY - g.current.startY;

      if (g.current.axis === null) {
        const a = lockAxis(dx, dy);
        if (!a) return;
        g.current.axis = a;
        if (a === 'v') {
          g.current.mode = 'vert';
          return;
        }
      }
      if (g.current.axis !== 'h') return;

      // Only decide to yield to native scroll BEFORE a drag has started. Once
      // dragging, reversing direction must not hand the gesture back to the
      // scroller mid-drag (that would leave the panels painted off-center).
      const sc = g.current.scroller;
      if (g.current.mode !== 'drag' && sc && canScrollFurther(sc as HTMLElement, dx)) {
        g.current.mode = 'scroll';
        return;
      }

      e.preventDefault(); // own the horizontal gesture
      const { tabs: ts, activeTab: at } = propsRef.current;
      const idx = ts.indexOf(at);
      const ni = neighborIndex(idx, dx, ts.length);
      const hasNeighbor = ni !== null;
      const dir: 'left' | 'right' = dx < 0 ? 'left' : 'right';

      if (g.current.mode !== 'drag') {
        g.current.mode = 'drag';
      }
      if (g.current.dir !== dir) {
        g.current.dir = dir;
        // Mount the neighbor (or null at an edge so the layer just rubber-bands).
        setPeek(hasNeighbor ? { dir, tab: ts[ni] } : null);
      }
      g.current.rawDx = dx;
      g.current.dragX = resist(dx, hasNeighbor, g.current.width);
      paint(g.current.dragX, false);
    };

    const finish = () => {
      if (g.current.mode !== 'drag') {
        reset();
        return;
      }
      const { tabs: ts, activeTab: at, onTabChange: change, onSwipeBackFromFirst: back } = propsRef.current;
      const idx = ts.indexOf(at);
      const dragX = g.current.dragX;
      const width = g.current.width;
      const ni = neighborIndex(idx, dragX, ts.length);
      const dir = g.current.dir;

      // First tab + rightward swipe past threshold → "back" (close), no neighbor.
      // Use the RAW (un-resisted) movement: dragX is rubber-banded to ~12% width
      // at an edge, so it could never reach the threshold otherwise.
      if (ni === null && dir === 'right' && idx === 0 && back && Math.abs(g.current.rawDx) >= Math.max(60, width * 0.22)) {
        paint(0, true);
        window.setTimeout(() => { reset(); setPeek(null); }, 240);
        back();
        return;
      }

      if (ni !== null && shouldCommit(dragX, width, true)) {
        const target = ts[ni];
        // Slide fully, then commit the tab change and snap back to center.
        paint(dir === 'left' ? -width : width, true);
        window.setTimeout(() => {
          change(target);
          setPeek(null);
          g.current.dir = null;
          requestAnimationFrame(() => paint(0, false));
          reset();
        }, 240);
      } else {
        paint(0, true);
        window.setTimeout(() => { setPeek(null); reset(); }, 240);
      }
    };

    // touchcancel = the OS/browser aborted the gesture (notification, multi-touch,
    // interruption) — NOT a user commit. Snap back without changing tabs.
    const cancel = () => {
      if (g.current.mode === 'drag') {
        paint(0, true);
        window.setTimeout(() => { setPeek(null); reset(); }, 240);
      } else {
        reset();
      }
    };

    el.addEventListener('touchstart', onStart, { passive: true });
    el.addEventListener('touchmove', onMove, { passive: false });
    el.addEventListener('touchend', finish, { passive: true });
    el.addEventListener('touchcancel', cancel, { passive: true });
    return () => {
      el.removeEventListener('touchstart', onStart);
      el.removeEventListener('touchmove', onMove);
      el.removeEventListener('touchend', finish);
      el.removeEventListener('touchcancel', cancel);
    };
  }, []);

  return (
    <div ref={containerRef} className="relative h-full w-full overflow-hidden">
      <div ref={activeRef} className="absolute inset-0 will-change-transform">
        {renderTab(activeTab, false)}
      </div>
      {peek && (
        <div
          ref={peekRef}
          className="absolute inset-0 will-change-transform"
          style={{ transform: `translate3d(${baseOffset(peek.dir)},0,0)` }}
        >
          {renderTab(peek.tab, true)}
        </div>
      )}
    </div>
  );
}
