import { Head } from '@inertiajs/react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Whiteboard, drawBoardEvent } from '@/Components/ClassRoom/Whiteboard';
import type { BoardEvent } from '@/Components/ClassRoom/Whiteboard';
import * as Icon from '@/Components/ClassRoom/Icons';
import { getEcho } from '@/lib/echo';
import { formatPersianTime } from '@/lib/persianDate';
import { getStoredTheme, setTheme, type Theme } from '@/lib/theme';
import { gradeQuality, OFFLINE_QUALITY, sampleQuality, watchSpeaking, type Quality } from '@/lib/roomMetrics';

type MediaPermissions = { audio: boolean; video: boolean; screen: boolean };
type MediaState = { audio: boolean; video: boolean; screen: boolean };

type Participant = {
  userId: number;
  name: string;
  isHost: boolean;
  roleKey: string;
  roleLabel: string;
  hand: boolean;
  handOrder: number | null;
  muted: boolean;
  kicked: boolean;
  online: boolean;
  lateJoin?: boolean;
  joinedAt?: string | null;
  permissions: MediaPermissions;
  media: MediaState;
};

type Message = {
  id: string;
  dbId?: number | null;
  userId: number;
  toUserId: number | null;
  name: string;
  isHost: boolean;
  body: string;
  time: string;
  createdAt?: string;
};

type Poll = {
  id: number;
  question: string;
  options: string[];
  counts: number[];
  totalVotes: number;
  myVote: number | null;
};

type FileRow = { id: number; name: string; size: number; by: string };
type Reaction = { id: number; emoji: string; name: string };
type PollQuestion = { id: number; body: string; options: string[] };
type RecentJoin = { userId: number; name: string; joinedAt: string };
type PresenceUser = { id: number; name: string; isHost: boolean };
type Signal = {
  id: string;
  dbId?: number;
  fromUserId: number;
  toUserId: number;
  type: 'offer' | 'answer' | 'ice' | 'renegotiate';
  data: Record<string, unknown>;
};

type Props = {
  classId: number;
  roomKey: string;
  title: string;
  status: 'scheduled' | 'live' | 'ended';
  you: {
    userId: number;
    name: string;
    isHost: boolean;
    roleKey: string;
    permissions: MediaPermissions;
  };
  settings: { allowStudentChat: boolean; allowStudentFiles: boolean };
  pollQuestions?: PollQuestion[];
  iceServers?: RTCIceServer[];
  realtimeKey?: string;
  mediaServer?: { name: string; region: string | null; load: number; capacity: number; healthy: boolean } | null;
};

function xsrf(): string {
  const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);
  return match ? decodeURIComponent(match[1]) : '';
}

async function api(path: string, body: Record<string, unknown> = {}): Promise<Response> {
  const response = await fetch(path, {
    method: 'POST',
    credentials: 'same-origin',
    headers: {
      'Content-Type': 'application/json',
      'X-Requested-With': 'XMLHttpRequest',
      'X-XSRF-TOKEN': xsrf(),
      Accept: 'application/json',
    },
    body: JSON.stringify(body),
  });
  if (!response.ok) {
    let message = 'عملیات انجام نشد. اتصال یا دسترسی خود را بررسی کنید.';
    try {
      const payload = await response.clone().json() as { message?: string };
      if (payload.message) message = payload.message;
    } catch { /* non-JSON error response */ }
    throw new Error(message);
  }
  return response;
}

function fmtSize(bytes: number): string {
  if (bytes > 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
  if (bytes > 1024) return `${Math.round(bytes / 1024)} KB`;
  return `${bytes} B`;
}

function initials(name: string): string {
  return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join('');
}

function Video({ stream, muted = false, contain = false }: { stream: MediaStream; muted?: boolean; contain?: boolean }) {
  const ref = useRef<HTMLVideoElement>(null);
  useEffect(() => {
    if (ref.current) ref.current.srcObject = stream;
  }, [stream]);
  return <video ref={ref} autoPlay playsInline muted={muted} className={contain ? 'fit-contain' : undefined} />;
}

function RemoteAudio({ stream, onBlocked }: { stream: MediaStream; onBlocked: () => void }) {
  const ref = useRef<HTMLAudioElement>(null);
  useEffect(() => {
    const audio = ref.current;
    if (!audio) return;
    audio.srcObject = stream;
    void audio.play().catch(onBlocked);
  }, [onBlocked, stream]);
  return <audio ref={ref} autoPlay playsInline className="rm-remote-audio" />;
}

/** Header pill: live signal bars + measured round-trip time. */
function ConnectionPill({ quality, transport }: { quality: Quality; transport: 'live' | 'fallback' | 'connecting' }) {
  const title = transport === 'live'
    ? 'اتصال لحظه‌ای برقرار است'
    : transport === 'fallback'
      ? 'اتصال لحظه‌ای قطع است؛ هر ۲ ثانیه بررسی می‌شود'
      : 'در حال اتصال…';

  return (
    <span className={`rm-pill rm-conn lvl-${quality.level}`} title={title}>
      <Icon.SignalBars level={quality.level} size={16} />
      <span className="rm-conn-text">
        {quality.rttMs !== null ? (
          <><b>{quality.rttMs}</b><small>ms</small></>
        ) : (
          <small>{transport === 'connecting' ? 'اتصال…' : 'بدون تماس'}</small>
        )}
      </span>
      {quality.lossPct !== null && quality.lossPct >= 2 && (
        <small className="rm-loss" title="افت بسته">{quality.lossPct.toFixed(0)}٪</small>
      )}
    </span>
  );
}

function PollCard({ poll, isHost, onVote, onClose }: {
  poll: Poll;
  isHost: boolean;
  onVote: (pollId: number, option: number) => void;
  onClose: () => void;
}) {
  const voted = poll.myVote !== null;
  return (
    <section className="rm-poll">
      <header>
        <span><Icon.Pin size={14} /> نظرسنجی</span>
        {isHost && <button type="button" onClick={onClose} title="بستن نظرسنجی"><Icon.Close size={14} /></button>}
      </header>
      <strong className="rm-poll-q">{poll.question}</strong>
      <div className="rm-poll-opts">
        {poll.options.map((option, index) => {
          const percent = poll.totalVotes > 0 ? Math.round((poll.counts[index] / poll.totalVotes) * 100) : 0;
          const selected = poll.myVote === index;
          const leading = voted && poll.counts[index] === Math.max(...poll.counts) && poll.totalVotes > 0;
          return (
            <button
              key={option + index}
              type="button"
              className={`rm-poll-opt${selected ? ' is-mine' : ''}${leading ? ' is-leading' : ''}`}
              disabled={voted}
              onClick={() => onVote(poll.id, index)}
            >
              <i className="rm-poll-fill" style={{ width: voted ? `${percent}%` : '0%' }} />
              <span className="rm-poll-label">
                {selected && <Icon.Check size={13} />}
                {option}
              </span>
              {voted && <small className="rm-poll-pct">{percent}٪</small>}
            </button>
          );
        })}
      </div>
      <footer>{poll.totalVotes} رأی{voted ? '' : ' · یک گزینه را انتخاب کنید'}</footer>
    </section>
  );
}

export default function Room({ classId, roomKey, title, status: initialStatus, you, settings: initialSettings, pollQuestions = [], iceServers = [], realtimeKey = '', mediaServer = null }: Props) {
  const base = `/class-room/${roomKey}`;
  const [status, setStatus] = useState(initialStatus);
  const [settings, setSettings] = useState(initialSettings);
  const [permissions, setPermissions] = useState(you.permissions);
  const [participants, setParticipants] = useState<Participant[]>([]);
  const [messages, setMessages] = useState<Message[]>([]);
  const [files, setFiles] = useState<FileRow[]>([]);
  const [handRaised, setHandRaised] = useState(false);
  const [chatMuted, setChatMuted] = useState(false);
  const [kicked, setKicked] = useState(false);
  const [mainView, setMainView] = useState<'media' | 'board'>('media');
  const [thread, setThread] = useState<'public' | number>('public');
  const [draft, setDraft] = useState('');
  const [uploading, setUploading] = useState(false);
  const [activePoll, setActivePoll] = useState<Poll | null>(null);
  const [showPollForm, setShowPollForm] = useState(false);
  const [pollQuestion, setPollQuestion] = useState('');
  const [pollOptions, setPollOptions] = useState(['', '']);
  const [reactions, setReactions] = useState<Reaction[]>([]);
  const [joinedCount, setJoinedCount] = useState(0);
  const [recentJoins, setRecentJoins] = useState<RecentJoin[]>([]);
  const [transport, setTransport] = useState<'connecting' | 'live' | 'fallback'>('connecting');
  const [quality, setQuality] = useState<Quality>(OFFLINE_QUALITY);
  const [mediaError, setMediaError] = useState('');
  const [previewStream, setPreviewStream] = useState<MediaStream | null>(null);
  const [remoteStreams, setRemoteStreams] = useState<Record<number, MediaStream>>({});
  const [screenSharing, setScreenSharing] = useState(false);
  const [recording, setRecording] = useState(false);
  const [savingRecording, setSavingRecording] = useState(false);
  const [speaking, setSpeaking] = useState<Record<number, boolean>>({});
  const [sidebar, setSidebar] = useState<'chat' | 'files'>('chat');
  const [pinnedUserId, setPinnedUserId] = useState<number | null>(null);
  const [mobilePanel, setMobilePanel] = useState<'stage' | 'chat' | 'people'>('stage');
  const [audioBlocked, setAudioBlocked] = useState(false);
  const [signalInbox, setSignalInbox] = useState<Signal[]>([]);
  const [theme, setThemeState] = useState<Theme>(() => getStoredTheme());
  const [peerRevision, setPeerRevision] = useState(0);

  const lastMessageDbId = useRef(0);
  const lastBoardDbId = useRef(0);
  const lastSignalDbId = useRef(0);
  const seenBoardEvents = useRef(new Set<string>());
  const chatScrollRef = useRef<HTMLDivElement>(null);
  const localStreamRef = useRef(new MediaStream());
  const screenStreamRef = useRef<MediaStream | null>(null);
  const peersRef = useRef(new Map<number, RTCPeerConnection>());
  const pendingCandidatesRef = useRef(new Map<number, RTCIceCandidateInit[]>());
  const recorderRef = useRef<MediaRecorder | null>(null);
  const recordingChunksRef = useRef<Blob[]>([]);
  const recordingStartRef = useRef<Date | null>(null);
  const recordingAudioContextRef = useRef<AudioContext | null>(null);
  const speakingStopsRef = useRef(new Map<number, () => void>());
  const speakingTrackIdsRef = useRef(new Map<number, string>());
  const httpQualityRef = useRef<Quality>(OFFLINE_QUALITY);

  const markAudioBlocked = useCallback(() => setAudioBlocked(true), []);
  const unlockRemoteAudio = useCallback(async () => {
    const outputs = Array.from(document.querySelectorAll<HTMLAudioElement>('.rm-remote-audio'));
    const results = await Promise.allSettled(outputs.map((audio) => audio.play()));
    setAudioBlocked(results.some((result) => result.status === 'rejected'));
  }, []);
  const toggleTheme = useCallback(() => {
    const next: Theme = theme === 'dark' ? 'light' : 'dark';
    setTheme(next);
    setThemeState(next);
  }, [theme]);

  const runAction = useCallback(async (path: string, body: Record<string, unknown> = {}) => {
    try {
      setMediaError('');
      await api(path, body);
      await syncOnce();
    } catch (error) {
      setMediaError(error instanceof Error ? error.message : 'عملیات انجام نشد.');
    }
  // syncOnce is declared below and is stable before this callback is invoked.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const hosts = useMemo(() => participants.filter((p) => p.isHost), [participants]);
  const students = useMemo(() => participants.filter((p) => !p.isHost), [participants]);
  const onlineParticipants = useMemo(
    () => participants.filter((p) => p.online && !p.kicked),
    [participants],
  );
  const onlineCount = onlineParticipants.length;
  const raisedHands = useMemo(
    () => onlineParticipants.filter((p) => p.hand).sort((a, b) => (a.handOrder ?? 0) - (b.handOrder ?? 0)),
    [onlineParticipants],
  );

  const threadMessages = useMemo(() => {
    if (thread === 'public') return messages.filter((m) => m.toUserId === null);
    if (!you.isHost) return messages.filter((m) => m.toUserId !== null);
    return messages.filter((m) => (m.userId === thread && m.toUserId !== null) || m.toUserId === thread);
  }, [messages, thread, you.isHost]);

  const mergeMessages = useCallback((incoming: Message[]) => {
    if (incoming.length === 0) return;
    setMessages((current) => {
      const byId = new Map(current.map((m) => [m.id, m]));
      incoming.forEach((m) => byId.set(m.id, { ...byId.get(m.id), ...m }));
      return [...byId.values()].slice(-400);
    });
  }, []);

  const syncOnce = useCallback(async () => {
    const started = performance.now();
    try {
      const response = await fetch(`${base}/sync?after_message=${lastMessageDbId.current}&after_board=${lastBoardDbId.current}&after_signal=${lastSignalDbId.current}`, {
        credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json' },
      });
      if (!response.ok) return;
      const data = await response.json();
      if (data.kicked) { setKicked(true); return; }
      setKicked(false);
      const httpQuality = gradeQuality(Math.max(1, Math.round(performance.now() - started)), null);
      httpQualityRef.current = httpQuality;
      setStatus(data.status);
      setSettings(data.settings);
      setHandRaised(data.you.handRaised);
      setChatMuted(data.you.chatMuted);
      setPermissions(data.you.permissions);
      setParticipants(data.participants);
      mergeMessages(data.messages ?? []);
      for (const board of data.board ?? []) {
        const eventId = String(board.id);
        if (seenBoardEvents.current.has(eventId)) continue;
        seenBoardEvents.current.add(eventId);
        drawBoardEvent(board.event as BoardEvent);
      }
      const messageDbIds = (data.messages ?? []).map((m: Message) => m.dbId ?? 0);
      const boardDbIds = (data.board ?? []).map((b: { dbId?: number }) => b.dbId ?? 0);
      if (messageDbIds.length > 0) lastMessageDbId.current = Math.max(lastMessageDbId.current, ...messageDbIds);
      if (boardDbIds.length > 0) lastBoardDbId.current = Math.max(lastBoardDbId.current, ...boardDbIds);
      const incomingSignals = (data.signals ?? []) as Signal[];
      if (incomingSignals.length > 0) {
        lastSignalDbId.current = Math.max(lastSignalDbId.current, ...incomingSignals.map((signal) => signal.dbId ?? 0));
        setSignalInbox((current) => [...current, ...incomingSignals].slice(-160));
      }
      setFiles(data.files ?? []);
      setActivePoll(data.poll ?? null);
      setReactions(data.reactions ?? []);
      setJoinedCount(data.joinedCount ?? 0);
      setRecentJoins(data.recentJoins ?? []);
    } catch {
      setTransport((current) => (current === 'live' ? current : 'fallback'));
    }
  }, [base, mergeMessages]);

  const sendSignal = useCallback(async (targetUserId: number, type: Signal['type'], data: Record<string, unknown>) => {
    await api(`${base}/signal`, { to_user_id: targetUserId, type, data });
  }, [base]);

  const createPeer = useCallback((targetUserId: number): RTCPeerConnection => {
    const existing = peersRef.current.get(targetUserId);
    if (existing) return existing;
    const peer = new RTCPeerConnection({ iceServers });
    localStreamRef.current.getTracks().forEach((track) => peer.addTrack(track, localStreamRef.current));
    peer.onicecandidate = (event) => {
      if (event.candidate) void sendSignal(targetUserId, 'ice', { candidate: event.candidate.toJSON() });
    };
    peer.ontrack = (event) => {
      // A screen track can arrive in a different MediaStream from the mic.
      // Merge tracks instead of replacing the user's stream, otherwise
      // starting screen share can silently remove their audio element.
      setRemoteStreams((current) => {
        const aggregate = current[targetUserId] ?? new MediaStream();
        const incoming = event.streams[0]?.getTracks() ?? [event.track];
        incoming.forEach((track) => {
          if (!aggregate.getTracks().some((existingTrack) => existingTrack.id === track.id)) aggregate.addTrack(track);
        });
        return { ...current, [targetUserId]: aggregate };
      });
      event.track.onended = () => {
        setRemoteStreams((current) => {
          const aggregate = current[targetUserId];
          if (!aggregate) return current;
          aggregate.removeTrack(event.track);
          if (aggregate.getTracks().some((track) => track.readyState === 'live')) return { ...current, [targetUserId]: aggregate };
          const next = { ...current };
          delete next[targetUserId];
          return next;
        });
      };
    };
    peer.onconnectionstatechange = () => {
      if (['failed', 'closed'].includes(peer.connectionState)) {
        peer.close();
        peersRef.current.delete(targetUserId);
        setRemoteStreams((current) => { const next = { ...current }; delete next[targetUserId]; return next; });
        setPeerRevision((revision) => revision + 1);
      }
    };
    peersRef.current.set(targetUserId, peer);
    return peer;
  }, [iceServers, sendSignal]);

  const makeOffer = useCallback(async (targetUserId: number) => {
    const peer = createPeer(targetUserId);
    if (peer.signalingState !== 'stable') return;
    try {
      const offer = await peer.createOffer();
      await peer.setLocalDescription(offer);
      await sendSignal(targetUserId, 'offer', { sdp: offer });
    } catch { /* simultaneous negotiation retries on the next media change */ }
  }, [createPeer, sendSignal]);

  const handleSignal = useCallback(async (signal: Signal) => {
    if (signal.toUserId !== you.userId || signal.fromUserId === you.userId) return;
    const peer = createPeer(signal.fromUserId);
    try {
      if (signal.type === 'offer') {
        if (peer.signalingState !== 'stable') await peer.setLocalDescription({ type: 'rollback' });
        await peer.setRemoteDescription(signal.data.sdp as RTCSessionDescriptionInit);
        for (const candidate of pendingCandidatesRef.current.get(signal.fromUserId) ?? []) await peer.addIceCandidate(candidate);
        pendingCandidatesRef.current.delete(signal.fromUserId);
        const answer = await peer.createAnswer();
        await peer.setLocalDescription(answer);
        await sendSignal(signal.fromUserId, 'answer', { sdp: answer });
      } else if (signal.type === 'answer') {
        await peer.setRemoteDescription(signal.data.sdp as RTCSessionDescriptionInit);
        for (const candidate of pendingCandidatesRef.current.get(signal.fromUserId) ?? []) await peer.addIceCandidate(candidate);
        pendingCandidatesRef.current.delete(signal.fromUserId);
      } else if (signal.type === 'ice' && signal.data.candidate) {
        const candidate = signal.data.candidate as RTCIceCandidateInit;
        if (peer.remoteDescription) await peer.addIceCandidate(candidate);
        else pendingCandidatesRef.current.set(signal.fromUserId, [...(pendingCandidatesRef.current.get(signal.fromUserId) ?? []), candidate]);
      } else if (signal.type === 'renegotiate') {
        await makeOffer(signal.fromUserId);
      }
    } catch { /* ICE collisions recover through the next sync cycle */ }
  }, [createPeer, makeOffer, sendSignal, you.userId]);

  useEffect(() => {
    if (signalInbox.length === 0) return;
    const pending = signalInbox;
    setSignalInbox([]);
    pending.forEach((signal) => void handleSignal(signal));
  }, [handleSignal, signalInbox]);

  // Polling must also create peer connections; previously this only happened
  // inside Reverb presence callbacks, leaving audio dead whenever sockets fell back.
  useEffect(() => {
    const onlineIds = new Set(onlineParticipants.map((participant) => participant.userId));
    peersRef.current.forEach((peer, userId) => {
      if (!onlineIds.has(userId)) {
        peer.close();
        peersRef.current.delete(userId);
        setRemoteStreams((current) => {
          if (!current[userId]) return current;
          const next = { ...current };
          delete next[userId];
          return next;
        });
      }
    });
    onlineParticipants
      .filter((participant) => participant.userId !== you.userId && you.userId < participant.userId && !peersRef.current.has(participant.userId))
      .forEach((participant) => void makeOffer(participant.userId));
  }, [makeOffer, onlineParticipants, peerRevision, you.userId]);

  useEffect(() => {
    void syncOnce();
    const echo = getEcho(realtimeKey);
    const presenceChannel = `classroom.${classId}`;
    const privateChannel = `classroom.${classId}.user.${you.userId}`;
    if (echo) {
      const connector = echo.connector as unknown as { pusher?: { connection?: { bind: (name: string, callback: () => void) => void } } };
      connector.pusher?.connection?.bind('connected', () => setTransport('live'));
      connector.pusher?.connection?.bind('unavailable', () => setTransport('fallback'));
      connector.pusher?.connection?.bind('failed', () => setTransport('fallback'));
      echo.join(presenceChannel)
        .here((users: PresenceUser[]) => {
          setTransport('live');
          users.filter((user) => user.id !== you.userId && you.userId < user.id).forEach((user) => void makeOffer(user.id));
        })
        .joining((user: PresenceUser) => {
          if (user.id !== you.userId && you.userId < user.id) void makeOffer(user.id);
          void syncOnce();
        })
        .leaving((user: PresenceUser) => {
          peersRef.current.get(user.id)?.close();
          peersRef.current.delete(user.id);
          setRemoteStreams((current) => { const next = { ...current }; delete next[user.id]; return next; });
          void syncOnce();
        })
        .listen('.board', (data: { id: string; dbId?: number; event: BoardEvent }) => {
          const eventId = String(data.id);
          if (seenBoardEvents.current.has(eventId)) return;
          seenBoardEvents.current.add(eventId);
          drawBoardEvent(data.event);
          if (data.dbId) lastBoardDbId.current = Math.max(lastBoardDbId.current, data.dbId);
        })
        .listen('.chat', (message: Message) => mergeMessages([message]));

      echo.private(privateChannel)
        .listen('.chat', (message: Message) => mergeMessages([message]))
        .listen('.signal', (signal: Signal) => {
          if (signal.dbId) lastSignalDbId.current = Math.max(lastSignalDbId.current, signal.dbId);
          void handleSignal(signal);
        })
        .listen('.control', (control: { type: string; targetUserId: number; permissions?: MediaPermissions }) => {
          if (control.targetUserId === you.userId && control.type === 'permissions' && control.permissions) setPermissions(control.permissions);
          if (control.targetUserId === you.userId && control.type === 'kicked') setKicked(true);
          if (control.targetUserId === you.userId && control.type === 'restored') setKicked(false);
          void syncOnce();
        });
    } else setTransport('fallback');

    const syncTimer = window.setInterval(syncOnce, 2200);
    const pageLeave = () => {
      void fetch(`${base}/leave`, {
        method: 'POST', credentials: 'same-origin', keepalive: true,
        headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-XSRF-TOKEN': xsrf() }, body: '{}',
      });
    };
    window.addEventListener('pagehide', pageLeave);
    return () => {
      window.clearInterval(syncTimer);
      window.removeEventListener('pagehide', pageLeave);
      echo?.leave(presenceChannel);
      echo?.leave(privateChannel);
      peersRef.current.forEach((peer) => peer.close());
      peersRef.current.clear();
      pendingCandidatesRef.current.clear();
      localStreamRef.current.getTracks().forEach((track) => track.stop());
      screenStreamRef.current?.getTracks().forEach((track) => track.stop());
    };
  }, [base, classId, handleSignal, makeOffer, mergeMessages, realtimeKey, syncOnce, you.userId]);

  // Poll WebRTC stats for the header ping/quality readout.
  useEffect(() => {
    const timer = window.setInterval(() => {
      void sampleQuality(peersRef.current.values()).then((next) => setQuality(next.rttMs === null ? httpQualityRef.current : next));
    }, 3000);
    return () => window.clearInterval(timer);
  }, []);

  // Speaking rings — watch every audio stream we hold.
  useEffect(() => {
    const stops = speakingStopsRef.current;
    const trackIds = speakingTrackIdsRef.current;

    const watch = (id: number, stream: MediaStream) => {
      const signature = stream.getAudioTracks().map((track) => track.id).sort().join(':');
      if (stops.has(id) && trackIds.get(id) === signature) return;
      stops.get(id)?.();
      stops.delete(id);
      trackIds.delete(id);
      if (!signature) {
        setSpeaking((current) => (current[id] ? { ...current, [id]: false } : current));
        return;
      }
      const stop = watchSpeaking(stream, (isSpeaking) => {
        setSpeaking((current) => (current[id] === isSpeaking ? current : { ...current, [id]: isSpeaking }));
      });
      stops.set(id, stop);
      trackIds.set(id, signature);
    };

    if (localStreamRef.current.getAudioTracks().length > 0) watch(you.userId, localStreamRef.current);
    Object.entries(remoteStreams).forEach(([id, stream]) => watch(Number(id), stream));

    // Drop watchers for peers who left.
    stops.forEach((stop, id) => {
      if (id !== you.userId && !remoteStreams[id]) {
        stop();
        stops.delete(id);
        trackIds.delete(id);
      }
    });

    return () => { /* watchers are torn down on unmount below */ };
  }, [remoteStreams, previewStream, you.userId]);

  useEffect(() => () => {
    speakingStopsRef.current.forEach((stop) => stop());
    speakingStopsRef.current.clear();
    speakingTrackIdsRef.current.clear();
  }, []);

  useEffect(() => {
    const scroller = chatScrollRef.current;
    if (!scroller) return;
    if (scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight < 140) scroller.scrollTop = scroller.scrollHeight;
  }, [threadMessages]);

  const publishMediaState = useCallback(async (sharing = screenSharing) => {
    const stream = localStreamRef.current;
    await api(`${base}/media-state`, {
      audio: stream.getAudioTracks().some((track) => track.enabled),
      video: stream.getVideoTracks().some((track) => track.enabled) && !sharing,
      screen: sharing,
    });
  }, [base, screenSharing]);

  const renegotiateAll = useCallback(() => {
    onlineParticipants.filter((p) => p.userId !== you.userId).forEach((p) => void makeOffer(p.userId));
  }, [makeOffer, onlineParticipants, you.userId]);

  const addLocalTrack = useCallback(async (kind: 'audio' | 'video') => {
    if (!navigator.mediaDevices?.getUserMedia) throw new Error('مرورگر شما دسترسی دوربین و میکروفون را پشتیبانی نمی‌کند.');
    const captured = await navigator.mediaDevices.getUserMedia({
      audio: kind === 'audio' ? { echoCancellation: true, noiseSuppression: true, autoGainControl: true } : false,
      video: kind === 'video' ? { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 24, max: 30 } } : false,
    });
    const track = kind === 'audio' ? captured.getAudioTracks()[0] : captured.getVideoTracks()[0];
    if (!track) throw new Error(kind === 'audio' ? 'میکروفون پیدا نشد.' : 'دوربین پیدا نشد.');
    localStreamRef.current.addTrack(track);
    peersRef.current.forEach((peer) => peer.addTrack(track, localStreamRef.current));
    setPreviewStream(new MediaStream(localStreamRef.current.getTracks()));
    renegotiateAll();
  }, [renegotiateAll]);

  const toggleAudio = async () => {
    if (!permissions.audio) { setMediaError('دسترسی میکروفون باید توسط معلم فعال شود.'); return; }
    setMediaError('');
    try {
      const track = localStreamRef.current.getAudioTracks()[0];
      if (track) track.enabled = !track.enabled; else await addLocalTrack('audio');
      setPreviewStream(new MediaStream(localStreamRef.current.getTracks()));
      await publishMediaState();
    } catch (error) {
      setMediaError(error instanceof Error ? error.message : 'میکروفون باز نشد. مجوز مرورگر را بررسی کنید.');
    }
  };

  const toggleVideo = async () => {
    if (!permissions.video) { setMediaError('دسترسی دوربین باید توسط معلم فعال شود.'); return; }
    setMediaError('');
    try {
      const track = localStreamRef.current.getVideoTracks()[0];
      if (track) track.enabled = !track.enabled; else await addLocalTrack('video');
      setPreviewStream(new MediaStream(localStreamRef.current.getTracks()));
      await publishMediaState();
    } catch (error) {
      setMediaError(error instanceof Error ? error.message : 'دوربین باز نشد. مجوز مرورگر را بررسی کنید.');
    }
  };

  const stopScreenShare = useCallback(async () => {
    const cameraTrack = localStreamRef.current.getVideoTracks()[0] ?? null;
    peersRef.current.forEach((peer) => {
      const sender = peer.getSenders().find((item) => item.track?.kind === 'video');
      if (sender) void sender.replaceTrack(cameraTrack);
    });
    screenStreamRef.current?.getTracks().forEach((track) => track.stop());
    screenStreamRef.current = null;
    setScreenSharing(false);
    setPreviewStream(new MediaStream(localStreamRef.current.getTracks()));
    try { await publishMediaState(false); } catch { /* class may have ended during sharing */ }
  }, [publishMediaState]);

  const toggleScreenShare = async () => {
    if (screenSharing) { await stopScreenShare(); return; }
    if (!permissions.screen) { setMediaError('دسترسی اشتراک صفحه باید توسط معلم فعال شود.'); return; }
    if (!navigator.mediaDevices?.getDisplayMedia) { setMediaError('مرورگر شما اشتراک صفحه را پشتیبانی نمی‌کند.'); return; }
    try {
      setMediaError('');
      const display = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: { ideal: 15, max: 24 } }, audio: true });
      const displayTrack = display.getVideoTracks()[0];
      screenStreamRef.current = display;
      setScreenSharing(true);
      setPreviewStream(display);
      peersRef.current.forEach((peer) => {
        const sender = peer.getSenders().find((item) => item.track?.kind === 'video');
        if (sender) void sender.replaceTrack(displayTrack); else peer.addTrack(displayTrack, display);
      });
      displayTrack.onended = () => void stopScreenShare();
      renegotiateAll();
      await publishMediaState(true);
    } catch (error) {
      setMediaError(error instanceof Error && error.name === 'NotAllowedError' ? 'اشتراک صفحه لغو شد یا مجوز داده نشد.' : 'اشتراک صفحه شروع نشد.');
    }
  };

  useEffect(() => {
    if (!permissions.audio) localStreamRef.current.getAudioTracks().forEach((track) => { track.enabled = false; });
    if (!permissions.video) localStreamRef.current.getVideoTracks().forEach((track) => { track.enabled = false; });
    if (!permissions.screen && screenStreamRef.current) void stopScreenShare();
    setPreviewStream(new MediaStream(localStreamRef.current.getTracks()));
  }, [permissions, stopScreenShare]);

  useEffect(() => {
    if (status !== 'ended') return;
    localStreamRef.current.getTracks().forEach((track) => { track.enabled = false; });
    if (screenStreamRef.current) void stopScreenShare();
    setPreviewStream(new MediaStream(localStreamRef.current.getTracks()));
  }, [status, stopScreenShare]);

  const send = async () => {
    const body = draft.trim();
    if (!body) return;
    let target: number | null = null;
    if (thread !== 'public') {
      target = you.isHost ? thread : (hosts.find((h) => h.online)?.userId ?? hosts[0]?.userId ?? null);
      if (target === null) return;
    }
    try {
      const response = await api(`${base}/message`, { body, to_user_id: target });
      const payload = await response.json();
      setDraft('');
      if (payload.message) mergeMessages([payload.message]);
    } catch (error) {
      setMediaError(error instanceof Error ? error.message : 'پیام ارسال نشد.');
    }
  };

  const upload = async (file: File) => {
    setUploading(true);
    const form = new FormData(); form.append('file', file);
    try {
      const response = await fetch(`${base}/file`, { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-XSRF-TOKEN': xsrf() }, body: form });
      if (!response.ok) throw new Error('فایل ارسال نشد. اندازه فایل باید کمتر از ۲۰ مگابایت باشد.');
      await syncOnce();
    } catch (error) {
      setMediaError(error instanceof Error ? error.message : 'فایل ارسال نشد.');
    } finally { setUploading(false); }
  };

  const submitPoll = async () => {
    const options = pollOptions.map((option) => option.trim()).filter(Boolean);
    if (!pollQuestion.trim() || options.length < 2) return;
    try {
      await api(`${base}/poll/open`, { question: pollQuestion.trim(), options });
      setShowPollForm(false);
      setPollQuestion('');
      setPollOptions(['', '']);
      await syncOnce();
    } catch (error) {
      setMediaError(error instanceof Error ? error.message : 'نظرسنجی ایجاد نشد.');
    }
  };

  const useQuestionPoll = (questionId: string) => {
    const question = pollQuestions.find((item) => String(item.id) === questionId);
    if (!question) return;
    setPollQuestion(question.body.replace(/<[^>]+>/g, ''));
    setPollOptions(question.options.slice(0, 6));
  };

  const togglePermission = async (participant: Participant, permission: keyof MediaPermissions) => {
    try {
      await api(`${base}/media-permission`, { user_id: participant.userId, permission, allowed: !participant.permissions[permission] });
      await syncOnce();
    } catch (error) {
      setMediaError(error instanceof Error ? error.message : 'دسترسی تغییر نکرد.');
    }
  };

  const startRecording = async () => {
    if (!you.isHost || recording) return;
    const source = screenStreamRef.current ?? localStreamRef.current;
    const videoTrack = source.getVideoTracks().find((track) => track.readyState === 'live' && track.enabled);
    if (!videoTrack) { setMediaError('برای ضبط، ابتدا دوربین یا اشتراک صفحه را روشن کنید.'); return; }
    try {
      const context = new AudioContext();
      const destination = context.createMediaStreamDestination();
      [localStreamRef.current, ...Object.values(remoteStreams)].forEach((stream) => {
        if (stream.getAudioTracks().length > 0) context.createMediaStreamSource(stream).connect(destination);
      });
      const recordingStream = new MediaStream([videoTrack, ...destination.stream.getAudioTracks()]);
      const mimeType = ['video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus', 'video/webm'].find((type) => MediaRecorder.isTypeSupported(type));
      const recorder = new MediaRecorder(recordingStream, mimeType ? { mimeType } : undefined);
      recordingAudioContextRef.current = context;
      recordingChunksRef.current = [];
      recordingStartRef.current = new Date();
      recorderRef.current = recorder;
      recorder.ondataavailable = (event) => { if (event.data.size > 0) recordingChunksRef.current.push(event.data); };
      recorder.onstop = async () => {
        const startedAt = recordingStartRef.current;
        const duration = startedAt ? Math.max(1, Math.round((Date.now() - startedAt.getTime()) / 1000)) : 1;
        const blob = new Blob(recordingChunksRef.current, { type: recorder.mimeType || 'video/webm' });
        const form = new FormData();
        form.append('recording', blob, `class-${classId}-${Date.now()}.webm`);
        form.append('duration_seconds', String(duration));
        form.append('started_at', startedAt?.toISOString() ?? new Date().toISOString());
        setSavingRecording(true);
        try {
          const response = await fetch(`${base}/recording`, { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-XSRF-TOKEN': xsrf(), Accept: 'application/json' }, body: form });
          if (!response.ok) setMediaError('فایل ضبط‌شده ذخیره نشد؛ حجم یا اتصال را بررسی کنید.');
        } finally {
          setSavingRecording(false); recordingChunksRef.current = [];
          recordingAudioContextRef.current?.close(); recordingAudioContextRef.current = null;
        }
      };
      recorder.start(1000); setRecording(true); setMediaError('');
    } catch { setMediaError('ضبط در این مرورگر شروع نشد. از Chrome یا Edge به‌روز استفاده کنید.'); }
  };

  const stopRecording = () => {
    if (recorderRef.current?.state === 'recording') recorderRef.current.stop();
    setRecording(false);
  };

  const setClassStatus = async (next: 'live' | 'ended') => {
    if (next === 'ended' && !confirm('کلاس برای همه پایان یابد؟')) return;
    if (next === 'ended' && recording) stopRecording();
    try {
      await api(`${base}/${next === 'live' ? 'start' : 'end'}`);
      setStatus(next);
      await syncOnce();
    } catch (error) {
      setMediaError(error instanceof Error ? error.message : 'وضعیت کلاس تغییر نکرد.');
    }
  };

  const canChat = status !== 'ended' && (you.isHost || (!chatMuted && (thread !== 'public' || settings.allowStudentChat)));
  const canUpload = status !== 'ended' && (you.isHost || settings.allowStudentFiles);
  const localAudioOn = localStreamRef.current.getAudioTracks().some((track) => track.enabled);
  const localVideoOn = localStreamRef.current.getVideoTracks().some((track) => track.enabled);
  const localHasVideo = !!previewStream && previewStream.getVideoTracks().some((t) => t.enabled);

  /**
   * Centre stage = whoever is presenting.
   * Priority: pinned > screen sharer > active speaker > first camera > self.
   */
  const stageUserId = useMemo(() => {
    if (pinnedUserId !== null && onlineParticipants.some((p) => p.userId === pinnedUserId)) return pinnedUserId;
    if (screenSharing) return you.userId;
    const sharer = onlineParticipants.find((p) => p.media.screen);
    if (sharer) return sharer.userId;
    const talker = onlineParticipants.find((p) => speaking[p.userId] && p.userId !== you.userId);
    if (talker) return talker.userId;
    const withCam = onlineParticipants.find((p) => p.media.video && p.userId !== you.userId);
    if (withCam) return withCam.userId;
    return you.userId;
  }, [pinnedUserId, onlineParticipants, screenSharing, speaking, you.userId]);

  const stagePerson = onlineParticipants.find((p) => p.userId === stageUserId);
  const stageIsMe = stageUserId === you.userId;
  const stageStream = stageIsMe ? previewStream : remoteStreams[stageUserId] ?? null;
  const stageHasVideo = stageIsMe
    ? localHasVideo || screenSharing
    : !!stageStream && (stagePerson?.media.video || stagePerson?.media.screen);
  const stageName = stageIsMe ? you.name : stagePerson?.name ?? '';
  const stageSharing = stageIsMe ? screenSharing : !!stagePerson?.media.screen;

  /** Everyone except whoever is on the centre stage — rendered in the left rail. */
  const railPeople = onlineParticipants.filter((p) => p.userId !== stageUserId);

  if (kicked) return (
    <div className="rm-gate" dir="rtl">
      <Head title={title} />
      <div className="rm-gate-card">
        <span className="rm-gate-icon"><Icon.KickUser size={30} /></span>
        <h2>دسترسی شما به کلاس بسته شد</h2>
        <p>اگر فکر می‌کنید اشتباهی رخ داده، با معلم کلاس هماهنگ کنید.</p>
        <a href="/dashboard" className="rm-btn-primary">بازگشت به پنل</a>
      </div>
    </div>
  );

  return (
    <div className="rm" dir="rtl">
      <Head title={`کلاس ${title}`} />
      <div className="rm-audio-layer" aria-hidden="true">
        {Object.entries(remoteStreams).map(([userId, stream]) => (
          <RemoteAudio key={userId} stream={stream} onBlocked={markAudioBlocked} />
        ))}
      </div>

      {/* ── Top bar ─────────────────────────────────────────────── */}
      <header className="rm-top">
        <div className="rm-brand">
          <span className={`rm-rec-dot${status === 'live' ? ' is-live' : ''}`} />
          <div className="rm-brand-text">
            <strong>{title}</strong>
            <small>{you.name} · {you.isHost ? 'میزبان' : 'دانش‌آموز'}</small>
          </div>
        </div>

        <div className="rm-top-meta">
          <ConnectionPill quality={quality} transport={transport} />
          {mediaServer && (
            <span className={`rm-pill rm-server${mediaServer.healthy ? ' is-healthy' : ''}`} title={`ظرفیت رله: ${mediaServer.load} از ${mediaServer.capacity}`}>
              <Icon.SignalBars level={mediaServer.healthy ? 3 : 1} size={14} />
              <span>{mediaServer.name}</span>
            </span>
          )}
          <span className={`rm-pill rm-status is-${status}`}>
            {status === 'live' ? 'زنده' : status === 'scheduled' ? 'آماده شروع' : 'پایان یافته'}
          </span>
          <span className="rm-pill" title={`${joinedCount} نفر تا کنون وارد شده‌اند`}>
            <Icon.People size={15} /> {onlineCount}
          </span>
          {audioBlocked && (
            <button type="button" className="rm-pill rm-sound" onClick={() => void unlockRemoteAudio()} title="فعال کردن صدای کلاس">
              <Icon.Volume size={15} /> فعال‌سازی صدا
            </button>
          )}
          <button type="button" className="rm-pill rm-icon-pill" onClick={toggleTheme} title={theme === 'dark' ? 'حالت روشن' : 'حالت تاریک'}>
            {theme === 'dark' ? <Icon.Sun size={15} /> : <Icon.Moon size={15} />}
          </button>
          <a href="/dashboard" className="rm-pill rm-leave" onClick={() => void api(`${base}/leave`)}>
            <Icon.Leave size={15} /> خروج
          </a>
        </div>
      </header>

      <div className="rm-body">
        {/* ── Right: chat + files ──────────────────────────────── */}
        <aside className={`rm-side${mobilePanel === 'chat' ? ' is-mobile-active' : ''}`}>
          <nav className="rm-side-tabs" role="tablist">
            <button type="button" role="tab" aria-selected={sidebar === 'chat'}
              className={sidebar === 'chat' ? 'is-active' : ''} onClick={() => setSidebar('chat')}>
              گفتگو
            </button>
            <button type="button" role="tab" aria-selected={sidebar === 'files'}
              className={sidebar === 'files' ? 'is-active' : ''} onClick={() => setSidebar('files')}>
              فایل‌ها {files.length > 0 && <b>{files.length}</b>}
            </button>
            {you.isHost && status !== 'ended' && (
              <button type="button" className={`rm-side-poll${showPollForm ? ' is-active' : ''}`}
                onClick={() => setShowPollForm(!showPollForm)} title="نظرسنجی جدید">
                <Icon.Poll size={15} />
              </button>
            )}
          </nav>

          {you.isHost && status !== 'ended' && (
            <div className="rm-room-settings">
              <button type="button" className={settings.allowStudentChat ? 'is-enabled' : ''} onClick={() => void runAction(`${base}/settings`, {
                allow_student_chat: !settings.allowStudentChat,
                allow_student_files: settings.allowStudentFiles,
              })}>
                {settings.allowStudentChat ? <Icon.Chat size={13} /> : <Icon.ChatOff size={13} />}
                گفتگوی عمومی
              </button>
              <button type="button" className={settings.allowStudentFiles ? 'is-enabled' : ''} onClick={() => void runAction(`${base}/settings`, {
                allow_student_chat: settings.allowStudentChat,
                allow_student_files: !settings.allowStudentFiles,
              })}>
                <Icon.Folder size={13} /> فایل دانش‌آموز
              </button>
            </div>
          )}

          {sidebar === 'chat' ? (
            <div className="rm-chat">
              <div className="rm-chat-scope">
                <select value={String(thread)} onChange={(e) => setThread(e.target.value === 'public' ? 'public' : Number(e.target.value))}>
                  <option value="public">گفتگوی عمومی</option>
                  {!you.isHost && hosts.length > 0 && <option value={hosts[0].userId}>خصوصی با {hosts[0].name}</option>}
                  {you.isHost && students.map((s) => <option key={s.userId} value={s.userId}>خصوصی با {s.name}</option>)}
                </select>
              </div>

              {showPollForm && you.isHost && status !== 'ended' && (
                <div className="rm-poll-form">
                  {pollQuestions.length > 0 && (
                    <select defaultValue="" onChange={(e) => { useQuestionPoll(e.target.value); e.target.value = ''; }}>
                      <option value="">انتخاب از بانک سؤال…</option>
                      {pollQuestions.map((q) => (
                        <option key={q.id} value={q.id}>{q.body.replace(/<[^>]+>/g, '').slice(0, 80)}</option>
                      ))}
                    </select>
                  )}
                  <input value={pollQuestion} onChange={(e) => setPollQuestion(e.target.value)} placeholder="سؤال نظرسنجی" />
                  {pollOptions.map((option, index) => (
                    <div key={index} className="rm-poll-row">
                      <input
                        value={option}
                        onChange={(e) => setPollOptions((c) => c.map((v, i) => (i === index ? e.target.value : v)))}
                        placeholder={`گزینه ${index + 1}`}
                      />
                      {pollOptions.length > 2 && (
                        <button type="button" onClick={() => setPollOptions((c) => c.filter((_, i) => i !== index))} title="حذف گزینه">
                          <Icon.Close size={13} />
                        </button>
                      )}
                    </div>
                  ))}
                  <footer>
                    {pollOptions.length < 6 && (
                      <button type="button" onClick={() => setPollOptions((c) => [...c, ''])}><Icon.Plus size={13} /> گزینه</button>
                    )}
                    <button type="button" className="is-primary" onClick={() => void submitPoll()}>ارسال و سنجاق</button>
                  </footer>
                </div>
              )}

              {activePoll && (
                <PollCard
                  poll={activePoll}
                  isHost={you.isHost}
                  onVote={(pollId, option) => void runAction(`${base}/poll/vote`, { poll_id: pollId, option_index: option })}
                  onClose={() => void runAction(`${base}/poll/close`)}
                />
              )}

              <div ref={chatScrollRef} className="rm-msgs">
                {threadMessages.map((message) => {
                  const mine = message.userId === you.userId;
                  return (
                    <article key={message.id} className={`rm-msg${mine ? ' is-mine' : ''}${message.isHost && !mine ? ' is-host' : ''}`}>
                      {!mine && <span className="rm-msg-from">{message.name}</span>}
                      <p>{message.body}</p>
                      <span className="rm-msg-time">
                        {message.toUserId !== null && <Icon.Lock size={10} />}
                        {formatPersianTime(message.time)}
                      </span>
                    </article>
                  );
                })}
                {threadMessages.length === 0 && <p className="rm-empty">گفتگو هنوز شروع نشده است.</p>}
              </div>

              <div className="rm-composer">
                <input
                  value={draft}
                  placeholder={canChat ? 'پیام بنویسید…' : chatMuted ? 'گفتگوی شما بسته است' : 'گفتگوی عمومی بسته است'}
                  disabled={!canChat}
                  onChange={(e) => setDraft(e.target.value)}
                  onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) void send(); }}
                />
                <button type="button" disabled={!canChat || !draft.trim()} onClick={() => void send()} title="ارسال">
                  <Icon.Send size={17} />
                </button>
              </div>
            </div>
          ) : (
            <div className="rm-files">
              {canUpload && (
                <label className={`rm-upload${uploading ? ' is-busy' : ''}`}>
                  <Icon.Clip size={15} /> {uploading ? 'در حال ارسال…' : 'ارسال فایل'}
                  <input type="file" hidden disabled={uploading} onChange={(e) => e.target.files?.[0] && void upload(e.target.files[0])} />
                </label>
              )}
              {files.map((file) => (
                <a key={file.id} className="rm-file" href={`${base}/file/${file.id}`}>
                  <Icon.Download size={15} />
                  <span className="rm-file-name">{file.name}</span>
                  <small>{fmtSize(file.size)} · {file.by}</small>
                </a>
              ))}
              {files.length === 0 && <p className="rm-empty">فایلی ارسال نشده است.</p>}
            </div>
          )}
        </aside>

        {/* ── Centre: stage ────────────────────────────────────── */}
        <main className={`rm-main${mobilePanel === 'stage' ? ' is-mobile-active' : ''}`}>
          {mediaError && (
            <div className="rm-alert">
              <Icon.Info size={15} /><span>{mediaError}</span>
              <button type="button" onClick={() => setMediaError('')}><Icon.Close size={13} /></button>
            </div>
          )}

          {audioBlocked && (
            <button type="button" className="rm-audio-notice" onClick={() => void unlockRemoteAudio()}>
              <Icon.Volume size={17} /> مرورگر پخش خودکار را بسته است؛ برای شنیدن کلاس بزنید.
            </button>
          )}

          {you.isHost && raisedHands.length > 0 && (
            <div className="rm-hands">
              <Icon.Hand size={14} />
              {raisedHands.slice(0, 4).map((p) => (
                <button key={p.userId} type="button" onClick={() => void runAction(`${base}/lower-hand`, { user_id: p.userId })} title="پایین بردن دست">
                  {p.name} <b>{p.handOrder}</b>
                </button>
              ))}
              {raisedHands.length > 4 && <small>+{raisedHands.length - 4}</small>}
            </div>
          )}

          <section className={`rm-stage${mainView === 'board' ? ' is-board' : ''}`}>
            {mainView === 'board' ? (
              <Whiteboard isHost={you.isHost && status !== 'ended'} base={base} />
            ) : (
              <div className={`rm-spotlight${speaking[stageUserId] ? ' is-speaking' : ''}`}>
                {stageHasVideo && stageStream ? (
                  <Video stream={stageStream} muted contain={stageSharing} />
                ) : (
                  <div className="rm-spot-avatar"><span>{initials(stageName)}</span></div>
                )}

                <div className="rm-spot-bar">
                  <span className="rm-spot-name">
                    {stageName}{stageIsMe && <small>شما</small>}
                    {stageSharing && <em><Icon.Screen size={12} /> اشتراک صفحه</em>}
                  </span>
                  <span className="rm-spot-icons">
                    {(stageIsMe ? localAudioOn : stagePerson?.media.audio)
                      ? <Icon.MicOn size={14} />
                      : <Icon.MicOff size={14} className="is-off" />}
                  </span>
                </div>

                {onlineCount <= 1 && (
                  <div className="rm-waiting">
                    <Icon.UserPlus size={26} />
                    <strong>منتظر ورود اعضای کلاس</strong>
                    <span>تصویر و صدا مستقیم و رمزنگاری‌شده منتقل می‌شود.</span>
                  </div>
                )}
                {status === 'scheduled' && !you.isHost && (
                  <div className="rm-waiting">
                    <Icon.Hourglass size={26} />
                    <strong>معلم هنوز کلاس را شروع نکرده است</strong>
                    <span>این صفحه را باز نگه دارید؛ کلاس خودکار آماده می‌شود.</span>
                  </div>
                )}
              </div>
            )}

            {reactions.length > 0 && (
              <div className="rm-reactions">
                {reactions.slice(0, 8).map((r) => <span key={r.id}><b>{r.emoji}</b>{r.name}</span>)}
              </div>
            )}
          </section>

          {/* ── Control bar ───────────────────────────────────── */}
          <footer className="rm-controls">
            {you.isHost && status === 'scheduled' && (
              <button type="button" className="rm-ctl is-primary" onClick={() => void setClassStatus('live')}>
                <Icon.Play size={18} /><span>شروع کلاس</span>
              </button>
            )}

            <button type="button" disabled={status === 'ended'} className={`rm-ctl${localAudioOn ? ' is-on' : ''}${!permissions.audio ? ' is-locked' : ''}`} onClick={() => void toggleAudio()}>
              {localAudioOn ? <Icon.MicOn size={18} /> : <Icon.MicOff size={18} />}
              <span>میکروفون</span>
              {!permissions.audio && <i className="rm-ctl-lock"><Icon.Lock size={9} /></i>}
            </button>

            <button type="button" disabled={status === 'ended'} className={`rm-ctl${localVideoOn ? ' is-on' : ''}${!permissions.video ? ' is-locked' : ''}`} onClick={() => void toggleVideo()}>
              {localVideoOn ? <Icon.CamOn size={18} /> : <Icon.CamOff size={18} />}
              <span>دوربین</span>
              {!permissions.video && <i className="rm-ctl-lock"><Icon.Lock size={9} /></i>}
            </button>

            <button type="button" disabled={status === 'ended'} className={`rm-ctl${screenSharing ? ' is-on' : ''}${!permissions.screen ? ' is-locked' : ''}`} onClick={() => void toggleScreenShare()}>
              {screenSharing ? <Icon.ScreenOff size={18} /> : <Icon.Screen size={18} />}
              <span>{screenSharing ? 'توقف اشتراک' : 'اشتراک صفحه'}</span>
              {!permissions.screen && <i className="rm-ctl-lock"><Icon.Lock size={9} /></i>}
            </button>

            <button type="button" className={`rm-ctl${mainView === 'board' ? ' is-on' : ''}`} onClick={() => setMainView((v) => (v === 'board' ? 'media' : 'board'))}>
              {mainView === 'board' ? <Icon.CamOn size={18} /> : <Icon.Board size={18} />}
              <span>{mainView === 'board' ? 'تصویر' : 'تخته'}</span>
            </button>

            {you.isHost && (
              <button type="button" className={`rm-ctl${recording ? ' is-rec' : ''}`} disabled={savingRecording || status === 'ended'} onClick={recording ? stopRecording : () => void startRecording()}>
                {recording ? <Icon.StopSquare size={18} /> : <Icon.Record size={18} />}
                <span>{savingRecording ? 'ذخیره…' : recording ? 'توقف ضبط' : 'ضبط'}</span>
              </button>
            )}

            {!you.isHost && (
              <button type="button" disabled={status === 'ended'} className={`rm-ctl${handRaised ? ' is-on' : ''}`} onClick={() => { setHandRaised(!handRaised); void runAction(`${base}/hand`, { raised: !handRaised }); }}>
                <Icon.Hand size={18} />
                <span>{handRaised ? 'دست پایین' : 'اجازه صحبت'}</span>
              </button>
            )}

            <div className="rm-emoji">
              {['👍', '👏', '✅', '❤️', '❓'].map((emoji) => (
                <button key={emoji} type="button" disabled={status === 'ended'} onClick={() => void runAction(`${base}/reaction`, { emoji })}>{emoji}</button>
              ))}
            </div>

            {you.isHost && status === 'live' && (
              <button type="button" className="rm-ctl is-danger" onClick={() => void setClassStatus('ended')}>
                <Icon.StopSquare size={18} /><span>پایان کلاس</span>
              </button>
            )}
          </footer>
        </main>

        {/* ── Left: participant rail ───────────────────────────── */}
        <aside className={`rm-rail${mobilePanel === 'people' ? ' is-mobile-active' : ''}`}>
          <header className="rm-rail-head">
            <strong>افراد</strong>
            <small>{onlineCount} آنلاین</small>
          </header>

          <div className="rm-rail-list">
            {railPeople.map((p) => {
              const stream = remoteStreams[p.userId];
              const isMe = p.userId === you.userId;
              const hasVideo = isMe ? localHasVideo : !!stream && (p.media.video || p.media.screen);
              const showStream = isMe ? previewStream : stream;
              return (
                <button
                  key={p.userId}
                  type="button"
                  className={`rm-tile${speaking[p.userId] ? ' is-speaking' : ''}${p.online ? '' : ' is-off'}`}
                  onClick={() => setPinnedUserId(pinnedUserId === p.userId ? null : p.userId)}
                  title={pinnedUserId === p.userId ? 'برداشتن سنجاق' : 'سنجاق روی صحنه'}
                >
                  {hasVideo && showStream
                    ? <Video stream={showStream} muted />
                    : <span className="rm-tile-avatar">{initials(p.name)}</span>}

                  <span className="rm-tile-bar">
                    <span className="rm-tile-name">{p.name}{isMe && ' (شما)'}</span>
                    {p.media.screen
                      ? <Icon.Screen size={11} />
                      : p.media.audio
                        ? <Icon.MicOn size={11} />
                        : <Icon.MicOff size={11} className="is-off" />}
                  </span>

                  {p.hand && <span className="rm-tile-hand"><Icon.Hand size={11} /> {p.handOrder}</span>}
                  {pinnedUserId === p.userId && <span className="rm-tile-pin"><Icon.Pin size={11} /></span>}
                </button>
              );
            })}
            {railPeople.length === 0 && <p className="rm-empty">کسی دیگر آنلاین نیست.</p>}
          </div>

          {you.isHost && status !== 'ended' && (
            <div className="rm-manage">
              <header>مدیریت دسترسی</header>
              {students.filter((p) => !p.kicked).map((p) => (
                <div key={p.userId} className={`rm-manage-row${p.online ? '' : ' is-off'}`}>
                  <span className="rm-manage-name">
                    <i className={p.online ? 'on' : ''} />{p.name}
                  </span>
                  <span className="rm-manage-acts">
                    <button type="button" className={p.permissions.audio ? 'is-allowed' : ''} title="اجازه میکروفون" onClick={() => void togglePermission(p, 'audio')}>
                      {p.permissions.audio ? <Icon.MicOn size={13} /> : <Icon.MicOff size={13} />}
                    </button>
                    <button type="button" className={p.permissions.video ? 'is-allowed' : ''} title="اجازه دوربین" onClick={() => void togglePermission(p, 'video')}>
                      {p.permissions.video ? <Icon.CamOn size={13} /> : <Icon.CamOff size={13} />}
                    </button>
                    <button type="button" className={p.permissions.screen ? 'is-allowed' : ''} title="اجازه اشتراک صفحه" onClick={() => void togglePermission(p, 'screen')}>
                      <Icon.Screen size={13} />
                    </button>
                    <button type="button" title="دعوت به صحبت" onClick={() => void runAction(`${base}/invite-speaker`, { user_id: p.userId })}>
                      <Icon.Megaphone size={13} />
                    </button>
                    <button type="button" className={p.muted ? 'is-danger is-active' : ''} title={p.muted ? 'باز کردن گفتگوی دانش‌آموز' : 'بستن گفتگوی دانش‌آموز'} onClick={() => void runAction(`${base}/mute`, { user_id: p.userId, muted: !p.muted })}>
                      {p.muted ? <Icon.ChatOff size={13} /> : <Icon.Chat size={13} />}
                    </button>
                    <button type="button" className="is-danger" title="اخراج از کلاس"
                      onClick={() => confirm(`«${p.name}» از کلاس خارج شود؟`) && void runAction(`${base}/kick`, { user_id: p.userId })}>
                      <Icon.KickUser size={13} />
                    </button>
                  </span>
                </div>
              ))}
              {students.length === 0 && <p className="rm-empty">دانش‌آموزی وارد نشده.</p>}
              {students.filter((p) => p.kicked).map((p) => (
                <button key={`restore-${p.userId}`} type="button" className="rm-restore" onClick={() => void runAction(`${base}/restore`, { user_id: p.userId })}>
                  <Icon.UserPlus size={12} /> بازگرداندن {p.name}
                </button>
              ))}
              {recentJoins.length > 0 && (
                <p className="rm-recent"><Icon.UserPlus size={11} /> {recentJoins.map((j) => `${j.name} ${j.joinedAt}`).join(' · ')}</p>
              )}
            </div>
          )}
        </aside>
      </div>

      <nav className="rm-mobile-nav" aria-label="بخش‌های کلاس">
        <button type="button" className={mobilePanel === 'stage' ? 'is-active' : ''} onClick={() => setMobilePanel('stage')}><Icon.CamOn size={18} /><span>کلاس</span></button>
        <button type="button" className={mobilePanel === 'chat' ? 'is-active' : ''} onClick={() => setMobilePanel('chat')}><Icon.Chat size={18} /><span>گفتگو</span></button>
        <button type="button" className={mobilePanel === 'people' ? 'is-active' : ''} onClick={() => setMobilePanel('people')}><Icon.People size={18} /><span>افراد</span><b>{onlineCount}</b></button>
      </nav>
    </div>
  );
}
