import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { ReactNode } from 'react';

// Small gap kept between the sheet's top edge (where the drag handle lives) and
// whatever sits above it, so the handle always stays fully on-screen.
const SHEET_TOP_GAP_PX = 16;

interface VisualViewportState {
  height: number;
  bottomInset: number;
}

interface MobileBottomSheetProps {
  children: ReactNode;
  legend?: ReactNode;
  editingBar?: ReactNode;
  onCoveredHeightChange?: (height: number) => void;
}

function getVisualViewportState(avoidKeyboard: boolean): VisualViewportState {
  if (!avoidKeyboard) {
    return {
      height: window.innerHeight,
      bottomInset: 0,
    };
  }

  const vv = window.visualViewport;
  if (!vv) {
    return {
      height: window.innerHeight,
      bottomInset: 0,
    };
  }

  const bottomInset = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
  return {
    height: vv.height,
    bottomInset,
  };
}

function useVisualViewportState(avoidKeyboard: boolean): VisualViewportState {
  const [state, setState] = useState(() => getVisualViewportState(avoidKeyboard));

  useEffect(() => {
    const vv = window.visualViewport;
    const update = () => {
      const next = getVisualViewportState(avoidKeyboard);
      setState((prev) =>
        prev.height === next.height && prev.bottomInset === next.bottomInset ? prev : next
      );
    };

    update();

    window.addEventListener('resize', update);
    window.addEventListener('orientationchange', update);
    if (avoidKeyboard) {
      vv?.addEventListener('resize', update);
      vv?.addEventListener('scroll', update);
    }

    return () => {
      window.removeEventListener('resize', update);
      window.removeEventListener('orientationchange', update);
      if (avoidKeyboard) {
        vv?.removeEventListener('resize', update);
        vv?.removeEventListener('scroll', update);
      }
    };
  }, [avoidKeyboard]);

  return state;
}

function clamp(value: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, value));
}

function isKeyboardEditableElement(element: HTMLElement): boolean {
  if (element instanceof HTMLTextAreaElement) return true;
  if (element instanceof HTMLInputElement) {
    return ![
      'button',
      'checkbox',
      'color',
      'file',
      'hidden',
      'image',
      'radio',
      'range',
      'reset',
      'submit',
    ].includes(element.type);
  }
  return element.isContentEditable;
}

function getKeyboardEditableElement(target: EventTarget | null): HTMLElement | null {
  if (!(target instanceof Element)) return null;

  const element = target.closest('input, textarea, [contenteditable]');
  if (!(element instanceof HTMLElement)) return null;

  return isKeyboardEditableElement(element) ? element : null;
}

export default function MobileBottomSheet({
  children,
  legend,
  editingBar,
  onCoveredHeightChange,
}: MobileBottomSheetProps) {
  const [keyboardAvoidanceActive, setKeyboardAvoidanceActive] = useState(false);
  const viewport = useVisualViewportState(keyboardAvoidanceActive);
  const sheetRef = useRef<HTMLDivElement>(null);
  const scrollRef = useRef<HTMLDivElement>(null);
  const dragStartYRef = useRef(0);
  const dragStartHeightRef = useRef(0);
  const scrollIntoViewTimerRef = useRef<number | null>(null);
  const [height, setHeight] = useState<number | null>(null);
  const [isDragging, setIsDragging] = useState(false);
  // The sheet is fixed to the window, but it logically lives below the app
  // header. Measure the top of our layout parent (the map area, which starts at
  // the header's bottom edge) so the sheet can never grow tall enough to slide
  // its drag handle up underneath the header, where it'd become unreachable and
  // leave the sheet stuck open.
  const [topInset, setTopInset] = useState(0);

  useLayoutEffect(() => {
    const measure = () => {
      const parent = sheetRef.current?.parentElement;
      setTopInset(parent ? Math.max(0, parent.getBoundingClientRect().top) : 0);
    };
    measure();
    window.addEventListener('resize', measure);
    window.addEventListener('orientationchange', measure);
    return () => {
      window.removeEventListener('resize', measure);
      window.removeEventListener('orientationchange', measure);
    };
  }, []);

  const heightBounds = useMemo(() => {
    const available = viewport.height;
    const min = Math.min(132, Math.max(104, available * 0.22));
    // Reserve the header strip (topInset) plus a small gap at the top so the
    // drag handle always stays on-screen and grabbable.
    const ceiling = available - topInset - SHEET_TOP_GAP_PX;
    return {
      min,
      initial: Math.min(available * 0.56, Math.max(330, available * 0.44)),
      max: Math.max(min, ceiling),
    };
  }, [viewport.height, topInset]);

  const currentHeight = clamp(height ?? heightBounds.initial, heightBounds.min, heightBounds.max);

  useEffect(() => {
    setHeight((value) =>
      value == null ? value : clamp(value, heightBounds.min, heightBounds.max)
    );
  }, [heightBounds]);

  useEffect(() => {
    onCoveredHeightChange?.(Math.round(currentHeight + viewport.bottomInset));
  }, [currentHeight, onCoveredHeightChange, viewport.bottomInset]);

  const handlePointerDown = useCallback(
    (e: React.PointerEvent) => {
      e.preventDefault();
      (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
      dragStartYRef.current = e.clientY;
      dragStartHeightRef.current = currentHeight;
      setIsDragging(true);
    },
    [currentHeight]
  );

  const handlePointerMove = useCallback(
    (e: React.PointerEvent) => {
      if (dragStartHeightRef.current === 0) return;
      const nextHeight = dragStartHeightRef.current + dragStartYRef.current - e.clientY;
      setHeight(clamp(nextHeight, heightBounds.min, heightBounds.max));
    },
    [heightBounds]
  );

  const handlePointerUp = useCallback(() => {
    if (dragStartHeightRef.current === 0) return;
    dragStartHeightRef.current = 0;
    setIsDragging(false);
  }, []);

  const handleSheetPointerDown = useCallback((event: React.PointerEvent) => {
    if (getKeyboardEditableElement(event.target)) return;

    const activeElement = document.activeElement;
    if (
      activeElement instanceof HTMLElement &&
      sheetRef.current?.contains(activeElement) &&
      isKeyboardEditableElement(activeElement)
    ) {
      activeElement.blur();
    }
  }, []);

  useEffect(() => {
    const sheet = sheetRef.current;
    if (!sheet) return;

    const handleFocusIn = (event: FocusEvent) => {
      const target = getKeyboardEditableElement(event.target);
      if (!target || !sheet.contains(target)) return;

      setKeyboardAvoidanceActive(true);
      const keyboardMinHeight = Math.min(heightBounds.max, Math.max(300, viewport.height * 0.55));
      setHeight((value) => Math.max(value ?? heightBounds.initial, keyboardMinHeight));
      if (scrollIntoViewTimerRef.current != null) {
        window.clearTimeout(scrollIntoViewTimerRef.current);
      }
      scrollIntoViewTimerRef.current = window.setTimeout(() => {
        target.scrollIntoView({ block: 'center', behavior: 'smooth' });
      }, 120);
    };

    const handleFocusOut = (event: FocusEvent) => {
      const nextTarget = getKeyboardEditableElement(event.relatedTarget);
      if (nextTarget && sheet.contains(nextTarget)) return;

      setKeyboardAvoidanceActive(false);
    };

    sheet.addEventListener('focusin', handleFocusIn);
    sheet.addEventListener('focusout', handleFocusOut);
    return () => {
      sheet.removeEventListener('focusin', handleFocusIn);
      sheet.removeEventListener('focusout', handleFocusOut);
      if (scrollIntoViewTimerRef.current != null) {
        window.clearTimeout(scrollIntoViewTimerRef.current);
      }
    };
  }, [heightBounds.initial, heightBounds.max, viewport.height]);

  return (
    <section
      ref={sheetRef}
      className="fixed inset-x-0 z-30 flex flex-col rounded-t-2xl bg-white dark:bg-navy-950 shadow-2xl border-t border-warm-200 dark:border-navy-700 overflow-hidden"
      onPointerDownCapture={handleSheetPointerDown}
      style={{
        bottom: viewport.bottomInset,
        height: currentHeight,
        paddingBottom: 'env(safe-area-inset-bottom)',
        transition:
          isDragging || viewport.bottomInset > 0
            ? undefined
            : 'height 140ms ease, bottom 180ms ease',
      }}
    >
      <div className="relative shrink-0 px-4 py-2">
        <div
          className="absolute inset-x-0 top-1/2 z-10 h-11 -translate-y-1/2 touch-none"
          onPointerDown={handlePointerDown}
          onPointerMove={handlePointerMove}
          onPointerUp={handlePointerUp}
          onPointerCancel={handlePointerUp}
        />
        <div
          className="pointer-events-none flex w-full items-center justify-center"
          role="presentation"
        >
          <span className="h-1.5 w-12 rounded-full bg-warm-300 dark:bg-navy-600" />
        </div>
      </div>

      {editingBar && <div className="shrink-0">{editingBar}</div>}

      {legend && (
        <div className="shrink-0 border-y border-warm-200 dark:border-navy-700">{legend}</div>
      )}

      <div
        ref={scrollRef}
        className="flex-1 min-h-0 overflow-y-auto overscroll-contain touch-pan-y"
      >
        {children}
      </div>
    </section>
  );
}
