import type { ConversationSummary } from '@chat-app/shared/chat';
import type { RemoteParticipant, Room } from 'livekit-client';
import { Track } from 'livekit-client';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';

import { useAuth } from '../context/AuthContext';
import { type CallMode, useCall } from '../context/CallContext';
import {
  getPttSettings,
  type PttSettings,
  subscribePttSettings,
} from '../lib/pttSettings';
import { useActiveSpeakers } from '../lib/useActiveSpeakers';
import { CallControls } from './CallControls';
import { CallParticipantTile } from './CallParticipantTile';
import { FocusIcon, GridIcon, LockIcon, MaximizeIcon, SpinnerIcon, UsersIcon } from './icons';
import { ParticipantVolumeMenu } from './ParticipantVolumeMenu';
import { ScreenShareDialog } from './ScreenShareDialog';
import { ScreenShareViewer } from './ScreenShareViewer';
import { SoundboardPanel } from './SoundboardPanel';

// Discord-style in-call dock rendered above the message list. Renders three
// visual modes driven by CallContext.callMode: grid, focus, fullscreen.
// The fullscreen variant absolute-positions itself over the conversation
// container so the rail+chat-list remain visible on the left.
interface Props {
  conversation: ConversationSummary;
}

interface Tile {
  // `user` = person with avatar/video/mute info. Speaking ring applies here.
  // `screen` = a separate screen-share window from a user. No speaking
  //           indicator, no mute, no avatar — just the stream.
  kind: 'user' | 'screen';
  // Stable id used for focus tracking + React keys. `user:${userId}` or
  // `screen:${userId}`.
  id: string;
  userId: string;
  displayName: string;
  avatarUrl: string | null;
  self: boolean;
  muted: boolean;
  deafened: boolean;
  video: boolean;
  videoTrack: MediaStreamTrack | null;
  // True iff this is the current user's own screen-share tile.
  sharing: boolean;
  // True iff this is a remote user's screen-share tile.
  remoteSharing: boolean;
}

export function InCallPanel({ conversation }: Props) {
  const { t } = useTranslation(['app']);
  const {
    state,
    room,
    remoteParticipants,
    isMuted,
    isE2EEActive,
    isScreenSharing,
    isCameraEnabled,
    isDeafened,
    remoteDeafen,
    remoteMute,
    remoteScreenShares,
    callMode,
    focusedId,
    toggleMute,
    startScreenShare,
    stopScreenShare,
    toggleCamera,
    toggleDeafen,
    hangup,
    setCallMode,
    setFocusedId,
  } = useCall();
  const { session } = useAuth();
  const myId = session?.user.id ?? null;
  const activeSpeakers = useActiveSpeakers(room);
  const [shareDialogOpen, setShareDialogOpen] = useState(false);
  const [soundboardOpen, setSoundboardOpen] = useState(false);
  const [volumeMenu, setVolumeMenu] = useState<
    { userId: string; displayName: string; x: number; y: number } | null
  >(null);

  const openVolumeMenu = (tile: Tile, e: React.MouseEvent) => {
    if (tile.self) return;
    if (tile.kind !== 'user') return;
    e.preventDefault();
    setVolumeMenu({
      userId: tile.userId,
      displayName: tile.displayName,
      x: e.clientX,
      y: e.clientY,
    });
  };

  const active =
    (state.kind === 'connected' ||
      state.kind === 'connecting' ||
      state.kind === 'outgoing') &&
    state.conversationId === conversation.id;
  if (!active) return null;

  const tiles = buildTiles({
    conversation,
    myId,
    room,
    remoteParticipants,
    isMuted,
    isDeafened,
    remoteDeafen,
    remoteMute,
    isScreenSharing,
    isCameraEnabled,
    remoteSharerIds: new Set(remoteScreenShares.map((s) => s.participantId)),
  });

  const duration =
    state.kind === 'connected'
      ? <LiveDuration startedAt={state.startedAt} />
      : null;

  const statusLabel =
    state.kind === 'outgoing'
      ? t('app:call.outgoing_ringing')
      : state.kind === 'connecting'
        ? t('app:call.connecting')
        : remoteParticipants.length === 0
          ? t('app:call.waiting_for_peers', { defaultValue: 'Warte auf andere…' })
          : t('app:call.connected');

  // A screen-share tile becomes the auto-focus target when no one explicitly
  // picked a tile yet. Focus ids now use Tile.id (kind-prefixed) so we can
  // distinguish a user's own avatar tile from their screen tile.
  const screenTile = tiles.find((p) => p.kind === 'screen');
  const effectiveFocusedId = focusedId ?? screenTile?.id ?? tiles[0]?.id ?? null;
  const speaker = tiles.find((p) => p.id === effectiveFocusedId) ?? tiles[0];

  const controls = (
    <CallControls
      muted={isMuted || !(room?.localParticipant?.isMicrophoneEnabled ?? false)}
      sharing={isScreenSharing}
      video={isCameraEnabled}
      deafened={isDeafened}
      onToggleMute={toggleMute}
      onToggleShare={() => {
        if (isScreenSharing) {
          void stopScreenShare();
        } else {
          setShareDialogOpen(true);
        }
      }}
      onToggleVideo={() => void toggleCamera()}
      onToggleDeafen={toggleDeafen}
      onToggleSoundboard={() => setSoundboardOpen((v) => !v)}
      soundboardOpen={soundboardOpen}
      onHangup={() => void hangup()}
      compact={callMode !== 'fullscreen'}
      glass={callMode === 'fullscreen'}
      disabledMedia={state.kind !== 'connected'}
    />
  );

  if (callMode === 'fullscreen') {
    // In fullscreen, a "manual focus" = user explicitly picked someone, OR
    // someone is sharing a screen, OR exactly one non-self speaker is talking
    // (auto-promote). Without that we show an even grid of all participants
    // (Discord default). Clicking a tile switches to the big-speaker layout.
    const speakingNonSelf = tiles.filter(
      (t: Tile) => activeSpeakers.has(t.userId) && !t.self && t.kind === 'user',
    );
    const autoSpeaker =
      focusedId === null && screenTile === undefined && speakingNonSelf.length === 1
        ? speakingNonSelf[0]
        : undefined;
    const hasFocus = focusedId !== null || screenTile !== undefined || autoSpeaker !== undefined;
    const effectiveSpeaker = hasFocus ? speaker ?? autoSpeaker : undefined;
    return (
      <>
        <FullscreenCall
          tiles={tiles}
          speaker={effectiveSpeaker}
          remoteScreenShares={remoteScreenShares}
          conversationMembers={conversation.members}
          activeSpeakers={activeSpeakers}
          e2ee={isE2EEActive}
          onExit={() => setCallMode('grid')}
          onFocusTile={(id) => {
            // Toggle: click the already-focused tile to return to grid.
            setFocusedId(focusedId === id ? null : id);
          }}
          onTileContextMenu={openVolumeMenu}
          controls={controls}
        />
        {volumeMenu && (
          <ParticipantVolumeMenu
            userId={volumeMenu.userId}
            displayName={volumeMenu.displayName}
            x={volumeMenu.x}
            y={volumeMenu.y}
            onClose={() => setVolumeMenu(null)}
          />
        )}
        <SoundboardPopover
          open={soundboardOpen}
          onClose={() => setSoundboardOpen(false)}
        />
      </>
    );
  }

  const title =
    conversation.type === 'group'
      ? conversation.name ?? t('app:chats.new_group')
      : conversation.peer?.displayName ?? '—';

  // Focus mode dedicates the entire call-panel vertical slot to the speaker so
  // the tile can grow in height (grid mode's 420px cap leaves it squashed).
  const sectionClass =
    callMode === 'focus'
      ? 'flex min-h-[420px] shrink-0 flex-col overflow-hidden border-b border-line bg-surface-2'
      : 'flex min-h-[280px] max-h-[420px] shrink-0 flex-col overflow-hidden border-b border-line bg-surface-2';
  const sectionHeight = callMode === 'focus' ? '75%' : '50%';

  return (
    <section
      aria-label={t('app:call.in_call', { defaultValue: 'Im Anruf' })}
      className={sectionClass}
      style={{ height: sectionHeight }}
    >
      <div className="flex items-center justify-between border-b border-line bg-surface-3 px-4 py-2">
        <div className="flex min-w-0 flex-col gap-0.5">
          <div className="flex items-center gap-2 text-sm font-semibold text-fg">
            <UsersIcon className="h-4 w-4 shrink-0 text-fg-muted" />
            <span className="truncate">{title}</span>
            <span className="text-fg-muted/50" aria-hidden="true">·</span>
            <span className="tabular-nums text-fg-muted">
              {duration ?? statusLabel}
              {state.kind !== 'connected' && (
                <SpinnerIcon className="ml-1 inline h-3 w-3" />
              )}
            </span>
          </div>
          {isE2EEActive && (
            <div className="flex items-center gap-1 text-[11px] font-medium text-emerald-600 dark:text-emerald-400">
              <LockIcon className="h-3 w-3" />
              <span>{t('app:call.e2ee_active', { defaultValue: 'E2E verschlüsselt' })}</span>
            </div>
          )}
        </div>
        <ModeToggles mode={callMode} onChange={setCallMode} />
      </div>

      <CallStage
        tiles={tiles}
        speaker={speaker}
        mode={callMode}
        activeSpeakers={activeSpeakers}
        e2ee={isE2EEActive}
        remoteScreenShares={remoteScreenShares}
        conversationMembers={conversation.members}
        onFocusTile={(id) => {
          setFocusedId(id);
          if (callMode === 'grid') setCallMode('focus');
        }}
        onTileContextMenu={openVolumeMenu}
        compact
      />

      {controls}

      <PttHint />

      <ScreenShareDialog
        open={shareDialogOpen}
        onClose={() => setShareDialogOpen(false)}
        onStart={async (opts) => {
          await startScreenShare(opts);
        }}
      />

      {volumeMenu && (
        <ParticipantVolumeMenu
          userId={volumeMenu.userId}
          displayName={volumeMenu.displayName}
          x={volumeMenu.x}
          y={volumeMenu.y}
          onClose={() => setVolumeMenu(null)}
        />
      )}

      <SoundboardPopover
        open={soundboardOpen}
        onClose={() => setSoundboardOpen(false)}
      />
    </section>
  );
}

// Fixed-position overlay so the popover sits above both docked + fullscreen
// call modes without needing a portal or parent-relative anchoring.
function SoundboardPopover({ open, onClose }: { open: boolean; onClose: () => void }) {
  if (!open) return null;
  return (
    <div className="pointer-events-none fixed inset-x-0 bottom-24 z-50 flex justify-center px-4">
      <div className="pointer-events-auto">
        <SoundboardPanel onClose={onClose} />
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

interface BuildArgs {
  conversation: ConversationSummary;
  myId: string | null;
  room: Room | null;
  remoteParticipants: RemoteParticipant[];
  isMuted: boolean;
  isDeafened: boolean;
  remoteDeafen: Record<string, boolean>;
  remoteMute: Record<string, boolean>;
  isScreenSharing: boolean;
  isCameraEnabled: boolean;
  remoteSharerIds: Set<string>;
}

function cameraTrackFor(
  participant: { videoTrackPublications: Map<string, { source: Track.Source; track?: { mediaStreamTrack: MediaStreamTrack } | undefined }> } | null,
): MediaStreamTrack | null {
  if (!participant) return null;
  for (const pub of participant.videoTrackPublications.values()) {
    if (pub.source === Track.Source.Camera && pub.track) {
      return pub.track.mediaStreamTrack ?? null;
    }
  }
  return null;
}

function buildTiles({
  conversation,
  myId,
  room,
  remoteParticipants,
  isMuted,
  isDeafened,
  remoteDeafen,
  remoteMute,
  isScreenSharing,
  isCameraEnabled,
  remoteSharerIds,
}: BuildArgs): Tile[] {
  const remoteById = new Map<string, RemoteParticipant>();
  for (const p of remoteParticipants) {
    if (p.identity) remoteById.set(p.identity, p);
  }
  const out: Tile[] = [];
  if (myId) {
    const me = conversation.members.find((m) => m.userId === myId) ?? null;
    // Source-of-truth for own mic: the LocalParticipant publication state.
    // `isMuted` reflects the toggle button, but if mic never published (no
    // device / permission denied) the toggle stays false while the real
    // state is "muted". Combine both so the badge always matches reality.
    const micLive = room?.localParticipant?.isMicrophoneEnabled ?? false;
    out.push({
      kind: 'user',
      id: 'user:' + myId,
      userId: myId,
      displayName: me?.profile?.displayName ?? '?',
      avatarUrl: me?.profile?.avatarUrl ?? null,
      self: true,
      muted: isMuted || !micLive,
      deafened: isDeafened,
      video: isCameraEnabled,
      videoTrack: cameraTrackFor(room?.localParticipant ?? null),
      sharing: false,
      remoteSharing: false,
    });
    if (isScreenSharing) {
      out.push({
        kind: 'screen',
        id: 'screen:' + myId,
        userId: myId,
        displayName: (me?.profile?.displayName ?? '?') + ' · Bildschirm',
        avatarUrl: me?.profile?.avatarUrl ?? null,
        self: true,
        muted: false,
        deafened: false,
        video: false,
        videoTrack: null,
        sharing: true,
        remoteSharing: false,
      });
    }
  }
  for (const m of conversation.members) {
    if (m.userId === myId) continue;
    const rp = remoteById.get(m.userId);
    if (!rp) continue;
    out.push({
      kind: 'user',
      id: 'user:' + m.userId,
      userId: m.userId,
      displayName: m.profile?.displayName ?? '?',
      avatarUrl: m.profile?.avatarUrl ?? null,
      self: false,
      // Peer's self-reported mute state via data channel. LiveKit's own
      // `isMicrophoneEnabled` no longer flips on mute since the pipeline
      // output track stays published. See remoteMute broadcast in CallContext.
      muted: remoteMute[m.userId] ?? false,
      // Deafen state arrives via LiveKit data channel; see CallContext.
      deafened: remoteDeafen[m.userId] ?? false,
      video: rp.isCameraEnabled,
      videoTrack: cameraTrackFor(rp),
      sharing: false,
      remoteSharing: false,
    });
    if (remoteSharerIds.has(m.userId)) {
      out.push({
        kind: 'screen',
        id: 'screen:' + m.userId,
        userId: m.userId,
        displayName: (m.profile?.displayName ?? '?') + ' · Bildschirm',
        avatarUrl: m.profile?.avatarUrl ?? null,
        self: false,
        muted: false,
        deafened: false,
        video: false,
        videoTrack: null,
        sharing: false,
        remoteSharing: true,
      });
    }
  }
  return out;
}

function LiveDuration({ startedAt }: { startedAt: string }) {
  const [, tick] = useState(0);
  useEffect(() => {
    const id = window.setInterval(() => tick((v) => v + 1), 1000);
    return () => window.clearInterval(id);
  }, []);
  return <>{formatElapsed(Date.now() - new Date(startedAt).getTime())}</>;
}

function formatElapsed(ms: number): string {
  const total = Math.max(0, Math.floor(ms / 1000));
  const hh = Math.floor(total / 3600);
  const mm = Math.floor((total % 3600) / 60);
  const ss = total % 60;
  const pad = (n: number) => n.toString().padStart(2, '0');
  return `${pad(hh)}:${pad(mm)}:${pad(ss)}`;
}

function ModeToggles({
  mode,
  onChange,
}: {
  mode: CallMode;
  onChange: (mode: CallMode) => void;
}) {
  return (
    <div className="flex items-center gap-1">
      <ModeButton active={mode === 'grid'} onClick={() => onChange('grid')} label="Grid">
        <GridIcon className="h-4 w-4" />
      </ModeButton>
      <ModeButton active={mode === 'focus'} onClick={() => onChange('focus')} label="Fokus">
        <FocusIcon className="h-4 w-4" />
      </ModeButton>
      <ModeButton
        active={mode === 'fullscreen'}
        onClick={() => onChange('fullscreen')}
        label="Vollbild"
      >
        <MaximizeIcon className="h-4 w-4" />
      </ModeButton>
    </div>
  );
}

function ModeButton({
  active,
  onClick,
  label,
  children,
}: {
  active: boolean;
  onClick: () => void;
  label: string;
  children: React.ReactNode;
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      aria-label={label}
      aria-pressed={active}
      title={label}
      className={
        'flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 ' +
        (active
          ? 'border-accent bg-accent text-accent-fg'
          : 'border-line bg-surface-2 text-fg-muted hover:bg-surface-3 hover:text-fg')
      }
    >
      {children}
    </button>
  );
}

interface StageProps {
  tiles: Tile[];
  speaker: Tile | undefined;
  mode: CallMode;
  activeSpeakers: Set<string>;
  e2ee: boolean;
  remoteScreenShares: {
    track: import('livekit-client').RemoteTrack;
    participantId: string;
    participantName: string;
  }[];
  conversationMembers: ConversationSummary['members'];
  onFocusTile: (id: string) => void;
  onTileContextMenu?: (tile: Tile, e: React.MouseEvent) => void;
  compact?: boolean;
}

// Dispatches a Tile to the right renderer. Screen-tiles show the screenshare
// stream directly (no avatar, no speaking ring, no mic indicator); user-tiles
// render via CallParticipantTile with all its chrome.
function TileRender({
  tile,
  activeSpeakers,
  e2ee,
  remoteScreenShares,
  conversationMembers,
  size,
  focused,
  onClick,
  onContextMenu,
}: {
  tile: Tile;
  activeSpeakers: Set<string>;
  e2ee: boolean;
  remoteScreenShares: StageProps['remoteScreenShares'];
  conversationMembers: StageProps['conversationMembers'];
  size?: 'default' | 'small';
  focused?: boolean;
  onClick?: () => void;
  onContextMenu?: (e: React.MouseEvent) => void;
}): JSX.Element {
  if (tile.kind === 'screen') {
    if (tile.self) {
      // Local screenshare preview — we don't mirror a copy of the outgoing
      // stream. Render a labelled placeholder card so the user knows their
      // share is live without double-encoding the stream.
      return (
        <div
          onClick={onClick}
          className={
            'relative flex h-full w-full items-center justify-center overflow-hidden rounded-[14px] border border-emerald-500/40 bg-emerald-500/5 text-emerald-700 dark:text-emerald-200 ' +
            (onClick ? 'cursor-pointer' : '')
          }
        >
          <div className="flex flex-col items-center gap-2 p-4 text-center">
            <div className="text-xs font-semibold uppercase tracking-wider">
              Live
            </div>
            <div className="text-sm">{tile.displayName}</div>
          </div>
        </div>
      );
    }
    const share = remoteScreenShares.find((s) => s.participantId === tile.userId);
    const member = conversationMembers.find((m) => m.userId === tile.userId);
    if (!share) return <div className="h-full w-full" />;
    return (
      <div
        onClick={onClick}
        className={'h-full w-full [&>div]:h-full [&>div]:w-full ' + (onClick ? 'cursor-pointer' : '')}
      >
        <ScreenShareViewer
          share={share}
          avatarUrl={member?.profile?.avatarUrl ?? tile.avatarUrl}
          displayName={member?.profile?.displayName ?? tile.displayName}
        />
      </div>
    );
  }
  return (
    <CallParticipantTile
      userId={tile.userId}
      displayName={tile.displayName}
      avatarUrl={tile.avatarUrl}
      me={tile.self}
      muted={tile.muted}
      deafened={tile.deafened}
      speaking={activeSpeakers.has(tile.userId)}
      video={tile.video}
      videoTrack={tile.videoTrack}
      e2ee={e2ee}
      {...(size ? { size } : {})}
      {...(focused ? { focused } : {})}
      {...(onClick ? { onClick } : {})}
      {...(onContextMenu ? { onContextMenu } : {})}
    />
  );
}

function CallStage({
  tiles,
  speaker,
  mode,
  activeSpeakers,
  e2ee,
  remoteScreenShares,
  conversationMembers,
  onFocusTile,
  onTileContextMenu,
  compact = false,
}: StageProps) {
  if (mode === 'focus' && speaker) {
    const others = tiles.filter((p) => p.id !== speaker.id);
    return (
      <div className="flex min-h-0 flex-1 flex-col gap-2.5 p-3">
        <div className="min-h-0 flex-1">
          <FocusedTile
            tile={speaker}
            e2ee={e2ee}
            activeSpeakers={activeSpeakers}
            remoteScreenShares={remoteScreenShares}
            conversationMembers={conversationMembers}
          />
        </div>
        {others.length > 0 && (
          <div className="flex h-[180px] gap-2.5 overflow-x-auto">
            {others.map((p) => (
              <div
                key={p.id}
                className="h-full w-[240px] shrink-0 [&>div]:h-full"
              >
                <TileRender
                  tile={p}
                  activeSpeakers={activeSpeakers}
                  e2ee={e2ee}
                  remoteScreenShares={remoteScreenShares}
                  conversationMembers={conversationMembers}
                  size="small"
                  onClick={() => onFocusTile(p.id)}
                  {...(onTileContextMenu
                    ? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(p, e) }
                    : {})}
                />
              </div>
            ))}
          </div>
        )}
      </div>
    );
  }

  // Grid
  const gridClass = gridColsFor(tiles.length);
  return (
    <div className={'min-h-0 flex-1 p-3 ' + (compact ? '' : 'p-4')}>
      <div className={'grid h-full gap-2 ' + gridClass}>
        {tiles.map((p) => (
          <div key={p.id} className="[&>div]:h-full [&>div]:w-full">
            <TileRender
              tile={p}
              activeSpeakers={activeSpeakers}
              e2ee={e2ee}
              remoteScreenShares={remoteScreenShares}
              conversationMembers={conversationMembers}
              onClick={() => onFocusTile(p.id)}
              {...(onTileContextMenu
                ? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(p, e) }
                : {})}
            />
          </div>
        ))}
      </div>
    </div>
  );
}

function FocusedTile({
  tile,
  e2ee,
  activeSpeakers,
  remoteScreenShares,
  conversationMembers,
}: {
  tile: Tile;
  e2ee: boolean;
  activeSpeakers: Set<string>;
  remoteScreenShares: StageProps['remoteScreenShares'];
  conversationMembers: StageProps['conversationMembers'];
}) {
  return (
    <div className="h-full [&>div]:h-full">
      <TileRender
        tile={tile}
        activeSpeakers={activeSpeakers}
        e2ee={e2ee}
        remoteScreenShares={remoteScreenShares}
        conversationMembers={conversationMembers}
        focused
      />
    </div>
  );
}

const GRID_PAGE_SIZE = 12;

function gridColsFor(n: number): string {
  // Explicit `grid-rows-*` so cells get a defined height (1fr of available
  // space). Without this, implicit rows default to auto → they size to
  // content, and a video element's intrinsic size blows the tile past the
  // container bounds (overlapping the toolbar below).
  if (n <= 1) return 'grid-cols-1 grid-rows-1';
  if (n === 2) return 'grid-cols-2 grid-rows-1';
  if (n === 3) return 'grid-cols-3 grid-rows-1';
  if (n === 4) return 'grid-cols-2 grid-rows-2';
  if (n <= 6) return 'grid-cols-3 grid-rows-2';
  if (n <= 9) return 'grid-cols-3 grid-rows-3';
  return 'grid-cols-4 grid-rows-3';
}

// Promote self + active speakers to the front of the tile list. Stable
// otherwise. Used by both pagination (so page 1 always carries the most
// "useful" tiles) and active-speaker promotion in fullscreen.
function prioritizeTiles(tiles: Tile[], activeSpeakers: Set<string>): Tile[] {
  const score = (t: Tile): number => {
    if (t.self) return 3;
    if (activeSpeakers.has(t.userId)) return 2;
    if (t.kind === 'screen') return 1;
    return 0;
  };
  return [...tiles].sort((a, b) => score(b) - score(a));
}

// ---------------------------------------------------------------------------
// Fullscreen cinema mode
// ---------------------------------------------------------------------------

interface FullscreenProps {
  tiles: Tile[];
  speaker: Tile | undefined;
  remoteScreenShares: StageProps['remoteScreenShares'];
  conversationMembers: StageProps['conversationMembers'];
  activeSpeakers: Set<string>;
  e2ee: boolean;
  onExit: () => void;
  onFocusTile: (id: string) => void;
  onTileContextMenu?: (tile: Tile, e: React.MouseEvent) => void;
  controls: React.ReactNode;
}

function FullscreenCall({
  tiles,
  speaker,
  remoteScreenShares,
  conversationMembers,
  activeSpeakers,
  e2ee,
  onExit: _onExit,
  onFocusTile,
  onTileContextMenu,
  controls,
}: FullscreenProps) {
  const [hintGone, setHintGone] = useState(false);
  const [page, setPage] = useState(0);
  useEffect(() => {
    const id = window.setTimeout(() => setHintGone(true), 3500);
    return () => window.clearTimeout(id);
  }, []);

  const hasFocus = speaker !== undefined;
  const others = hasFocus ? tiles.filter((p) => p.id !== speaker!.id) : [];

  // Active-speaker reorder + paginate. When more than GRID_PAGE_SIZE tiles
  // exist, slice them into pages. Reset to page 0 if the page count drops
  // below the current page (someone left).
  const sortedGridTiles = useMemo(
    () => prioritizeTiles(tiles, activeSpeakers),
    [tiles, activeSpeakers],
  );
  const pageCount = Math.max(1, Math.ceil(sortedGridTiles.length / GRID_PAGE_SIZE));
  useEffect(() => {
    if (page >= pageCount) setPage(0);
  }, [pageCount, page]);
  const visibleTiles = sortedGridTiles.slice(
    page * GRID_PAGE_SIZE,
    (page + 1) * GRID_PAGE_SIZE,
  );
  const gridClass = gridColsFor(visibleTiles.length);

  return (
    <div className="fixed inset-0 z-[60] flex flex-col overflow-hidden bg-surface">
      {/* Content area. pb-24 reserves ~96px space at the bottom for the
          floating controls bar so tiles never sit behind it. */}
      <div className="relative flex min-h-0 flex-1 flex-col pb-24">
        {hasFocus ? (
          <>
            <div
              className="relative min-h-0 flex-1 cursor-pointer p-4 pb-2 [&>div]:h-full [&>div]:w-full"
              onClick={() => onFocusTile(speaker!.id)}
              title="Zurück zur Übersicht"
            >
              <TileRender
                tile={speaker!}
                activeSpeakers={activeSpeakers}
                e2ee={e2ee}
                remoteScreenShares={remoteScreenShares}
                conversationMembers={conversationMembers}
                focused
                {...(onTileContextMenu
                  ? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(speaker!, e) }
                  : {})}
              />
            </div>
            {others.length > 0 && (
              <div className="flex h-[160px] shrink-0 gap-2.5 overflow-x-auto px-4 pb-2">
                {others.map((p) => (
                  <div
                    key={p.id}
                    className="h-full w-[220px] shrink-0 [&>div]:h-full [&>div]:w-full"
                  >
                    <TileRender
                      tile={p}
                      activeSpeakers={activeSpeakers}
                      e2ee={e2ee}
                      remoteScreenShares={remoteScreenShares}
                      conversationMembers={conversationMembers}
                      size="small"
                      onClick={() => onFocusTile(p.id)}
                      {...(onTileContextMenu
                        ? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(p, e) }
                        : {})}
                    />
                  </div>
                ))}
              </div>
            )}
          </>
        ) : (
          <div className="min-h-0 flex-1 p-4">
            <div className={'grid h-full gap-2 ' + gridClass}>
              {visibleTiles.map((p) => (
                <div key={p.id} className="min-h-0 min-w-0 [&>div]:h-full [&>div]:w-full">
                  <TileRender
                    tile={p}
                    activeSpeakers={activeSpeakers}
                    e2ee={e2ee}
                    remoteScreenShares={remoteScreenShares}
                    conversationMembers={conversationMembers}
                    onClick={() => onFocusTile(p.id)}
                    {...(onTileContextMenu
                      ? { onContextMenu: (e: React.MouseEvent) => onTileContextMenu(p, e) }
                      : {})}
                  />
                </div>
              ))}
            </div>
            {pageCount > 1 && (
              <div className="mt-2 flex items-center justify-center gap-3 text-xs text-fg-muted">
                <button
                  type="button"
                  onClick={() => setPage((p) => (p === 0 ? pageCount - 1 : p - 1))}
                  className="cursor-pointer rounded-md border border-line bg-surface-2 px-2 py-1 hover:bg-surface-3"
                  aria-label="Vorherige Seite"
                >
                  ‹
                </button>
                <span className="tabular-nums">
                  {page + 1} / {pageCount}
                </span>
                <button
                  type="button"
                  onClick={() => setPage((p) => (p + 1) % pageCount)}
                  className="cursor-pointer rounded-md border border-line bg-surface-2 px-2 py-1 hover:bg-surface-3"
                  aria-label="Nächste Seite"
                >
                  ›
                </button>
              </div>
            )}
          </div>
        )}
      </div>

      {!hintGone && (
        <div
          aria-hidden="true"
          className="pointer-events-none absolute left-1/2 top-5 z-10 animate-fs-hint rounded-lg border border-white/10 bg-black/60 px-3.5 py-1.5 text-[11px] font-medium tracking-wide text-white/70 backdrop-blur-md"
        >
          Esc zum Verlassen
        </div>
      )}

      <div className="pointer-events-auto absolute bottom-4 left-1/2 -translate-x-1/2">
        {controls}
      </div>
    </div>
  );
}

function PttHint() {
  const [ptt, setPtt] = useState<PttSettings>(() => getPttSettings());
  useEffect(() => subscribePttSettings(setPtt), []);
  if (!ptt.enabled) return null;
  return (
    <p className="border-t border-line bg-surface-3 py-2 text-center text-[11px] text-fg-muted">
      Push-to-Talk:&nbsp;
      <kbd className="rounded border border-line bg-surface-2 px-1.5 py-0.5 font-mono text-[10px] text-fg">
        {ptt.keyLabel}
      </kbd>
    </p>
  );
}
