import Hls from 'hls.js'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Maximize, Volume2, VolumeX } from 'lucide-react'
import type { PublicChannelDto } from '@/shared/api/types'
import {
  AnalogFilter,
  ChannelFlash,
  ChannelLogo,
  NextUpBanner,
  ScreenClock,
} from './PlayerOverlays'

const STORAGE_KEY = 'tw:player'

/** Читает сохранённые громкость/mute из localStorage (с валидацией и дефолтами). */
function readStoredAudio(): { volume: number; muted: boolean } {
  try {
    const raw = localStorage.getItem(STORAGE_KEY)
    if (raw) {
      const parsed = JSON.parse(raw) as { volume?: unknown; muted?: unknown }
      const volume = typeof parsed.volume === 'number' ? Math.min(1, Math.max(0, parsed.volume)) : 1
      const muted = typeof parsed.muted === 'boolean' ? parsed.muted : true
      return { volume, muted }
    }
  } catch {
    /* недоступен/битый localStorage — дефолты */
  }
  return { volume: 1, muted: true }
}

/**
 * Восстанавливает сохранённую громкость/mute и запускает воспроизведение; если браузер блокирует
 * автоплей со звуком — откатывается на воспроизведение без звука (о чём сообщает <c>onMuted</c>).
 */
function startPlaybackWithAudio(
  video: HTMLVideoElement,
  audio: { volume: number; muted: boolean },
  onMuted: (muted: boolean) => void,
) {
  video.volume = audio.volume
  video.muted = audio.muted
  video.play().catch(() => {
    video.muted = true
    onMuted(true)
    void video.play().catch(() => undefined)
  })
}

/**
 * Насколько плеер вправе отстать от живого края, прежде чем его вернут туда прыжком. Порог заметно
 * больше окна плейлиста: обычное отставание — это секунды буфера, и дёргать зрителя из-за них
 * нельзя. Догонять ускорением тут нечего — отставание набирается минутами, а не секундами.
 */
const MaxDriftSeconds = 30

/** Живой край в шкале плеера: у hls.js он свой, у нативного HLS — конец seekable-диапазона. */
function liveEdgeOf(video: HTMLVideoElement, hls: Hls | null): number | null {
  if (hls) return hls.liveSyncPosition ?? null
  const { seekable } = video
  return seekable.length > 0 ? seekable.end(seekable.length - 1) : null
}

/**
 * HLS-плеер линейного канала. Это живой эфир: ни перемотки, ни паузы — только звук, громкость и
 * полноэкранный режим. Cookie tw_stream уже выдана к монтированию.
 */
export function ChannelPlayer({
  slug,
  channel,
  nextUp,
  flash,
  onUnavailable,
}: Readonly<{
  slug: string
  /** Канал, чьи оверлеи рисуем. Всё опционально: канал без логотипа и без шума — норма. */
  channel?: PublicChannelDto
  /** Название следующей программы, когда до неё осталось меньше минуты. */
  nextUp?: string | null
  /** Показать чёрный кадр с номером — переключение по номерам, как на телевизоре. */
  flash?: boolean
  onUnavailable?: () => void
}>) {
  const { t } = useTranslation()
  const containerRef = useRef<HTMLDivElement>(null)
  const videoRef = useRef<HTMLVideoElement>(null)
  const hlsRef = useRef<Hls | null>(null)
  const [muted, setMuted] = useState(() => readStoredAudio().muted)
  const [volume, setVolume] = useState(() => readStoredAudio().volume)

  // Последние настройки звука без пересоздания HLS-эффекта + сохранение в localStorage.
  const audioRef = useRef({ volume, muted })
  useEffect(() => {
    audioRef.current = { volume, muted }
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify({ volume, muted }))
    } catch {
      /* localStorage недоступен — не критично */
    }
  }, [volume, muted])
  const [controlsVisible, setControlsVisible] = useState(true)
  const hideTimerRef = useRef<number | null>(null)
  const overControlsRef = useRef(false)

  const clearHideTimer = useCallback(() => {
    if (hideTimerRef.current !== null) {
      window.clearTimeout(hideTimerRef.current)
      hideTimerRef.current = null
    }
  }, [])

  // Прячем панель и курсор после бездействия — но не когда курсор на панели или видео на паузе.
  const scheduleHide = useCallback(() => {
    clearHideTimer()
    hideTimerRef.current = window.setTimeout(() => {
      if (!overControlsRef.current && videoRef.current && !videoRef.current.paused) {
        setControlsVisible(false)
      }
    }, 2500)
  }, [clearHideTimer])

  const revealControls = useCallback(() => {
    setControlsVisible(true)
    scheduleHide()
  }, [scheduleHide])

  useEffect(() => clearHideTimer, [clearHideTimer])

  useEffect(() => {
    const video = videoRef.current
    if (!video) return
    const src = `/api/channels/${slug}/live.m3u8`
    let hls: Hls | null = null

    const startPlayback = () => startPlaybackWithAudio(video, audioRef.current, setMuted)

    /**
     * Возвращает плеер к живому краю, если он отстал. Отставание копится, пока вкладка в фоне:
     * браузер душит таймеры, hls.js не успевает тянуть сегменты, а видео доигрывает буфер. Вернувшись
     * на вкладку, зритель видит программу, которая в эфире давно кончилась, — для линейного канала
     * это поломка, а не задержка: догонять эфир нельзя, его можно только застать.
     */
    const syncToLive = () => {
      const edge = liveEdgeOf(video, hls)
      if (edge === null || !Number.isFinite(edge)) return
      if (edge - video.currentTime <= MaxDriftSeconds) return

      video.currentTime = edge
      if (video.paused) void video.play().catch(() => undefined)
    }

    // Слушатели нативной ветки — держим ссылки, чтобы снять их в cleanup (симметрично hls.destroy()).
    const onNativeError = () => onUnavailable?.()
    // Возврат на вкладку — единственный момент, когда отставание можно поймать в нативной ветке:
    // своего события «плейлист обновился» она не даёт.
    const onVisible = () => {
      if (!document.hidden) syncToLive()
    }
    document.addEventListener('visibilitychange', onVisible)

    if (Hls.isSupported()) {
      hls = new Hls({ liveSyncDurationCount: 3, enableWorker: true, lowLatencyMode: false })
      hlsRef.current = hls
      hls.loadSource(src)
      hls.attachMedia(video)
      hls.on(Hls.Events.MANIFEST_PARSED, startPlayback)
      // Проверяем дрейф на каждом обновлении медиаплейлиста, а не только по возврату на вкладку:
      // так же чинится залипание после сетевого сбоя, когда буфер доигрался, а окно ушло вперёд.
      hls.on(Hls.Events.LEVEL_UPDATED, syncToLive)

      // Живой эфир: краткий сетевой сбой сегмента или media-ошибку сперва пробуем восстановить
      // (hls.js рекомендует startLoad / recoverMediaError), и только исчерпав попытки — уходим в offline.
      let recoverAttempts = 0
      // Счётчик про подряд идущие неудачи: за долгий просмотр три разных сбоя — это норма, и гасить
      // из-за них канал нельзя. Успешно доехавший сегмент означает, что сбой позади.
      hls.on(Hls.Events.FRAG_BUFFERED, () => {
        recoverAttempts = 0
      })
      hls.on(Hls.Events.ERROR, (_event, data) => {
        if (!data.fatal) return
        if (data.type === Hls.ErrorTypes.NETWORK_ERROR && recoverAttempts < 3) {
          recoverAttempts += 1
          // −1 — «продолжить с живого края»: окно за время сбоя ушло вперёд, и возвращаться
          // к позиции, на которой всё встало, значит доигрывать вчерашний эфир.
          hls?.startLoad(-1)
          return
        }
        if (data.type === Hls.ErrorTypes.MEDIA_ERROR && recoverAttempts < 3) {
          recoverAttempts += 1
          hls?.recoverMediaError()
          return
        }
        hls?.destroy()
        hls = null
        hlsRef.current = null
        onUnavailable?.()
      })
    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
      video.src = src
      video.addEventListener('loadedmetadata', startPlayback)
      video.addEventListener('error', onNativeError)
    } else {
      onUnavailable?.()
    }

    return () => {
      hls?.destroy()
      hlsRef.current = null
      document.removeEventListener('visibilitychange', onVisible)
      video.removeEventListener('loadedmetadata', startPlayback)
      video.removeEventListener('error', onNativeError)
    }
  }, [slug, onUnavailable])

  // Установить громкость (0..1); 0 = mute, >0 запоминаем как последний уровень для «размьютить».
  const applyVolume = (value: number) => {
    const video = videoRef.current
    if (!video) return
    const clamped = Math.min(1, Math.max(0, value))
    video.volume = clamped
    video.muted = clamped === 0
    setMuted(clamped === 0)
    if (clamped > 0) setVolume(clamped)
  }

  const toggleMute = () => {
    const video = videoRef.current
    if (!video) return
    if (video.muted || video.volume === 0) {
      applyVolume(volume > 0 ? volume : 0.5)
    } else {
      video.muted = true
      setMuted(true)
    }
  }

  const toggleFullscreen = () => {
    const container = containerRef.current
    if (!container) return
    if (document.fullscreenElement) void document.exitFullscreen()
    else void container.requestFullscreen().catch(() => undefined)
  }

  return (
    <div
      ref={containerRef}
      onMouseMove={revealControls}
      onMouseLeave={() => {
        clearHideTimer()
        if (videoRef.current && !videoRef.current.paused) setControlsVisible(false)
      }}
      className={`relative aspect-video w-full overflow-hidden rounded-md border border-border bg-black ${
        controlsVisible ? '' : 'cursor-none'
      }`}
    >
      <video
        ref={videoRef}
        playsInline
        muted
        className="h-full w-full"
        style={
          channel && channel.analogFilterStrength > 0
            ? {
                filter: `saturate(${1 + channel.analogFilterStrength * 0.4}) contrast(${1 + channel.analogFilterStrength * 0.15}) blur(${channel.analogFilterStrength * 0.6}px)`,
              }
            : undefined
        }
        onDoubleClick={toggleFullscreen}
        onPlay={scheduleHide}
        onPause={() => {
          clearHideTimer()
          setControlsVisible(true)
        }}
      />

      {channel && channel.analogFilterStrength > 0 && (
        <AnalogFilter strength={channel.analogFilterStrength} />
      )}
      {channel?.logoImageId && (
        <ChannelLogo
          imageId={channel.logoImageId}
          corner={channel.logoCorner}
          opacity={channel.logoOpacity}
        />
      )}
      {channel?.showClock && <ScreenClock />}
      {nextUp && <NextUpBanner title={nextUp} />}
      {flash && channel && <ChannelFlash number={channel.number} name={channel.name} />}

      <div
        onMouseEnter={() => {
          overControlsRef.current = true
          clearHideTimer()
          setControlsVisible(true)
        }}
        onMouseLeave={() => {
          overControlsRef.current = false
          scheduleHide()
        }}
        className={`absolute inset-x-0 bottom-0 flex items-center gap-3 bg-gradient-to-t from-black/70 to-transparent px-3 py-2 text-white transition-opacity ${
          controlsVisible ? 'opacity-100' : 'pointer-events-none opacity-0'
        }`}
      >
        <div className="flex items-center gap-2">
          <button type="button" onClick={toggleMute} aria-label="mute">
            {muted ? <VolumeX className="h-5 w-5" /> : <Volume2 className="h-5 w-5" />}
          </button>
          <input
            type="range"
            min={0}
            max={1}
            step={0.05}
            value={muted ? 0 : volume}
            onChange={(e) => applyVolume(Number(e.target.value))}
            aria-label={t('air.volume')}
            className="h-1 w-20 cursor-pointer accent-emerald-400"
          />
        </div>

        <span className="ml-auto flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-red-500">
          <span className="h-2 w-2 animate-pulse rounded-full bg-red-500" />
          {t('air.live')}
        </span>

        <button type="button" onClick={toggleFullscreen} aria-label="fullscreen">
          <Maximize className="h-5 w-5" />
        </button>
      </div>
    </div>
  )
}
