import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useVisualViewportInsets } from '../../hooks/useVisualViewport';

export interface BottomSheetProps {
  open: boolean;
  onClose: () => void;
  title: string;
  children: React.ReactNode;
}

/** Vertical drag distance (px) past which releasing the handle/header
 *  commits to closing the sheet, rather than snapping back open. */
const SWIPE_DISMISS_THRESHOLD_PX = 80;

/**
 * Mobile-only bottom sheet (Phase A). Slides up from the bottom of the
 * viewport to host one of the existing side-panel components
 * (BlocksPanel/PagesPanel/LayersPanel/AssetsPanel/GuidedStyles) unchanged,
 * as the mobile replacement for the desktop fixed left/right panel columns.
 * See `MobilePanelBar`, which owns which sheet (if any) is open.
 *
 * Deliberately NOT a generic replacement for `src/ui/Modal.tsx` (centered
 * dialog chrome) -- this is anchored to the bottom, sized to ~65dvh, and
 * has its own drag-handle affordance + internally scrollable body, all of
 * which Modal doesn't need for its centered use cases.
 *
 * Phase B adds two more deferred behaviors on top of Phase A's static sheet:
 *  - **Swipe-to-dismiss**: a touchstart/move/end drag tracked on the handle
 *    row + header (not the scrollable body, so it never fights a panel's own
 *    vertical scroll) that follows the finger while dragging down and either
 *    commits to `onClose()` past `SWIPE_DISMISS_THRESHOLD_PX` or snaps back
 *    with a short transition otherwise.
 *  - **On-screen-keyboard clearance**: `useVisualViewportInsets` reports how
 *    much the visual viewport has shrunk from the bottom (i.e. the
 *    keyboard's height); that's surfaced as a `--keyboard-inset` CSS custom
 *    property (see editor.css) which both lifts the whole backdrop/sheet
 *    clear of the keyboard AND caps the sheet's own max-height so it never
 *    extends into the space the keyboard occupies. A `focusin` listener also
 *    scrolls the newly-focused input into view once the keyboard has had a
 *    moment to animate in, as a second line of defense for a field that's
 *    still off-screen after the resize alone (e.g. deep in a long panel).
 */
export const BottomSheet: React.FC<BottomSheetProps> = ({ open, onClose, title, children }) => {
  useEffect(() => {
    if (!open) return;
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    };
    window.addEventListener('keydown', handleKeyDown);

    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
      document.body.style.overflow = prevOverflow;
    };
  }, [open, onClose]);

  const { keyboardInset } = useVisualViewportInsets();

  const bodyRef = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (!open) return;
    const bodyEl = bodyRef.current;
    if (!bodyEl) return;
    const handleFocusIn = (e: FocusEvent) => {
      const target = e.target as HTMLElement | null;
      if (!target) return;
      const tag = target.tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable) {
        // Give the on-screen keyboard a moment to finish animating in
        // before scrolling -- doing it immediately measures the pre-keyboard
        // layout and can undershoot.
        window.setTimeout(() => {
          target.scrollIntoView({ block: 'center', behavior: 'smooth' });
        }, 300);
      }
    };
    bodyEl.addEventListener('focusin', handleFocusIn);
    return () => bodyEl.removeEventListener('focusin', handleFocusIn);
  }, [open]);

  // Swipe-to-dismiss: tracked only on the handle row + header (never the
  // scrollable `.mobile-sheet-body`, which needs its own vertical touch
  // scrolling to keep working unimpeded).
  //
  // The authoritative drag distance lives in a REF (`dragOffsetRef`),
  // updated synchronously and imperatively on every touchmove -- NOT solely
  // in the `dragOffset` state used for rendering the drag transform. Two
  // touchmoves can fire back-to-back inside the same synchronous event
  // dispatch (e.g. a fast real swipe, or synthetic touch events fired
  // without a yield between them), and React batches their `setDragOffset`
  // calls into a single pending update that hasn't committed yet by the
  // time `touchend` runs in that same tick -- reading the `dragOffset`
  // STATE value directly in `handleTouchEnd` would then see a stale
  // (pre-drag) value and wrongly decide the swipe didn't clear the
  // threshold. The ref sidesteps that entirely (plain synchronous mutation,
  // no batching). Relatedly, `onClose()` mutates an ANCESTOR component's
  // state (MobileChromeContext's `closeSheet`) -- it must be called as a
  // plain statement in the event handler, never from inside a `setState`
  // functional updater (that runs during this component's own render phase
  // and trips React's "Cannot update a component while rendering a
  // different component" warning).
  const dragStartYRef = useRef<number | null>(null);
  const dragOffsetRef = useRef(0);
  const [dragOffset, setDragOffset] = useState(0);

  const handleTouchStart = useCallback((e: React.TouchEvent) => {
    if (e.touches.length !== 1) return;
    dragStartYRef.current = e.touches[0].clientY;
  }, []);

  const handleTouchMove = useCallback((e: React.TouchEvent) => {
    if (dragStartYRef.current === null) return;
    const delta = e.touches[0].clientY - dragStartYRef.current;
    // Only follow downward drags -- dragging up shouldn't do anything (the
    // sheet is already fully open; there's no "expand further" state).
    const next = Math.max(0, delta);
    dragOffsetRef.current = next;
    setDragOffset(next);
  }, []);

  const handleTouchEnd = useCallback(() => {
    dragStartYRef.current = null;
    const finalOffset = dragOffsetRef.current;
    dragOffsetRef.current = 0;
    setDragOffset(0);
    if (finalOffset > SWIPE_DISMISS_THRESHOLD_PX) onClose();
  }, [onClose]);

  if (!open) return null;

  return (
    <div
      className="mobile-sheet-backdrop"
      onClick={(e) => {
        if (e.target === e.currentTarget) onClose();
      }}
      style={{ ['--keyboard-inset' as any]: `${keyboardInset}px` }}
    >
      {/* Deliberately NOT aria-modal: the tab bar stays reachable/operable
          while a sheet is open (tapping another tab swaps sheets directly),
          so the rest of the screen must not be marked inert to assistive
          tech the way a true modal dialog would be. */}
      <div
        className="mobile-sheet"
        role="dialog"
        aria-label={title}
        style={{
          transform: dragOffset ? `translateY(${dragOffset}px)` : undefined,
          transition: dragOffset ? 'none' : 'transform 0.2s ease',
        }}
      >
        <div
          className="mobile-sheet-handle-row"
          onTouchStart={handleTouchStart}
          onTouchMove={handleTouchMove}
          onTouchEnd={handleTouchEnd}
          onTouchCancel={handleTouchEnd}
        >
          <span className="mobile-sheet-handle" aria-hidden="true" />
        </div>
        <div
          className="mobile-sheet-header"
          onTouchStart={handleTouchStart}
          onTouchMove={handleTouchMove}
          onTouchEnd={handleTouchEnd}
          onTouchCancel={handleTouchEnd}
        >
          <span className="mobile-sheet-title">{title}</span>
          <button
            type="button"
            className="mobile-sheet-close"
            onClick={onClose}
            aria-label={`Close ${title}`}
          >
            <i className="fa fa-times" aria-hidden="true" />
          </button>
        </div>
        <div className="mobile-sheet-body" ref={bodyRef}>{children}</div>
      </div>
    </div>
  );
};
