import { useEffect, useRef, useState, type RefObject } from 'react'
import type { MediaPlayerInstance } from '@vidstack/react'
import { useSubtitleStyles } from '../../lib/subtitle-style'
import { usePlayerRuntimeStore } from '../../stores/player-runtime-store'
import { usePreferencesStore } from '../../stores/preferences-store'

/**
 * Custom subtitle renderer that bypasses both the browser's <track> machinery
 * and vidstack's <Captions> component. We fetch the VTT ourselves, parse it
 * into a flat list of cues, then poll the player's currentTime against that
 * list to figure out what to draw.
 *
 * Why not use TextTrack.activeCues? Two reasons:
 *   1. vidstack's wrapped TextTrackList and the underlying <video>.textTracks
 *      don't always agree on `mode`, so cues sometimes never load.
 *   2. vidstack's TextTrack uses a 'cue-change' event name, the DOM standard
 *      uses 'cuechange'; subscribing to one misses the other.
 *
 * Going direct sidesteps both problems entirely.
 */

interface Cue {
  start: number
  end: number
  text: string
}

interface Props {
  playerRef: RefObject<MediaPlayerInstance | null>
  subtitleUrl: string | null
  videoWidth?: number
  videoHeight?: number
}

function parseTimeStamp(h: string | undefined, m: string, s: string, ms: string): number {
  const hh = h ? parseInt(h.replace(':', ''), 10) : 0
  return hh * 3600 + parseInt(m, 10) * 60 + parseInt(s, 10) + parseInt(ms, 10) / 1000
}

function parseVTT(raw: string): Cue[] {
  const text = raw.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n')
  const blocks = text.split(/\n\n+/)
  const cues: Cue[] = []
  for (const block of blocks) {
    const lines = block.split('\n')
    let tsLine = -1
    for (let i = 0; i < lines.length; i++) {
      if (lines[i].includes('-->')) {
        tsLine = i
        break
      }
    }
    if (tsLine === -1) continue
    // Accept both WebVTT dots and SRT commas for the milliseconds separator.
    // Jellyfin sometimes serves SRT content even on the .vtt endpoint.
    const m = lines[tsLine].match(
      /(\d+:)?(\d+):(\d+)[.,](\d+)\s+-->\s+(\d+:)?(\d+):(\d+)[.,](\d+)/,
    )
    if (!m) continue
    const start = parseTimeStamp(m[1], m[2], m[3], m[4])
    const end = parseTimeStamp(m[5], m[6], m[7], m[8])
    const body = lines
      .slice(tsLine + 1)
      .filter(l => l.trim() !== '')
      .join('\n')
      .replace(/<[^>]+>/g, '')
      .trim()
    if (body) cues.push({ start, end, text: body })
  }
  // Sort so the polling-loop break optimisation (and any out-of-order
  // source files) behave correctly.
  cues.sort((a, b) => a.start - b.start)
  if (cues.length === 0 && text.trim().length > 100) {
    console.warn('[subtitles] parseVTT produced zero cues from non-empty content. First 200 chars:', text.trim().slice(0, 200))
  }
  return cues
}

export default function SubtitleOverlay({ playerRef, subtitleUrl, videoWidth, videoHeight }: Props) {
  const { className, style } = useSubtitleStyles()
  const subtitlePosition = usePreferencesStore(s => s.subtitlePosition)
  const cuesRef = useRef<Cue[]>([])
  const [activeCues, setActiveCues] = useState<string[]>([])

  useEffect(() => {
    cuesRef.current = []
    setActiveCues([])
    if (!subtitleUrl) return
    let cancelled = false
    fetch(subtitleUrl, { credentials: 'omit' })
      .then(r => {
        if (!r.ok) throw new Error(`vtt fetch ${r.status}`)
        return r.text()
      })
      .then(text => {
        if (cancelled) return
        cuesRef.current = parseVTT(text)
      })
      .catch(err => {
        if (!cancelled) {
          console.warn('[subtitles] failed to load VTT', err)
        }
      })
    return () => {
      cancelled = true
    }
  }, [subtitleUrl])

  useEffect(() => {
    let lastSig = ''
    let cancelled = false
    function tick() {
      if (cancelled) return
      const player = playerRef.current
      const cues = cuesRef.current
      if (!player || cues.length === 0) {
        if (lastSig !== '') {
          lastSig = ''
          setActiveCues([])
        }
        return
      }
      // Effective time = player time + (-offset). A positive offset means
      // "subtitles later", which we implement by checking cues against an
      // earlier time. Negative = "subtitles earlier" (cues against later).
      const offsetSec = (usePreferencesStore.getState().subtitleDelayMs
        + usePlayerRuntimeStore.getState().subtitleOffsetMs) / 1000
      const t = (player.currentTime ?? 0) - offsetSec
      if (!Number.isFinite(t)) return
      const matches: string[] = []
      for (const c of cues) {
        if (t >= c.start && t < c.end) matches.push(c.text)
      }
      const sig = matches.join('\x1f')
      if (sig !== lastSig) {
        lastSig = sig
        setActiveCues(matches)
      }
    }
    tick()
    const interval = setInterval(tick, 100)
    return () => {
      cancelled = true
      clearInterval(interval)
    }
  }, [playerRef, subtitleUrl])

  if (activeCues.length === 0) return null

  // Build a centred box with the video's intrinsic aspect ratio.  When the
  // viewport and video aspects differ (portrait monitor playing 16:9,
  // landscape playing 9:16, etc.) object-fit:contain letterboxes the video.
  // A fixed bottom-32 / top-24 then lands inside the black bars and Chrome's
  // compositor clips short overlays there.  By matching the video aspect in
  // a flex-centred container we position the text relative to the *content*
  // area instead of the viewport.
  const vW = videoWidth || 1920
  const vH = videoHeight || 1080

  return (
    <div className="absolute inset-0 z-[100] flex items-center justify-center pointer-events-none">
      <div
        className="relative w-full"
        style={{
          aspectRatio: `${vW} / ${vH}`,
          maxHeight: '100%',
          transform: 'translateZ(0)',
          willChange: 'transform',
          backfaceVisibility: 'hidden',
        }}
      >
        <div
          className={className}
          style={{
            ...style,
            ...(subtitlePosition === 'bottom'
              ? { bottom: 32 }
              : { top: 24 }),
          }}
        >
          {activeCues.map((cue, ci) => (
            <div key={ci} className={ci > 0 ? 'mt-1.5' : ''}>
              <span data-cue className="whitespace-pre-line">
                {cue}
              </span>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}
