import { useState, useCallback, useRef } from 'react';
import type { AudioData } from './useAudioAnalyzer';
import type { OscilloscopeMode } from './useOscilloscopeRenderer';

interface ExportOptions {
  width: number;
  height: number;
  fps: number;
  mode: OscilloscopeMode;
  audioFile: File;
  format?: 'webm' | 'mp4';
  quality?: 'low' | 'medium' | 'high';
}

// WebGL shaders
const VERTEX_SHADER = `
  attribute vec2 a_position;
  uniform vec2 u_resolution;
  
  void main() {
    vec2 clipSpace = (a_position / u_resolution) * 2.0 - 1.0;
    gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
  }
`;

const TRACE_FRAGMENT_SHADER = `
  precision mediump float;
  uniform vec4 u_color;
  
  void main() {
    gl_FragColor = u_color;
  }
`;

function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | null {
  const shader = gl.createShader(type);
  if (!shader) return null;

  gl.shaderSource(shader, source);
  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error('Shader compile error:', gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    return null;
  }

  return shader;
}

function createProgram(gl: WebGLRenderingContext, vertexShader: WebGLShader, fragmentShader: WebGLShader): WebGLProgram | null {
  const program = gl.createProgram();
  if (!program) return null;

  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);

  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error('Program link error:', gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
    return null;
  }

  return program;
}

export function useVideoExporter() {
  const [isExporting, setIsExporting] = useState(false);
  const [progress, setProgress] = useState(0);
  const [exportedUrl, setExportedUrl] = useState<string | null>(null);
  const cancelRef = useRef(false);

  const exportVideo = useCallback(async (
    audioData: AudioData,
    audioFile: File,
    options: ExportOptions
  ) => {
    setIsExporting(true);
    setProgress(0);
    setExportedUrl(null);
    cancelRef.current = false;

    const { width, height, fps, mode } = options;
    const totalSamples = audioData.leftChannel.length;
    const samplesPerFrame = Math.floor(audioData.sampleRate / fps);

    const log = (...args: unknown[]) => {
      console.log('[useVideoExporter]', ...args);
    };

    log('export start', {
      width,
      height,
      fps,
      mode,
      analyzerSampleRate: audioData.sampleRate,
      totalSamples,
      samplesPerFrame,
      estimatedDuration: totalSamples / audioData.sampleRate,
    });

    // Create WebGL canvas for rendering
    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;

    const gl = canvas.getContext('webgl', {
      preserveDrawingBuffer: true,
      antialias: true,
      alpha: false,
    });

    if (!gl) {
      console.error('WebGL not available');
      setIsExporting(false);
      return null;
    }

    // Set up WebGL program
    const traceVS = createShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
    const traceFS = createShader(gl, gl.FRAGMENT_SHADER, TRACE_FRAGMENT_SHADER);

    if (!traceVS || !traceFS) {
      setIsExporting(false);
      return null;
    }

    const traceProgram = createProgram(gl, traceVS, traceFS);

    if (!traceProgram) {
      setIsExporting(false);
      return null;
    }

    const positionBuffer = gl.createBuffer();

    if (!positionBuffer) {
      setIsExporting(false);
      return null;
    }

    const tracePositionLocation = gl.getAttribLocation(traceProgram, 'a_position');
    const traceResolutionLocation = gl.getUniformLocation(traceProgram, 'u_resolution');
    const traceColorLocation = gl.getUniformLocation(traceProgram, 'u_color');

    if (!traceResolutionLocation || !traceColorLocation) {
      setIsExporting(false);
      return null;
    }

    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    gl.viewport(0, 0, width, height);

    // Helper to draw a trace
    const drawTrace = (vertices: number[], color: [number, number, number, number]) => {
      if (vertices.length < 4) return;
      gl.useProgram(traceProgram);
      gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.DYNAMIC_DRAW);
      gl.enableVertexAttribArray(tracePositionLocation);
      gl.vertexAttribPointer(tracePositionLocation, 2, gl.FLOAT, false, 0, 0);
      gl.uniform2f(traceResolutionLocation, width, height);
      gl.uniform4f(traceColorLocation, color[0], color[1], color[2], color[3]);
      gl.lineWidth(2);
      gl.drawArrays(gl.LINE_STRIP, 0, vertices.length / 2);
    };

    // Function to render a single frame at a specific sample position
    const renderFrameAtSample = (startSample: number): void => {
      gl.clearColor(0, 0, 0, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);

      const endSample = Math.min(startSample + samplesPerFrame, totalSamples);

      const leftColor: [number, number, number, number] = [0, 1, 0, 1];
      const rightColor: [number, number, number, number] = [0, 0.8, 1, 1];
      const xyColor: [number, number, number, number] = [1, 0.5, 0, 1];
      const dividerColor: [number, number, number, number] = [0.2, 0.2, 0.2, 1];

      if (mode === 'combined') {
        const vertices: number[] = [];
        const samplesPerPixel = samplesPerFrame / width;
        const centerY = height / 2;
        for (let x = 0; x < width; x++) {
          const sampleIndex = Math.floor(startSample + x * samplesPerPixel);
          if (sampleIndex >= totalSamples) break;
          const sample = (audioData.leftChannel[sampleIndex] + audioData.rightChannel[sampleIndex]) / 2;
          const y = centerY - sample * (height * 0.4);
          vertices.push(x, y);
        }
        drawTrace(vertices, leftColor);
      } else if (mode === 'separate') {
        const halfHeight = height / 2;
        const samplesPerPixel = samplesPerFrame / width;

        // Left channel (top half)
        const leftVertices: number[] = [];
        const leftCenterY = halfHeight / 2;
        for (let x = 0; x < width; x++) {
          const sampleIndex = Math.floor(startSample + x * samplesPerPixel);
          if (sampleIndex >= totalSamples) break;
          const sample = audioData.leftChannel[sampleIndex];
          const y = leftCenterY - sample * (halfHeight * 0.35);
          leftVertices.push(x, y);
        }
        drawTrace(leftVertices, leftColor);

        // Right channel (bottom half)
        const rightVertices: number[] = [];
        const rightCenterY = halfHeight + halfHeight / 2;
        for (let x = 0; x < width; x++) {
          const sampleIndex = Math.floor(startSample + x * samplesPerPixel);
          if (sampleIndex >= totalSamples) break;
          const sample = audioData.rightChannel[sampleIndex];
          const y = rightCenterY - sample * (halfHeight * 0.35);
          rightVertices.push(x, y);
        }
        drawTrace(rightVertices, rightColor);

        // Divider
        drawTrace([0, halfHeight, width, halfHeight], dividerColor);
      } else if (mode === 'all') {
        const topHeight = height / 2;
        const bottomHeight = height / 2;
        const halfWidth = width / 2;
        const samplesPerPixel = samplesPerFrame / halfWidth;

        // Left channel (top-left)
        const leftVertices: number[] = [];
        const leftCenterY = topHeight / 2;
        for (let x = 0; x < halfWidth; x++) {
          const sampleIndex = Math.floor(startSample + x * samplesPerPixel);
          if (sampleIndex >= totalSamples) break;
          const sample = audioData.leftChannel[sampleIndex];
          const y = leftCenterY - sample * (topHeight * 0.35);
          leftVertices.push(x, y);
        }
        drawTrace(leftVertices, leftColor);

        // Right channel (top-right)
        const rightVertices: number[] = [];
        const rightCenterY = topHeight / 2;
        for (let x = 0; x < halfWidth; x++) {
          const sampleIndex = Math.floor(startSample + x * samplesPerPixel);
          if (sampleIndex >= totalSamples) break;
          const sample = audioData.rightChannel[sampleIndex];
          const y = rightCenterY - sample * (topHeight * 0.35);
          rightVertices.push(halfWidth + x, y);
        }
        drawTrace(rightVertices, rightColor);

        // XY mode (bottom half)
        const xyVertices: number[] = [];
        const xyCenterX = width / 2;
        const xyCenterY = topHeight + bottomHeight / 2;
        const xyScale = Math.min(halfWidth, bottomHeight) * 0.35;
        for (let i = startSample; i < endSample; i++) {
          const x = xyCenterX + audioData.leftChannel[i] * xyScale;
          const y = xyCenterY - audioData.rightChannel[i] * xyScale;
          xyVertices.push(x, y);
        }
        drawTrace(xyVertices, xyColor);

        // Dividers
        drawTrace([0, topHeight, width, topHeight], dividerColor);
        drawTrace([halfWidth, 0, halfWidth, topHeight], dividerColor);
      }
    };

    // Capture stream at the target FPS
    const videoStream = canvas.captureStream(fps);

    // Decode audio
    let audioContext: AudioContext;
    try {
      audioContext = new AudioContext({ sampleRate: audioData.sampleRate });
    } catch {
      log('AudioContext({sampleRate}) failed; falling back to default AudioContext()');
      audioContext = new AudioContext();
    }
    await audioContext.resume();

    const audioArrayBuffer = await audioFile.arrayBuffer();
    const audioBuffer = await audioContext.decodeAudioData(audioArrayBuffer);
    log('decoded audio', {
      ctxSampleRate: audioContext.sampleRate,
      duration: audioBuffer.duration,
      channels: audioBuffer.numberOfChannels,
    });

    const audioSource = audioContext.createBufferSource();
    audioSource.buffer = audioBuffer;

    const audioDestination = audioContext.createMediaStreamDestination();
    audioSource.connect(audioDestination);

    const combinedStream = new MediaStream([
      ...videoStream.getVideoTracks(),
      ...audioDestination.stream.getAudioTracks(),
    ]);

    // Prefer VP8 for broad compatibility
    let mimeType = 'video/webm;codecs=vp8,opus';
    if (!MediaRecorder.isTypeSupported(mimeType)) {
      mimeType = 'video/webm;codecs=vp9,opus';
    }
    if (!MediaRecorder.isTypeSupported(mimeType)) {
      mimeType = 'video/webm';
    }

    log('MediaRecorder setup', {
      requestedMimeType: mimeType,
      videoBitsPerSecond: 8000000,
      audioBitsPerSecond: 256000,
    });

    const mediaRecorder = new MediaRecorder(combinedStream, {
      mimeType,
      videoBitsPerSecond: 8000000,
      audioBitsPerSecond: 256000,
    });

    const chunks: Blob[] = [];
    let chunkBytes = 0;

    mediaRecorder.onstart = () =>
      log('MediaRecorder onstart', { state: mediaRecorder.state, mimeType: mediaRecorder.mimeType });

    mediaRecorder.ondataavailable = (e) => {
      const size = e?.data?.size ?? 0;
      log('MediaRecorder ondataavailable', {
        size,
        type: e?.data?.type,
        recorderState: mediaRecorder.state,
      });

      if (e.data && e.data.size > 0) {
        chunks.push(e.data);
        chunkBytes += e.data.size;
      }
    };

    return new Promise<string>((resolve, reject) => {
      let stopped = false;
      let stopReason: string = 'unknown';
      let lastRenderedFrame = -1;
      let lastLoggedSecond = -1;
      let rafId = 0;
      let safetyTimer: number | null = null;

      const stopRecorder = (reason: string) => {
        if (stopped) return;
        stopped = true;
        stopReason = reason;

        log('stopRecorder()', {
          reason,
          recorderState: mediaRecorder.state,
          chunks: chunks.length,
          chunkBytes,
        });

        if (rafId) cancelAnimationFrame(rafId);
        if (safetyTimer) window.clearTimeout(safetyTimer);

        if (reason === 'cancel') {
          try {
            audioSource.stop();
          } catch {
            // ignore
          }
        }

        try {
          if (mediaRecorder.state === 'recording') {
            log('calling mediaRecorder.stop()');
            mediaRecorder.stop();
          }
        } catch (e) {
          log('mediaRecorder.stop() failed', e);
        }
      };

      audioSource.onended = () => {
        log('audioSource.onended');
        try {
          const endSample = Math.max(0, totalSamples - samplesPerFrame);
          renderFrameAtSample(endSample);
        } catch (e) {
          log('final frame render failed', e);
        }
        stopRecorder('audio_ended');
      };

      mediaRecorder.onstop = async () => {
        log('MediaRecorder onstop', { stopReason, chunks: chunks.length, chunkBytes });

        // Cleanup WebGL
        gl.deleteProgram(traceProgram);
        gl.deleteBuffer(positionBuffer);

        try {
          await audioContext.close();
        } catch {
          // ignore
        }

        try {
          combinedStream.getTracks().forEach((t) => t.stop());
        } catch {
          // ignore
        }

        const finalMime = mediaRecorder.mimeType || mimeType;
        const blob = new Blob(chunks, { type: finalMime });
        log('final blob', {
          mime: finalMime,
          blobSize: blob.size,
          chunks: chunks.length,
          chunkBytes,
        });

        if (blob.size === 0) {
          setIsExporting(false);
          reject(new Error('Export failed: empty recording blob'));
          return;
        }

        const url = URL.createObjectURL(blob);
        setExportedUrl(url);
        setIsExporting(false);
        setProgress(100);
        resolve(url);
      };

      mediaRecorder.onerror = (e) => {
        log('MediaRecorder onerror', e);
        setIsExporting(false);
        reject(e);
      };

      // Start without timeslice - this creates a single continuous WebM file
      mediaRecorder.start();
      log('mediaRecorder.start() called', { state: mediaRecorder.state, mimeType: mediaRecorder.mimeType });

      const exportStart = audioContext.currentTime;
      audioSource.start(0);
      log('audioSource.start() called', { exportStart, duration: audioBuffer.duration });

      // Safety timeout: for very long files (6+ hours = 21600+ seconds), add generous buffer
      const safetyDuration = Math.ceil(audioBuffer.duration * 1000 + 30000); // 30s buffer
      log('safety timer set', { safetyDuration, durationSeconds: audioBuffer.duration });
      safetyTimer = window.setTimeout(() => {
        log('safety timeout hit');
        stopRecorder('safety_timeout');
      }, safetyDuration);

      const renderLoop = () => {
        if (stopped) return;

        if (cancelRef.current) {
          log('cancelRef triggered');
          stopRecorder('cancel');
          return;
        }

        const t = Math.max(0, audioContext.currentTime - exportStart);

        // Heartbeat every 10 seconds for long exports
        const sec = Math.floor(t / 10) * 10;
        if (sec !== lastLoggedSecond && sec > 0) {
          lastLoggedSecond = sec;
          log('heartbeat', {
            t: t.toFixed(1),
            duration: audioBuffer.duration.toFixed(1),
            percentComplete: ((t / audioBuffer.duration) * 100).toFixed(1),
            recorderState: mediaRecorder.state,
            chunks: chunks.length,
            chunkBytes,
          });
        }

        // Guard: if audio should have ended but didn't, stop
        if (t > audioBuffer.duration + 2) {
          log('duration guard hit', { t, duration: audioBuffer.duration });
          stopRecorder('duration_guard');
          return;
        }

        const frameIndex = Math.floor(t * fps);

        if (frameIndex !== lastRenderedFrame) {
          const startSample = Math.min(frameIndex * samplesPerFrame, totalSamples - 1);
          renderFrameAtSample(startSample);
          lastRenderedFrame = frameIndex;

          // Update progress less frequently for performance
          if (frameIndex % 60 === 0) {
            setProgress(Math.min(99, Math.floor((startSample / totalSamples) * 100)));
          }
        }

        rafId = requestAnimationFrame(renderLoop);
      };

      rafId = requestAnimationFrame(renderLoop);
    });
  }, []);

  const reset = useCallback(() => {
    if (exportedUrl) {
      URL.revokeObjectURL(exportedUrl);
    }
    cancelRef.current = true;
    setExportedUrl(null);
    setProgress(0);
  }, [exportedUrl]);

  return {
    isExporting,
    progress,
    exportedUrl,
    exportVideo,
    reset,
  };
}
