import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { PictureInPicture2 } from 'lucide-react'
import { LiveKitRoom, usePersistentUserChoices } from '@livekit/components-react'
import type { RoomOptions } from 'livekit-client'
import '@livekit/components-styles'
import '@/styles/room.css'
import { joinConference, resolveConference } from '@/api/conferences'
import { ApiError, errorDetail } from '@/api/client'
import { useAuth } from '@/auth/useAuth'
import { useChat } from '@/hooks/useChat'
import { useFullscreen } from '@/hooks/useFullscreen'
import { useRoomPiP } from '@/hooks/useRoomPiP'
import { RoomTopbar } from '@/components/room/RoomTopbar'
import { RoomStage } from '@/components/room/RoomStage'
import { RoomToolbar } from '@/components/room/RoomToolbar'
import { ChatPanel } from '@/components/room/ChatPanel'
import { DeviceSettingsDialog } from '@/components/room/DeviceSettingsDialog'
import { loadAudioOutputDeviceId } from '@/lib/audioOutputDevice'
import { loadStageLayoutMode, saveStageLayoutMode, type StageLayoutMode } from '@/lib/stageLayoutMode'

interface RoomJoinState {
  livekitUrl: string
  token: string
  roomName: string
  conferenceId?: string
  /** Номер конференции — для инвайт-чипа в топбаре (см. RoomTopbar). Есть только сразу после создания. */
  number?: string
  /** `JoinOut.chat_enabled` — при `false` кнопка чата и панель не рендерятся. */
  chatEnabled?: boolean
}

/**
 * Экран комнаты конференции (тёмная тема, `data-theme="room"`).
 *
 * Публичный маршрут: гость входит без JWT-аутентификации приложения — его
 * LiveKit-токен получен через `POST /conferences/{id}/guest-join` (см.
 * JoinPage) и передан сюда через navigation state, ровно как и токен
 * авторизованного пользователя. RoomPage сам никогда не проверяет сессию —
 * подключение к LiveKit целиком определяется переданными `token`/`livekitUrl`.
 *
 * `:slug` в адресе — тот же идентификатор (slug или номер), что пользователь
 * вводил на JoinPage; используется только для повторного резолва при прямой
 * перезагрузке страницы (join-state из state теряется при F5). Если состояния
 * нет — не пытаемся угадать креды сами: гостю это в принципе невозможно
 * (нет сохранённых имени/пароля), поэтому просто уводим на `/join?q=:slug`,
 * где резолв и вход повторяются штатным флоу.
 */
export function RoomPage() {
  const { slug } = useParams<{ slug: string }>()
  const location = useLocation()
  const navigate = useNavigate()
  const { status } = useAuth()

  const initialState = (location.state as RoomJoinState | null) ?? null
  const [joinState, setJoinState] = useState<RoomJoinState | null>(initialState)
  const [error, setError] = useState<string | null>(null)

  // useCallback: стабильная ссылка на обработчик — иначе на каждом рендере
  // RoomPage пересобирался бы объект-пропс `onDisconnected`, а с ним и
  // внутренний useEffect @livekit/components-react (он в списке зависимостей
  // хука useLiveKitRoom), лишний раз пересоздавая подписки на события Room.
  const handleDisconnected = useCallback(() => {
    navigate(status === 'authenticated' ? '/lobby' : '/join')
  }, [navigate, status])

  useEffect(() => {
    if (joinState || !slug) return
    // Сессия ещё восстанавливается (silent-refresh) — подождём, чтобы не
    // отправить авторизованного пользователя в гостевой флоу по ошибке.
    if (status === 'loading') return

    if (status !== 'authenticated') {
      navigate(`/join?q=${encodeURIComponent(slug)}`, { replace: true })
      return
    }

    // Авторизованный пользователь перезагрузил страницу комнаты — пробуем
    // молча переподключиться тем же slug/номером, что был в адресе.
    let cancelled = false

    async function reconnect() {
      try {
        const info = await resolveConference(slug!)
        if (info.requires_password) {
          if (!cancelled) navigate(`/join?q=${encodeURIComponent(slug!)}`, { replace: true })
          return
        }
        const result = await joinConference(info.id)
        if (!cancelled) {
          setJoinState({
            livekitUrl: result.livekit_url,
            token: result.token,
            roomName: result.room_name,
            conferenceId: result.conference_id,
            chatEnabled: result.chat_enabled,
          })
        }
      } catch (err) {
        if (cancelled) return
        if (err instanceof ApiError && (err.status === 404 || err.status === 410)) {
          setError('Конференция недоступна или уже завершилась')
        } else if (err instanceof ApiError && err.status === 403 && errorDetail(err) === 'password_required') {
          navigate(`/join?q=${encodeURIComponent(slug!)}`, { replace: true })
        } else {
          setError('Не удалось подключиться к конференции. Попробуйте войти ещё раз')
        }
      }
    }

    void reconnect()
    return () => {
      cancelled = true
    }
  }, [joinState, slug, status, navigate])

  // WS-соединение чата живёт независимо от того, открыта ли панель, — иначе
  // при сворачивании панели терялась бы история и обрывался счётчик
  // непрочитанных. Хук вызывается безусловно (до ранних return ниже) —
  // conferenceId/token до входа в комнату ещё не известны, enabled это учитывает.
  const chat = useChat({
    conferenceId: joinState?.conferenceId,
    token: joinState?.token,
    enabled: Boolean(joinState?.chatEnabled),
  })
  const [chatOpen, setChatOpen] = useState(false)
  // Сколько сообщений уже «видели» (на момент последнего открытия панели) —
  // считается прямо в теле рендера, под охраной сравнения («adjusting state
  // during rendering», тот же санкционированный паттерн, что и focus-логика
  // в RoomStage.tsx); useEffect тут не подходит — react-hooks/set-state-in-effect,
  // а ref нельзя ни читать, ни писать во время рендера — react-hooks/refs.
  const [chatSeenCount, setChatSeenCount] = useState(0)
  if (chatOpen && chatSeenCount !== chat.messages.length) {
    setChatSeenCount(chat.messages.length)
  }
  // Счётчик непрочитанных — сообщения, накопленные с момента последнего
  // открытия панели (включая историю при первом подключении, если панель
  // открывается впервые уже после её загрузки); при закрытой панели счётчик
  // сохраняется (см. design/mockups/room.html, .collapsed-demo).
  const chatUnreadCount = Math.max(0, chat.messages.length - chatSeenCount)

  // Чат выключен на сервере (close-код 4404) — панель нужно скрыть, даже если
  // изначально JoinOut.chat_enabled был true (рассинхрон с админкой в моменте).
  const chatVisible = Boolean(joinState?.chatEnabled) && !chat.unavailable

  // Корневой контейнер комнаты — цель для fullscreen и источник video-элемента
  // для video-PiP-фолбэка.
  const roomRootRef = useRef<HTMLDivElement>(null)
  const fullscreen = useFullscreen(roomRootRef)
  const pip = useRoomPiP(roomRootRef)
  const [settingsOpen, setSettingsOpen] = useState(false)

  // Вид сцены живёт здесь, а не в `RoomStage`: переключатели — в тулбаре и в
  // шторке настроек, а сцена их только читает (общий предок).
  //
  // Режим показа персистится между заходами в комнату (`lib/stageLayoutMode.ts`,
  // localStorage) — это устойчивое предпочтение: кто любит «плитки», хочет их
  // и завтра. А вот «скрыть остальных» намеренно НЕ сохраняется: это разовое
  // действие по ходу разговора, и войти в новую конференцию сразу без половины
  // участников — сюрприз, а не удобство. Сбрасывается вместе с уходом со
  // страницы комнаты.
  const [layoutMode, setLayoutMode] = useState<StageLayoutMode>(loadStageLayoutMode)
  const [hideOthers, setHideOthers] = useState(false)

  const handleLayoutModeChange = useCallback((mode: StageLayoutMode) => {
    setLayoutMode(mode)
    saveStageLayoutMode(mode)
  }, [])

  // Ключ трека, который сцена показывает крупно. Живёт ЗДЕСЬ, а не только
  // внутри `RoomStage`, потому что при открытии мини-плеера сцена
  // размонтируется в основном окне и монтируется заново в PiP-окне (портал
  // ниже) — без этого мостика новый экземпляр начинал бы выбор фокуса с нуля и
  // открывал мини-окно на самом пользователе. Подробнее — докстринг `RoomStage`.
  const [stageFocusKey, setStageFocusKey] = useState<string | null>(null)

  // Сохранённый выбор устройств — читаем через собственный вызов
  // usePersistentUserChoices (независимый от того, что использует
  // DeviceSettingsDialog: там свой вызов хука со своим состоянием). ВАЖНО:
  // ссылка на `roomOptions` должна быть стабильной между рендерами — иначе
  // LiveKitRoom получает «новый» объект `options` и переподключается
  // Это безопасно мемоизировать по `userChoices`,
  // потому что `userChoices` ЭТОГО вызова хука меняется, только если МЫ САМИ
  // вызовем saveAudioInputDeviceId/saveVideoInputDeviceId НА НЁМ — а мы этого
  // не делаем (сохранение — только в DeviceSettingsDialog).
  const { userChoices } = usePersistentUserChoices()
  const roomOptions = useMemo<RoomOptions>(
    () => ({
      audioCaptureDefaults: { deviceId: userChoices.audioDeviceId || undefined },
      videoCaptureDefaults: { deviceId: userChoices.videoDeviceId || undefined },
      // Аудиовыход (колонки/наушники/bluetooth) — отдельный персист, не через
      // usePersistentUserChoices: LocalUserChoices LiveKit про него не знает
      // (см. lib/audioOutputDevice.ts). Читаем один раз при монтировании —
      // как и с audio/videoDeviceId, смена применяется через DeviceSettingsDialog
      // (setActiveMediaDevice), а не пересозданием roomOptions.
      audioOutput: { deviceId: loadAudioOutputDeviceId() || undefined },
    }),
    [userChoices],
  )

  if (error) {
    return (
      <div data-theme="room" className="room-error">
        <p>{error}</p>
        <button type="button" className="btn btn-primary" onClick={() => navigate('/lobby')}>
          Вернуться в лобби
        </button>
      </div>
    )
  }

  if (!joinState) {
    return (
      <div data-theme="room" className="room-loader" role="status" aria-live="polite">
        Подключаемся к конференции…
      </div>
    )
  }

  return (
    <div data-theme="room" ref={roomRootRef}>
      <LiveKitRoom
        serverUrl={joinState.livekitUrl}
        token={joinState.token}
        connect
        audio
        video
        options={roomOptions}
        onDisconnected={handleDisconnected}
      >
        <div data-lk-theme="default" className="room-shell">
          <RoomTopbar roomName={joinState.roomName} slug={slug} number={joinState.number} />
          <div className="room-main">
            {pip.mode === 'document' ? (
              // Сцена сейчас рисуется в PiP-окне (через createPortal ниже) —
              // основное окно вместо неё показывает заглушку с возвратом.
              <div className="room-pip-placeholder">
                <PictureInPicture2 aria-hidden="true" />
                <p>Конференция открыта в отдельном мини-окне</p>
                <button type="button" className="btn btn-secondary" onClick={pip.toggle}>
                  Вернуть в это окно
                </button>
              </div>
            ) : (
              <RoomStage
                layoutMode={layoutMode}
                hideOthers={hideOthers}
                onShowOthers={() => setHideOthers(false)}
                onHideOthers={() => setHideOthers(true)}
                initialFocusKey={stageFocusKey}
                onFocusKeyChange={setStageFocusKey}
              />
            )}
            {chatVisible && chatOpen && (
              <ChatPanel
                messages={chat.messages}
                status={chat.status}
                statusMessage={chat.statusMessage}
                onSend={chat.sendMessage}
                onClose={() => setChatOpen(false)}
              />
            )}
          </div>
          <RoomToolbar
            chatVisible={chatVisible}
            chatOpen={chatOpen}
            chatUnreadCount={chatUnreadCount}
            onToggleChat={() => setChatOpen((open) => !open)}
            onToggleSettings={() => setSettingsOpen(true)}
            fullscreenSupported={fullscreen.supported}
            fullscreenActive={fullscreen.active}
            onToggleFullscreen={fullscreen.toggle}
            pipSupported={pip.supported}
            pipActive={pip.active}
            onTogglePiP={pip.toggle}
            layoutMode={layoutMode}
            onLayoutModeChange={handleLayoutModeChange}
            hideOthers={hideOthers}
            onHideOthersChange={setHideOthers}
          />
        </div>
        {settingsOpen && (
          <DeviceSettingsDialog
            onClose={() => setSettingsOpen(false)}
            layoutMode={layoutMode}
            onLayoutModeChange={handleLayoutModeChange}
            hideOthers={hideOthers}
            onHideOthersChange={setHideOthers}
          />
        )}
        {/* Портал внутрь LiveKitRoom (не как сосед снаружи!) — RoomStage,
            рендерящийся в PiP-окне, продолжает читать RoomContext/треки.
            `variant="pip"` — мини-плеер
            показывает только активное окно (одну плитку), без карусели/грида
            основного окна. `initialFocusKey` — то, что было крупно в основном
            окне: без него мини-окно открывалось на самом пользователе. */}
        {pip.pipWindow &&
          createPortal(
            <RoomStage variant="pip" initialFocusKey={stageFocusKey} onFocusKeyChange={setStageFocusKey} />,
            pip.pipWindow.document.body,
          )}
      </LiveKitRoom>
    </div>
  )
}
