// Bottom-up «horseshoe» sheet that wraps the mobile chat column for
// the media viewer. Sister of `MobileSettingsHorseshoe` (which wraps
// the DM list with the same idiom for the Settings tree) — same
// clip-path carve, same VAUL easing, same Strict-Mode-safe entry
// animation, same `keepMounted` delayed unmount.
//
// Differences from the settings sheet:
//   • Tap-to-open only. No drag-up origin — opening is driven from
//     `useOpenMediaViewer` called by `ImageContent.onClick`. The
//     20px handle band is the ONLY drag-sensitive area; touches on
//     the viewer body below run zoom / pan / horizontal swipe
//     instead.
//   • `RAIL_FRACTION = 3 / 4` (75% of viewport) vs settings's 2/3.
//   • Silhouette bg = `Background.Container` (deepest Dawn tone) for
//     a dark image backdrop, vs settings's `SurfaceVariant.Container`.
//   • Wrapped content is the chat column, not the DM list.
//   • No `--vojo-safe-top` reset — there's no PageNav inside the
//     viewer body that would inherit it; the viewer body has its own
//     padding policy.

import React, { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useAtomValue } from 'jotai';
import { useTranslation } from 'react-i18next';
import { mediaViewerAtom } from '../../state/mediaViewer';
import { useCloseMediaViewer } from '../../state/hooks/mediaViewer';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { VOJO_HORSESHOE_VOID_COLOR } from '../../styles/horseshoe';
import { MediaViewerBody } from './MediaViewerBody';
import * as css from './MobileMediaViewerHorseshoe.css';

const VAUL_EASING = 'cubic-bezier(0.32, 0.72, 0, 1)';
const ANIMATION_MS = 250;
// Drag distance past which release commits the close. Matches the
// settings sheet's 80px (the only commit gesture there too).
const COMMIT_THRESHOLD_PX = 80;
// Sheet height as a fraction of the viewport — product spec.
// Capped at runtime by the wrapper container's actual height
// (see `railHeightPx` calc below) so it can't spill past the
// chat header when chrome outside the chat column (e.g.
// `HorseshoeContainer.bottomRail` during a call) shrinks the
// wrapper.
const RAIL_VIEWPORT_FRACTION = 0.8;
// Absolute floor for tiny / split-screen viewports — the sheet
// becomes useless below this. NB: floor cannot exceed the
// container's ceiling (`Math.min` is applied last), so in extreme
// squeeze cases (split-screen, oversized call rail) the sheet
// silently shrinks below the floor rather than clipping the chat
// header.
const RAIL_MIN_PX = 200;
// Headroom reserved at the TOP of the container for the chat
// header (`PageHeader` = folds `Header size="600"`, ~64px) plus
// the 12px horseshoe void gap above the silhouette's rounded
// TL/TR carves. With this in place the sheet's top edge stops
// just below the chat header even when the wrapper container
// shrinks (e.g. `HorseshoeContainer.bottomRail` is on during a
// call). Previously the headroom was 8px which let the sheet
// climb over the header in tight viewports.
const CHAT_HEADER_RESERVED_PX = 76;
// Same emerge window as the settings sheet so the silhouette's
// rounding + void gap finish exactly when the gesture qualifies to
// commit.
const HORSESHOE_EMERGE_PX = 80;

// Symmetric cubic in-out — slow start, fast middle, slow finish.
// Same curve the settings / profile horseshoes use for their emerge
// (rationale at the top of `MobileSettingsHorseshoe.tsx`).
const easeInOutCubic = (t: number): number =>
  t < 0.5 ? 4 * t * t * t : 1 - ((-2 * t + 2) ** 3) / 2;

type DragState = {
  inputType: 'touch' | 'pointer';
  startY: number;
  deltaY: number;
};

type MobileMediaViewerHorseshoeProps = {
  children: ReactNode;
};

function MobileMediaViewerHorseshoeImpl({ children }: MobileMediaViewerHorseshoeProps) {
  const { t } = useTranslation();
  const entry = useAtomValue(mediaViewerAtom);
  const closeSheet = useCloseMediaViewer();

  const [drag, setDrag] = useState<DragState | null>(null);
  const [viewportHeight, setViewportHeight] = useState(() =>
    typeof window === 'undefined' ? 800 : window.innerHeight
  );

  useEffect(() => {
    const onResize = () => setViewportHeight(window.innerHeight);
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  // Measure the wrapper container directly via `ResizeObserver` so
  // the rail height tracks whatever vertical space is actually
  // available to the chat column. The viewport-based fraction is
  // the *intent* (90% of the screen — product spec), but
  // `HorseshoeContainer.bottomRail` (the call rail) sits OUTSIDE
  // the chat column inside the same viewport and shrinks our
  // wrapper when active. Without the container clamp, 90% of the
  // viewport could exceed the wrapper's height, and the sheet
  // would render up to / past the chat header which sits inside
  // the wrapper's appBody.
  const containerRef = useRef<HTMLDivElement>(null);
  const [containerHeight, setContainerHeight] = useState(() =>
    typeof window === 'undefined' ? 800 : window.innerHeight
  );

  useLayoutEffect(() => {
    const el = containerRef.current;
    if (!el) return undefined;
    setContainerHeight(el.clientHeight);
    const ro = new ResizeObserver((entries) => {
      const cr = entries[0]?.contentRect;
      if (cr) setContainerHeight(cr.height);
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  // Final rail = viewport × fraction, then floor-then-ceiling
  // clamped. Container (minus chat-header reserve) is the HARD
  // ceiling: when the wrapper shrinks (call rail / split-screen)
  // the sheet shrinks below `RAIL_MIN_PX` rather than spilling
  // over the chat header. Order matters — `Math.min` last makes
  // container win against floor.
  const idealRailPx = Math.round(viewportHeight * RAIL_VIEWPORT_FRACTION);
  const railHeightPx = Math.min(
    Math.max(0, containerHeight - CHAT_HEADER_RESERVED_PX),
    Math.max(RAIL_MIN_PX, idealRailPx)
  );
  const open = !!entry;

  // Entry-animation gate — see `MobileSettingsHorseshoe` for the
  // full rationale. `hasEnteredRef` mirror guards the atom-clearing
  // unmount cleanup against React 18 strict-mode dev rehearsal.
  const [hasEntered, setHasEntered] = useState(false);
  const hasEnteredRef = useRef(false);
  useLayoutEffect(() => {
    const id = requestAnimationFrame(() => {
      hasEnteredRef.current = true;
      setHasEntered(true);
    });
    return () => cancelAnimationFrame(id);
  }, []);

  // Keep the viewer body mounted through the 250ms exit slide so
  // the user sees the image slide down with the sheet instead of
  // an empty panel collapsing.
  const [keepMounted, setKeepMounted] = useState(open);
  useEffect(() => {
    if (open) {
      setKeepMounted(true);
      return undefined;
    }
    const id = window.setTimeout(() => setKeepMounted(false), ANIMATION_MS);
    return () => window.clearTimeout(id);
  }, [open]);

  // Persist the last opened entry through the exit animation so the
  // body keeps rendering the image as it slides down.
  const lastEntryRef = useRef(entry);
  if (entry) lastEntryRef.current = entry;

  const baseExpanded = open && hasEntered ? railHeightPx : 0;
  // Drag is close-only (positive deltaY); clamp negative deltas to
  // 0 so an upward reversal cancels rather than expanding past the
  // open height.
  const expandedPx = drag
    ? Math.max(0, Math.min(railHeightPx, baseExpanded - drag.deltaY))
    : baseExpanded;
  const expandedFraction = railHeightPx > 0 ? expandedPx / railHeightPx : 0;
  const isDragging = drag !== null;
  const horseshoeActive = expandedPx > 0;

  const handleRef = useRef<HTMLDivElement>(null);

  const dragRef = useRef<DragState | null>(null);
  dragRef.current = drag;
  const entryRef = useRef(entry);
  entryRef.current = entry;
  const closeSheetRef = useRef(closeSheet);
  closeSheetRef.current = closeSheet;

  // Clear the atom if the wrapper unmounts while the sheet is open
  // (route change). Strict-mode rehearsal guard via `hasEnteredRef`
  // — without it, the cleanup fires before the entry rAF and a
  // deep-link cold-start would clear the atom mid-rehearsal.
  useEffect(
    () => () => {
      if (!hasEnteredRef.current) return;
      if (entryRef.current) closeSheetRef.current();
    },
    []
  );

  // Hardware Escape (also dispatched by the global Android-back
  // handler via the portal-marker pattern below). Skip inputs /
  // contenteditable so typing inside a future caption field doesn't
  // dismiss the sheet.
  useEffect(() => {
    if (!open) return undefined;
    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key !== 'Escape') return;
      const target = e.target as HTMLElement | null;
      if (
        target &&
        (target.tagName === 'INPUT' ||
          target.tagName === 'TEXTAREA' ||
          target.isContentEditable)
      ) {
        return;
      }
      closeSheetRef.current();
    };
    window.addEventListener('keydown', onKeyDown);
    return () => window.removeEventListener('keydown', onKeyDown);
  }, [open]);

  // Drag-to-close only — handle band at the top of the silhouette.
  // No document-level open-drag listener; opening is tap-only via
  // `useOpenMediaViewer`.
  //
  // CLAMP, not early-return — see `MobileSettingsHorseshoe` for
  // the bug-fix rationale. Reversing the gesture must drag deltaY
  // back toward 0, not leave the stale value.
  useEffect(() => {
    const handleEl = handleRef.current;
    if (!handleEl) return undefined;

    const applyMove = (clientY: number, e: TouchEvent | PointerEvent) => {
      const d = dragRef.current;
      if (!d) return;
      const rawDelta = clientY - d.startY;
      const nextDelta = Math.max(0, rawDelta);
      if (e.cancelable) e.preventDefault();
      setDrag({ ...d, deltaY: nextDelta });
    };

    const applyEnd = () => {
      const d = dragRef.current;
      if (!d) return;
      if (d.deltaY > COMMIT_THRESHOLD_PX) {
        closeSheetRef.current();
      }
      setDrag(null);
    };

    const onHandleTouchStart = (e: TouchEvent) => {
      if (dragRef.current) return;
      if (!entryRef.current) return;
      const touch = e.touches[0];
      setDrag({ inputType: 'touch', startY: touch.clientY, deltaY: 0 });
    };
    const onTouchMove = (e: TouchEvent) => {
      const d = dragRef.current;
      if (!d || d.inputType !== 'touch') return;
      applyMove(e.touches[0].clientY, e);
    };
    const onTouchEnd = () => {
      const d = dragRef.current;
      if (!d || d.inputType !== 'touch') return;
      applyEnd();
    };

    const onHandlePointerDown = (e: PointerEvent) => {
      if (e.pointerType === 'touch') return;
      if (dragRef.current) return;
      if (!entryRef.current) return;
      if (e.button !== 0) return;
      setDrag({ inputType: 'pointer', startY: e.clientY, deltaY: 0 });
    };
    const onPointerMove = (e: PointerEvent) => {
      if (e.pointerType === 'touch') return;
      const d = dragRef.current;
      if (!d || d.inputType !== 'pointer') return;
      applyMove(e.clientY, e);
    };
    const onPointerEnd = (e: PointerEvent) => {
      if (e.pointerType === 'touch') return;
      const d = dragRef.current;
      if (!d || d.inputType !== 'pointer') return;
      applyEnd();
    };

    handleEl.addEventListener('touchstart', onHandleTouchStart, { passive: true });
    document.addEventListener('touchmove', onTouchMove, { passive: false });
    document.addEventListener('touchend', onTouchEnd, { passive: true });
    document.addEventListener('touchcancel', onTouchEnd, { passive: true });
    handleEl.addEventListener('pointerdown', onHandlePointerDown);
    document.addEventListener('pointermove', onPointerMove, { passive: false });
    document.addEventListener('pointerup', onPointerEnd, { passive: true });
    document.addEventListener('pointercancel', onPointerEnd, { passive: true });

    return () => {
      handleEl.removeEventListener('touchstart', onHandleTouchStart);
      document.removeEventListener('touchmove', onTouchMove);
      document.removeEventListener('touchend', onTouchEnd);
      document.removeEventListener('touchcancel', onTouchEnd);
      handleEl.removeEventListener('pointerdown', onHandlePointerDown);
      document.removeEventListener('pointermove', onPointerMove);
      document.removeEventListener('pointerup', onPointerEnd);
      document.removeEventListener('pointercancel', onPointerEnd);
    };
  }, []);

  // Geometry — same emerge ramp as the settings / profile horseshoes
  // (rationale at the top of `MobileSettingsHorseshoe`).
  let horseshoeRamp: number;
  if (isDragging) {
    horseshoeRamp = easeInOutCubic(Math.min(1, expandedPx / HORSESHOE_EMERGE_PX));
  } else {
    horseshoeRamp = expandedFraction > 0 ? 1 : 0;
  }
  const silhouetteRadiusPx = horseshoeRamp * css.HORSESHOE_RADIUS_PX;
  const appBodyRadiusPx = horseshoeRamp * css.HORSESHOE_RADIUS_PX;
  const appBodyGapPx = horseshoeRamp * css.HORSESHOE_GAP_PX;
  const appBodyMaskBottomPx = expandedPx + appBodyGapPx;

  const appBodyClipPath = `inset(0px 0px ${appBodyMaskBottomPx}px 0px round 0px 0px ${appBodyRadiusPx}px ${appBodyRadiusPx}px)`;

  const silhouetteTransition = isDragging
    ? 'none'
    : `height ${ANIMATION_MS}ms ${VAUL_EASING}, border-top-left-radius ${ANIMATION_MS}ms ${VAUL_EASING}, border-top-right-radius ${ANIMATION_MS}ms ${VAUL_EASING}`;
  const appBodyTransition = isDragging ? 'none' : `clip-path ${ANIMATION_MS}ms ${VAUL_EASING}`;

  const containerStyle: React.CSSProperties = {
    backgroundColor: horseshoeActive ? VOJO_HORSESHOE_VOID_COLOR : undefined,
  };

  const renderEntry = entry ?? lastEntryRef.current;
  const renderBody = !!renderEntry && (keepMounted || isDragging);

  // Marker portal for the global Android-back handler — when
  // `portalContainer.firstChild` matches our `data-vojo-…-active`
  // attribute, the back-button dispatches an Escape keydown that
  // the `keydown` effect above catches.
  const portalTarget =
    typeof document !== 'undefined'
      ? document.getElementById('portalContainer') ?? document.body
      : null;

  return (
    <div ref={containerRef} className={css.container} style={containerStyle}>
      {open && portalTarget
        ? createPortal(
            <div
              data-vojo-media-viewer-sheet-active="true"
              aria-hidden="true"
              style={{ display: 'none' }}
            />,
            portalTarget
          )
        : null}
      <div
        className={css.appBody}
        style={{
          clipPath: appBodyClipPath,
          transition: appBodyTransition,
          overscrollBehaviorY: 'contain',
        }}
      >
        {children}
      </div>

      <div
        className={css.silhouette}
        style={{
          height: `${expandedPx}px`,
          borderTopLeftRadius: `${silhouetteRadiusPx}px`,
          borderTopRightRadius: `${silhouetteRadiusPx}px`,
          transition: silhouetteTransition,
          visibility: expandedPx > 0 ? 'visible' : 'hidden',
          // Reset `--vojo-safe-top` so any descendant that paints a
          // `padding-top: var(--vojo-safe-top)` for status-bar safe-
          // area (e.g. PageNav-style headers, if anyone adds one
          // here later) doesn't push the viewer header down inside
          // the sheet — the sheet itself sits below the status bar.
          // Mirror of the settings horseshoe.
          ['--vojo-safe-top' as string]: '0px',
        }}
        role="dialog"
        aria-label={renderEntry?.body ?? t('MediaViewer.title', 'Media viewer')}
      >
        <div className={css.panelContent} style={{ height: `${railHeightPx}px` }}>
          <div
            ref={handleRef}
            className={css.panelHandle}
            aria-label={t('MediaViewer.drag_to_close', 'Drag down to close')}
          >
            <div className={css.panelHandleBar} />
          </div>
          <div className={css.panelBody}>
            {renderBody && renderEntry && (
              <MediaViewerBody entry={renderEntry} requestClose={() => closeSheetRef.current()} />
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

// Top-level branch — pass through on non-mobile (the desktop right
// pane handles the same atom over there). The mobile branch owns
// the drag / atom hooks in a sub-component so we don't run them on
// desktop renders.
export function MobileMediaViewerHorseshoe({
  children,
}: MobileMediaViewerHorseshoeProps): React.ReactElement {
  const isMobile = useScreenSizeContext() === ScreenSize.Mobile;
  if (!isMobile) {
    return children as React.ReactElement;
  }
  return <MobileMediaViewerHorseshoeImpl>{children}</MobileMediaViewerHorseshoeImpl>;
}
