import {
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
  type CSSProperties,
  type MouseEvent as ReactMouseEvent,
} from 'react';

export interface AudioWaveformProps {
  /** Pre-computed peak samples (0..1). Mutually exclusive with `audioUrl`. */
  peaks?: number[];
  /** Audio file URL — when set + peaks absent, peaks are derived via WebAudio. */
  audioUrl?: string;
  /** Number of bars to render. Default 96. */
  bars?: number;
  /** Current playback position (0..1) — drives the progress overlay. */
  progress?: number;
  /** Called when the user clicks a bar (returns 0..1 position). */
  onSeek?: (position: number) => void;
  width?: number;
  height?: number;
  color?: string;
  progressColor?: string;
  ariaLabel?: string;
  className?: string;
  style?: CSSProperties;
}

/**
 * `<AudioWaveform>` — canvas-rendered waveform with click-to-seek.
 *
 * Wave 13.G.2. Accepts a pre-computed `peaks` array (cheapest) OR
 * lazy-decodes peaks via WebAudio for an `audioUrl`. While the decode
 * is in flight (or in environments without `AudioContext`) a flat
 * placeholder is rendered.
 */
export function AudioWaveform({
  peaks: peaksProp,
  audioUrl,
  bars: barsProp = 96,
  progress = 0,
  onSeek,
  width = 480,
  height = 80,
  color = 'var(--bl-accent, #6366f1)',
  progressColor = 'var(--bl-text-primary, #111)',
  ariaLabel,
  className,
  style,
}: AudioWaveformProps) {
  // Clamp to a sane minimum — zero or negative bars would divide-by-zero
  // on canvas and produce an empty render.
  const bars = Math.max(1, Math.floor(barsProp));
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const titleId = useId();
  const [decoded, setDecoded] = useState<number[] | null>(null);

  // Lazy WebAudio decode when only audioUrl is supplied.
  useEffect(() => {
    if (peaksProp || !audioUrl) return;
    if (typeof window === 'undefined') return;
    let cancelled = false;
    (async () => {
      try {
        const res = await fetch(audioUrl);
        const buf = await res.arrayBuffer();
        const Ctx =
          (
            window as unknown as {
              AudioContext?: typeof AudioContext;
              webkitAudioContext?: typeof AudioContext;
            }
          ).AudioContext ??
          (window as unknown as { webkitAudioContext?: typeof AudioContext })
            .webkitAudioContext;
        if (!Ctx) return;
        const ac = new Ctx();
        let audio;
        try {
          audio = await ac.decodeAudioData(buf);
        } finally {
          // Free the AudioContext slot — browsers cap the count per page.
          void ac.close?.().catch(() => {});
        }
        if (cancelled) return;
        const channel = audio.getChannelData(0);
        const block = Math.max(1, Math.floor(channel.length / bars));
        const out = new Array<number>(bars).fill(0);
        for (let i = 0; i < bars; i++) {
          const start = i * block;
          let max = 0;
          for (let j = 0; j < block && start + j < channel.length; j++) {
            const v = Math.abs(channel[start + j] ?? 0);
            if (v > max) max = v;
          }
          out[i] = Math.min(1, max);
        }
        if (!cancelled) setDecoded(out);
      } catch {
        /* swallow — placeholder remains */
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [peaksProp, audioUrl, bars]);

  const finalPeaks = useMemo(() => {
    if (peaksProp && peaksProp.length > 0) return resampleTo(peaksProp, bars);
    if (decoded && decoded.length > 0) return decoded;
    return new Array<number>(bars).fill(0.04);
  }, [peaksProp, decoded, bars]);

  // Paint.
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const dpr =
      typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1;
    canvas.width = Math.floor(width * dpr);
    canvas.height = Math.floor(height * dpr);
    const ctx = canvas.getContext('2d');
    if (!ctx) return;
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.scale(dpr, dpr);
    ctx.clearRect(0, 0, width, height);
    const slot = width / finalPeaks.length;
    const barW = Math.max(1, slot * 0.65);
    const mid = height / 2;
    const progressX = Math.max(0, Math.min(1, progress)) * width;
    finalPeaks.forEach((p, i) => {
      const x = slot * i + (slot - barW) / 2;
      const h = Math.max(2, p * height * 0.9);
      ctx.fillStyle = x + barW <= progressX ? progressColor : color;
      ctx.fillRect(x, mid - h / 2, barW, h);
    });
  }, [finalPeaks, width, height, color, progressColor, progress]);

  const onClick = (e: ReactMouseEvent<HTMLCanvasElement>) => {
    if (!onSeek) return;
    const rect = e.currentTarget.getBoundingClientRect();
    onSeek(Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)));
  };

  return (
    <div
      role="img"
      aria-labelledby={titleId}
      data-testid="bl-audio-waveform"
      className={className}
      style={{ display: 'inline-block', ...style }}
    >
      <span id={titleId} hidden>
        {ariaLabel ?? 'Audio waveform'}
      </span>
      <canvas
        ref={canvasRef}
        onClick={onClick}
        style={{
          width,
          height,
          cursor: onSeek ? 'pointer' : 'default',
          display: 'block',
        }}
      />
    </div>
  );
}

/** Resample any peaks array up/down to `bars` length. */
function resampleTo(src: number[], bars: number): number[] {
  if (src.length === bars) return src;
  if (src.length === 0) return new Array<number>(bars).fill(0);
  const out = new Array<number>(bars);
  const ratio = src.length / bars;
  for (let i = 0; i < bars; i++) {
    const start = Math.floor(i * ratio);
    const end = Math.max(start + 1, Math.floor((i + 1) * ratio));
    let max = 0;
    for (let j = start; j < end && j < src.length; j++) {
      const v = Math.abs(src[j] ?? 0);
      if (v > max) max = v;
    }
    out[i] = Math.min(1, max);
  }
  return out;
}
