import { useEffect, useRef, useState } from 'react';
import { Volume2, VolumeX } from 'lucide-react';

type AudioStreamPlayerProps = {
  wsUrl: string;
  onError?: (message: string) => void;
};

const SAMPLE_RATE = 44100;
const CHANNELS = 2;

const buildWsUrl = (wsPath: string) => {
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  const token = localStorage.getItem('BEARER_TOKEN') ?? '';
  const separator = wsPath.includes('?') ? '&' : '?';
  return `${protocol}//${window.location.host}${wsPath}${separator}token=${encodeURIComponent(token)}`;
};

const WORKLET_CODE = `
class PCMProcessor extends AudioWorkletProcessor {
  constructor() {
    super();
    this.buffer = new Float32Array(0);
    this.port.onmessage = (e) => {
      const incoming = e.data;
      const merged = new Float32Array(this.buffer.length + incoming.length);
      merged.set(this.buffer);
      merged.set(incoming, this.buffer.length);
      this.buffer = merged;
      const max = ${SAMPLE_RATE * CHANNELS * 2};
      if (this.buffer.length > max) {
        this.buffer = this.buffer.slice(this.buffer.length - max);
      }
    };
  }
  process(inputs, outputs) {
    const output = outputs[0];
    if (!output || output.length === 0) return true;
    const channels = output.length;
    const frameSize = output[0].length;
    const samplesNeeded = frameSize * channels;
    if (this.buffer.length >= samplesNeeded) {
      for (let i = 0; i < frameSize; i++) {
        for (let ch = 0; ch < channels; ch++) {
          output[ch][i] = this.buffer[i * channels + ch];
        }
      }
      this.buffer = this.buffer.slice(samplesNeeded);
    } else {
      for (let ch = 0; ch < channels; ch++) {
        output[ch].fill(0);
      }
    }
    return true;
  }
}
registerProcessor('pcm-processor', PCMProcessor);
`;

const workletBlobUrl = URL.createObjectURL(new Blob([WORKLET_CODE], { type: 'application/javascript' }));

export const AudioStreamPlayer = ({ wsUrl, onError }: AudioStreamPlayerProps) => {
  const [muted, setMuted] = useState(false);
  const [started, setStarted] = useState(false);
  const ctxRef = useRef<AudioContext | null>(null);
  const nodeRef = useRef<AudioWorkletNode | null>(null);
  const wsRef = useRef<WebSocket | null>(null);
  const gainRef = useRef<GainNode | null>(null);

  // The effect below runs once per `wsUrl` and registers listeners that outlive every render after it, so a
  // named `onError` dependency would either tear the stream down on each render or freeze the first render's
  // callback. A ref is the third option: one stream, current callback.
  const onErrorRef = useRef(onError);
  onErrorRef.current = onError;

  useEffect(() => {
    let disposed = false;
    let audioCtx: AudioContext | null = null;

    const init = async () => {
      try {
        audioCtx = new AudioContext({ sampleRate: SAMPLE_RATE });
        ctxRef.current = audioCtx;

        await audioCtx.audioWorklet.addModule(workletBlobUrl);
        if (disposed) {
          audioCtx.close();
          return;
        }

        const workletNode = new AudioWorkletNode(audioCtx, 'pcm-processor', {
          outputChannelCount: [CHANNELS],
        });
        nodeRef.current = workletNode;

        const gainNode = audioCtx.createGain();
        gainRef.current = gainNode;
        workletNode.connect(gainNode);
        gainNode.connect(audioCtx.destination);

        const ws = new WebSocket(buildWsUrl(wsUrl));
        ws.binaryType = 'arraybuffer';
        wsRef.current = ws;

        ws.addEventListener('open', () => {
          if (!disposed) setStarted(true);
        });

        ws.addEventListener('message', (ev) => {
          if (disposed || !(ev.data instanceof ArrayBuffer)) return;

          // Resume context if suspended (autoplay policy — will unlock on user gesture)
          if (audioCtx && audioCtx.state === 'suspended') {
            audioCtx.resume();
          }

          const int16 = new Int16Array(ev.data);
          const float32 = new Float32Array(int16.length);
          for (let i = 0; i < int16.length; i++) {
            float32[i] = int16[i]! / 32768;
          }

          workletNode.port.postMessage(float32);
        });

        ws.addEventListener('error', () => {
          if (!disposed) onErrorRef.current?.('Audio stream connection failed');
        });

        ws.addEventListener('close', () => {
          if (!disposed) setStarted(false);
        });
      } catch (err) {
        if (!disposed) {
          onErrorRef.current?.(err instanceof Error ? err.message : 'Audio playback failed');
        }
      }
    };

    init();

    return () => {
      disposed = true;
      try {
        wsRef.current?.close();
      } catch {
        /* ignore */
      }
      wsRef.current = null;
      try {
        nodeRef.current?.disconnect();
      } catch {
        /* ignore */
      }
      nodeRef.current = null;
      try {
        audioCtx?.close();
      } catch {
        /* ignore */
      }
      ctxRef.current = null;
      gainRef.current = null;
    };
  }, [wsUrl]);

  useEffect(() => {
    if (gainRef.current) {
      gainRef.current.gain.value = muted ? 0 : 1;
    }
  }, [muted]);

  return (
    <button
      onClick={() => setMuted((m) => !m)}
      className="p-1.5 rounded hover:bg-duck-dark/10 transition-colors cursor-pointer"
      title={muted ? 'Unmute' : 'Mute'}
    >
      {muted ? (
        <VolumeX className={`h-4 w-4 ${started ? 'text-red-500' : 'text-duck-dark/40'}`} />
      ) : (
        <Volume2 className={`h-4 w-4 ${started ? 'text-duck-teal' : 'text-duck-dark/40'}`} />
      )}
    </button>
  );
};
