/**
 * Чистая функция выбора «сцены в фокусе» (демонстрация экрана).
 *
 * Никаких зависимостей от React/DOM/LiveKit SDK в РАНТАЙМЕ — на вход только
 * примитивы, на выход тоже примитивы; можно покрыть unit-тестом при появлении
 * раннера (vitest в проект сознательно не вводим, тестируем вручную).
 * Единственный импорт из SDK — `import type` у `stageTrackKey` (тип стирается
 * при компиляции, рантайм-зависимости не добавляет).
 *
 * Ключ трека — НЕ sid публикации и НЕ голая identity участника, а составной
 * `${identity}:${source}` (см. `RoomStage.tsx`, функция `stageTrackKey`):
 *   - голой identity недостаточно — у демонстратора одновременно живут camera-
 *     и screenshare-трек, и по одной identity нельзя понять, какой из них в
 *     фокусе;
 *   - чистый sid публикации нестабилен для камеры: каждое вкл/выкл камеры
 *     (`setCameraEnabled`) — это unpublish/publish с НОВЫМ sid, а при
 *     выключенной камере используется placeholder без sid вовсе (см. исходный
 *     комментарий в RoomStage.tsx про гонку placeholder → реальный трек). Если
 *     бы ключом был sid, сфокусированный участник терял бы фокус на каждом
 *     цикле мьюта камеры.
 *   `identity:source` даёт и устойчивость (не меняется при мьюте камеры), и
 *   однозначность (camera и screen_share одного участника — разные ключи).
 */

import type { TrackReferenceOrPlaceholder } from '@livekit/components-react'

/** Вид источника трека на сцене. */
export type StageFocusKind = 'camera' | 'screen_share'

/**
 * Ключ трека для `pickStageFocus` — см. обоснование схемы в начале файла.
 * Живёт здесь (а не в `RoomStage.tsx`), потому что нужен обеим сторонам:
 * сцене — чтобы считать фокус, плитке (`RoomParticipantTile`) — чтобы понять,
 * закреплена ли именно она, и каким ключом сообщить о нажатии на «закрепить».
 */
export function stageTrackKey(t: TrackReferenceOrPlaceholder): string {
  return `${t.participant.identity}:${t.source}`
}

export interface PickStageFocusInput {
  /** Ключи текущих камера-треков (один на участника — трек либо его плейсхолдер). */
  cameraKeys: readonly string[]
  /** Ключи текущих screenshare-треков (один на активную демонстрацию). */
  screenShareKeys: readonly string[]
  /**
   * Подмножество `cameraKeys` — говорящие сейчас участники, УПОРЯДОЧЕННОЕ по
   * громкости (первый — самый громкий; см. `Room.activeSpeakers`, который
   * сортирует именно так) — для фолбэка на активного спикера и, при
   * `followSpeaker`, для живого переключения фокуса.
   */
  speakingCameraKeys: readonly string[]
  /**
   * Подмножество `cameraKeys` с ЖИВЫМ видео (камера включена и не в мьюте) —
   * среди нескольких одновременно говорящих такой участник выигрывает у
   * говорящего с выключенной камерой: показывать крупно аватар-заглушку, когда
   * рядом говорит человек с картинкой, бессмысленно. Не указан — приоритета
   * нет, берётся первый (самый громкий) говорящий, как было раньше.
   */
  cameraKeysWithVideo?: readonly string[]
  /** Объединённый набор ключей (camera+screenshare) с ПРЕДЫДУЩЕГО рендера — определяет, какие screenshare-ключи «новые». */
  prevKeys: readonly string[]
  /** Ключ, что был в фокусе на предыдущем рендере; `null` — фокус ещё не выбирался. */
  prevFocusKey: string | null
  /**
   * Ключ трека, ЗАКРЕПЛЁННОГО пользователем (кнопка-булавка на плитке
   * основного окна; состояние живёт в `RoomStage.tsx` и переезжает в
   * мини-плеер через `RoomPage`, см. там `initialPinnedKey`); `null` —
   * закрепления нет.
   * Закрепление держит фокус вопреки говорящим, но уступает ЛЮБОЙ активной
   * демонстрации экрана (формулировка оператора: «перебивается только чьей-либо
   * демонстрацией экрана») — а когда демонстрация закончилась, фокус
   * возвращается именно на закреплённого, а не на того, кто был до неё:
   * правило закрепления стоит ВЫШЕ удержания предыдущего фокуса.
   * Ключ закреплённого участника, покинувшего комнату, игнорируется (его нет
   * ни в `cameraKeys`, ни в `screenShareKeys`) — снимает закрепление вызывающая
   * сторона.
   */
  pinnedKey?: string | null
  /**
   * Фокус должен ЖИВО следовать за активным спикером (переключаться сразу, а
   * не удерживать текущий). С 0.0.6 включено и для мини-плеера (PiP), и для
   * основного окна — решение оператора (этап 3, задача 3.2). Защита от
   * дребезга — на стороне вызывающего: источник «говорящих» — throttled
   * `useSpeakingParticipants()` поверх `RoomEvent.ActiveSpeakersChanged` плюс
   * удержание в ~1.2 с (см. `useSteadySpeakers` в `RoomStage.tsx`, с 0.0.25 —
   * в обеих сценах), не сырой дребезжащий `participant.isSpeaking`.
   * По умолчанию `false` — фокус удерживается (см. правило 5).
   */
  followSpeaker?: boolean
  /**
   * Живая демонстрация экрана в фокусе НЕ уступает заговорившему участнику
   * (правило 3). Демонстрация — это содержательный центр разговора, и уводить
   * её из фокуса на каждую реплику нельзя.
   *
   * С 0.0.25 включено в ОБЕИХ сценах. До этого мини-плеер (PiP) намеренно
   * ходил с `false` — «одна плитка, всегда показываем говорящего»; на практике
   * это выглядело как поломка: демонстрация в мини-окне пропадала, стоило
   * кому-то сказать слово. По умолчанию всё ещё `false` — это поведение
   * функции без явного запроса удержания.
   */
  holdScreenShare?: boolean
  /**
   * Ключ локального участника (та же схема `identity:source`) — предпоследний
   * фолбэк, ПЕРЕД чисто первым элементом набора: если фокуса ещё не было и
   * никто не говорит, лучше показать «себя», чем произвольного первого
   * участника (актуально для свежего открытия PiP). Не указан — фолбэк не
   * меняется (последний, по порядку `cameraKeys`).
   */
  localKey?: string | null
}

export interface PickStageFocusResult {
  /** Ключ трека в фокусе; `null` — треков нет вовсе. */
  focusKey: string | null
  kind: StageFocusKind | null
}

/**
 * Выбирает говорящего, которого стоит показать крупно: среди живых говорящих
 * (упорядоченных по громкости) сначала ищем того, у кого включена камера, и
 * только если такого нет — берём самого громкого как есть.
 */
function pickSpeakerKey(
  speakingCameraKeys: readonly string[],
  cameraKeys: readonly string[],
  cameraKeysWithVideo: readonly string[],
): string | null {
  const liveSpeakers = speakingCameraKeys.filter((key) => cameraKeys.includes(key))
  return liveSpeakers.find((key) => cameraKeysWithVideo.includes(key)) ?? liveSpeakers[0] ?? null
}

/**
 * Выбирает, какой трек показать крупно (в `FocusLayoutContainer`).
 *
 * Правила (PiP показывает только активное окно):
 * 1. Last-wins: если с прошлого рендера появился НОВЫЙ screenshare-ключ —
 *    фокус безусловно переходит на него (последний из новых, если появилось
 *    сразу несколько), даже если до этого в фокусе была камера или другая
 *    демонстрация. Так же ведут себя типовые UI конференций (Google Meet).
 * 2. Закрепление (`pinnedKey`): закреплённый участник
 *    забирает фокус у говорящих и у удержания предыдущего фокуса, но уступает
 *    ЛЮБОЙ активной демонстрации экрана. Поэтому правило и стоит выше
 *    удержания (правило 5): как только демонстрация закончилась и
 *    `screenShareKeys` опустел, фокус возвращается на закреплённого, а не
 *    остаётся на том, кто был в фокусе до демонстрации.
 * 3. `holdScreenShare`: демонстрация, уже стоящая в фокусе, не уступает
 *    заговорившему — иначе окно уводило бы шэр на каждую реплику.
 * 4. `followSpeaker`: если сейчас есть говорящий — фокус СРАЗУ переходит на
 *    него, даже если текущий фокус ещё жив; среди одновременно говорящих
 *    предпочитаем того, у кого включена камера (`cameraKeysWithVideo`).
 *    При `followSpeaker: false` этот шаг пропускается — см. правило 5.
 * 5. Иначе, если текущий фокус жив (остался среди camera/screenshare-ключей) —
 *    держим его: НЕ дёргаем фокус на каждый ре-рендер (изменение состава
 *    участников, дребезг isSpeaking и т.п.). Это и есть «стабильный фолбэк»,
 *    когда никто не говорит — держим предыдущего активного.
 * 6. Иначе (фокуса не было или он пропал) — приоритет активной демонстрации
 *    над камерой; среди камер — активный спикер (снова с приоритетом камеры),
 *    иначе `localKey` (если указан и жив), иначе первая по порядку.
 */
export function pickStageFocus({
  cameraKeys,
  screenShareKeys,
  speakingCameraKeys,
  cameraKeysWithVideo = [],
  prevKeys,
  prevFocusKey,
  pinnedKey = null,
  followSpeaker = false,
  holdScreenShare = false,
  localKey = null,
}: PickStageFocusInput): PickStageFocusResult {
  if (cameraKeys.length === 0 && screenShareKeys.length === 0) {
    return { focusKey: null, kind: null }
  }

  const prevKeySet = new Set(prevKeys)
  const newScreenShareKeys = screenShareKeys.filter((key) => !prevKeySet.has(key))
  if (newScreenShareKeys.length > 0) {
    return { focusKey: newScreenShareKeys[newScreenShareKeys.length - 1], kind: 'screen_share' }
  }

  if (pinnedKey) {
    // Закреплена сама демонстрация — она и есть «активная демонстрация»,
    // уступать нечему (UI позволяет закрепить любую плитку, включая шэр).
    if (screenShareKeys.includes(pinnedKey)) {
      return { focusKey: pinnedKey, kind: 'screen_share' }
    }
    // Закреплена камера: пока в комнате идёт чья-то демонстрация, она
    // перебивает закрепление (правило оператора) — идём дальше по списку.
    if (cameraKeys.includes(pinnedKey) && screenShareKeys.length === 0) {
      return { focusKey: pinnedKey, kind: 'camera' }
    }
  }

  if (holdScreenShare && prevFocusKey && screenShareKeys.includes(prevFocusKey)) {
    return { focusKey: prevFocusKey, kind: 'screen_share' }
  }

  if (followSpeaker) {
    const liveSpeaker = pickSpeakerKey(speakingCameraKeys, cameraKeys, cameraKeysWithVideo)
    if (liveSpeaker) {
      return { focusKey: liveSpeaker, kind: 'camera' }
    }
  }

  if (prevFocusKey && screenShareKeys.includes(prevFocusKey)) {
    return { focusKey: prevFocusKey, kind: 'screen_share' }
  }
  if (prevFocusKey && cameraKeys.includes(prevFocusKey)) {
    return { focusKey: prevFocusKey, kind: 'camera' }
  }

  if (screenShareKeys.length > 0) {
    return { focusKey: screenShareKeys[screenShareKeys.length - 1], kind: 'screen_share' }
  }

  const speaking = pickSpeakerKey(speakingCameraKeys, cameraKeys, cameraKeysWithVideo)
  if (speaking) {
    return { focusKey: speaking, kind: 'camera' }
  }

  if (localKey && cameraKeys.includes(localKey)) {
    return { focusKey: localKey, kind: 'camera' }
  }

  return { focusKey: cameraKeys[0] ?? null, kind: cameraKeys[0] ? 'camera' : null }
}
