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,
  IconButton,
  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 { toastQueueAtom } from '../state/toast';
import { CallEmbed, useCallControlState } from '../plugins/call';
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
import { useMatrixClient } from '../hooks/useMatrixClient';
import { previewRingtone, startRingtone, unlockRingtoneAudio } from '../utils/ringtones';
import { useCallMembersChange, useCallSession } from '../hooks/useCall';
import { useCallJoinLeaveSounds } from '../hooks/useCallJoinLeaveSounds';
import { useCallQuality } from '../hooks/useCallQuality';
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 { ExitFullscreenIcon, FullscreenIcon } from '../features/call/Controls';
import { useTheme, ThemeKind } from '../hooks/useTheme';
import { useReducedMotion } from '../hooks/useReducedMotion';
import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings';
import { getStateEvent, getStateEvents, getMemberName } 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';
import { zIndices } from '../styles/zIndex';

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 [ringtoneVolume] = useSetting(settingsAtom, 'ringtoneVolume');
  const [ringtoneId] = useSetting(settingsAtom, 'ringtoneId');

  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],
    ),
  );

  useEffect(() => {
    if (info.notificationType !== 'ring') return undefined;
    const stop = startRingtone(ringtoneId, Math.max(0, Math.min(1, ringtoneVolume / 100)));
    return stop;
  }, [info.notificationType, ringtoneId, ringtoneVolume]);

  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">
                {getMemberName(info.room, 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" alignItems="Center">
                  <Text size="H3" align="Center" truncate>
                    {roomName}
                  </Text>
                  <Text size="T300" align="Center">
                    {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>
  );
}

type IncomingCallBannerProps = {
  dm: boolean;
  info: IncomingCallInfo;
  onIgnore: () => void;
  onAnswer: (room: Room, video: boolean) => void;
  onReject: (room: Room, eventId: string) => void;
};
/**
 * Compact, non-intrusive incoming-call notification shown when the user is
 * ALREADY in a call. Unlike the full-screen `IncomingCall` overlay this is a
 * corner banner that does not take over the screen, and it plays a single
 * soft ping (via the one-shot ringtone preview) rather than the looping ring,
 * so it doesn't talk over the active call.
 */
function IncomingCallBanner({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallBannerProps) {
  const mx = useMatrixClient();
  const useAuthentication = useMediaAuthentication();
  const { room } = info;
  const isVideo = info.intent === 'video';

  const [ringtoneVolume] = useSetting(settingsAtom, 'ringtoneVolume');
  const [ringtoneId] = useSetting(settingsAtom, 'ringtoneId');

  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],
    ),
  );

  // Single soft ping (non-looping) on arrival, respecting the chosen ringtone
  // + volume. We intentionally do NOT loop here — the user is mid-call — and we
  // ping exactly once per incoming call, not again if the user happens to tweak
  // ringtone settings while the banner is showing.
  const pingedRef = useRef<string | undefined>(undefined);
  useEffect(() => {
    if (info.notificationType !== 'ring') return;
    if (pingedRef.current === info.refEventId) return;
    pingedRef.current = info.refEventId;
    previewRingtone(ringtoneId, Math.max(0, Math.min(1, ringtoneVolume / 100)));
  }, [info.notificationType, info.refEventId, ringtoneId, ringtoneVolume]);

  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]);

  const callerName = getMemberName(info.room, info.sender);

  return (
    <Box
      direction="Column"
      gap="300"
      style={{
        position: 'fixed',
        top: config.space.S400,
        right: config.space.S400,
        zIndex: zIndices.inCallBanner,
        width: toRem(300),
        maxWidth: `calc(100vw - 2 * ${config.space.S400})`,
        padding: config.space.S300,
        background: color.Surface.Container,
        border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
        borderRadius: config.radii.R400,
        boxShadow: `0 8px 32px ${color.Other.Shadow}`,
      }}
      role="alert"
      aria-label={`Incoming ${isVideo ? 'video' : 'voice'} call from ${roomName}`}
    >
      <Box gap="300" alignItems="Center">
        <Box shrink="No">
          <Avatar size="300" className={CallAvatarAnimation}>
            <RoomAvatar
              roomId={room.roomId}
              src={avatarUrl}
              alt={roomName}
              renderFallback={() => (
                <RoomIcon
                  roomType={room.getType()}
                  size="200"
                  joinRule={room.getJoinRule()}
                  filled
                />
              )}
            />
          </Avatar>
        </Box>
        <Box grow="Yes" direction="Column" gap="100" style={{ minWidth: 0 }}>
          <Text size="T300" truncate>
            {roomName}
          </Text>
          <Text size="T200" priority="300" truncate>
            {isVideo ? 'Incoming video call' : 'Incoming voice call'}
            {dm ? '' : ` · ${callerName}`}
          </Text>
        </Box>
      </Box>
      <Box gap="200">
        <Button
          style={{ flexGrow: 1 }}
          variant="Success"
          fill="Solid"
          size="300"
          radii="300"
          onClick={() => onAnswer(room, isVideo)}
          before={<Icon size="100" src={isVideo ? Icons.VideoCamera : Icons.Phone} filled />}
        >
          <Text as="span" size="B300">
            Answer
          </Text>
        </Button>
        <Button
          style={{ flexGrow: 1 }}
          variant={dm ? 'Critical' : 'Secondary'}
          fill="Soft"
          size="300"
          radii="300"
          outlined
          onClick={() => (dm ? onReject(room, info.refEventId) : onIgnore())}
          before={<Icon size="100" src={Icons.Cross} filled />}
        >
          <Text as="span" size="B300">
            {dm ? 'Reject' : 'Ignore'}
          </Text>
        </Button>
      </Box>
    </Box>
  );
}

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

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

  // C-L6: handleTimelineEvent awaits decryption before calling setState; guard
  // against the component unmounting during that await.
  const mountedRef = useRef(true);
  useEffect(
    () => () => {
      mountedRef.current = false;
    },
    [],
  );

  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 (room?.isCallRoom()) return;

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

      // C-L6: bail if we unmounted while awaiting decryption above.
      if (!mountedRef.current) return;

      // Caller-side: a participant declined a call we're hosting in this room.
      // Without this the caller's UI keeps "ringing" until the notification
      // lifetime expires, with no indication the callee said no.
      if (event.getType() === EventType.RTCDecline) {
        const decliner = event.getSender();
        if (
          data.liveEvent &&
          room &&
          decliner &&
          decliner !== mx.getSafeUserId() &&
          callEmbed?.roomId === room.roomId
        ) {
          const declinerName = getMemberName(room, decliner);
          setToast({
            id: `rtc-decline-${event.getId() ?? decliner}`,
            displayName: declinerName,
            body: 'Declined your call',
            roomName: room.name,
            roomId: room.roomId,
          });
        }
        return;
      }

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

      const sender = event.getSender();
      const content = event.getContent<IRTCNotificationContent>();
      // Trust the caller's sender_ts only when it's within 20s of the server's
      // timestamp in EITHER direction. A fast caller clock made a fresh invite
      // look expired-in-the-future; a SLOW one left sender_ts in the past so
      // `Date.now() >= senderTs + lifetime` hid/dismissed the ring for a
      // genuinely fresh invite (COR-3). Fall back to the server ts on large skew.
      const senderTs =
        typeof content.sender_ts === 'number' &&
        Math.abs(content.sender_ts - event.getTs()) <= 20000
          ? content.sender_ts
          : event.getTs();
      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 in rooms where the call button is visible: DMs or invite-only rooms
      // with no space parent. Persistent voice rooms (call rooms), space channels,
      // restricted rooms, and public rooms must never trigger ringing.
      if (room.isCallRoom()) return;
      const isDirect = directs.has(room.roomId);
      const isSpaceChild = getStateEvents(room, StateEvent.SpaceParent).length > 0;
      const joinRule = room.getJoinRule();
      const isPrivateInviteGroup = !isSpaceChild && joinRule === JoinRule.Invite;
      if (!isDirect && !isPrivateInviteGroup) 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, callEmbed, setToast],
  );

  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) => {
      // Best-effort: the local UI dismisses regardless (below), but a failed
      // decline used to reject uncaught. Log it instead of surfacing — the caller
      // will time the call out on their side even if our decline never lands.
      mx.sendEvent(room.roomId, EventType.RTCDecline, {
        'm.relates_to': {
          rel_type: RelationType.Reference,
          event_id: eventId,
        },
      }).catch((err) => {
        console.error('Failed to send call decline:', err);
      });
      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) return null;
  // Already in this room's own call — no notification at all.
  if (callEmbed?.roomId === callInfo.room.roomId) {
    return null;
  }
  // In a different call already: show the compact, non-intrusive banner
  // instead of the full-screen takeover overlay.
  if (joined) {
    return (
      <IncomingCallBanner
        dm={dm}
        info={callInfo}
        onIgnore={handleIgnore}
        onAnswer={handleAnswer}
        onReject={handleReject}
      />
    );
  }
  return (
    <IncomingCall
      dm={dm}
      info={callInfo}
      onIgnore={handleIgnore}
      onAnswer={handleAnswer}
      onReject={handleReject}
    />
  );
}

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

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

  return null;
}

/**
 * PiP status indicators:
 * - Bottom-left badge: local mic muted (matches Discord/Slack convention — bottom-left = "your" mic)
 * - Top-right badge: all remote participants are muted (quiet room warning)
 *
 * Deliberately separated so users never mistake remote-mute state for their own.
 */
function PipMuteOverlay({ callEmbed }: { callEmbed: CallEmbed }) {
  const mx = useMatrixClient();
  const controlState = useCallControlState(callEmbed.control);
  const allRemoteMuted = useRemoteAllMuted(callEmbed);

  const localMicMuted = !controlState.microphone;
  const localUserId = mx.getSafeUserId();
  const localDisplayName = getMxIdLocalPart(localUserId) ?? localUserId;

  // Dark translucent scrim is intentional: these badges overlay arbitrary
  // video, so a theme surface token would not guarantee legibility.
  const badgeStyle: React.CSSProperties = {
    position: 'absolute',
    zIndex: 3,
    background: 'rgba(0,0,0,0.65)',
    backdropFilter: 'blur(4px)',
    borderRadius: config.radii.R300,
    padding: `${config.space.S100} ${config.space.S200}`,
    display: 'flex',
    alignItems: 'center',
    gap: config.space.S100,
    pointerEvents: 'none',
    lineHeight: 1,
    userSelect: 'none',
  };

  return (
    <>
      {localMicMuted && (
        <div
          aria-label={`Your microphone is muted (${localDisplayName})`}
          title="Your microphone is muted"
          style={{ ...badgeStyle, bottom: config.space.S200, left: config.space.S200 }}
        >
          <Icon size="100" src={Icons.MicMute} filled style={{ color: color.Critical.Main }} />
          <Text as="span" size="T200" style={{ color: color.Critical.Main }}>
            You
          </Text>
        </div>
      )}
      {allRemoteMuted && (
        <div
          aria-label="All other participants are muted"
          title="All other participants are muted"
          style={{ ...badgeStyle, top: config.space.S200, right: config.space.S200 }}
        >
          <Icon size="50" src={Icons.MicMute} style={{ color: color.Warning.Main }} />
          <Text as="span" size="T200" style={{ color: color.Warning.Main }}>
            All muted
          </Text>
        </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);

  // C-L3 — prime the ringtone AudioContext on the first user gesture of the
  // session so the first incoming-call ring isn't silent (a fresh context stays
  // suspended until a gesture, and an incoming ring has none of its own).
  useEffect(() => {
    const prime = () => {
      unlockRingtoneAudio();
      window.removeEventListener('pointerdown', prime);
      window.removeEventListener('keydown', prime);
    };
    window.addEventListener('pointerdown', prime, { once: true, passive: true });
    window.addEventListener('keydown', prime, { once: true, passive: true });
    return () => {
      window.removeEventListener('pointerdown', prime);
      window.removeEventListener('keydown', prime);
    };
  }, []);

  // 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 (!callEmbed) {
      // The embed (and its spotlight) is torn down with the call; drop the latch
      // so a stale ref can't act on the next call's fresh embed.
      pipAutoSpotlightRef.current = false;
      return;
    }
    // Spotlight is wanted only while in pip with an active screenshare. Release
    // it when EITHER ends — including leaving pip (returning to the call room).
    // The release must not sit behind a `!pipMode` early-return, or a
    // screenshare→pip→back sequence leaves the auto-enabled spotlight stuck on
    // with pipAutoSpotlightRef latched true. The ref gates release so we only
    // ever undo a spotlight we turned on (never one the user set).
    // NB: `control.spotlight` is read below but deliberately NOT a dependency —
    // this effect reacts to pip/screenshare *intent*, not to spotlight changes.
    // Adding it as a dep would re-run on every manual spotlight toggle and fight
    // the user.
    const wantSpotlight = pipMode && pipScreenshare;
    if (wantSpotlight) {
      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 reduced = useReducedMotion();
  const wallpaperStyle = React.useMemo(
    () => getChatBg(chatBackground, isDark, reduced),
    [chatBackground, isDark, reduced],
  );

  const [pipIsFullscreen, setPipIsFullscreen] = useState(false);
  useEffect(() => {
    const onFsChange = () => setPipIsFullscreen(!!document.fullscreenElement);
    document.addEventListener('fullscreenchange', onFsChange);
    return () => document.removeEventListener('fullscreenchange', onFsChange);
  }, []);

  const handlePipFullscreen = useCallback(() => {
    if (document.fullscreenElement) {
      document.exitFullscreen();
    } else {
      callEmbedRef.current?.requestFullscreen();
    }
  }, [callEmbedRef]);

  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');
        let savedPos: { left: number; top: number } | null = null;
        if (saved) {
          try {
            const raw = JSON.parse(saved) as { left?: unknown; top?: unknown };
            // Validate shape + finiteness: a corrupt value would otherwise feed
            // NaN into Math.min and produce an invalid `NaNpx` CSS value.
            if (
              raw &&
              typeof raw.left === 'number' &&
              Number.isFinite(raw.left) &&
              typeof raw.top === 'number' &&
              Number.isFinite(raw.top)
            ) {
              savedPos = { left: raw.left, top: raw.top };
            }
          } catch {
            savedPos = 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={{ display: 'flex', gap: config.space.S100, alignItems: 'center' }}>
                {document.fullscreenEnabled && (
                  <IconButton
                    type="button"
                    size="300"
                    radii="300"
                    variant="Surface"
                    fill="None"
                    aria-label={pipIsFullscreen ? 'Exit fullscreen' : 'Fullscreen camera'}
                    onClick={(e) => {
                      e.stopPropagation();
                      handlePipFullscreen();
                    }}
                    style={{
                      // Dark scrim is intentional for legibility over arbitrary video.
                      background: 'rgba(0,0,0,0.65)',
                      backdropFilter: 'blur(4px)',
                      color: '#fff',
                    }}
                  >
                    {pipIsFullscreen ? <ExitFullscreenIcon /> : <FullscreenIcon />}
                  </IconButton>
                )}
                <div
                  style={{
                    background: 'rgba(0,0,0,0.65)',
                    backdropFilter: 'blur(4px)',
                    borderRadius: config.radii.R300,
                    padding: `${config.space.S100} ${config.space.S200}`,
                    color: '#fff',
                    fontSize: '11px',
                    fontWeight: 600,
                    pointerEvents: 'none',
                  }}
                >
                  ↗ Return to call
                </div>
              </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>
  );
}
