/**
 * Lazy Web Audio graph wrapped around the player's <video> element.
 *
 * The graph is only created the first time we need to alter audio
 * (delay, boost above 100%, compressor on). Until then audio plays
 * through the native path and the existing volume slider works
 * normally. Once attached, audio routes through:
 *
 *   <video> → MediaElementSource → DelayNode → DynamicsCompressorNode
 *           → GainNode → destination
 *
 * Important caveats:
 *   - createMediaElementAudioSource is one-shot per element. If vidstack
 *     swaps the underlying <video>, we have to detach and rebuild against
 *     the new element.
 *   - When the graph exists, the element's own `.volume` is multiplied
 *     by the graph's gain. We keep gain >= 1 (boost) and let the slider
 *     drive .volume; otherwise the slider would feel "weaker" than 100%.
 *   - Compressor is in the chain at all times; when "off", we set its
 *     ratio to 1 to make it a no-op rather than re-wiring the graph.
 */

export interface AudioGraphState {
  delayMs: number       // DelayNode delayTime (ms)
  boost: number         // GainNode gain (1.0 = unity)
  compressor: boolean   // DynamicsCompressorNode active
}

interface AudioGraph {
  ctx: AudioContext
  source: MediaElementAudioSourceNode
  delay: DelayNode
  compressor: DynamicsCompressorNode
  gain: GainNode
  videoEl: HTMLVideoElement
}

let graph: AudioGraph | null = null

/** True when an audio graph is currently attached. */
export function hasAudioGraph(): boolean {
  return graph !== null
}

/**
 * Ensure an audio graph is attached to the given video element. If a graph
 * already exists for a different element, tear it down and rebuild. Calling
 * this with the same element is idempotent.
 */
export function ensureAudioGraph(video: HTMLVideoElement): AudioGraph | null {
  if (graph && graph.videoEl === video) return graph
  if (graph) detachAudioGraph()

  try {
    const Ctor = window.AudioContext || window.webkitAudioContext
    if (!Ctor) return null
    const ctx: AudioContext = new Ctor()
    const source = ctx.createMediaElementSource(video)
    const delay = ctx.createDelay(2.0)
    delay.delayTime.value = 0
    const compressor = ctx.createDynamicsCompressor()
    compressor.threshold.value = -50
    compressor.knee.value = 30
    compressor.ratio.value = 1 // off by default; flipping nightMode raises this
    compressor.attack.value = 0.003
    compressor.release.value = 0.25
    const gain = ctx.createGain()
    gain.gain.value = 1

    source.connect(delay)
    delay.connect(compressor)
    compressor.connect(gain)
    gain.connect(ctx.destination)

    graph = { ctx, source, delay, compressor, gain, videoEl: video }
    return graph
  } catch {
    return null
  }
}

/**
 * Apply state to the active graph. If state is "default" (no delay, no
 * boost, no compressor) and there's no graph yet, this is a no-op so we
 * keep the native audio path. If a graph already exists, we keep it
 * attached - one-shot MediaElementSource means we can't undo it cleanly.
 */
export function applyAudioGraphState(video: HTMLVideoElement, state: AudioGraphState) {
  const isDefault = state.delayMs === 0 && state.boost === 1 && !state.compressor
  if (isDefault && !graph) return
  const g = ensureAudioGraph(video)
  if (!g) return

  // Resume the context if it was suspended (autoplay policy)
  if (g.ctx.state === 'suspended') g.ctx.resume().catch(() => {})

  g.delay.delayTime.value = Math.max(0, Math.min(2, state.delayMs / 1000))
  g.gain.gain.value = Math.max(0, Math.min(3, state.boost))
  // Compressor on: fairly aggressive ratio for night-mode use
  g.compressor.ratio.value = state.compressor ? 12 : 1
  g.compressor.threshold.value = state.compressor ? -24 : -50
}

/**
 * Detach and dispose the current graph. Call when the underlying video
 * element changes (vidstack reload) or when the player unmounts.
 */
export function detachAudioGraph() {
  if (!graph) return
  try {
    graph.source.disconnect()
    graph.delay.disconnect()
    graph.compressor.disconnect()
    graph.gain.disconnect()
    graph.ctx.close()
  } catch {
    /* ignored */
  }
  graph = null
}
