import { useCallback, useEffect, useRef, useState } from 'react'
import { useLocalParticipant } from '@livekit/components-react'
import type { LocalVideoTrack } from 'livekit-client'
import type { BackgroundProcessorWrapper } from '@livekit/track-processors'
import {
  createBackgroundProcessor,
  loadBackgroundKey,
  NO_BACKGROUND,
  resolveBackgroundUrl,
  saveBackgroundKey,
  type BackgroundKey,
  type CustomBackground,
} from '@/lib/virtualBackground'

export type VirtualBackgroundStatus = 'idle' | 'loading' | 'error'

export interface UseVirtualBackgroundResult {
  /** Текущий выбор (`none` / `default:<id>` / `custom:<uuid>`). */
  backgroundKey: BackgroundKey
  /** Сменить фон; сразу же персистится (см. `saveBackgroundKey`). */
  selectBackground: (key: BackgroundKey) => void
  /** `loading` — идёт первая загрузка библиотеки/модели либо применение к треку. */
  status: VirtualBackgroundStatus
}

/**
 * Применение выбранного фона к ПУБЛИКУЕМОМУ треку камеры участника.
 *
 * Фон навешивается процессором на сам трек (`LocalVideoTrack.setProcessor`),
 * а НЕ через пересоздание `RoomOptions`: ссылка на `roomOptions` в `RoomPage`
 * обязана оставаться стабильной, иначе `LiveKitRoom` переподключается к
 * комнате (см. комментарий там же).
 *
 * Что здесь неочевидно:
 *
 * 1. **Трек живёт не всё время.** Выключение камеры в тулбаре не «глушит»
 *    трек, а останавливает и снимает его с публикации; включение создаёт
 *    НОВЫЙ `LocalVideoTrack` — без процессора. Поэтому эффект синхронизации
 *    следит за идентичностью трека и навешивает фон заново на каждый новый.
 * 2. **Смена камеры фон не теряет.** Переключение устройства
 *    (`setActiveMediaDevice` в `DeviceSettingsDialog`) идёт через
 *    `LocalTrack.restartTrack`, а тот сам перезапускает уже установленный
 *    процессор на новом источнике — трек при этом остаётся тем же объектом,
 *    и эффект ниже даже не срабатывает.
 * 3. **Операции строго последовательны.** `setProcessor`/`stopProcessor`
 *    асинхронны и небыстры (первый раз — ещё и скачивание модели); быстрые
 *    клики по разным фонам без очереди наложились бы друг на друга и оставили
 *    трек в непредсказуемом состоянии. Всё проходит через `chainRef`.
 * 4. **Смена картинки не пересоздаёт процессор** — `switchTo` меняет её на
 *    лету, без разрыва конвейера и видимых артефактов у других участников.
 */
export function useVirtualBackground(
  enabled: boolean,
  customBackgrounds: CustomBackground[],
): UseVirtualBackgroundResult {
  const { cameraTrack } = useLocalParticipant()
  const track = (cameraTrack?.track as LocalVideoTrack | undefined) ?? null

  const [backgroundKey, setBackgroundKey] = useState<BackgroundKey>(loadBackgroundKey)
  const [status, setStatus] = useState<VirtualBackgroundStatus>('idle')

  const selectBackground = useCallback((key: BackgroundKey) => {
    setBackgroundKey(key)
    saveBackgroundKey(key)
  }, [])

  const processorRef = useRef<BackgroundProcessorWrapper | null>(null)
  // К какому треку и с какой картинкой процессор реально привязан сейчас —
  // именно ФАКТИЧЕСКОЕ состояние, а не желаемое: по нему эффект понимает,
  // что делать, и не переустанавливает уже установленное.
  const appliedRef = useRef<{ track: LocalVideoTrack | null; url: string | null }>({
    track: null,
    url: null,
  })
  const chainRef = useRef<Promise<void>>(Promise.resolve())

  const desiredUrl = enabled ? resolveBackgroundUrl(backgroundKey, customBackgrounds) : null

  // Выбранной своей картинки больше нет (пользователь удалил её в профиле) —
  // сбрасываем выбор на «без фона» явно, чтобы состояние не осталось висеть
  // указателем в пустоту. Правится во время рендера под охраной сравнения —
  // тот же санкционированный приём, что и у `chatSeenCount` в `RoomPage`.
  if (enabled && backgroundKey !== NO_BACKGROUND && desiredUrl === null) {
    setBackgroundKey(NO_BACKGROUND)
    saveBackgroundKey(NO_BACKGROUND)
  }

  useEffect(() => {
    const applied = appliedRef.current
    if (track === applied.track && desiredUrl === applied.url) return

    let cancelled = false
    const run = async () => {
      if (cancelled) return
      try {
        // Трек сменился (камеру выключили/включили) — прежний процессор
        // принадлежал прежнему треку, вместе с ним он и уходит.
        if (track !== applied.track && processorRef.current) {
          await processorRef.current.destroy()
          processorRef.current = null
        }

        if (!track || !desiredUrl) {
          if (track && processorRef.current) {
            await track.stopProcessor()
            // Safari-обход: `stopProcessor` внутри livekit-client меняет трек
            // в элементе «на месте», переиспользуя тот же объект `MediaStream`
            // (внутренний `detach` промахивается мимо обработанного трека,
            // который реально висел в элементе). Из-за этого собственная
            // защита библиотеки от мерцания (`srcObject !== mediaStream` в
            // `attachToElement`) не даёт сработать её же обходному пути для
            // чёрного кадра в Safari. Пересоздаём `MediaStream` вручную —
            // detach обнулит `srcObject`, attach создаст новый объект, защита
            // пропустит штатный `play()`. Элемент остаётся в своём документе,
            // никуда не переезжает. Копия массива обязательна: `detach`
            // правит `attachedElements` по ходу перебора. Уберётся, если
            // библиотека когда-нибудь починит эту защиту.
            for (const el of [...track.attachedElements]) {
              track.detach(el)
              track.attach(el)
            }
            await processorRef.current.destroy()
            processorRef.current = null
          }
          appliedRef.current = { track, url: null }
          setStatus('idle')
          return
        }

        setStatus('loading')
        if (processorRef.current) {
          // Тот же трек, другая картинка — меняем на лету.
          await processorRef.current.switchTo({ mode: 'virtual-background', imagePath: desiredUrl })
        } else {
          const processor = await createBackgroundProcessor(desiredUrl)
          if (cancelled) {
            await processor.destroy()
            return
          }
          await track.setProcessor(processor)
          processorRef.current = processor
        }
        appliedRef.current = { track, url: desiredUrl }
        setStatus('idle')
      } catch {
        // Не смогли применить фон (нет поддержки, не доехала модель, трек
        // умер по дороге) — фича необязательная, встреча продолжается без неё.
        appliedRef.current = { track, url: null }
        setStatus('error')
      }
    }

    chainRef.current = chainRef.current.then(run, run)
    return () => {
      cancelled = true
    }
  }, [track, desiredUrl])

  // Уход из комнаты: процессор держит конвейер обработки кадров и модель —
  // без явного освобождения они пережили бы саму страницу.
  useEffect(
    () => () => {
      const processor = processorRef.current
      processorRef.current = null
      if (processor) void processor.destroy()
    },
    [],
  )

  return { backgroundKey, selectBackground, status }
}
