import { useCallback, useEffect, useRef, useState } from 'react'

/**
 * Сообщение чата в проводном формате сервера — 1:1 с pydantic-схемой
 * `ChatMessageOut` backend (зафиксированный протокол WS).
 * `created_at` — UTC ISO-8601 (в БД и API только UTC), конвертация в
 * локальное время — на стороне ChatPanel (`lib/localTime.ts`).
 */
export interface ChatMessageOut {
  id: number
  author_id: string | null
  author_name: string
  is_guest: boolean
  text: string
  created_at: string
}

/**
 * Статус WS-соединения чата: `connecting` — сокет открывается,
 * `open` — транспорт открыт (auth уже отправлен, история может ещё
 * загружаться), `closed` — соединение штатно закрыто/не запускалось,
 * `error` — обрыв по невалидному токену или отказу в доступе
 * (см. `statusMessage` для пояснения пользователю).
 */
export type ChatConnectionStatus = 'connecting' | 'open' | 'closed' | 'error'

/**
 * Один участник в очереди поднятых рук (задача B1) — 1:1 с pydantic-схемой
 * `HandQueueEntryOut` backend. `identity` — тот же формат, что и
 * `Participant.identity` в LiveKit (`str(user_id)` либо `guest:{id}`),
 * пригоден для прямого сравнения с `localParticipant.identity`/
 * `participant.identity` на сцене.
 */
export interface HandQueueEntry {
  identity: string
  name: string
  raised_at: string
}

/** Источник трека, принудительно выключенного организатором (задача B2). */
export type ForcedMuteSource = 'microphone' | 'camera'

/**
 * Одно событие принудительного мьюта (задача B2) — рассылается ВСЕМ
 * участникам конференции (канал общий, адресной доставки нет), поэтому
 * несёт `identity` затронутого: получатель сам решает, про него ли это
 * (см. `ForcedMuteWatcher` — сравнивает с `localParticipant.identity`).
 * `nonce` — счётчик хука, растёт на каждое полученное событие: тот же
 * `source`/`identity` два раза подряд (например, повторный клик
 * организатора на уже выключенный трек) должен переоткрыть тост, а не
 * молча схлопнуться в один и тот же объект по `useEffect`-сравнению.
 */
export interface ForcedMuteEvent {
  identity: string
  source: ForcedMuteSource
  nonce: number
}

type IncomingFrame =
  | { type: 'history'; messages: ChatMessageOut[] }
  | { type: 'message'; message: ChatMessageOut }
  | { type: 'error'; code: string }
  | { type: 'hand_queue'; queue: HandQueueEntry[] }
  | { type: 'forced_mute'; identity: string; source: ForcedMuteSource }

interface UseChatOptions {
  /** id конференции — пока не известен (страница ещё не подключилась к LiveKit), WS не открываем. */
  conferenceId: string | undefined
  /** LiveKit-токен из JoinOut — им же авторизуем WS-сессию чата (см. протокол). */
  token: string | undefined
  /** `JoinOut.chat_enabled` — при `false` хук ничего не подключает. */
  enabled: boolean
}

interface UseChatResult {
  messages: ChatMessageOut[]
  status: ChatConnectionStatus
  /** Пояснение для баннера при обрыве/ошибке — `null`, если показывать нечего. */
  statusMessage: string | null
  /** `true` после close-кода 4404 (чат выключен на сервере/конференция не найдена) — панель нужно скрыть. */
  unavailable: boolean
  /** Отправить сообщение (1..2000 символов после strip, пустое/слишком длинное — игнорируется). */
  sendMessage: (text: string) => void
  /**
   * Очередь поднятых рук, упорядоченная по времени поднятия — сервер
   * присылает полный снапшот при любом изменении (см. `schemas/room_events.py`
   * backend), поэтому клиенту не нужно вести собственное состояние очереди.
   * Пуста, пока WS не открыт/не пришёл первый снапшот.
   */
  handQueue: HandQueueEntry[]
  /** Поднять СВОЮ руку — повторный вызов на уже поднятой руке безвреден (идемпотентно на сервере). */
  raiseHand: () => void
  /**
   * Опустить руку — свою (без аргумента) либо чужую по `identity` (только
   * организатору, иначе сервер отклонит `{type:"error",code:"forbidden"}`,
   * см. `statusMessage`).
   */
  lowerHand: (identity?: string) => void
  /** Последнее событие принудительного мьюта (задача B2) — `null` до первого. */
  lastForcedMute: ForcedMuteEvent | null
}

/** Close-коды сервера — см. зафиксированный протокол WS. */
const CLOSE_TOKEN_INVALID = 4401
const CLOSE_FORBIDDEN = 4403
const CLOSE_UNAVAILABLE = 4404

/** Собрать WS-адрес чата от текущего `window.location` (ws:// на http, wss:// на https). */
function buildChatWsUrl(conferenceId: string): string {
  const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
  return `${wsProtocol}//${window.location.host}/api/v1/conferences/${conferenceId}/chat`
}

/**
 * WS-клиент комнаты конференции (несмотря на имя — не только чат, задача
 * B1). Реализует зафиксированный протокол: connect → `{type:"auth"}` →
 * `{type:"history"}` → `{type:"hand_queue"}` → далее входящие
 * `{type:"message"}`/`{type:"hand_queue"}`/`{type:"error"}`.
 *
 * Очередь поднятых рук (`raiseHand`/`lowerHand`/`handQueue`) едет по тому же
 * соединению, что и чат, — переиспользование уже открытого аутентифицированного
 * WS дешевле отдельного эндпоинта (см. `backend/api/chat.py`). Следствие:
 * поднять руку нельзя, если чат выключен настройкой инстанса (`enabled=false`,
 * соединение вообще не открывается) — принятый компромисс, обоснование в
 * коммите задачи B1.
 *
 * Optimistic-append собственных сообщений чата НЕ делается: сервер всегда
 * присылает наше же сообщение обратно echo-фреймом `message` — если
 * добавлять его на клиенте сразу при отправке, оно задублируется в списке.
 * Очередь рук устроена иначе: сервер шлёт ПОЛНЫЙ снапшот на каждое
 * изменение, поэтому `raiseHand`/`lowerHand` ничего не трогают в состоянии
 * сами — ждут снапшот.
 */
export function useChat({ conferenceId, token, enabled }: UseChatOptions): UseChatResult {
  const [messages, setMessages] = useState<ChatMessageOut[]>([])
  const [handQueue, setHandQueue] = useState<HandQueueEntry[]>([])
  const [lastForcedMute, setLastForcedMute] = useState<ForcedMuteEvent | null>(null)
  const forcedMuteNonceRef = useRef(0)
  // `wsStatus` меняется ТОЛЬКО из колбэков реального WS-соединения (см. ниже) —
  // никогда синхронно в теле эффекта, иначе react-hooks/set-state-in-effect
  // (эффект без активной подписки, только синхронизирующий производное
  // значение, — по факту тот самый анти-паттерн, который правило и ловит).
  // Начальное значение — `connecting`: типичный случай — подключение начинается
  // сразу при монтировании хука (RoomPage монтируется один раз на комнату);
  // при повторном подключении (смена conferenceId/token) статус до первого
  // события нового сокета может на короткое время показывать значение от
  // предыдущего соединения — не считается веб-сокет-ready до первого
  // onopen/onerror/onclose. Когда чат выключен/данных для подключения ещё
  // нет, наружу отдаём производный статус `closed` без всякого state — см.
  // `status` ниже.
  const [wsStatus, setWsStatus] = useState<ChatConnectionStatus>('connecting')
  const [statusMessage, setStatusMessage] = useState<string | null>(null)
  const [unavailable, setUnavailable] = useState(false)
  const wsRef = useRef<WebSocket | null>(null)
  const canConnect = enabled && Boolean(conferenceId) && Boolean(token)

  useEffect(() => {
    if (!canConnect || !conferenceId || !token) {
      return
    }

    // Флаг «эффект пересоздан/компонент размонтирован» — без него обработчики
    // СТАРОГО сокета (onclose и т.п.), сработавшие асинхронно уже после того,
    // как эффект пересоздался для нового conferenceId/token, могли бы
    // перетереть состояние уже актуального соединения.
    let stale = false

    const ws = new WebSocket(buildChatWsUrl(conferenceId))
    wsRef.current = ws

    ws.onopen = () => {
      if (stale) return
      setMessages([])
      setHandQueue([])
      setStatusMessage(null)
      setUnavailable(false)
      setWsStatus('open')
      ws.send(JSON.stringify({ type: 'auth', token }))
    }

    ws.onmessage = (event) => {
      if (stale) return
      let frame: IncomingFrame
      try {
        frame = JSON.parse(event.data as string) as IncomingFrame
      } catch {
        return
      }
      if (frame.type === 'history') {
        setMessages(frame.messages)
      } else if (frame.type === 'message') {
        setMessages((prev) => [...prev, frame.message])
      } else if (frame.type === 'hand_queue') {
        setHandQueue(frame.queue)
      } else if (frame.type === 'forced_mute') {
        forcedMuteNonceRef.current += 1
        setLastForcedMute({
          identity: frame.identity,
          source: frame.source,
          nonce: forcedMuteNonceRef.current,
        })
      } else if (frame.type === 'error') {
        // Ошибка отдельной операции (например, отклонённое сообщение или
        // запрет опустить чужую руку не-организатору, code:"forbidden") —
        // соединение не рвётся, просто короткое пояснение пользователю.
        setStatusMessage(`Ошибка чата: ${frame.code}`)
      }
    }

    ws.onerror = () => {
      if (stale) return
      setWsStatus('error')
    }

    ws.onclose = (event) => {
      wsRef.current = null
      if (stale) return
      if (event.code === CLOSE_TOKEN_INVALID) {
        setWsStatus('error')
        setStatusMessage('Сессия чата истекла — обновите страницу, чтобы переподключиться')
      } else if (event.code === CLOSE_FORBIDDEN) {
        setWsStatus('error')
        setStatusMessage('Нет доступа к чату этой конференции')
      } else if (event.code === CLOSE_UNAVAILABLE) {
        setWsStatus('closed')
        setUnavailable(true)
      } else {
        setWsStatus('closed')
      }
    }

    return () => {
      stale = true
      wsRef.current = null
      ws.close()
    }
  }, [canConnect, conferenceId, token])

  const sendMessage = useCallback((text: string) => {
    const trimmed = text.trim()
    if (!trimmed || trimmed.length > 2000) return
    const ws = wsRef.current
    if (!ws || ws.readyState !== WebSocket.OPEN) return
    ws.send(JSON.stringify({ type: 'message', text: trimmed }))
  }, [])

  const raiseHand = useCallback(() => {
    const ws = wsRef.current
    if (!ws || ws.readyState !== WebSocket.OPEN) return
    ws.send(JSON.stringify({ type: 'raise_hand' }))
  }, [])

  const lowerHand = useCallback((identity?: string) => {
    const ws = wsRef.current
    if (!ws || ws.readyState !== WebSocket.OPEN) return
    ws.send(JSON.stringify({ type: 'lower_hand', identity: identity ?? null }))
  }, [])

  // Наружу — производный статус: пока подключаться нечем (выключено/нет
  // conferenceId/token), всегда `closed`, даже если внутренний `wsStatus`
  // ещё хранит значение от предыдущего подключения.
  const status: ChatConnectionStatus = canConnect ? wsStatus : 'closed'

  return {
    messages,
    status,
    statusMessage,
    unavailable,
    sendMessage,
    handQueue,
    raiseHand,
    lowerHand,
    lastForcedMute,
  }
}
