// Wrapper around the room's chat column that adds the user-profile
// surface on top.
//
// Mobile (≤ 750): the top «horseshoe» rail. Panel and chat header
// live as siblings inside a single `silhouette` wrapper whose own
// `overflow:hidden + border-bottom-radius` cuts the bottom of the
// combined block. There's no separate header-radius / panel-radius
// hand-off mid-drag — the rounding is a property of the silhouette
// itself, so it stays visually anchored to «the bottom of whatever's
// currently last-visible» throughout the gesture without seams or
// blips. Replaces the legacy two-element layout where panel was
// `position: absolute` over a chat column that owned the header in
// flow; each had its own radius animated by the now-deleted
// `horseshoeEmerge` curve.
//
// Tablet + Desktop (> 750): no top horseshoe. The wrapper is a thin
// passthrough; the profile renders as a right-side pane via
// `RoomViewProfileSidePanel`, which `Room.tsx` mounts as a sibling
// of the chat column.

import React, { ReactNode, useEffect, useRef, useState } from 'react';
import { useAtomValue } from 'jotai';
import { config } from 'folds';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { userRoomProfileAtom } from '../../state/userRoomProfile';
import {
  useCloseUserRoomProfile,
  useOpenUserRoomProfile,
} from '../../state/hooks/userRoomProfile';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { useIsOneOnOne, useRoom } from '../../hooks/useRoom';
import { useSpaceOptionally } from '../../hooks/useSpace';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { getMemberAvatarMxc } from '../../utils/room';
import { getMxIdLocalPart, guessDmRoomUserId, mxcUrlToHttp } from '../../utils/matrix';
import { UserRoomProfile } from '../../components/user-profile/UserRoomProfile';
import { stopPropagation } from '../../utils/keyboard';
import colorMXID from '../../../util/colorMXID';
import * as css from './RoomViewProfilePanel.css';

// Card height as a fraction of the viewport.
const RAIL_HEIGHT_FRACTION = 0.42;
// Past this many pixels of drag the gesture commits (open or close).
const COMMIT_THRESHOLD_PX = 80;
const ANIMATION_MS = 250;
// Drag distance over which the horseshoe-radius + chat-gap +
// chat-radius ramp from 0 to their full value during finger-drag.
// Matched to COMMIT_THRESHOLD_PX so the silhouette is fully formed
// exactly when the gesture qualifies to commit — the user sees the
// rounding "earn" itself across the whole gesture instead of
// snapping in immediately. Combined with the easeInOutCubic curve
// below, gives a gentle start (corners barely emerge in first
// ~10px) → fast middle (the bulk of the round-in happens around
// the visual halfway point) → soft settle near commit.
const HORSESHOE_EMERGE_PX = 80;

// Vaul / iOS system curve (asymmetric ease-out — steep accelerate,
// long decelerate). Reads as "spring-like" without the overshoot of
// an actual spring, which is what makes Apple sheet transitions feel
// non-mechanical. Used on the CSS release transitions only — the
// release phase wants instant feedback + soft settle. Vaul itself
// also reserves this curve for release and tracks the finger linearly
// during drag; we intentionally diverge from that during drag (see
// `easeInOutCubic` below) because the user explicitly asked for a
// gentle-start ramp on the rounding, not direct-manipulation.
const VAUL_EASING = 'cubic-bezier(0.32, 0.72, 0, 1)';

// Symmetric cubic in-out S-curve — slow start, fast middle, slow
// finish. NOT a Vaul-curve approximation: Vaul's curve is asymmetric
// (steep accelerate, long decelerate) so it grows fast from the
// first pixel of drag, which would defeat the explicit user ask of
// «не сразу скругление». Symmetric in-out gives the rounding a
// barely-visible start that only blossoms around the halfway point,
// which is the felt-feel the user signed off on. Release animations
// keep the asymmetric Vaul curve in CSS where direct manipulation
// is over and the auto-animation can take centre stage.
const easeInOutCubic = (t: number): number =>
  t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;

type RoomViewProfilePanelProps = {
  header: ReactNode;
  children: ReactNode;
};

type DragState = {
  source: 'header' | 'panel';
  // Origin tag — `touch` for finger drags, `pointer` for mouse/pen.
  // Each path filters by this so an in-flight drag started by one
  // input device can never be advanced or ended by the other. The
  // failure mode it prevents: hybrid laptop (touchscreen + mouse)
  // where a touchstart fires but its matching touchend gets dropped
  // by the OS / browser, leaving `dragRef.current` populated; if the
  // user then moves the mouse, the pointer-path handlers would
  // otherwise apply mouse clientY against the touch's startY and
  // commit a bogus close. Tagging makes each path self-isolating.
  inputType: 'touch' | 'pointer';
  startY: number;
  deltaY: number;
};

// Mobile-only top horseshoe. Slides down from above on drag-down on
// the chat header (1:1 DM only); slides up to close on drag-up
// anywhere on the panel. Panel and chat header share a single
// `silhouette` wrapper whose `overflow:hidden + border-bottom-radius`
// owns the visible rounded bottom — see file header for the
// architectural rationale.
function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps) {
  const { t } = useTranslation();
  const profileState = useAtomValue(userRoomProfileAtom);
  const close = useCloseUserRoomProfile();
  const openProfile = useOpenUserRoomProfile();
  const room = useRoom();
  const space = useSpaceOptionally();
  const mx = useMatrixClient();
  const useAuthentication = useMediaAuthentication();
  const isOneOnOne = useIsOneOnOne();

  const myUserId = mx.getSafeUserId();
  const peerCandidate = isOneOnOne ? guessDmRoomUserId(room, myUserId) : undefined;
  const headerDragPeer =
    peerCandidate && peerCandidate !== myUserId ? peerCandidate : undefined;
  const headerDragEnabled = !!headerDragPeer;

  const [drag, setDrag] = useState<DragState | null>(null);
  const [avatarMode, setAvatarMode] = useState(false);

  const headerRef = useRef<HTMLDivElement>(null);
  const panelRef = useRef<HTMLDivElement>(null);

  // Close profile when the room changes — atom is global state and
  // would otherwise carry the previous room's userId into this room.
  useEffect(() => () => close(), [room.roomId, close]);

  // Reset avatar-zoom mode whenever the rendered user changes.
  useEffect(() => {
    setAvatarMode(false);
  }, [profileState?.userId]);

  const [railHeightPx, setRailHeightPx] = useState(() => {
    if (typeof window === 'undefined') return 400;
    return Math.round(window.innerHeight * RAIL_HEIGHT_FRACTION);
  });
  useEffect(() => {
    const onResize = () => {
      setRailHeightPx(Math.round(window.innerHeight * RAIL_HEIGHT_FRACTION));
    };
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  const open = !!profileState;
  const baseExpanded = open ? railHeightPx : 0;
  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 liveUserId =
    profileState?.userId ?? (drag?.source === 'header' ? headerDragPeer : undefined);
  const lastUserIdRef = useRef<string | undefined>(undefined);
  if (liveUserId) lastUserIdRef.current = liveUserId;
  const renderUserId = liveUserId ?? lastUserIdRef.current;

  const renderUserAvatarMxc = renderUserId
    ? getMemberAvatarMxc(room, renderUserId)
    : undefined;
  const renderUserAvatarUrl =
    (renderUserAvatarMxc &&
      mxcUrlToHttp(mx, renderUserAvatarMxc, useAuthentication, 720, 720, 'scale')) ??
    undefined;

  const dragRef = useRef<DragState | null>(null);
  dragRef.current = drag;
  const profileStateRef = useRef(profileState);
  profileStateRef.current = profileState;
  const openProfileRef = useRef(openProfile);
  openProfileRef.current = openProfile;
  const closeRef = useRef(close);
  closeRef.current = close;
  const roomIdRef = useRef(room.roomId);
  roomIdRef.current = room.roomId;
  const spaceIdRef = useRef(space?.roomId);
  spaceIdRef.current = space?.roomId;

  useEffect(() => {
    const headerEl = headerRef.current;
    const panelEl = panelRef.current;

    const dragOpenCords = (): { x: number; y: number; width: number; height: number } => {
      const rect = headerRef.current?.getBoundingClientRect();
      if (rect) return { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
      return { x: 0, y: 0, width: 0, height: 0 };
    };

    // Shared commit + cancel logic between touch and pointer paths.
    const applyMove = (clientY: number, e: TouchEvent | PointerEvent) => {
      const d = dragRef.current;
      if (!d) return;
      const rawDelta = clientY - d.startY;
      let nextDelta = rawDelta;
      if (d.source === 'header') {
        nextDelta = Math.max(0, rawDelta);
      } else {
        if (rawDelta > 0) return;
        nextDelta = rawDelta;
      }
      if (e.cancelable) e.preventDefault();
      setDrag({ ...d, deltaY: nextDelta });
    };

    const applyEnd = () => {
      const d = dragRef.current;
      if (!d) return;
      if (d.source === 'header' && d.deltaY > COMMIT_THRESHOLD_PX) {
        if (headerDragPeer) {
          openProfileRef.current(
            roomIdRef.current,
            spaceIdRef.current,
            headerDragPeer,
            dragOpenCords()
          );
        }
      } else if (d.source === 'panel' && -d.deltaY > COMMIT_THRESHOLD_PX) {
        closeRef.current();
      }
      setDrag(null);
    };

    // === Touch path === Unchanged from the touch-only implementation.
    // Touch events have implicit pointer-capture semantics (touchmove
    // keeps firing on the touchstart element until touchend) so we
    // don't need to escalate to document-level listeners the way the
    // mouse path below does. Kept separate from the pointer path so
    // touch UX is bit-identical to the prior version.
    const onHeaderTouchStart = (e: TouchEvent) => {
      if (dragRef.current) return;
      if (profileStateRef.current) return;
      if (!headerDragEnabled) return;
      const touch = e.touches[0];
      setDrag({ source: 'header', inputType: 'touch', startY: touch.clientY, deltaY: 0 });
    };

    const onPanelTouchStart = (e: TouchEvent) => {
      if (dragRef.current) return;
      if (!profileStateRef.current) return;
      const touch = e.touches[0];
      setDrag({ source: 'panel', 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();
    };

    // === Mouse / pen path === Pointer events gated on
    // `pointerType !== 'touch'` so touch devices stay on the touch
    // path above and never double-fire. pointermove + pointerup are
    // attached to `document` (not the trigger element) so the drag
    // keeps tracking when the mouse cursor wanders past the element
    // bounds — common when the user yanks the rail up fast and
    // overshoots the panel. The alternative (setPointerCapture on the
    // trigger) would steal pointerup from inner interactive children
    // (kebab popout button inside the user card, mention links) and
    // break their click handlers — document-level listeners avoid
    // that hijack entirely.
    const onHeaderPointerDown = (e: PointerEvent) => {
      if (e.pointerType === 'touch') return;
      if (dragRef.current) return;
      if (profileStateRef.current) return;
      if (!headerDragEnabled) return;
      if (e.button !== 0) return;
      setDrag({ source: 'header', inputType: 'pointer', startY: e.clientY, deltaY: 0 });
    };

    const onPanelPointerDown = (e: PointerEvent) => {
      if (e.pointerType === 'touch') return;
      if (dragRef.current) return;
      if (!profileStateRef.current) return;
      if (e.button !== 0) return;
      setDrag({ source: 'panel', inputType: 'pointer', startY: e.clientY, deltaY: 0 });
    };

    const onDocumentPointerMove = (e: PointerEvent) => {
      if (e.pointerType === 'touch') return;
      const d = dragRef.current;
      if (!d || d.inputType !== 'pointer') return;
      applyMove(e.clientY, e);
    };

    const onDocumentPointerEnd = (e: PointerEvent) => {
      if (e.pointerType === 'touch') return;
      const d = dragRef.current;
      if (!d || d.inputType !== 'pointer') return;
      applyEnd();
    };

    if (headerEl) {
      headerEl.addEventListener('touchstart', onHeaderTouchStart, { passive: true });
      headerEl.addEventListener('touchmove', onTouchMove, { passive: false });
      headerEl.addEventListener('touchend', onTouchEnd, { passive: true });
      headerEl.addEventListener('touchcancel', onTouchEnd, { passive: true });
      headerEl.addEventListener('pointerdown', onHeaderPointerDown);
    }
    if (panelEl) {
      panelEl.addEventListener('touchstart', onPanelTouchStart, { passive: true });
      panelEl.addEventListener('touchmove', onTouchMove, { passive: false });
      panelEl.addEventListener('touchend', onTouchEnd, { passive: true });
      panelEl.addEventListener('touchcancel', onTouchEnd, { passive: true });
      panelEl.addEventListener('pointerdown', onPanelPointerDown);
    }
    document.addEventListener('pointermove', onDocumentPointerMove, { passive: false });
    document.addEventListener('pointerup', onDocumentPointerEnd, { passive: true });
    document.addEventListener('pointercancel', onDocumentPointerEnd, { passive: true });

    return () => {
      if (headerEl) {
        headerEl.removeEventListener('touchstart', onHeaderTouchStart);
        headerEl.removeEventListener('touchmove', onTouchMove);
        headerEl.removeEventListener('touchend', onTouchEnd);
        headerEl.removeEventListener('touchcancel', onTouchEnd);
        headerEl.removeEventListener('pointerdown', onHeaderPointerDown);
      }
      if (panelEl) {
        panelEl.removeEventListener('touchstart', onPanelTouchStart);
        panelEl.removeEventListener('touchmove', onTouchMove);
        panelEl.removeEventListener('touchend', onTouchEnd);
        panelEl.removeEventListener('touchcancel', onTouchEnd);
        panelEl.removeEventListener('pointerdown', onPanelPointerDown);
      }
      document.removeEventListener('pointermove', onDocumentPointerMove);
      document.removeEventListener('pointerup', onDocumentPointerEnd);
      document.removeEventListener('pointercancel', onDocumentPointerEnd);
    };
  }, [headerDragEnabled, headerDragPeer]);

  // Horseshoe geometry — single ramp factor drives the silhouette
  // bottom radius, the chat top radius, AND the chat margin-top gap.
  //
  //   • Finger-drag: live formula over the first HORSESHOE_EMERGE_PX
  //     (80px = COMMIT_THRESHOLD_PX) of drag, so the rounding and
  //     gap visibly emerge across the whole commit gesture and finish
  //     exactly when the gesture qualifies to commit.
  //   • Release animation: binary on/off, paired with a CSS
  //     transition so the values decay smoothly in lockstep with the
  //     250ms panel-height / header-collapse animations.
  //
  // The same ramp factor feeds all three properties so they always
  // appear and disappear together — no per-element timing skew that
  // could create a perceived «blip».
  const horseshoeRamp = isDragging
    ? easeInOutCubic(Math.min(1, expandedPx / HORSESHOE_EMERGE_PX))
    : horseshoeActive
    ? 1
    : 0;

  const silhouetteRadiusPx = horseshoeRamp * css.HORSESHOE_RADIUS_PX;
  const chatRadiusPx = horseshoeRamp * css.HORSESHOE_RADIUS_PX;
  const chatGapPx = horseshoeRamp * css.HORSESHOE_GAP_PX;

  const panelViewportTransition = isDragging
    ? 'none'
    : `height ${ANIMATION_MS}ms ${VAUL_EASING}`;
  const headerViewportTransition = isDragging
    ? 'none'
    : `grid-template-rows ${ANIMATION_MS}ms ${VAUL_EASING}`;
  const silhouetteTransition = isDragging
    ? 'none'
    : `border-bottom-left-radius ${ANIMATION_MS}ms ${VAUL_EASING}, border-bottom-right-radius ${ANIMATION_MS}ms ${VAUL_EASING}`;
  const chatBodyTransition = isDragging
    ? 'none'
    : `margin-top ${ANIMATION_MS}ms ${VAUL_EASING}, border-top-left-radius ${ANIMATION_MS}ms ${VAUL_EASING}, border-top-right-radius ${ANIMATION_MS}ms ${VAUL_EASING}`;

  const containerStyle: React.CSSProperties = {
    backgroundColor: horseshoeActive ? '#090909' : undefined,
  };

  return (
    <div className={css.container} style={containerStyle}>
      <div
        className={css.silhouette}
        style={{
          borderBottomLeftRadius: `${silhouetteRadiusPx}px`,
          borderBottomRightRadius: `${silhouetteRadiusPx}px`,
          transition: silhouetteTransition,
        }}
      >
        <FocusTrap
          focusTrapOptions={{
            initialFocus: false,
            // Mobile: only swipe-up dismisses the rail. Click on chat /
            // scroll on chat must NOT close it AND must remain
            // interactive.
            clickOutsideDeactivates: false,
            allowOutsideClick: () => true,
            escapeDeactivates: stopPropagation,
            onDeactivate: () => {
              if (profileStateRef.current) close();
            },
            checkCanFocusTrap: () => Promise.resolve(),
          }}
          // Don't toggle `active` on drag — flipping it to false makes
          // focus-trap-react fire `onDeactivate` synthetically, so a
          // plain panel tap (deltaY=0) closes the rail. Trap stays
          // active for the entire open lifetime.
          active={open}
        >
          <div
            ref={panelRef}
            className={css.panelViewport}
            style={{
              height: `${expandedPx}px`,
              transition: panelViewportTransition,
              visibility: expandedPx > 0 ? 'visible' : 'hidden',
            }}
          >
            <div
              className={css.panelContent}
              style={{ height: `${railHeightPx}px` }}
            >
              {avatarMode ? (
                <button
                  type="button"
                  className={css.avatarFullView}
                  onClick={() => setAvatarMode(false)}
                  aria-label={t('Room.collapse_avatar')}
                >
                  {renderUserAvatarUrl ? (
                    <img
                      className={css.avatarFullImage}
                      src={renderUserAvatarUrl}
                      alt={renderUserId ?? ''}
                      draggable={false}
                    />
                  ) : (
                    <div
                      className={css.avatarFullFallback}
                      style={{ backgroundColor: colorMXID(renderUserId ?? '') }}
                    >
                      {(renderUserId && getMxIdLocalPart(renderUserId)?.[0]) ?? '?'}
                    </div>
                  )}
                </button>
              ) : (
                <div className={css.panelInner}>
                  {/* No visible scrollbar — the card content (hero +
                      ~4 info rows + a chip row) almost always fits,
                      and on the rare overflow case (lots of
                      moderation alerts) we keep functional scrolling
                      via the `panelScroll` class which sets
                      `overflow: auto` + `scrollbar-width: none` /
                      `::-webkit-scrollbar` hidden. */}
                  <div className={css.panelScroll}>
                    <div style={{ padding: config.space.S400 }}>
                      {renderUserId && (
                        <UserRoomProfile
                          userId={renderUserId}
                          onAvatarClick={() => setAvatarMode(true)}
                        />
                      )}
                    </div>
                  </div>
                  <div className={css.panelHandle} aria-label={t('Room.drag_to_close')}>
                    <div className={css.panelHandleBar} />
                  </div>
                </div>
              )}
            </div>
          </div>
        </FocusTrap>

        <div
          ref={headerRef}
          className={css.headerViewport}
          style={{
            // Collapse the chat header to 0 height as the panel
            // expands — same `1fr → 0fr` row-track trick the legacy
            // layout used. CSS-grid track interpolation is animatable
            // (unlike `height: auto`) and avoids measuring the
            // header's intrinsic height in JS. As the row collapses,
            // the silhouette's bottom edge moves from header-bottom
            // up to panel-bottom; the rounded corners stay anchored
            // there because they belong to the silhouette wrapper.
            gridTemplateRows: `${1 - expandedFraction}fr`,
            transition: headerViewportTransition,
            // While the rail is mostly open the (collapsed) header
            // shouldn't intercept drag-back-to-open gestures —
            // `pan-x` lets horizontal Android-back-edge gestures
            // still work.
            touchAction: headerDragEnabled ? 'pan-x' : undefined,
          }}
        >
          <div className={css.headerViewportInner}>{header}</div>
        </div>
      </div>

      <div
        className={css.chatBody}
        style={{
          marginTop: `${chatGapPx}px`,
          borderTopLeftRadius: chatRadiusPx ? `${chatRadiusPx}px` : undefined,
          borderTopRightRadius: chatRadiusPx ? `${chatRadiusPx}px` : undefined,
          // Clip chat content to the rounded top corners. Folds
          // popouts use Portals → escape this clip; the timeline
          // keeps its own scroll container.
          overflow: chatRadiusPx > 0 ? 'hidden' : undefined,
          transition: chatBodyTransition,
          // Suppress browser pull-to-refresh / scroll-chaining so a
          // header-drag-down doesn't fire pull-to-refresh briefly
          // before the touchmove handler intercepts.
          overscrollBehaviorY: 'contain',
        }}
      >
        {children}
      </div>
    </div>
  );
}

// Top-level router. On non-mobile we pass through — the side-pane
// surface is rendered by `Room.tsx` as a sibling, so the wrapper just
// hands `header` and `children` back to the parent. The mobile branch
// owns its own state machine in a sub-component so we don't run
// drag/avatar hooks on desktop renders.
export function RoomViewProfilePanel({ header, children }: RoomViewProfilePanelProps) {
  const isMobile = useScreenSizeContext() === ScreenSize.Mobile;
  if (!isMobile) {
    return (
      <>
        {header}
        {children}
      </>
    );
  }
  return <MobileProfileHorseshoe header={header}>{children}</MobileProfileHorseshoe>;
}
