import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import FocusTrap from 'focus-trap-react';
import {
  Avatar,
  Box,
  Button,
  color,
  config,
  Dialog,
  Icon,
  Icons,
  Overlay,
  OverlayBackdrop,
  OverlayCenter,
  Text,
  toRem,
} from 'folds';
import {
  EventTimelineSetHandlerMap,
  EventType,
  JoinRule,
  RelationType,
  Room,
  RoomEvent,
} from 'matrix-js-sdk';
import { IRTCNotificationContent, RTCNotificationType } from 'matrix-js-sdk/lib/matrixrtc/types';
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
import {
  CallEmbedContextProvider,
  CallEmbedRefContextProvider,
  useCallHangupEvent,
  useCallJoined,
  useCallThemeSync,
  useCallMemberSoundSync,
  useCallStart,
} from '../hooks/useCallEmbed';
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
import { CallEmbed, useCallControlState } from '../plugins/call';
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
import { useMatrixClient } from '../hooks/useMatrixClient';
import CallSound from '../../../public/sound/call.ogg';
import { useCallMembersChange, useCallSession } from '../hooks/useCall';
import { useRemoteAllMuted } from '../hooks/useCallSpeakers';
import { useRoomAvatar, useRoomName } from '../hooks/useRoomMeta';
import { mDirectAtom } from '../state/mDirectList';
import { useMediaAuthentication } from '../hooks/useMediaAuthentication';
import { mxcUrlToHttp, getMxIdLocalPart } from '../utils/matrix';
import { RoomAvatar, RoomIcon } from './room-avatar';
import { useRoomNavigate } from '../hooks/useRoomNavigate';
import { getChatBg } from '../features/lotus/chatBackground';
import { useTheme, ThemeKind } from '../hooks/useTheme';
import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings';
import { getStateEvent, getMemberDisplayName } from '../utils/room';
import { StateEvent } from '../../types/matrix/room';
import { getPowersLevelFromMatrixEvent } from '../hooks/usePowerLevels';
import { getRoomCreatorsForRoomId } from '../hooks/useRoomCreators';
import { getRoomPermissionsAPI } from '../hooks/useRoomPermissions';
import { useLivekitSupport } from '../hooks/useLivekitSupport';
import { CallAvatarAnimation } from '../styles/Animations.css';
import { webRTCSupported } from '../utils/rtc';

const PIP_MIN_W = 200;
const PIP_MIN_H = 112;

type Corner = 'se' | 'sw' | 'ne' | 'nw';

/** Normalise the element to top/left positioning so resize math is uniform. */
function normaliseToTopLeft(el: HTMLElement) {
  const rect = el.getBoundingClientRect();
  el.style.left = `${rect.left}px`;
  el.style.top = `${rect.top}px`;
  el.style.width = `${rect.width}px`;
  el.style.height = `${rect.height}px`;
  el.style.right = 'auto';
  el.style.bottom = 'auto';
}

type IncomingCallInfo = {
  room: Room;
  sender: string;
  senderTs: number;
  lifetime: number;
  intent?: string;
  notificationType: RTCNotificationType;
  refEventId: string;
};
type IncomingCallProps = {
  dm: boolean;
  info: IncomingCallInfo;
  onIgnore: () => void;
  onAnswer: (room: Room, video: boolean) => void;
  onReject: (room: Room, eventId: string) => void;
};
function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallProps) {
  const mx = useMatrixClient();
  const useAuthentication = useMediaAuthentication();
  const livekitSupported = useLivekitSupport();
  const rtcSupported = webRTCSupported();
  const canAnswer = livekitSupported && rtcSupported;
  const { room } = info;

  const audioRef = useRef<HTMLAudioElement>(null);

  const roomName = useRoomName(room);
  const roomAvatar = useRoomAvatar(room, dm);
  const avatarUrl = roomAvatar
    ? (mxcUrlToHttp(mx, roomAvatar, useAuthentication, 96, 96, 'crop') ?? undefined)
    : undefined;

  const session = useCallSession(room);
  useCallMembersChange(
    session,
    useCallback(
      (members) => {
        if (members.length === 0) {
          onIgnore();
        }
      },
      [onIgnore],
    ),
  );

  const playSound = useCallback(() => {
    const audioElement = audioRef.current;
    audioElement?.play().catch(() => undefined);
  }, []);

  useEffect(() => {
    const audioEl = audioRef.current;
    if (info.notificationType === 'ring') {
      playSound();
    }
    return () => {
      if (audioEl) {
        audioEl.pause();
        audioEl.currentTime = 0;
      }
    };
  }, [playSound, info.notificationType]);

  useEffect(() => {
    const remaining = info.senderTs + info.lifetime - Date.now();
    if (remaining <= 0) {
      onIgnore();
      return;
    }
    const id = setTimeout(onIgnore, remaining);
    return () => clearTimeout(id);
  }, [info.senderTs, info.lifetime, onIgnore]);

  return (
    <>
      <Overlay open backdrop={<OverlayBackdrop />}>
        <OverlayCenter>
          <FocusTrap
            focusTrapOptions={{
              initialFocus: false,
              onDeactivate: () => onIgnore(),
              clickOutsideDeactivates: false,
              escapeDeactivates: false,
            }}
          >
            <Dialog style={{ maxWidth: toRem(324) }}>
              <Box style={{ padding: config.space.S400 }} direction="Column" gap="700">
                <Text size="T200" align="Center">
                  {getMemberDisplayName(info.room, info.sender) ??
                    getMxIdLocalPart(info.sender) ??
                    info.sender}
                </Text>
                <Box direction="Column" gap="500" alignItems="Center">
                  <Box shrink="No">
                    <Avatar size="500" className={CallAvatarAnimation}>
                      <RoomAvatar
                        roomId={room.roomId}
                        src={avatarUrl}
                        alt={roomName}
                        renderFallback={() => (
                          <RoomIcon
                            roomType={room.getType()}
                            size="400"
                            joinRule={room.getJoinRule()}
                            filled
                          />
                        )}
                      />
                    </Avatar>
                  </Box>
                  <Box grow="Yes" direction="Column" gap="100">
                    <Text size="H3" align="Center" truncate>
                      {roomName}
                    </Text>
                    <Text size="T300">
                      {info.intent === 'video' ? 'Incoming Video Call' : 'Incoming Voice Call'}
                    </Text>
                  </Box>
                </Box>
                {!livekitSupported && (
                  <Text
                    style={{ margin: 'auto', color: color.Critical.Main }}
                    size="L400"
                    align="Center"
                  >
                    Your homeserver does not support calling.
                  </Text>
                )}
                {!webRTCSupported() && (
                  <Text
                    style={{ margin: 'auto', color: color.Critical.Main }}
                    size="L400"
                    align="Center"
                  >
                    Your browser does not support WebRTC, which is required for calling.
                  </Text>
                )}
                <Box direction="Column" gap="300">
                  <Button
                    style={{ flexGrow: 1 }}
                    variant="Success"
                    size="400"
                    radii="400"
                    onClick={() => onAnswer(room, info.intent === 'video')}
                    before={
                      <Icon
                        size="200"
                        src={info.intent === 'video' ? Icons.VideoCamera : Icons.Phone}
                        filled
                      />
                    }
                    disabled={!canAnswer}
                  >
                    <Text as="span" size="B400">
                      Answer
                    </Text>
                  </Button>
                  <Button
                    style={{ flexGrow: 1 }}
                    variant={dm ? 'Critical' : 'Secondary'}
                    fill="Soft"
                    size="400"
                    radii="400"
                    onClick={() => (dm ? onReject(room, info.refEventId) : onIgnore())}
                    before={<Icon size="200" src={Icons.Cross} filled />}
                  >
                    <Text as="span" size="B400">
                      {dm ? 'Reject' : 'Ignore'}
                    </Text>
                  </Button>
                </Box>
              </Box>
            </Dialog>
          </FocusTrap>
        </OverlayCenter>
      </Overlay>
      <audio ref={audioRef} loop style={{ display: 'none' }}>
        <source src={CallSound} type="audio/ogg" />
      </audio>
    </>
  );
}

type IncomingCallListenerProps = {
  callEmbed?: CallEmbed;
  joined?: boolean;
};
function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps) {
  const mx = useMatrixClient();
  const directs = useAtomValue(mDirectAtom);
  const { navigateRoom } = useRoomNavigate();

  const [callInfo, setCallInfo] = useState<IncomingCallInfo>();
  const dm = callInfo ? directs.has(callInfo.room.roomId) : false;
  const startCall = useCallStart(dm);

  const handleTimelineEvent: EventTimelineSetHandlerMap[RoomEvent.Timeline] = useCallback(
    async (event, room, toStartOfTimeline, removed, data) => {
      // only process rtc notification reference events.
      // we do not want to wait to decrypt all events.
      if (event.getRelation()?.rel_type !== RelationType.Reference) return;

      if (event.isEncrypted()) {
        if (!event.isBeingDecrypted()) {
          await event.attemptDecryption(mx.getCrypto() as CryptoBackend);
        }
        await event.getDecryptionPromise();
      }

      if (
        !room ||
        event.getType() !== EventType.RTCNotification ||
        event.getSender() === mx.getSafeUserId() ||
        !data.liveEvent
      ) {
        return;
      }

      const sender = event.getSender();
      const content = event.getContent<IRTCNotificationContent>();
      const senderTs =
        content.sender_ts - event.getTs() > 20000 ? event.getTs() : content.sender_ts;
      const lifetime = Math.min(content.lifetime, 120000);
      const notificationType = content.notification_type;
      const relation =
        event.getRelation()?.rel_type === RelationType.Reference ? event.getRelation() : undefined;
      const refEventId = relation?.event_id;

      const mention =
        content['m.mentions']?.room ||
        content['m.mentions']?.user_ids?.includes(mx.getSafeUserId());
      if (!sender || !refEventId || !mention || Date.now() >= senderTs + lifetime) {
        return;
      }

      const powerLevelsEvent = getStateEvent(room, StateEvent.RoomPowerLevels);
      const powerLevels = getPowersLevelFromMatrixEvent(powerLevelsEvent);
      const creators = getRoomCreatorsForRoomId(mx, room.roomId);
      const permissions = getRoomPermissionsAPI(creators, powerLevels);

      const hasCallPermission = permissions.stateEvent(
        StateEvent.GroupCallMemberPrefix,
        mx.getSafeUserId(),
      );
      if (!hasCallPermission) return;

      // Only ring for DMs or private non-space group chats.
      // Space voice channels and public rooms fire room-level RTC notifications
      // whenever anyone joins — ringing every member is incorrect behaviour.
      const isDirect = directs.has(room.roomId);
      const isSpaceChild = !!getStateEvent(room, StateEvent.SpaceParent);
      const joinRule = room.getJoinRule();
      const isPrivateGroup =
        !isSpaceChild &&
        (joinRule === JoinRule.Invite ||
          joinRule === JoinRule.Knock ||
          joinRule === JoinRule.Restricted);
      if (!isDirect && !isPrivateGroup) return;

      const info: IncomingCallInfo = {
        room,
        sender,
        senderTs,
        lifetime,
        intent:
          'm.call.intent' in content && typeof content['m.call.intent'] === 'string'
            ? content['m.call.intent']
            : undefined,
        notificationType,
        refEventId,
      };

      setCallInfo(info);
    },
    [mx, directs],
  );

  useEffect(() => {
    mx.on(RoomEvent.Timeline, handleTimelineEvent);
    return () => {
      mx.removeListener(RoomEvent.Timeline, handleTimelineEvent);
    };
  }, [mx, handleTimelineEvent]);

  const handleIgnore = useCallback(() => {
    setCallInfo(undefined);
  }, []);

  const handleReject = useCallback(
    (room: Room, eventId: string) => {
      mx.sendEvent(room.roomId, EventType.RTCDecline, {
        'm.relates_to': {
          rel_type: RelationType.Reference,
          event_id: eventId,
        },
      });
      setCallInfo(undefined);
    },
    [mx],
  );

  const handleAnswer = useCallback(
    (room: Room, video: boolean) => {
      startCall(room, { microphone: true, video, sound: true });
      setCallInfo(undefined);
      navigateRoom(room.roomId);
    },
    [startCall, navigateRoom],
  );

  if (callInfo && callEmbed?.roomId === callInfo.room.roomId) {
    return null;
  }
  return !joined && callInfo ? (
    <IncomingCall
      dm={dm}
      info={callInfo}
      onIgnore={handleIgnore}
      onAnswer={handleAnswer}
      onReject={handleReject}
    />
  ) : null;
}

function CallUtils({ embed }: { embed: CallEmbed }) {
  const setCallEmbed = useSetAtom(callEmbedAtom);

  useCallMemberSoundSync(embed);
  useCallThemeSync(embed);
  useCallHangupEvent(
    embed,
    useCallback(() => {
      setCallEmbed(undefined);
    }, [setCallEmbed]),
  );

  return null;
}

/** Shown inside the PiP window when the local microphone is muted. */
function PipMuteOverlay({ callEmbed }: { callEmbed: CallEmbed }) {
  const allMuted = useRemoteAllMuted(callEmbed);
  if (!allMuted) return null;
  return (
    <div
      aria-label="Microphone muted"
      style={{
        position: 'absolute',
        bottom: '8px',
        left: '8px',
        zIndex: 3,
        background: 'rgba(0,0,0,0.60)',
        backdropFilter: 'blur(4px)',
        borderRadius: '6px',
        padding: '3px 7px',
        display: 'flex',
        alignItems: 'center',
        gap: '4px',
        pointerEvents: 'none',
        color: color.Critical.Main,
        fontSize: '13px',
        lineHeight: 1,
        userSelect: 'none',
      }}
    >
      <Icon size="100" src={Icons.MicMute} filled />
    </div>
  );
}

type CallEmbedProviderProps = {
  children?: ReactNode;
};
export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
  const callEmbed = useAtomValue(callEmbedAtom);
  const callEmbedRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>;
  const joined = useCallJoined(callEmbed);

  const selectedRoom = useSelectedRoom();
  const chat = useAtomValue(callChatAtom);
  const screenSize = useScreenSizeContext();

  const chatOnlyView = chat && screenSize !== ScreenSize.Desktop;
  const inCallRoom = callEmbed && selectedRoom === callEmbed.roomId;
  const callActive = callEmbed && joined;
  const callVisible = inCallRoom && callActive && !chatOnlyView;
  const pipMode = callActive && !inCallRoom;
  const { navigateRoom } = useRoomNavigate();

  const { screenshare: pipScreenshare } = useCallControlState(callEmbed?.control);

  // Sync pip mode into CallControl so it can adjust behavior accordingly
  useEffect(() => {
    if (!callEmbed) return;
    callEmbed.control.setPipMode(!!pipMode);
  }, [pipMode, callEmbed]);

  // When entering pip with screenshare active (or screenshare starts while in pip),
  // enable spotlight so the screenshare fills the pip window.
  // When screenshare ends, release the spotlight we auto-enabled.
  const pipAutoSpotlightRef = React.useRef(false);
  useEffect(() => {
    if (!pipMode || !callEmbed) return;
    if (pipScreenshare) {
      if (!callEmbed.control.spotlight) {
        callEmbed.control.toggleSpotlight();
        pipAutoSpotlightRef.current = true;
      }
    } else if (pipAutoSpotlightRef.current) {
      if (callEmbed.control.spotlight) callEmbed.control.toggleSpotlight();
      pipAutoSpotlightRef.current = false;
    }
  }, [pipMode, pipScreenshare, callEmbed]);

  const theme = useTheme();
  const isDark = theme.kind === ThemeKind.Dark;
  const [chatBackground] = useSetting(settingsAtom, 'chatBackground');
  const wallpaperStyle = React.useMemo(
    () => getChatBg(chatBackground, isDark),
    [chatBackground, isDark],
  );

  const pipDragRef = React.useRef<{
    startX: number;
    startY: number;
    origLeft: number;
    origTop: number;
    dragged: boolean;
  } | null>(null);
  const activeDragCleanupRef = React.useRef<(() => void) | null>(null);
  React.useEffect(
    () => () => {
      activeDragCleanupRef.current?.();
    },
    [],
  );

  // Track previous pipMode to only reset position when entering/exiting pip
  const prevPipModeRef = React.useRef(false);

  React.useEffect(() => {
    const el = callEmbedRef.current;
    if (!el) return;
    const wasInPip = prevPipModeRef.current;
    prevPipModeRef.current = !!pipMode;
    if (pipMode) {
      if (!wasInPip) {
        const saved = localStorage.getItem('pip-position');
        const savedPos = saved ? (JSON.parse(saved) as { left: number; top: number }) : null;
        el.style.right = 'auto';
        el.style.bottom = 'auto';
        if (savedPos) {
          el.style.left = `${Math.max(0, Math.min(savedPos.left, window.innerWidth - 280))}px`;
          el.style.top = `${Math.max(0, Math.min(savedPos.top, window.innerHeight - 158))}px`;
        } else {
          el.style.left = `${window.innerWidth - 280 - 16}px`;
          el.style.top = `${window.innerHeight - 158 - 72}px`;
        }
        el.style.width = '280px';
        el.style.height = '158px';
        el.style.borderRadius = '12px';
        el.style.overflow = 'hidden';
        el.style.zIndex = '99';
        el.style.boxShadow = '0 8px 32px rgba(0,0,0,0.55)';
        el.style.border = '1px solid rgba(255,255,255,0.1)';
      }
      el.style.visibility = 'visible';
    } else {
      if (wasInPip) {
        // Exiting pip: clear all pip styles; syncCallEmbedPlacement will restore correct position
        el.style.top = '';
        el.style.left = '';
        el.style.bottom = '';
        el.style.right = '';
        el.style.width = '';
        el.style.height = '';
        el.style.borderRadius = '';
        el.style.overflow = '';
        el.style.zIndex = '';
        el.style.boxShadow = '';
        el.style.border = '';
      }
      // syncCallEmbedPlacement owns top/left/width/height; don't clear them on visibility changes
      el.style.visibility = callVisible ? '' : 'hidden';
    }
  }, [pipMode, callVisible]);

  React.useEffect(() => {
    if (!pipMode) return;
    const onPipWindowResize = (): void => {
      const el = callEmbedRef.current;
      if (!el) return;
      // Normalise bottom/right → top/left so clamp math works regardless of initial position.
      if (!el.style.left || el.style.left === 'auto') normaliseToTopLeft(el);
      const l = parseFloat(el.style.left);
      const t = parseFloat(el.style.top);
      if (!isNaN(l))
        el.style.left = `${Math.max(0, Math.min(l, window.innerWidth - el.offsetWidth))}px`;
      if (!isNaN(t))
        el.style.top = `${Math.max(0, Math.min(t, window.innerHeight - el.offsetHeight))}px`;
    };
    window.addEventListener('resize', onPipWindowResize);
    return () => window.removeEventListener('resize', onPipWindowResize);
  }, [pipMode, callEmbedRef]);

  const handlePipDoubleClick = (e: React.MouseEvent) => {
    const el = callEmbedRef.current;
    if (!el) return;
    e.stopPropagation();
    const margin = 16;
    const w = el.offsetWidth;
    const h = el.offsetHeight;
    const elRect = el.getBoundingClientRect();
    const cx = elRect.left + w / 2;
    const cy = elRect.top + h / 2;
    const snapLeft = cx < window.innerWidth / 2 ? margin : window.innerWidth - w - margin;
    const snapTop = cy < window.innerHeight / 2 ? margin : window.innerHeight - h - margin;
    el.style.left = `${snapLeft}px`;
    el.style.top = `${snapTop}px`;
    el.style.right = 'auto';
    el.style.bottom = 'auto';
    el.style.transition = 'left 0.18s ease, top 0.18s ease';
    setTimeout(() => {
      if (el) el.style.transition = '';
    }, 200);
    localStorage.setItem('pip-position', JSON.stringify({ left: snapLeft, top: snapTop }));
  };

  const handlePipMouseDown = (e: React.MouseEvent) => {
    const el = callEmbedRef.current;
    if (!el) return;
    const rect = el.getBoundingClientRect();
    pipDragRef.current = {
      startX: e.clientX,
      startY: e.clientY,
      origLeft: rect.left,
      origTop: rect.top,
      dragged: false,
    };
    const onMove = (ev: MouseEvent) => {
      if (!pipDragRef.current || !el) return;
      const dx = ev.clientX - pipDragRef.current.startX;
      const dy = ev.clientY - pipDragRef.current.startY;
      if (!pipDragRef.current.dragged && Math.abs(dx) + Math.abs(dy) > 5) {
        pipDragRef.current.dragged = true;
        document.body.style.cursor = 'grabbing';
        document.body.style.userSelect = 'none';
      }
      if (pipDragRef.current.dragged) {
        el.style.left = `${Math.max(
          0,
          Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx),
        )}px`;
        el.style.top = `${Math.max(
          0,
          Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy),
        )}px`;
        el.style.right = 'auto';
        el.style.bottom = 'auto';
      }
    };
    const onUp = () => {
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
      document.body.style.cursor = '';
      document.body.style.userSelect = '';
      activeDragCleanupRef.current = null;
      if (el && pipDragRef.current?.dragged) {
        const savedRect = el.getBoundingClientRect();
        localStorage.setItem(
          'pip-position',
          JSON.stringify({ left: savedRect.left, top: savedRect.top }),
        );
      }
      setTimeout(() => {
        if (pipDragRef.current) pipDragRef.current.dragged = false;
      }, 0);
    };
    activeDragCleanupRef.current = () => {
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
      document.body.style.cursor = '';
      document.body.style.userSelect = '';
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  };

  const handlePipTouchStart = (e: React.TouchEvent) => {
    const el = callEmbedRef.current;
    if (!el || e.touches.length !== 1) return;
    const touch = e.touches[0];
    const rect = el.getBoundingClientRect();
    pipDragRef.current = {
      startX: touch.clientX,
      startY: touch.clientY,
      origLeft: rect.left,
      origTop: rect.top,
      dragged: false,
    };
    const onTouchMove = (ev: TouchEvent) => {
      if (!pipDragRef.current || !el || ev.touches.length !== 1) return;
      ev.preventDefault();
      const t = ev.touches[0];
      const dx = t.clientX - pipDragRef.current.startX;
      const dy = t.clientY - pipDragRef.current.startY;
      if (!pipDragRef.current.dragged && Math.abs(dx) + Math.abs(dy) > 5)
        pipDragRef.current.dragged = true;
      if (pipDragRef.current.dragged) {
        el.style.left = `${Math.max(
          0,
          Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx),
        )}px`;
        el.style.top = `${Math.max(
          0,
          Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy),
        )}px`;
        el.style.right = 'auto';
        el.style.bottom = 'auto';
      }
    };
    const onTouchEnd = () => {
      document.removeEventListener('touchmove', onTouchMove);
      document.removeEventListener('touchend', onTouchEnd);
      activeDragCleanupRef.current = null;
      if (el && pipDragRef.current?.dragged) {
        const savedRect = el.getBoundingClientRect();
        localStorage.setItem(
          'pip-position',
          JSON.stringify({ left: savedRect.left, top: savedRect.top }),
        );
      }
      setTimeout(() => {
        if (pipDragRef.current) pipDragRef.current.dragged = false;
      }, 0);
    };
    activeDragCleanupRef.current = () => {
      document.removeEventListener('touchmove', onTouchMove);
      document.removeEventListener('touchend', onTouchEnd);
    };
    document.addEventListener('touchmove', onTouchMove, { passive: false });
    document.addEventListener('touchend', onTouchEnd);
  };

  function applyResize(
    el: HTMLElement,
    corner: Corner,
    sx: number,
    sy: number,
    sw: number,
    sh: number,
    sl: number,
    st: number,
    cx: number,
    cy: number,
  ) {
    const dx = cx - sx;
    const dy = cy - sy;
    let w = sw;
    let h = sh;
    let l = sl;
    let t = st;
    if (corner === 'se') {
      w = sw + dx;
      h = sh + dy;
    }
    if (corner === 'sw') {
      w = sw - dx;
      h = sh + dy;
      l = sl + sw - Math.max(PIP_MIN_W, w);
    }
    if (corner === 'ne') {
      w = sw + dx;
      h = sh - dy;
      t = st + sh - Math.max(PIP_MIN_H, h);
    }
    if (corner === 'nw') {
      w = sw - dx;
      h = sh - dy;
      l = sl + sw - Math.max(PIP_MIN_W, w);
      t = st + sh - Math.max(PIP_MIN_H, h);
    }
    w = Math.max(PIP_MIN_W, Math.min(w, window.innerWidth));
    h = Math.max(PIP_MIN_H, Math.min(h, window.innerHeight));
    l = Math.max(0, Math.min(l, window.innerWidth - PIP_MIN_W));
    t = Math.max(0, Math.min(t, window.innerHeight - PIP_MIN_H));
    el.style.width = `${w}px`;
    el.style.height = `${h}px`;
    el.style.left = `${l}px`;
    el.style.top = `${t}px`;
  }

  const handleResizeMouseDown = (e: React.MouseEvent, corner: Corner) => {
    e.stopPropagation();
    e.preventDefault();
    const el = callEmbedRef.current;
    if (!el) return;
    normaliseToTopLeft(el);
    const sx = e.clientX;
    const sy = e.clientY;
    const sw = el.offsetWidth;
    const sh = el.offsetHeight;
    const sl = parseFloat(el.style.left);
    const st = parseFloat(el.style.top);
    document.body.style.cursor = `${corner}-resize`;
    document.body.style.userSelect = 'none';
    const onMove = (ev: MouseEvent) => {
      applyResize(el, corner, sx, sy, sw, sh, sl, st, ev.clientX, ev.clientY);
    };
    const onUp = () => {
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
      document.body.style.cursor = '';
      document.body.style.userSelect = '';
      activeDragCleanupRef.current = null;
    };
    activeDragCleanupRef.current = () => {
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
      document.body.style.cursor = '';
      document.body.style.userSelect = '';
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  };

  const handleResizeTouchStart = (e: React.TouchEvent, corner: Corner) => {
    e.stopPropagation();
    e.preventDefault();
    const el = callEmbedRef.current;
    if (!el || e.touches.length !== 1) return;
    normaliseToTopLeft(el);
    const touch = e.touches[0];
    const sx = touch.clientX;
    const sy = touch.clientY;
    const sw = el.offsetWidth;
    const sh = el.offsetHeight;
    const sl = parseFloat(el.style.left);
    const st = parseFloat(el.style.top);
    const onMove = (ev: TouchEvent) => {
      if (ev.touches.length !== 1) return;
      ev.preventDefault();
      const t = ev.touches[0];
      applyResize(el, corner, sx, sy, sw, sh, sl, st, t.clientX, t.clientY);
    };
    const onEnd = () => {
      document.removeEventListener('touchmove', onMove);
      document.removeEventListener('touchend', onEnd);
      activeDragCleanupRef.current = null;
    };
    activeDragCleanupRef.current = () => {
      document.removeEventListener('touchmove', onMove);
      document.removeEventListener('touchend', onEnd);
    };
    document.addEventListener('touchmove', onMove, { passive: false });
    document.addEventListener('touchend', onEnd);
  };

  return (
    <CallEmbedContextProvider value={callEmbed}>
      {callEmbed && <CallUtils embed={callEmbed} />}
      <CallEmbedRefContextProvider value={callEmbedRef}>
        <IncomingCallListener callEmbed={callEmbed} joined={joined} />
        {children}
      </CallEmbedRefContextProvider>
      <div
        data-call-embed-container
        style={{
          visibility: callVisible ? undefined : 'hidden',
          position: 'fixed',
          top: 0,
          left: 0,
          width: '100%',
          height: '50%',
          ...(callVisible && !pipMode ? wallpaperStyle : {}),
        }}
        ref={callEmbedRef}
      >
        {pipMode && callEmbed && (
          <>
            <div
              role="button"
              tabIndex={0}
              aria-label="Return to call"
              onMouseDown={handlePipMouseDown}
              onTouchStart={handlePipTouchStart}
              onDoubleClick={handlePipDoubleClick}
              onClick={() => {
                if (!pipDragRef.current?.dragged) navigateRoom(callEmbed.roomId);
              }}
              onKeyDown={(e) => e.key === 'Enter' && navigateRoom(callEmbed.roomId)}
              style={{
                position: 'absolute',
                inset: 0,
                zIndex: 1,
                background: 'transparent',
                cursor: 'grab',
                display: 'flex',
                alignItems: 'flex-start',
                justifyContent: 'flex-end',
                padding: '6px',
              }}
            >
              <div
                style={{
                  background: 'rgba(0,0,0,0.65)',
                  backdropFilter: 'blur(4px)',
                  borderRadius: '6px',
                  padding: '3px 8px',
                  color: '#fff',
                  fontSize: '11px',
                  fontWeight: 600,
                  pointerEvents: 'none',
                }}
              >
                ↗ Return to call
              </div>
            </div>
            <PipMuteOverlay callEmbed={callEmbed} />
            {(['se', 'sw', 'ne', 'nw'] as Corner[]).map((corner) => {
              const s = corner.includes('s');
              const e2 = corner.includes('e');
              const dots = [
                [3, 3],
                [3, 10],
                [10, 3],
              ].map(([a, b]) => ({
                position: 'absolute' as const,
                width: 5,
                height: 5,
                borderRadius: '50%',
                background: 'rgba(255,255,255,0.65)',
                boxShadow: '0 0 3px rgba(0,0,0,0.4)',
                [s ? 'bottom' : 'top']: a,
                [e2 ? 'right' : 'left']: b,
              }));
              return (
                <div
                  key={corner}
                  onMouseDown={(ev) => handleResizeMouseDown(ev, corner)}
                  onTouchStart={(ev) => handleResizeTouchStart(ev, corner)}
                  onClick={(ev) => ev.stopPropagation()}
                  style={{
                    position: 'absolute',
                    width: '24px',
                    height: '24px',
                    [s ? 'bottom' : 'top']: 0,
                    [e2 ? 'right' : 'left']: 0,
                    cursor: `${corner}-resize`,
                    zIndex: 2,
                  }}
                >
                  {dots.map((style, i) => (
                    <div key={i} style={style} />
                  ))}
                </div>
              );
            })}
          </>
        )}
      </div>
    </CallEmbedContextProvider>
  );
}
