import React,{useCallback,useEffect,useMemo,useRef,useState}from'react';
import{createRoot}from'react-dom/client';
import{Room,RoomEvent,Track}from'livekit-client';
import QRCode from'qrcode';
import'./styles.css';


const ICONS = {
  attachment: '/icons/attachment.png',
  cameraOff: '/icons/camera_off.png',
  cameraOn: '/icons/camera_on.png',
  micOff: '/icons/micro_off.png',
  micOn: '/icons/micro_on.png'
};


function apiUrl(path) {
  return path;
}


async function readJson(response) {
  const data = await response.json().catch(() => ({}));
  if (!response.ok) throw new Error(data.detail || 'Ошибка запроса');
  return data;
}


function routeFromLocation() {
  const path = window.location.pathname;
  if (path === '/') return { name: 'home' };
  if (path === '/call/overloaded') return { name: 'error', code: '503' };
  const match = path.match(/^\/call\/([^/]+)$/);
  if (match) return { name: 'call', roomName: decodeURIComponent(match[1]) };
  return { name: 'error', code: '404' };
}


function getInviteToken() {
  return new URL(window.location.href).searchParams.get('invite') || '';
}


function randomGuestName() {
  const a = Date.now().toString(36).slice(-4);
  const b = Math.random().toString(36).slice(2, 6);
  return `Гость-${a}${b}`.slice(0, 24);
}


function instaRoomTitle() {
  return `Инста-${Math.random().toString(36).slice(2, 8)}`;
}


function Toasts({ items }) {
  return (
    <div className='toast-stack'>
      {items.map((toast) => (
        <div className={`toast ${toast.type}`} key={toast.id}>{toast.message}</div>
      ))}
    </div>
  );
}


function useToasts() {
  const [items, setItems] = useState([]);

  const showToast = useCallback((message, type = 'info') => {
    const id = crypto.randomUUID ? crypto.randomUUID() : String(Date.now() + Math.random());
    setItems((current) => current.concat({ id, message, type }));
    window.setTimeout(() => {
      setItems((current) => current.filter((item) => item.id !== id));
    }, 2600);
  }, []);

  return [items, showToast];
}


function ErrorPage({ code }) {
  const copy = {
    '403': ['Приглашение недействительно', 'Ссылка приглашения недействительна или была заменена. Запросите новую ссылку у организатора.'],
    '410': ['Звонок завершён', 'Эта встреча уже закончена. Попросите организатора прислать новую ссылку.'],
    '503': ['Сервис временно недоступен', 'Сервер перегружен. Подождите немного и обновите страницу или зайдите позже.'],
    '404': ['Комната не найдена', 'Такой страницы нет или ссылка устарела. Проверьте адрес или создайте новый звонок на главной странице.']
  };
  const [title, message] = copy[code] || copy['404'];

  useEffect(() => {
    document.title = `${title} · Linkra`;
  }, [title]);

  return (
    <main className='container error-page'>
      <section className='card error-card'>
        <p className='error-code'>{code}</p>
        <h1>{title}</h1>
        <p className='muted'>{message}</p>
        <div className='row centered'>
          <a className='btn primary' href='/'>На главную</a>
          {code === '503' ? <button className='btn secondary' type='button' onClick={() => window.location.reload()}>Обновить страницу</button> : null}
        </div>
      </section>
    </main>
  );
}


function HomePage({ showToast }) {
  const [roomTitle, setRoomTitle] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);
  const [instaLoading, setInstaLoading] = useState(false);
  const [result, setResult] = useState(null);
  const qrRef = useRef(null);

  useEffect(() => {
    document.title = 'Созвон по ссылке · Linkra';
  }, []);

  useEffect(() => {
    if (!result || !qrRef.current) return;
    const target = result.short_invite_link || result.invite_link;
    QRCode.toCanvas(qrRef.current, target, { width: 200, margin: 1 }).catch(() => {});
  }, [result]);

  const createCall = async ({ quickJoin = false } = {}) => {
    const title = quickJoin ? instaRoomTitle() : roomTitle.trim();
    const roomPassword = quickJoin ? '' : password.trim();
    if (!quickJoin && title.length < 4) {
      showToast('Название комнаты должно содержать минимум 4 символа', 'error');
      return null;
    }
    if (!quickJoin && roomPassword.length > 0 && roomPassword.length < 4) {
      showToast('Пароль должен содержать минимум 4 символа', 'error');
      return null;
    }

    const setBusy = quickJoin ? setInstaLoading : setLoading;
    setBusy(true);
    try {
      const response = await fetch(apiUrl('/api/calls'), {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          room_title: title,
          password: roomPassword,
          quick_join: quickJoin
        })
      });
      return await readJson(response);
    } catch (error) {
      showToast(error.message || 'Не удалось создать звонок', 'error');
      return null;
    } finally {
      setBusy(false);
    }
  };

  const handleCreate = async () => {
    const data = await createCall();
    if (!data) return;
    setResult(data);
    showToast('Ссылка на звонок готова', 'success');
  };

  const handleInsta = async () => {
    const data = await createCall({ quickJoin: true });
    if (!data) return;
    const target = data.short_invite_link || data.invite_link;
    sessionStorage.setItem(`linkra-insta:${data.room_name}`, JSON.stringify({
      invite_token: data.invite_token,
      display_name: randomGuestName(),
      mic_enabled: false,
      camera_enabled: false
    }));
    await navigator.clipboard.writeText(target).catch(() => {});
    window.location.href = target;
  };

  const copyText = async (value) => {
    await navigator.clipboard.writeText(value);
    showToast('Ссылка скопирована', 'success');
  };

  return (
    <main className='container home-shell'>
      <div className='home-grid'>
        <header className='home-hero'>
          <h1>Созвон по ссылке</h1>
          <p className='muted home-hero-lead'>Создай комнату, отправь ссылку собеседнику: видео, чат и демонстрация экрана в браузере, без приложений.</p>
          <ul className='home-features'>
            <li>Камера и микрофон без установки</li>
            <li>Чат и файлы в комнате</li>
            <li>Короткая ссылка и QR для телефона</li>
            <li>Кнопка «Инста» для быстрого входа</li>
          </ul>
        </header>

        <section className='card home-card'>
          <div className='home-card-header'>
            <h2>Новая комната</h2>
          </div>

          {!result ? (
            <div className='stack'>
              <label className='field'>
                <span>Название комнаты</span>
                <input value={roomTitle} onChange={(event) => setRoomTitle(event.target.value)} type='text' placeholder='Например, Планёрка команды' maxLength='100' />
              </label>
              <label className='field'>
                <span>Пароль комнаты <small className='muted'>(необязательно)</small></span>
                <input value={password} onChange={(event) => setPassword(event.target.value)} type='text' placeholder='Например, team2026' maxLength='100' />
              </label>
              <div className='home-actions'>
                <button className='btn primary' type='button' disabled={loading} onClick={handleCreate}>{loading ? 'Создаю...' : 'Создать звонок'}</button>
                <button className='btn secondary' type='button' disabled={instaLoading} onClick={handleInsta}>{instaLoading ? 'Создаём...' : 'Инста'}</button>
              </div>
            </div>
          ) : (
            <div className='stack'>
              <label className='field'>
                <span>Ссылка приглашения</span>
                <input value={result.short_invite_link || result.invite_link} readOnly onFocus={(event) => event.target.select()} />
              </label>
              <button className='btn primary' type='button' onClick={() => copyText(result.short_invite_link || result.invite_link)}>Копировать</button>
              <details>
                <summary>Показать полную ссылку и QR-код</summary>
                <label className='field'>
                  <span>Полная ссылка</span>
                  <input value={result.invite_link} readOnly onFocus={(event) => event.target.select()} />
                </label>
                <button className='btn secondary' type='button' onClick={() => copyText(result.invite_link)}>Копировать полную</button>
                <div className='qr-box'><canvas ref={qrRef}></canvas></div>
              </details>
              <p className='muted'>Название комнаты: {result.room_title}</p>
              <a className='btn primary link-btn' href={result.short_invite_link || result.invite_link}>Перейти в звонок</a>
            </div>
          )}
        </section>
      </div>
    </main>
  );
}


function MediaTrack({ track, muted = false, className = '' }) {
  const ref = useRef(null);

  useEffect(() => {
    if (!track || !ref.current) return undefined;
    track.attach(ref.current);
    return () => {
      track.detach(ref.current);
    };
  }, [track]);

  return <video ref={ref} autoPlay playsInline muted={muted} className={className} />;
}


function RemoteAudioTrack({ track, volume }) {
  const gainRef = useRef(null);

  useEffect(() => {
    if (!track?.mediaStreamTrack) return undefined;
    const AudioContextClass = window.AudioContext || window.webkitAudioContext;
    if (!AudioContextClass) return undefined;
    const context = new AudioContextClass();
    const stream = new MediaStream([track.mediaStreamTrack]);
    const source = context.createMediaStreamSource(stream);
    const gain = context.createGain();
    gain.gain.value = volume;
    source.connect(gain);
    gain.connect(context.destination);
    gainRef.current = gain;
    context.resume().catch(() => {});

    return () => {
      source.disconnect();
      gain.disconnect();
      gainRef.current = null;
      context.close().catch(() => {});
    };
  }, [track]);

  useEffect(() => {
    if (gainRef.current) gainRef.current.gain.value = volume;
  }, [volume]);

  return null;
}


function VideoTile({ item, active, volume, onVolumeChange }) {
  const camera = item.tracks.find((entry) => entry.kind === 'video' && entry.source !== Track.Source.ScreenShare);
  const screen = item.tracks.find((entry) => entry.kind === 'video' && entry.source === Track.Source.ScreenShare);
  const audio = item.tracks.filter((entry) => entry.kind === 'audio');
  const initial = (item.name || '?').trim().charAt(0).toUpperCase() || '?';
  const volumePercent = Math.round(volume * 100);

  return (
    <div className={`video-tile${active ? ' speaking' : ''}${screen ? ' has-screen' : ''}`}>
      <div className='video-label-row'>
        <span>{item.name}</span>
        <span className='muted'>{item.isLocal ? 'ты' : ''}</span>
      </div>
      {!item.isLocal ? (
        <label className='participant-volume'>
          <span>Громкость: {volumePercent}%</span>
          <input
            aria-label={`Громкость участника ${item.name}`}
            max='2'
            min='0'
            onChange={(event) => onVolumeChange(item.identity, Number(event.target.value))}
            step='0.05'
            type='range'
            value={volume}
          />
        </label>
      ) : null}
      {screen ? (
        <div className='screen-slot'>
          <MediaTrack track={screen.track} className='screen-share-video' muted={item.isLocal} />
        </div>
      ) : null}
      <div className='media-slot'>
        {camera ? <MediaTrack track={camera.track} className={item.isLocal ? 'local-video' : ''} muted={item.isLocal} /> : <div className='avatar-placeholder'>{initial}</div>}
        {audio.map((entry) => item.isLocal ? null : <RemoteAudioTrack key={entry.id} track={entry.track} volume={volume} />)}
      </div>
    </div>
  );
}


function CallPage({ roomName, showToast }) {
  const [call, setCall] = useState(null);
  const [loadError, setLoadError] = useState(null);
  const [displayName, setDisplayName] = useState('');
  const [password, setPassword] = useState('');
  const [micEnabled, setMicEnabled] = useState(true);
  const [cameraEnabled, setCameraEnabled] = useState(true);
  const [joining, setJoining] = useState(false);
  const [room, setRoom] = useState(null);
  const [participants, setParticipants] = useState([]);
  const [participantVolumes, setParticipantVolumes] = useState({});
  const [activeSpeakers, setActiveSpeakers] = useState([]);
  const [messages, setMessages] = useState([]);
  const [chatText, setChatText] = useState('');
  const [config, setConfig] = useState({ max_attachment_size_mb: 100, turn_ice_servers: [] });
  const [elapsed, setElapsed] = useState(0);
  const previewRef = useRef(null);
  const previewStreamRef = useRef(null);
  const fileRef = useRef(null);

  const inviteToken = useMemo(getInviteToken, []);
  const joined = !!room;

  const refreshParticipants = useCallback((nextRoom) => {
    if (!nextRoom) return;
    const mapParticipant = (participant, isLocal) => {
      const tracks = Array.from(participant.trackPublications.values())
        .filter((publication) => publication.track)
        .map((publication) => ({
          id: publication.trackSid || publication.track.sid || `${participant.identity}-${publication.source}`,
          track: publication.track,
          source: publication.source,
          kind: publication.kind || publication.track.kind
        }));
      return {
        identity: participant.identity,
        name: participant.name || participant.identity,
        isLocal,
        tracks
      };
    };
    setParticipants([
      mapParticipant(nextRoom.localParticipant, true),
      ...Array.from(nextRoom.remoteParticipants.values()).map((participant) => mapParticipant(participant, false))
    ]);
  }, []);

  useEffect(() => {
    fetch(apiUrl(`/api/calls/${encodeURIComponent(roomName)}`))
      .then(readJson)
      .then((data) => {
        if (!data.is_active) {
          setLoadError('410');
          return;
        }
        setCall(data);
        document.title = `${data.room_title} · Linkra`;
      })
      .catch(() => setLoadError('404'));
    fetch(apiUrl('/api/config')).then(readJson).then(setConfig).catch(() => {});
  }, [roomName]);

  useEffect(() => {
    const raw = sessionStorage.getItem(`linkra-insta:${roomName}`);
    if (!raw) return;
    try {
      const data = JSON.parse(raw);
      if (data.invite_token && data.invite_token !== inviteToken) return;
      setDisplayName(data.display_name || randomGuestName());
      setMicEnabled(Boolean(data.mic_enabled));
      setCameraEnabled(Boolean(data.camera_enabled));
    } catch {
      return;
    }
  }, [inviteToken, roomName]);

  useEffect(() => {
    if (joined || !navigator.mediaDevices) return undefined;
    let cancelled = false;

    const run = async () => {
      if (previewStreamRef.current) {
        previewStreamRef.current.getTracks().forEach((track) => track.stop());
        previewStreamRef.current = null;
      }
      if (!cameraEnabled && !micEnabled) {
        if (previewRef.current) previewRef.current.srcObject = null;
        return;
      }
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ audio: micEnabled, video: cameraEnabled });
        if (cancelled) {
          stream.getTracks().forEach((track) => track.stop());
          return;
        }
        previewStreamRef.current = stream;
        if (previewRef.current) previewRef.current.srcObject = cameraEnabled ? new MediaStream(stream.getVideoTracks()) : null;
      } catch {
        showToast('Не удалось получить доступ к камере или микрофону', 'error');
      }
    };

    run();
    return () => {
      cancelled = true;
    };
  }, [cameraEnabled, joined, micEnabled, showToast]);

  useEffect(() => {
    if (!joined) return undefined;
    const started = Date.now();
    const interval = window.setInterval(() => {
      setElapsed(Math.floor((Date.now() - started) / 1000));
    }, 1000);
    return () => window.clearInterval(interval);
  }, [joined]);

  useEffect(() => () => {
    if (previewStreamRef.current) previewStreamRef.current.getTracks().forEach((track) => track.stop());
    if (room) room.disconnect();
  }, [room]);

  const publishData = async (payload) => {
    if (!room) return;
    const encoded = new TextEncoder().encode(JSON.stringify(payload));
    await room.localParticipant.publishData(encoded, { reliable: true });
  };

  const updateParticipantVolume = useCallback((identity, volume) => {
    const safeVolume = Math.min(2, Math.max(0, volume));
    setParticipantVolumes((current) => ({
      ...current,
      [identity]: safeVolume
    }));
  }, []);

  const joinCall = async () => {
    const name = displayName.trim();
    if (!name) {
      showToast('Введи имя', 'error');
      return;
    }
    if (!inviteToken) {
      setLoadError('403');
      return;
    }
    setJoining(true);
    try {
      const response = await fetch(apiUrl(`/api/calls/${encodeURIComponent(roomName)}/join`), {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          invite_token: inviteToken,
          display_name: name,
          password,
          with_video: cameraEnabled,
          with_audio: micEnabled
        })
      });
      const data = await readJson(response);
      if (previewStreamRef.current) {
        previewStreamRef.current.getTracks().forEach((track) => track.stop());
        previewStreamRef.current = null;
      }
      const nextRoom = new Room({ adaptiveStream: true, dynacast: true });
      const refresh = () => refreshParticipants(nextRoom);
      nextRoom
        .on(RoomEvent.TrackSubscribed, refresh)
        .on(RoomEvent.TrackUnsubscribed, refresh)
        .on(RoomEvent.LocalTrackPublished, refresh)
        .on(RoomEvent.LocalTrackUnpublished, refresh)
        .on(RoomEvent.ParticipantConnected, refresh)
        .on(RoomEvent.ParticipantDisconnected, refresh)
        .on(RoomEvent.ActiveSpeakersChanged, (speakers) => setActiveSpeakers(speakers.map((speaker) => speaker.identity)))
        .on(RoomEvent.DataReceived, (payload, participant) => {
          const text = new TextDecoder().decode(payload);
          const message = JSON.parse(text);
          if (message.type === 'chat') {
            setMessages((current) => current.concat({
              id: crypto.randomUUID ? crypto.randomUUID() : String(Date.now() + Math.random()),
              author: participant?.name || participant?.identity || 'Участник',
              text: message.text,
              own: false
            }));
          }
          if (message.type === 'file') {
            setMessages((current) => current.concat({
              id: crypto.randomUUID ? crypto.randomUUID() : String(Date.now() + Math.random()),
              author: participant?.name || participant?.identity || 'Участник',
              attachment: message.attachment,
              own: false
            }));
          }
        })
        .on(RoomEvent.Disconnected, () => {
          setRoom(null);
          setParticipants([]);
        });
      const connectOptions = {};
      if (config.turn_ice_servers?.length) {
        connectOptions.rtcConfig = {
          iceServers: config.turn_ice_servers
        };
      }
      await nextRoom.connect(data.server_url, data.participant_token, connectOptions);
      await nextRoom.localParticipant.setMicrophoneEnabled(micEnabled);
      await nextRoom.localParticipant.setCameraEnabled(cameraEnabled);
      setRoom(nextRoom);
      refreshParticipants(nextRoom);
      sessionStorage.removeItem(`linkra-insta:${roomName}`);
      showToast('Ты в комнате', 'success');
    } catch (error) {
      showToast(error.message || 'Не удалось войти в звонок', 'error');
    } finally {
      setJoining(false);
    }
  };

  const leaveCall = async () => {
    if (!room) return;
    await room.disconnect();
    setRoom(null);
    setParticipants([]);
  };

  const toggleMic = async () => {
    if (!room) {
      setMicEnabled((value) => !value);
      return;
    }
    const next = !micEnabled;
    await room.localParticipant.setMicrophoneEnabled(next);
    setMicEnabled(next);
    refreshParticipants(room);
  };

  const toggleCamera = async () => {
    if (!room) {
      setCameraEnabled((value) => !value);
      return;
    }
    const next = !cameraEnabled;
    await room.localParticipant.setCameraEnabled(next);
    setCameraEnabled(next);
    refreshParticipants(room);
  };

  const toggleScreen = async () => {
    if (!room) return;
    const enabled = Array.from(room.localParticipant.trackPublications.values()).some((publication) => publication.source === Track.Source.ScreenShare && publication.track);
    await room.localParticipant.setScreenShareEnabled(!enabled);
    refreshParticipants(room);
  };

  const finishCall = async () => {
    if (!window.confirm('Завершить звонок для всех участников?')) return;
    try {
      await fetch(apiUrl(`/api/calls/${encodeURIComponent(roomName)}/finish`), {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ invite_token: inviteToken })
      }).then(readJson);
      await leaveCall();
      setLoadError('410');
    } catch (error) {
      showToast(error.message || 'Не удалось завершить звонок', 'error');
    }
  };

  const sendChat = async () => {
    const text = chatText.trim();
    if (!text || !room) return;
    try {
      await fetch(apiUrl(`/api/calls/${encodeURIComponent(roomName)}/chat-events`), {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          invite_token: inviteToken,
          display_name: displayName.trim(),
          participant_identity: room.localParticipant.identity,
          text
        })
      }).catch(() => {});
      await publishData({ type: 'chat', text });
      setMessages((current) => current.concat({
        id: crypto.randomUUID ? crypto.randomUUID() : String(Date.now() + Math.random()),
        author: `${displayName.trim()} (ты)`,
        text,
        own: true
      }));
      setChatText('');
    } catch (error) {
      showToast(error.message || 'Не удалось отправить сообщение', 'error');
    }
  };

  const sendFile = async (file) => {
    if (!file || !room) return;
    if (file.size > config.max_attachment_size_mb * 1024 * 1024) {
      showToast(`Максимальный размер файла — ${config.max_attachment_size_mb} МБ`, 'error');
      return;
    }
    const formData = new FormData();
    formData.append('file', file);
    formData.append('display_name', displayName.trim() || 'Участник');
    formData.append('participant_identity', room.localParticipant.identity);
    try {
      const attachment = await fetch(apiUrl(`/api/calls/${encodeURIComponent(roomName)}/attachments?invite_token=${encodeURIComponent(inviteToken)}`), {
        method: 'POST',
        body: formData
      }).then(readJson);
      await publishData({ type: 'file', attachment });
      setMessages((current) => current.concat({
        id: crypto.randomUUID ? crypto.randomUUID() : String(Date.now() + Math.random()),
        author: `${displayName.trim()} (ты)`,
        attachment,
        own: true
      }));
      showToast('Файл отправлен', 'success');
    } catch (error) {
      showToast(error.message || 'Не удалось загрузить файл', 'error');
    } finally {
      if (fileRef.current) fileRef.current.value = '';
    }
  };

  const copyInvite = async () => {
    await navigator.clipboard.writeText(window.location.href);
    showToast('Ссылка скопирована', 'success');
  };

  const time = `${Math.floor(elapsed / 60).toString().padStart(2, '0')}:${(elapsed % 60).toString().padStart(2, '0')}`;

  if (loadError) return <ErrorPage code={loadError} />;
  if (!call) return <main className='container'><section className='card'>Загрузка...</section></main>;

  return (
    <main className='container wide'>
      {!joined ? (
        <section className='card join-card'>
          <div className='prejoin-layout'>
            <div className='preview-stage'>
              {cameraEnabled ? <video ref={previewRef} className='prejoin-preview-video' autoPlay playsInline muted></video> : <div className='preview-placeholder'><div className='avatar-placeholder'>?</div><p>Камера выключена</p></div>}
            </div>
            <div className='stack'>
              <h1>{call.room_title}</h1>
              <p className='muted'>Проверь камеру и микрофон перед входом в комнату.</p>
              <label className='field'>
                <span>Твоё имя</span>
                <input value={displayName} onChange={(event) => setDisplayName(event.target.value)} type='text' placeholder='Например, Артём' maxLength='50' />
              </label>
              {call.has_password ? (
                <label className='field'>
                  <span>Пароль комнаты</span>
                  <input value={password} onChange={(event) => setPassword(event.target.value)} type='text' placeholder='Введите пароль комнаты' maxLength='100' />
                </label>
              ) : null}
              <div className='row'>
                <button className={`toggle-btn ${micEnabled ? 'active' : 'inactive'}`} type='button' onClick={toggleMic}>
                  <img className='control-icon' src={micEnabled ? ICONS.micOn : ICONS.micOff} alt='' />
                  <span>{micEnabled ? 'Микрофон включён' : 'Микрофон выключен'}</span>
                </button>
                <button className={`toggle-btn ${cameraEnabled ? 'active' : 'inactive'}`} type='button' onClick={toggleCamera}>
                  <img className='control-icon' src={cameraEnabled ? ICONS.cameraOn : ICONS.cameraOff} alt='' />
                  <span>{cameraEnabled ? 'Камера включена' : 'Камера выключена'}</span>
                </button>
              </div>
              <button className='btn primary' type='button' disabled={joining} onClick={joinCall}>{joining ? 'Подключаем...' : 'Войти в звонок'}</button>
            </div>
          </div>
        </section>
      ) : (
        <>
          <section className='card meeting-card'>
            <div className='meeting-topbar'>
              <div>
                <div className='meeting-title'>Созвон</div>
                <h1 className='meeting-room-name'>{call.room_title}</h1>
                <p className='muted'>В комнате: {participants.length}</p>
              </div>
              <div className='meeting-controls'>
                <button className={`icon-btn ${micEnabled ? '' : 'is-off'}`} type='button' onClick={toggleMic} aria-label={micEnabled ? 'Выключить микрофон' : 'Включить микрофон'}>
                  <img className='control-icon' src={micEnabled ? ICONS.micOn : ICONS.micOff} alt='' />
                </button>
                <button className={`icon-btn ${cameraEnabled ? '' : 'is-off'}`} type='button' onClick={toggleCamera} aria-label={cameraEnabled ? 'Выключить камеру' : 'Включить камеру'}>
                  <img className='control-icon' src={cameraEnabled ? ICONS.cameraOn : ICONS.cameraOff} alt='' />
                </button>
                <button className='icon-btn' type='button' onClick={toggleScreen}>Screen</button>
                <button className='btn secondary' type='button' onClick={copyInvite}>Ссылка</button>
                <div className='meeting-timer'>{time}</div>
              </div>
            </div>
            <div className='videos'>
              {participants.map((item) => (
                <VideoTile
                  active={activeSpeakers.includes(item.identity)}
                  item={item}
                  key={item.identity}
                  onVolumeChange={updateParticipantVolume}
                  volume={participantVolumes[item.identity] ?? 1}
                />
              ))}
            </div>
          </section>

          <aside className='card chat-card'>
            <h2>Чат комнаты</h2>
            <div className='chat-messages'>
              {messages.map((message) => (
                <div className={`chat-message${message.own ? ' own' : ''}`} key={message.id}>
                  <div className='chat-author'>{message.author}</div>
                  {message.attachment ? (
                    <a className='file-link' href={`${message.attachment.download_url}?invite_token=${encodeURIComponent(inviteToken)}`} target='_blank' rel='noreferrer'>{message.attachment.file_name}</a>
                  ) : <div className='chat-text'>{message.text}</div>}
                </div>
              ))}
            </div>
            <div className='chat-form'>
              <input value={chatText} onChange={(event) => setChatText(event.target.value)} onKeyDown={(event) => event.key === 'Enter' ? sendChat() : null} placeholder='Напиши сообщение...' maxLength='1000' />
              <input className='hidden-file-input' ref={fileRef} type='file' onChange={(event) => sendFile(event.target.files?.[0])} />
              <button className='icon-btn attachment-btn' type='button' onClick={() => fileRef.current?.click()} aria-label='Прикрепить файл'>
                <img className='control-icon' src={ICONS.attachment} alt='' />
              </button>
              <button className='btn primary' type='button' onClick={sendChat}>Отправить</button>
            </div>
          </aside>

          <div className='call-bottom-actions is-visible'>
            <button className='btn secondary' type='button' onClick={leaveCall}>Выйти</button>
            <button className='btn danger' type='button' onClick={finishCall}>Завершить</button>
          </div>
        </>
      )}
    </main>
  );
}


function App() {
  const [route, setRoute] = useState(routeFromLocation);
  const [toasts, showToast] = useToasts();

  useEffect(() => {
    const onPop = () => setRoute(routeFromLocation());
    window.addEventListener('popstate', onPop);
    return () => window.removeEventListener('popstate', onPop);
  }, []);

  return (
    <>
      <Toasts items={toasts} />
      {route.name === 'home' ? <HomePage showToast={showToast} /> : null}
      {route.name === 'call' ? <CallPage roomName={route.roomName} showToast={showToast} /> : null}
      {route.name === 'error' ? <ErrorPage code={route.code} /> : null}
    </>
  );
}


createRoot(document.getElementById('root')).render(<App />);
