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;
  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 PresenceUser = { id: number; name: string; isHost: boolean };

/** WebRTC signalling envelope (offer/answer carry data.sdp, ice carries data.candidate). */
type Signal = {
  dbId?: number;
  fromUserId: number;
  toUserId: number;
  type: 'offer' | 'answer' | 'ice' | 'renegotiate';
  data: Record<string, unknown>;
};

/**
 * Per-peer state for perfect negotiation. Three fixed transceivers in a stable
 * order — audio, camera, screen — so screen-share and camera flow at the same
 * time and the receiver can tell them apart by transceiver identity.
 */
type PeerState = {
  pc: RTCPeerConnection;
  polite: boolean;
  makingOffer: boolean;
  ignoreOffer: boolean;
  audioTx: RTCRtpTransceiver;
  camTx: RTCRtpTransceiver;
  screenTx: RTCRtpTransceiver;
  pendingCandidates: RTCIceCandidateInit[];
  opChain: Promise<void>;
};

/** Remote video split into its two independent streams. */
type RemoteVideo = { cam: MediaStream | null; screen: MediaStream | null };

type Stage = 'auto' | 'board' | string; // 'u:<id>' for a pinned person

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 };
  stage?: Stage;
  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 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, stage: initialStage = 'auto',
  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 [stage, setStage] = useState<Stage>(initialStage);
  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 [activeThread, setActiveThread] = useState<'public' | number>('public');
  const [openThreads, setOpenThreads] = useState<number[]>([]);
  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 [transport, setTransport] = useState<'connecting' | 'live' | 'fallback'>('connecting');
  const [quality, setQuality] = useState<Quality>(OFFLINE_QUALITY);
  const [mediaError, setMediaError] = useState('');
  const [previewCam, setPreviewCam] = useState<MediaStream | null>(null);
  const [previewScreen, setPreviewScreen] = useState<MediaStream | null>(null);
  const [remoteStreams, setRemoteStreams] = useState<Record<number, MediaStream>>({}); // audio only
  const [remoteVideo, setRemoteVideo] = useState<Record<number, RemoteVideo>>({});
  const [myMedia, setMyMedia] = useState<MediaState>({ audio: false, video: false, screen: 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 [mobilePanel, setMobilePanel] = useState<'stage' | 'chat' | 'people'>('stage');
  const [audioBlocked, setAudioBlocked] = useState(false);
  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 processedSignals = useRef(new Set<number>());
  const chatScrollRef = useRef<HTMLDivElement>(null);

  // Local media tracks — kept in refs so peer setup reads the latest without re-render churn.
  const audioTrackRef = useRef<MediaStreamTrack | null>(null);
  const cameraTrackRef = useRef<MediaStreamTrack | null>(null);
  const screenTrackRef = useRef<MediaStreamTrack | null>(null);

  const peersRef = useRef(new Map<number, PeerState>());
  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 refreshPreview = useCallback(() => {
    setPreviewCam(cameraTrackRef.current ? new MediaStream([cameraTrackRef.current]) : null);
    setPreviewScreen(screenTrackRef.current ? new MediaStream([screenTrackRef.current]) : null);
  }, []);

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

  // ── chat threads ─────────────────────────────────────────────────────────
  const threadMessages = useMemo(() => {
    if (activeThread === 'public') return messages.filter((m) => m.toUserId === null);
    const other = activeThread;
    return messages.filter((m) => m.toUserId !== null && (
      (m.userId === you.userId && m.toUserId === other) ||
      (m.userId === other && m.toUserId === you.userId)
    ));
  }, [messages, activeThread, you.userId]);

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

  // A private message involving me surfaces its conversation as a tab, chat-app style.
  useEffect(() => {
    const counterparts = new Set<number>();
    for (const m of messages) {
      if (m.toUserId === null) continue;
      if (m.userId === you.userId && m.toUserId) counterparts.add(m.toUserId);
      else if (m.toUserId === you.userId) counterparts.add(m.userId);
    }
    if (counterparts.size === 0) return;
    setOpenThreads((prev) => {
      const next = [...prev];
      counterparts.forEach((id) => { if (!next.includes(id)) next.push(id); });
      return next.length === prev.length ? prev : next;
    });
  }, [messages, you.userId]);

  // ── WebRTC: perfect negotiation ──────────────────────────────────────────
  const sendSignal = useCallback(async (targetUserId: number, type: Signal['type'], data: Record<string, unknown>) => {
    try {
      await api(`${base}/signal`, { to_user_id: targetUserId, type, data });
    } catch { /* peer may have left; ICE restart / next media change re-drives negotiation */ }
  }, [base]);

  const dropPeer = useCallback((userId: number) => {
    const state = peersRef.current.get(userId);
    if (!state) return;
    try { state.pc.close(); } catch { /* already closed */ }
    peersRef.current.delete(userId);
    setRemoteStreams((current) => {
      if (!current[userId]) return current;
      const next = { ...current };
      delete next[userId];
      return next;
    });
    setRemoteVideo((current) => {
      if (!current[userId]) return current;
      const next = { ...current };
      delete next[userId];
      return next;
    });
  }, []);

  const ensurePeer = useCallback((remoteId: number): PeerState => {
    const existing = peersRef.current.get(remoteId);
    if (existing) return existing;

    const pc = new RTCPeerConnection({ iceServers });
    // Fixed order — audio, camera, screen — so both peers agree on mids and the
    // receiver can identify each track by transceiver identity.
    const audioTx = pc.addTransceiver('audio', { direction: 'sendrecv' });
    const camTx = pc.addTransceiver('video', { direction: 'sendrecv' });
    const screenTx = pc.addTransceiver('video', { direction: 'sendrecv' });

    const state: PeerState = {
      pc,
      polite: you.userId > remoteId, // higher id yields on glare
      makingOffer: false,
      ignoreOffer: false,
      audioTx, camTx, screenTx,
      pendingCandidates: [],
      opChain: Promise.resolve(),
    };

    if (audioTrackRef.current) void audioTx.sender.replaceTrack(audioTrackRef.current);
    if (cameraTrackRef.current) void camTx.sender.replaceTrack(cameraTrackRef.current);
    if (screenTrackRef.current) void screenTx.sender.replaceTrack(screenTrackRef.current);

    pc.onnegotiationneeded = async () => {
      try {
        state.makingOffer = true;
        await pc.setLocalDescription();
        if (pc.localDescription) await sendSignal(remoteId, 'offer', { sdp: pc.localDescription });
      } catch { /* rolled back by a colliding remote offer */ } finally {
        state.makingOffer = false;
      }
    };

    pc.onicecandidate = (event) => {
      if (event.candidate) void sendSignal(remoteId, 'ice', { candidate: event.candidate.toJSON() });
    };

    pc.ontrack = (event) => {
      const track = event.track;
      if (event.transceiver === audioTx || track.kind === 'audio') {
        setRemoteStreams((current) => {
          const stream = current[remoteId] ?? new MediaStream();
          if (!stream.getTracks().some((t) => t.id === track.id)) stream.addTrack(track);
          return { ...current, [remoteId]: stream };
        });
      } else {
        const slot: keyof RemoteVideo = event.transceiver === screenTx ? 'screen' : 'cam';
        setRemoteVideo((current) => {
          const bundle = current[remoteId] ?? { cam: null, screen: null };
          return { ...current, [remoteId]: { ...bundle, [slot]: new MediaStream([track]) } };
        });
        track.onended = () => setRemoteVideo((current) => {
          const bundle = current[remoteId];
          if (!bundle) return current;
          return { ...current, [remoteId]: { ...bundle, [slot]: null } };
        });
      }
    };

    pc.onconnectionstatechange = () => {
      if (pc.connectionState === 'failed') {
        try { pc.restartIce(); } catch { /* older browsers: fall through to teardown */ }
      }
      if (pc.connectionState === 'closed') {
        peersRef.current.delete(remoteId);
        setPeerRevision((r) => r + 1);
      }
    };

    peersRef.current.set(remoteId, state);
    return state;
  }, [iceServers, sendSignal, you.userId]);

  const processSignal = useCallback(async (signal: Signal) => {
    if (signal.toUserId !== you.userId || signal.fromUserId === you.userId) return;
    const state = ensurePeer(signal.fromUserId);
    const pc = state.pc;
    try {
      if (signal.type === 'offer' || signal.type === 'answer') {
        const description = signal.data.sdp as RTCSessionDescriptionInit;
        const collision = signal.type === 'offer' && (state.makingOffer || pc.signalingState !== 'stable');
        state.ignoreOffer = !state.polite && collision;
        if (state.ignoreOffer) return;
        await pc.setRemoteDescription(description);
        for (const candidate of state.pendingCandidates) {
          try { await pc.addIceCandidate(candidate); } catch { /* stale candidate */ }
        }
        state.pendingCandidates = [];
        if (signal.type === 'offer') {
          await pc.setLocalDescription();
          if (pc.localDescription) await sendSignal(signal.fromUserId, 'answer', { sdp: pc.localDescription });
        }
      } else if (signal.type === 'ice') {
        const candidate = signal.data.candidate as RTCIceCandidateInit | undefined;
        if (!candidate) return;
        if (pc.remoteDescription) {
          try { await pc.addIceCandidate(candidate); } catch { if (!state.ignoreOffer) { /* ignore */ } }
        } else {
          state.pendingCandidates.push(candidate);
        }
      } else if (signal.type === 'renegotiate') {
        await pc.setLocalDescription();
        if (pc.localDescription) await sendSignal(signal.fromUserId, 'offer', { sdp: pc.localDescription });
      }
    } catch { /* recovers on the next negotiation cycle */ }
  }, [ensurePeer, sendSignal, you.userId]);

  // Single, de-duplicated entry point for signals from both Reverb and polling.
  const onSignal = useCallback((signal: Signal) => {
    if (signal.dbId) {
      if (processedSignals.current.has(signal.dbId)) return;
      processedSignals.current.add(signal.dbId);
      lastSignalDbId.current = Math.max(lastSignalDbId.current, signal.dbId);
      if (processedSignals.current.size > 600) {
        processedSignals.current = new Set([...processedSignals.current].slice(-300));
      }
    }
    if (signal.toUserId !== you.userId || signal.fromUserId === you.userId) return;
    const state = ensurePeer(signal.fromUserId);
    state.opChain = state.opChain.then(() => processSignal(signal)).catch(() => {});
  }, [ensurePeer, processSignal, you.userId]);

  // ── polling sync ─────────────────────────────────────────────────────────
  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);
      if (data.stage) setStage(data.stage);
      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);
      for (const signal of (data.signals ?? []) as Signal[]) onSignal(signal);
      setFiles(data.files ?? []);
      setActivePoll(data.poll ?? null);
      setReactions(data.reactions ?? []);
    } catch {
      setTransport((current) => (current === 'live' ? current : 'fallback'));
    }
  }, [base, mergeMessages, onSignal]);

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

  // Keep a peer connection for every online participant (both sides symmetric —
  // perfect negotiation resolves the resulting glare). Works over sockets or poll.
  useEffect(() => {
    const onlineIds = new Set(onlineParticipants.map((p) => p.userId));
    peersRef.current.forEach((_state, userId) => {
      if (!onlineIds.has(userId)) dropPeer(userId);
    });
    onlineParticipants
      .filter((p) => p.userId !== you.userId)
      .forEach((p) => ensurePeer(p.userId));
  }, [dropPeer, ensurePeer, 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).forEach((user) => ensurePeer(user.id));
        })
        .joining((user: PresenceUser) => {
          if (user.id !== you.userId) ensurePeer(user.id);
          void syncOnce();
        })
        .leaving((user: PresenceUser) => {
          dropPeer(user.id);
          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]))
        .listen('.stage', (data: { stage: Stage }) => { if (data.stage) setStage(data.stage); });

      echo.private(privateChannel)
        .listen('.chat', (message: Message) => mergeMessages([message]))
        .listen('.signal', (signal: Signal) => onSignal(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((state) => { try { state.pc.close(); } catch { /* noop */ } });
      peersRef.current.clear();
      [audioTrackRef, cameraTrackRef, screenTrackRef].forEach((r) => { r.current?.stop(); r.current = null; });
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [base, classId, realtimeKey, you.userId]);

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

  // Speaking rings — watch every remote audio stream, plus our own mic.
  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 (audioTrackRef.current) watch(you.userId, new MediaStream([audioTrackRef.current]));
    Object.entries(remoteStreams).forEach(([id, stream]) => watch(Number(id), stream));

    stops.forEach((stop, id) => {
      if (id !== you.userId && !remoteStreams[id]) {
        stop(); stops.delete(id); trackIds.delete(id);
      }
    });
  }, [remoteStreams, myMedia.audio, 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]);

  // ── media publishing + controls ──────────────────────────────────────────
  const publishMediaState = useCallback(async () => {
    const next = { audio: !!audioTrackRef.current, video: !!cameraTrackRef.current, screen: !!screenTrackRef.current };
    setMyMedia(next);
    try { await api(`${base}/media-state`, next); } catch { /* state also flows through /sync */ }
  }, [base]);

  const applyToPeers = useCallback((pick: (s: PeerState) => RTCRtpSender, track: MediaStreamTrack | null) => {
    peersRef.current.forEach((state) => { void pick(state).replaceTrack(track); });
  }, []);

  const toggleAudio = async () => {
    if (!permissions.audio) { setMediaError('دسترسی میکروفون باید توسط معلم فعال شود.'); return; }
    setMediaError('');
    try {
      if (audioTrackRef.current) {
        applyToPeers((s) => s.audioTx.sender, null);
        audioTrackRef.current.stop();
        audioTrackRef.current = null;
      } else {
        const captured = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } });
        const track = captured.getAudioTracks()[0];
        if (!track) throw new Error('میکروفون پیدا نشد.');
        audioTrackRef.current = track;
        applyToPeers((s) => s.audioTx.sender, track);
      }
      await publishMediaState();
    } catch (error) {
      setMediaError(error instanceof Error ? error.message : 'میکروفون باز نشد. مجوز مرورگر را بررسی کنید.');
    }
  };

  // Releasing the camera track (not just disabling it) turns the laptop LED off.
  const toggleVideo = async () => {
    if (!permissions.video) { setMediaError('دسترسی دوربین باید توسط معلم فعال شود.'); return; }
    setMediaError('');
    try {
      if (cameraTrackRef.current) {
        applyToPeers((s) => s.camTx.sender, null);
        cameraTrackRef.current.stop();
        cameraTrackRef.current = null;
      } else {
        const captured = await navigator.mediaDevices.getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 24, max: 30 } } });
        const track = captured.getVideoTracks()[0];
        if (!track) throw new Error('دوربین پیدا نشد.');
        cameraTrackRef.current = track;
        applyToPeers((s) => s.camTx.sender, track);
      }
      refreshPreview();
      await publishMediaState();
    } catch (error) {
      setMediaError(error instanceof Error ? error.message : 'دوربین باز نشد. مجوز مرورگر را بررسی کنید.');
    }
  };

  const stopScreen = useCallback(async () => {
    if (!screenTrackRef.current) return;
    applyToPeers((s) => s.screenTx.sender, null);
    screenTrackRef.current.stop();
    screenTrackRef.current = null;
    refreshPreview();
    await publishMediaState();
  }, [applyToPeers, publishMediaState, refreshPreview]);

  // Screen share is independent of the camera — both can run at once (camera → PiP).
  const toggleScreen = async () => {
    if (screenTrackRef.current) { await stopScreen(); 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 track = display.getVideoTracks()[0];
      screenTrackRef.current = track;
      applyToPeers((s) => s.screenTx.sender, track);
      track.onended = () => void stopScreen();
      refreshPreview();
      await publishMediaState();
    } catch (error) {
      setMediaError(error instanceof Error && error.name === 'NotAllowedError' ? 'اشتراک صفحه لغو شد یا مجوز داده نشد.' : 'اشتراک صفحه شروع نشد.');
    }
  };

  // Teacher revoked a permission → drop that media immediately.
  useEffect(() => {
    if (!permissions.audio && audioTrackRef.current) { applyToPeers((s) => s.audioTx.sender, null); audioTrackRef.current.stop(); audioTrackRef.current = null; void publishMediaState(); }
    if (!permissions.video && cameraTrackRef.current) { applyToPeers((s) => s.camTx.sender, null); cameraTrackRef.current.stop(); cameraTrackRef.current = null; refreshPreview(); void publishMediaState(); }
    if (!permissions.screen && screenTrackRef.current) { void stopScreen(); }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [permissions]);

  useEffect(() => {
    if (status !== 'ended') return;
    [audioTrackRef, cameraTrackRef, screenTrackRef].forEach((r) => { r.current?.stop(); r.current = null; });
    applyToPeers((s) => s.audioTx.sender, null);
    applyToPeers((s) => s.camTx.sender, null);
    applyToPeers((s) => s.screenTx.sender, null);
    refreshPreview();
    setMyMedia({ audio: false, video: false, screen: false });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [status]);

  // ── chat / files / polls ──────────────────────────────────────────────────
  const send = async () => {
    const body = draft.trim();
    if (!body) return;
    const target: number | null = activeThread === 'public' ? null : activeThread;
    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 openPrivate = (userId: number) => {
    setOpenThreads((prev) => (prev.includes(userId) ? prev : [...prev, userId]));
    setActiveThread(userId);
    setSidebar('chat');
    setMobilePanel('chat');
  };

  const closeThread = (userId: number) => {
    setOpenThreads((prev) => prev.filter((id) => id !== userId));
    setActiveThread((current) => (current === userId ? 'public' : current));
  };

  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 : 'دسترسی تغییر نکرد.');
    }
  };

  // ── host stage control ─────────────────────────────────────────────────────
  const changeStage = useCallback((mode: 'auto' | 'board' | 'user', userId?: number) => {
    const optimistic: Stage = mode === 'board' ? 'board' : mode === 'user' ? `u:${userId}` : 'auto';
    setStage(optimistic);
    void runAction(`${base}/stage`, { mode, user_id: userId ?? null });
  }, [base, runAction]);

  // ── recording ──────────────────────────────────────────────────────────────
  const startRecording = async () => {
    if (!you.isHost || recording) return;
    const videoTrack = screenTrackRef.current ?? cameraTrackRef.current;
    if (!videoTrack || videoTrack.readyState !== 'live') { setMediaError('برای ضبط، ابتدا دوربین یا اشتراک صفحه را روشن کنید.'); return; }
    try {
      const context = new AudioContext();
      const destination = context.createMediaStreamDestination();
      const audioStreams = [audioTrackRef.current ? new MediaStream([audioTrackRef.current]) : null, ...Object.values(remoteStreams)].filter(Boolean) as MediaStream[];
      audioStreams.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('فایل ضبط‌شده ذخیره نشد؛ حجم یا اتصال را بررسی کنید.');
        } catch { 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 && (activeThread !== 'public' || settings.allowStudentChat)));
  // The teacher can always upload; students only when the teacher enables it.
  const canUpload = status !== 'ended' && (you.isHost || settings.allowStudentFiles);

  // ── stage resolution (teacher-controlled, auto-follows the speaker) ────────
  const showBoard = stage === 'board';
  const stageUserId = useMemo(() => {
    if (showBoard) return null;
    if (typeof stage === 'string' && stage.startsWith('u:')) {
      const pinned = Number(stage.slice(2));
      if (onlineParticipants.some((p) => p.userId === pinned)) return pinned;
    }
    const sharer = onlineParticipants.find((p) => p.media.screen);
    if (sharer) return sharer.userId;
    const talker = onlineParticipants.find((p) => speaking[p.userId] && p.media.audio);
    if (talker) return talker.userId;
    const hostCam = onlineParticipants.find((p) => p.isHost && p.media.video);
    if (hostCam) return hostCam.userId;
    const anyCam = onlineParticipants.find((p) => p.media.video || p.media.screen);
    if (anyCam) return anyCam.userId;
    return null;
  }, [showBoard, stage, onlineParticipants, speaking]);

  const stagePerson = stageUserId !== null ? onlineParticipants.find((p) => p.userId === stageUserId) ?? null : null;
  const stageIsMe = stageUserId === you.userId;

  // Main stage picture prefers screen-share; camera rides along as a picture-in-picture.
  const stageMain: MediaStream | null = stageIsMe
    ? (screenTrackRef.current ? previewScreen : previewCam)
    : (stagePerson?.media.screen ? remoteVideo[stageUserId as number]?.screen ?? null : remoteVideo[stageUserId as number]?.cam ?? null);
  const stageIsScreen = stageIsMe ? !!screenTrackRef.current : !!stagePerson?.media.screen;
  const stagePip: MediaStream | null = stageIsMe
    ? (screenTrackRef.current && cameraTrackRef.current ? previewCam : null)
    : (stagePerson?.media.screen && stagePerson?.media.video ? remoteVideo[stageUserId as number]?.cam ?? null : null);
  const stageAudioOn = stageIsMe ? myMedia.audio : !!stagePerson?.media.audio;
  const stageName = stageIsMe ? you.name : stagePerson?.name ?? '';
  const stageLabel = showBoard ? 'تخته کلاس' : stage !== 'auto' ? 'سنجاق‌شده' : 'گوینده فعال';

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

  const threadName = (id: number) => participants.find((p) => p.userId === id)?.name ?? 'کاربر';

  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="افراد آنلاین">
            <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">
        {/* ── 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>
          )}

          {/* stage header: what everyone is looking at + host scene controls */}
          <div className="rm-stage-head">
            <span className="rm-scene-label">
              {showBoard ? <Icon.Board size={14} /> : <Icon.CamOn size={14} />}
              {stageLabel}
            </span>
            {you.isHost && status !== 'ended' && (
              <div className="rm-scene-switch" role="group" aria-label="انتخاب صحنه برای همه">
                <button type="button" className={stage === 'auto' ? 'is-active' : ''} onClick={() => changeStage('auto')} title="گوینده فعال به‌صورت خودکار">
                  <Icon.People size={14} /> خودکار
                </button>
                <button type="button" className={showBoard ? 'is-active' : ''} onClick={() => changeStage(showBoard ? 'auto' : 'board')} title="تخته را برای همه نمایش بده">
                  <Icon.Board size={14} /> تخته
                </button>
              </div>
            )}
          </div>

          <section className={`rm-stage${showBoard ? ' is-board' : ''}`}>
            {showBoard ? (
              <Whiteboard isHost={you.isHost && status !== 'ended'} base={base} />
            ) : (
              <div className={`rm-spotlight${stageUserId !== null && speaking[stageUserId] ? ' is-speaking' : ''}`}>
                {stageMain ? (
                  <>
                    <Video stream={stageMain} muted contain={stageIsScreen} />
                    {stagePip && (
                      <div className="rm-pip"><Video stream={stagePip} muted /></div>
                    )}
                  </>
                ) : (
                  <div className="rm-stage-empty" />
                )}

                {stageName && (
                  <div className="rm-spot-bar">
                    <span className="rm-spot-name">
                      {stageName}{stageIsMe && <small>شما</small>}
                      {stageIsScreen && <em><Icon.Screen size={12} /> اشتراک صفحه</em>}
                    </span>
                    <span className="rm-spot-icons">
                      {stageAudioOn ? <Icon.MicOn size={14} /> : <Icon.MicOff size={14} className="is-off" />}
                    </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${myMedia.audio ? ' is-on' : ''}${!permissions.audio ? ' is-locked' : ''}`} onClick={() => void toggleAudio()}>
              {myMedia.audio ? <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${myMedia.video ? ' is-on' : ''}${!permissions.video ? ' is-locked' : ''}`} onClick={() => void toggleVideo()}>
              {myMedia.video ? <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${myMedia.screen ? ' is-on' : ''}${!permissions.screen ? ' is-locked' : ''}`} onClick={() => void toggleScreen()}>
              {myMedia.screen ? <Icon.ScreenOff size={18} /> : <Icon.Screen size={18} />}
              <span>{myMedia.screen ? 'توقف اشتراک' : 'اشتراک صفحه'}</span>
              {!permissions.screen && <i className="rm-ctl-lock"><Icon.Lock size={9} /></i>}
            </button>

            {you.isHost && (
              <button type="button" className={`rm-ctl${showBoard ? ' is-on' : ''}`} disabled={status === 'ended'} onClick={() => changeStage(showBoard ? 'auto' : 'board')}>
                <Icon.Board size={18} />
                <span>{showBoard ? 'بستن تخته' : 'تخته'}</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>

        {/* ── Right column: chat (top) + people (bottom) ────────── */}
        <aside className={`rm-side${mobilePanel !== 'stage' ? ' is-mobile-active' : ''}`}>
          {/* chat / files */}
          <div className={`rm-chatpane${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>

            {sidebar === 'chat' ? (
              <div className="rm-chat">
                {/* conversation tabs */}
                <div className="rm-threads" role="tablist">
                  <button type="button" className={`rm-thread${activeThread === 'public' ? ' is-active' : ''}`} onClick={() => setActiveThread('public')}>
                    <Icon.People size={13} /> عمومی
                  </button>
                  {openThreads.map((id) => (
                    <span key={id} className={`rm-thread${activeThread === id ? ' is-active' : ''}`}>
                      <button type="button" className="rm-thread-open" onClick={() => setActiveThread(id)}>
                        <Icon.Lock size={11} /> {threadName(id)}
                      </button>
                      <button type="button" className="rm-thread-x" onClick={() => closeThread(id)} title="بستن گفتگو"><Icon.Close size={11} /></button>
                    </span>
                  ))}
                </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">{activeThread === 'public' ? 'گفتگو هنوز شروع نشده است.' : 'پیام خصوصی بنویسید…'}</p>
                  )}
                </div>

                <div className="rm-composer">
                  <input
                    value={draft}
                    placeholder={canChat ? (activeThread === 'public' ? 'پیام عمومی…' : `پیام خصوصی به ${threadName(activeThread as number)}…`) : 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>
                ) : (
                  <p className="rm-files-locked"><Icon.Lock size={13} /> ارسال فایل برای دانش‌آموزان توسط معلم بسته است.</p>
                )}
                {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>
            )}
          </div>

          {/* people (compact rows) */}
          <div className={`rm-people${mobilePanel === 'people' ? ' is-mobile-active' : ''}`}>
            <header className="rm-people-head">
              <strong>افراد کلاس</strong>
              <small>{onlineCount} آنلاین</small>
              {you.isHost && status !== 'ended' && (
                <div className="rm-people-settings">
                  <button type="button" className={settings.allowStudentChat ? 'is-enabled' : ''}
                    onClick={() => void runAction(`${base}/settings`, { allow_student_chat: !settings.allowStudentChat, allow_student_files: settings.allowStudentFiles })}
                    title={settings.allowStudentChat ? 'بستن گفتگوی عمومی دانش‌آموزان' : 'باز کردن گفتگوی عمومی دانش‌آموزان'}>
                    {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 })}
                    title={settings.allowStudentFiles ? 'بستن ارسال فایل دانش‌آموزان' : 'اجازه ارسال فایل دانش‌آموزان'}>
                    <Icon.Folder size={13} />
                  </button>
                </div>
              )}
            </header>

            <div className="rm-people-list">
              {onlineParticipants.map((p) => {
                const isMe = p.userId === you.userId;
                const pinned = stage === `u:${p.userId}`;
                const chatable = !isMe && (you.isHost ? true : p.isHost);
                return (
                  <div key={p.userId} className={`rm-prow${p.isHost ? ' is-host' : ''}${speaking[p.userId] ? ' is-speaking' : ''}`}>
                    <span className={`rm-prow-dot${p.online ? ' on' : ''}`} />
                    <button
                      type="button"
                      className="rm-prow-name"
                      disabled={!chatable}
                      onClick={() => chatable && openPrivate(p.userId)}
                      title={chatable ? 'گفتگوی خصوصی' : undefined}
                    >
                      {p.name}{isMe && ' (شما)'}
                      {p.roleLabel && <em>{p.roleLabel}</em>}
                      {p.hand && <i className="rm-prow-hand"><Icon.Hand size={11} /> {p.handOrder}</i>}
                    </button>
                    <span className="rm-prow-media">
                      {p.media.screen && <Icon.Screen size={13} />}
                      {p.media.video && <Icon.CamOn size={13} />}
                      {p.media.audio ? <Icon.MicOn size={13} /> : <Icon.MicOff size={13} className="is-off" />}
                    </span>

                    {you.isHost && status !== 'ended' && (
                      <span className="rm-prow-acts">
                        <button type="button" className={pinned ? 'is-active' : ''} title={pinned ? 'برداشتن از صحنه' : 'نمایش روی صحنه برای همه'} onClick={() => changeStage(pinned ? 'auto' : 'user', p.userId)}>
                          <Icon.Pin size={13} />
                        </button>
                        {!p.isHost && (
                          <>
                            <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" 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>
                );
              })}
              {onlineParticipants.length === 0 && <p className="rm-empty">هنوز کسی وارد نشده.</p>}
              {you.isHost && 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>
              ))}
            </div>
          </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>
  );
}
