import { useEffect, useRef, useState } from 'react'
import WaveSurfer from 'wavesurfer.js'
import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js'
import { Play, Pause, ZoomIn, ZoomOut, Repeat, X } from 'lucide-react'
import { mmss } from '@/lib/format'

interface Props {
  url: string
  dur: number
  /**
   * Precomputed waveform overview (0..1 per bucket). Without it wavesurfer
   * downloads and DECODES the whole file before drawing — fine for a 3-minute
   * take, a blank screen for an 80-minute set's 100 MB tracks. With peaks it
   * paints immediately and the media element streams the audio by Range.
   */
  peaks?: number[]
  /** called with current playback time (s) on every frame + on seek */
  onTime: (t: number) => void
  /** fired when the track reaches its end — drives auto-advance */
  onFinish?: () => void
  /** start playing as soon as the transport is ready (auto-advance landing) */
  autoPlay?: boolean
  /**
   * Fixed labelled points, in CLIP seconds — candidate cut times, the ear's
   * answer, the nominal. Drawn as zero-width regions rather than absolutely
   * positioned divs on purpose: the waveform zooms and scrolls, and an overlay
   * positioned in page pixels drifts off the audio the moment it does.
   */
  markers?: { t: number; label: string; color: string }[]
  /**
   * Imperative seek. `n` is a nonce: clicking the same marker twice must seek
   * twice, which a bare `t` cannot express because the value doesn't change.
   */
  seek?: { t: number; n: number }
}

/** marker regions are identified by id prefix so drag-selection never eats them */
const MARK_ID = 'mark-'

const MIN_ZOOM = 0 // 0 = fit-to-width
const MAX_ZOOM = 600 // px per second
const ZOOM_STEP = 1.6

/**
 * A take's transport: wavesurfer waveform with Audacity-style zoom/scroll and
 * drag-to-select + loop. Zoom: +/- buttons or ctrl/⌘ + wheel. Loop: drag a span
 * on the waveform, toggle Repeat to loop it (great for auditing a transition or
 * a single moment while iterating on the sound). Space = play/pause.
 */
export function WaveformPlayer({ url, dur, peaks, onTime, onFinish, autoPlay, markers, seek }: Props) {
  const elRef = useRef<HTMLDivElement>(null)
  const wsRef = useRef<WaveSurfer | null>(null)
  const regionsRef = useRef<ReturnType<typeof RegionsPlugin.create> | null>(null)
  const activeRegionRef = useRef<{ start: number; end: number; play: (from?: boolean) => void } | null>(null)
  const onTimeRef = useRef(onTime)
  onTimeRef.current = onTime
  const onFinishRef = useRef(onFinish)
  onFinishRef.current = onFinish
  const autoPlayRef = useRef(autoPlay)
  autoPlayRef.current = autoPlay
  const markersRef = useRef(markers)
  markersRef.current = markers
  const loopRef = useRef(true)

  const [playing, setPlaying] = useState(false)
  const [ready, setReady] = useState(false)
  const [t, setT] = useState(0)
  const [zoom, setZoom] = useState(MIN_ZOOM)
  const [loop, setLoop] = useState(true)
  const [region, setRegion] = useState<{ start: number; end: number } | null>(null)

  useEffect(() => {
    loopRef.current = loop
  }, [loop])

  useEffect(() => {
    if (!elRef.current) return
    const regions = RegionsPlugin.create()
    // With precomputed peaks we must ALSO supply the media element. Passing
    // `peaks` + `url` alone makes wavesurfer treat the track as fully
    // pre-decoded and it never wires up playback — the waveform draws, the
    // transport looks fine, and pressing play throws "No audio loaded". Owning
    // the <audio> keeps rendering (peaks) and playback (Range-streamed) separate.
    const media = peaks?.length ? new Audio() : undefined
    if (media) {
      media.preload = 'metadata'
      media.crossOrigin = 'anonymous'
      media.src = url
    }
    const ws = WaveSurfer.create({
      container: elRef.current,
      height: 76,
      waveColor: '#6a6a70', // ink-faint
      progressColor: '#e8e8ea', // ink
      cursorColor: '#d900ff', // brand magenta — the now
      cursorWidth: 2,
      barWidth: 2,
      barGap: 1,
      barRadius: 1,
      normalize: true,
      autoScroll: true,
      // Either we own the media (peaks path) or wavesurfer fetches + decodes
      // the file itself (fine for the short heads).
      ...(media ? { media, peaks: [peaks!], duration: dur } : { url }),
      plugins: [regions],
    })
    wsRef.current = ws
    regionsRef.current = regions
    setReady(false)
    setPlaying(false)
    setRegion(null)
    activeRegionRef.current = null

    const emit = (time: number) => {
      setT(time)
      onTimeRef.current(time)
    }
    ws.on('ready', () => {
      setReady(true)
      for (const m of markersRef.current ?? []) {
        if (m.t < 0 || m.t > dur) continue     // outside this clip: don't fake it
        regions.addRegion({
          id: MARK_ID + m.label,
          start: m.t,
          content: m.label,
          color: m.color,
          drag: false,
          resize: false,
        })
      }
      // Auto-advance lands here. Browsers only allow this because the user
      // already gestured on the previous track; if they haven't, play()
      // rejects and we simply stay paused rather than throwing.
      if (autoPlayRef.current) ws.play().catch(() => {})
    })
    ws.on('timeupdate', emit)
    ws.on('interaction', () => {
      activeRegionRef.current = null
      emit(ws.getCurrentTime())
    })
    ws.on('play', () => setPlaying(true))
    ws.on('pause', () => setPlaying(false))
    ws.on('finish', () => {
      setPlaying(false)
      onFinishRef.current?.()
    })

    // single drag-selection that can be looped
    regions.enableDragSelection({ color: 'rgba(217, 0, 255, 0.12)' })
    regions.on('region-created', (r) => {
      if (r.id.startsWith(MARK_ID)) return    // a marker, not a user selection
      for (const other of regions.getRegions())
        if (other.id !== r.id && !other.id.startsWith(MARK_ID)) other.remove()
      activeRegionRef.current = r
      setRegion({ start: r.start, end: r.end })
    })
    regions.on('region-updated', (r) => {
      if (!r.id.startsWith(MARK_ID)) setRegion({ start: r.start, end: r.end })
    })
    regions.on('region-in', (r) => {
      if (r.id.startsWith(MARK_ID)) return
      activeRegionRef.current = r
    })
    regions.on('region-out', (r) => {
      if (activeRegionRef.current === r && loopRef.current) r.play(true)
    })
    regions.on('region-clicked', (r, e) => {
      e.stopPropagation()
      // Clicking a marker jumps to it. It has zero width, so `play(true)` would
      // start and immediately hit region-out; seek explicitly instead.
      if (r.id.startsWith(MARK_ID)) {
        ws.setTime(r.start)
        ws.play().catch(() => {})
        return
      }
      activeRegionRef.current = r
      r.play(true)
    })

    return () => {
      ws.destroy()
      if (media) {
        media.pause()
        media.removeAttribute('src')
        media.load()          // release the connection; otherwise switching
      }                       // tracks leaves the old stream downloading
    }
  }, [url, peaks, dur])

  // Apply zoom when it changes (and once ready).
  //
  // `zoom()` needs a DECODED buffer to rescale, and on the peaks path there
  // isn't one — wavesurfer throws "No audio loaded", the same message the
  // peaks+url trap produced but from a completely different cause. It fires
  // from an effect, so it escapes as an unhandled React error and blanks the
  // whole page. Zoom is a convenience; never let it take the tool down.
  useEffect(() => {
    if (!ready || !wsRef.current || zoom <= MIN_ZOOM) return
    try {
      wsRef.current.zoom(zoom)
    } catch {
      /* peaks-only render: nothing to rescale, keep the fit-to-width view */
    }
  }, [zoom, ready])

  // imperative seek from the parent (marker buttons / keyboard)
  useEffect(() => {
    const ws = wsRef.current
    if (!ready || !ws || !seek) return
    ws.setTime(Math.max(0, Math.min(dur, seek.t)))
    ws.play().catch(() => {})
  }, [seek?.n, ready])   // eslint-disable-line react-hooks/exhaustive-deps

  const toggle = () => wsRef.current?.playPause()
  const zoomBy = (factor: number) =>
    setZoom((z) => {
      const base = z <= 0 ? 40 : z
      return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, factor > 1 ? base * factor : z <= 40 ? 0 : z / ZOOM_STEP))
    })
  const clearRegion = () => {
    // clear the SELECTION only — clearRegions() would wipe the markers too
    for (const r of regionsRef.current?.getRegions() ?? [])
      if (!r.id.startsWith(MARK_ID)) r.remove()
    activeRegionRef.current = null
    setRegion(null)
  }

  return (
    <div
      tabIndex={0}
      onKeyDown={(e) => {
        if (e.code === 'Space') {
          e.preventDefault()
          toggle()
        } else if (e.key === 'l') {
          setLoop((v) => !v)
        } else if (e.key === '+' || e.key === '=') {
          zoomBy(ZOOM_STEP)
        } else if (e.key === '-') {
          zoomBy(1 / ZOOM_STEP)
        }
      }}
      onWheel={(e) => {
        if (e.ctrlKey || e.metaKey) {
          e.preventDefault()
          zoomBy(e.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP)
        }
      }}
      className="group rounded-md outline-none focus-visible:ring-2 focus-visible:ring-magenta/60"
    >
      <div className="flex items-center gap-3">
        <button
          onClick={toggle}
          disabled={!ready}
          aria-label={playing ? 'Pause' : 'Play'}
          className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md
                     bg-ink text-surface transition-opacity hover:opacity-90
                     disabled:opacity-30"
        >
          {playing ? <Pause size={18} fill="currentColor" /> : <Play size={18} fill="currentColor" />}
        </button>
        <div ref={elRef} className="min-w-0 flex-1" />
      </div>

      <div className="mt-1 flex items-center justify-between gap-2 font-mono text-xs text-ink-muted tnum">
        {/* testid: the smoke test reads the CLOCK to prove playback; several
            other elements share `tnum`, and a positional selector silently
            picked the header's counter instead. */}
        <span data-testid="transport-clock">{ready ? mmss(t) : 'loading…'}</span>

        <div className="flex items-center gap-1">
          {region && (
            <>
              <button
                onClick={() => setLoop((v) => !v)}
                title="Loop the selection (L)"
                aria-pressed={loop}
                className={`flex items-center gap-1 rounded px-1.5 py-0.5 transition-colors ${
                  loop ? 'bg-magenta/15 text-magenta' : 'text-ink-faint hover:text-ink'
                }`}
              >
                <Repeat size={12} /> {mmss(region.start)}–{mmss(region.end)}
              </button>
              <button
                onClick={clearRegion}
                title="Clear selection"
                className="flex items-center rounded px-1 py-0.5 text-ink-faint hover:text-ink"
              >
                <X size={12} />
              </button>
              <span className="mx-1 text-hairline">|</span>
            </>
          )}
          <button
            onClick={() => zoomBy(1 / ZOOM_STEP)}
            disabled={!ready}
            title="Zoom out (− / ctrl-wheel)"
            className="flex items-center rounded px-1 py-0.5 text-ink-faint hover:text-ink disabled:opacity-30"
          >
            <ZoomOut size={13} />
          </button>
          <button
            onClick={() => zoomBy(ZOOM_STEP)}
            disabled={!ready}
            title="Zoom in (+ / ctrl-wheel)"
            className="flex items-center rounded px-1 py-0.5 text-ink-faint hover:text-ink disabled:opacity-30"
          >
            <ZoomIn size={13} />
          </button>
          <span className="w-14 text-right text-ink-faint">{mmss(dur)}</span>
        </div>
      </div>

      {!region && (
        <p className="mt-0.5 text-[10px] text-ink-faint/70">
          drag on the waveform to select &amp; loop a span · ctrl-wheel to zoom
        </p>
      )}
    </div>
  )
}
