// Offscreen document for persistent video recording
// This document survives page navigation and holds the MediaRecorder + chunks

console.log('[Offscreen] Offscreen document loaded');

// Recording state
let mediaRecorder = null;
let recordedChunks = [];
let drainedUpTo = 0; // Index tracking how many chunks have been drained for progressive upload
let mediaStream = null;
let displayStreamRef = null;
let recordingStartTime = null;
let pausedDuration = 0;
let lastPauseTime = null;
let isRecording = false;
let isPaused = false;

// Mic audio mixing state
let audioContext = null;
let mixedAudioDestination = null;
let systemAudioSource = null;
let micSource = null;
let micStream = null;
let isMicEnabled = false;

// Start mic capture directly via getUserMedia (extension origin = one-time permission)
async function startMicCapture() {
  console.log('[Offscreen] Starting direct mic capture via getUserMedia');
  try {
    micStream = await navigator.mediaDevices.getUserMedia({
      audio: {
        echoCancellation: true,
        noiseSuppression: true,
        autoGainControl: true,
      },
      video: false,
    });

    // Monitor for track ending (e.g., permission revoked)
    micStream.getTracks().forEach(track => {
      track.addEventListener('ended', () => {
        console.warn('[Offscreen] Mic track ended unexpectedly');
        isMicEnabled = false;
        chrome.runtime.sendMessage({
          action: 'offscreen:micTrackEnded',
        }).catch(() => {});
      });
    });

    isMicEnabled = true;
    console.log('[Offscreen] Mic capture started, tracks:', micStream.getAudioTracks().length);
    return { success: true };
  } catch (error) {
    console.error('[Offscreen] Failed to capture mic:', error);
    return { success: false, error: String(error) };
  }
}

// Stop mic capture
function stopMicCapture() {
  if (micStream) {
    micStream.getTracks().forEach(track => track.stop());
    micStream = null;
  }
  if (micSource) {
    try { micSource.disconnect(); } catch (e) { /* Ignore */ }
    micSource = null;
  }
  isMicEnabled = false;
  console.log('[Offscreen] Mic capture stopped');
}

// Toggle mic mute (enable/disable tracks without stopping the stream)
async function toggleMicMute(muted) {
  if (!micStream) {
    if (!muted) {
      // Unmute requested but no stream exists — start fresh capture
      console.log('[Offscreen] No mic stream for unmute, starting fresh capture');
      return await enableMicAudioDuringRecording();
    }
    return { success: false, error: 'No active mic stream' };
  }
  micStream.getTracks().forEach(track => {
    track.enabled = !muted;
  });
  isMicEnabled = !muted;
  console.log('[Offscreen] Mic mute toggled:', muted, 'isMicEnabled:', isMicEnabled);
  return { success: true };
}

// Check mic permission state
async function checkMicPermission() {
  try {
    const result = await navigator.permissions.query({ name: 'microphone' });
    console.log('[Offscreen] Mic permission state:', result.state);
    return { success: true, state: result.state };
  } catch (error) {
    console.error('[Offscreen] Failed to check mic permission:', error);
    return { success: false, error: String(error) };
  }
}

// Clean up mic audio
function cleanupMicAudio() {
  stopMicCapture();
}

// Connect mic stream to the audio mixer (used during recording)
function connectMicToMixer() {
  if (!micStream || !audioContext || !mixedAudioDestination) {
    console.warn('[Offscreen] Cannot connect mic to mixer - missing dependencies');
    return false;
  }
  if (micSource) {
    // Already connected
    return true;
  }
  micSource = audioContext.createMediaStreamSource(micStream);
  micSource.connect(mixedAudioDestination);
  console.log('[Offscreen] Mic connected to audio mixer');
  return true;
}

async function enableMicAudioDuringRecording() {
  if (!isRecording || !mediaStream) {
    return { success: false, error: 'No active recording' };
  }

  if (!mixedAudioDestination) {
    return { success: false, error: 'Audio mixer not initialized' };
  }

  // Start mic capture if not already running
  if (!micStream || !isMicEnabled) {
    const result = await startMicCapture();
    if (!result.success) {
      return result;
    }
  }

  // Ensure AudioContext is running
  if (audioContext && audioContext.state === 'suspended') {
    await audioContext.resume();
  }

  // Connect mic to mixer
  if (connectMicToMixer()) {
    return { success: true };
  }
  return { success: false, error: 'Failed to connect mic to mixer' };
}

// Get current recording duration in milliseconds
function getRecordingDuration() {
  if (!recordingStartTime) return 0;

  let duration = Date.now() - recordingStartTime - pausedDuration;

  // If currently paused, subtract the current pause time
  if (isPaused && lastPauseTime) {
    duration -= (Date.now() - lastPauseTime);
  }

  return Math.max(0, duration);
}

// Track silent oscillator for cleanup
let silentOscillator = null;
let silentGainNode = null;
let dataFlushInterval = null;
let statusInterval = null;
let isStartingRecording = false;

// Start recording using getDisplayMedia
async function startRecording(streamId, enableSystemAudio, enableMicAudio = false) {
  try {
    // Guard against concurrent startRecording calls
    if (isStartingRecording || isRecording) {
      console.log('[Offscreen] Recording already starting or in progress, skipping');
      return { success: false, error: 'Recording already in progress' };
    }
    isStartingRecording = true;

    console.log('[Offscreen] Starting recording, enableSystemAudio:', enableSystemAudio, 'enableMicAudio:', enableMicAudio);

    // Clean up any previous recording
    await cleanup();

    // Use getDisplayMedia to capture screen - this will show the picker dialog
    let displayStream;
    try {
      displayStream = await navigator.mediaDevices.getDisplayMedia({
        video: {
          displaySurface: 'browser',
        },
        audio: enableSystemAudio,
        selfBrowserSurface: 'include',
      });
    } catch (displayError) {
      // User clicked Cancel on the picker — don't retry
      if (displayError.name === 'NotAllowedError') {
        throw displayError;
      }
      // Only retry without audio if the error is about audio capture capability
      if (enableSystemAudio) {
        console.log('[Offscreen] System audio not available, falling back to video only');
        displayStream = await navigator.mediaDevices.getDisplayMedia({
          video: {
            displaySurface: 'browser',
          },
          audio: false,
          selfBrowserSurface: 'include',
        });
      } else {
        throw displayError;
      }
    }

    console.log('[Offscreen] Display stream obtained');
    displayStreamRef = displayStream;

    // Monitor for unexpected track endings
    displayStream.getTracks().forEach(track => {
      track.addEventListener('ended', () => {
        console.error('[Offscreen] Track ended unexpectedly:', track.kind);
        if (isRecording) {
          stopRecording();
        }
      });
    });

    // Start mic capture directly if enabled
    if (enableMicAudio) {
      const micResult = await startMicCapture();
      if (!micResult.success) {
        console.warn('[Offscreen] Mic capture failed, continuing without mic:', micResult.error);
      }
    }

    // Combine streams: video from display, audio from system + mic
    const videoTrack = displayStream.getVideoTracks()[0];
    const systemAudioTracks = displayStream.getAudioTracks();

    let combinedTracks = [videoTrack];

    // Always create a mixer so mic can be enabled mid-recording
    if (!audioContext) {
      audioContext = new AudioContext();
    }

    // CRITICAL: Resume AudioContext - it starts in 'suspended' state in Chrome
    if (audioContext.state === 'suspended') {
      await audioContext.resume();
      console.log('[Offscreen] AudioContext resumed, state:', audioContext.state);
    }

    mixedAudioDestination = audioContext.createMediaStreamDestination();

    // Add system audio to the mix (if available)
    if (systemAudioTracks.length > 0) {
      const systemAudioStream = new MediaStream(systemAudioTracks);
      systemAudioSource = audioContext.createMediaStreamSource(systemAudioStream);
      systemAudioSource.connect(mixedAudioDestination);
      console.log('[Offscreen] System audio connected to mixer');
    } else {
      // CRITICAL: Create silent oscillator to ensure audio data flows through the mixer
      // Without this, MediaRecorder may wait indefinitely for audio data
      silentOscillator = audioContext.createOscillator();
      silentGainNode = audioContext.createGain();
      silentGainNode.gain.value = 0; // Silent
      silentOscillator.connect(silentGainNode);
      silentGainNode.connect(mixedAudioDestination);
      silentOscillator.start();
      console.log('[Offscreen] Silent audio source created for stability');
    }

    // Add mic audio to the mix (if mic was successfully captured)
    if (enableMicAudio && micStream && isMicEnabled) {
      connectMicToMixer();
    }

    // Always include an audio track (silence if no sources yet)
    combinedTracks.push(...mixedAudioDestination.stream.getAudioTracks());

    // Create combined media stream
    mediaStream = new MediaStream(combinedTracks);

    // Validate all tracks are live before starting
    const allTracks = mediaStream.getTracks();
    console.log('[Offscreen] Combined tracks:', allTracks.map(t => `${t.kind}:${t.readyState}:${t.enabled}`).join(', '));

    if (allTracks.some(t => t.readyState !== 'live')) {
      throw new Error('One or more tracks not in live state');
    }

    // Create MediaRecorder with codec fallback
    const mimeTypes = [
      'video/webm;codecs=vp9,opus',
      'video/webm;codecs=vp8,opus',
      'video/webm;codecs=vp9',
      'video/webm;codecs=vp8',
      'video/webm',
    ];

    let selectedMimeType = 'video/webm';
    for (const mimeType of mimeTypes) {
      if (MediaRecorder.isTypeSupported(mimeType)) {
        selectedMimeType = mimeType;
        break;
      }
    }
    console.log('[Offscreen] Using mimeType:', selectedMimeType);

    mediaRecorder = new MediaRecorder(mediaStream, {
      mimeType: selectedMimeType,
    });

    recordedChunks = [];
    drainedUpTo = 0;
    recordingStartTime = Date.now();
    pausedDuration = 0;
    lastPauseTime = null;

    mediaRecorder.ondataavailable = (event) => {
      console.log('[Offscreen] ondataavailable fired, data size:', event.data?.size || 0);
      if (event.data && event.data.size > 0) {
        recordedChunks.push(event.data);
        console.log('[Offscreen] Chunk stored, total chunks:', recordedChunks.length);
        // Report to background
        chrome.runtime.sendMessage({
          action: 'offscreen:chunkReceived',
          chunkSize: event.data.size,
          totalChunks: recordedChunks.length,
        }).catch(() => {});
      }
    };

    mediaRecorder.onstop = () => {
      const totalSize = recordedChunks.reduce((sum, chunk) => sum + chunk.size, 0);
      console.log('[Offscreen] MediaRecorder stopped, chunks:', recordedChunks.length, 'totalSize:', totalSize);
      isRecording = false;
      isPaused = false;

      // Clear intervals
      if (dataFlushInterval) {
        clearInterval(dataFlushInterval);
        dataFlushInterval = null;
      }
      if (statusInterval) {
        clearInterval(statusInterval);
        statusInterval = null;
      }

      // Notify background that recording stopped
      const duration = getRecordingDuration();
      console.log('[Offscreen] Sending recordingStopped, duration:', duration);
      chrome.runtime.sendMessage({
        action: 'offscreen:recordingStopped',
        duration: duration,
      }).catch((e) => console.error('[Offscreen] Failed to send stop notification:', e));
    };

    mediaRecorder.onerror = (event) => {
      console.error('[Offscreen] MediaRecorder error:', event.error?.name, event.error?.message);
      console.error('[Offscreen] Stream state at error:', mediaStream?.getTracks().map(t => ({
        kind: t.kind,
        state: t.readyState,
        enabled: t.enabled
      })));
      isRecording = false;
      isPaused = false;

      // Clear intervals
      if (dataFlushInterval) {
        clearInterval(dataFlushInterval);
        dataFlushInterval = null;
      }
      if (statusInterval) {
        clearInterval(statusInterval);
        statusInterval = null;
      }

      cleanup();

      // Notify background of error
      chrome.runtime.sendMessage({
        action: 'offscreen:recordingError',
        error: event.error?.message || 'MediaRecorder error occurred',
      }).catch((e) => console.error('[Offscreen] Failed to send error notification:', e));
    };

    // Start recording with 1 second timeslice to collect chunks
    mediaRecorder.start(1000);
    isRecording = true;
    isPaused = false;

    // CRITICAL: Add manual requestData() fallback to force data flush
    // Chrome can silently buffer without firing ondataavailable if no actual data arrives
    dataFlushInterval = setInterval(() => {
      if (!isRecording || !mediaRecorder) {
        clearInterval(dataFlushInterval);
        dataFlushInterval = null;
        return;
      }
      if (mediaRecorder.state === 'recording' && recordedChunks.length === 0) {
        // No chunks yet after timeslice, force a data request
        try {
          mediaRecorder.requestData();
          console.log('[Offscreen] Forced requestData() - no chunks received yet');
        } catch (e) {
          // Ignore - may fail if already stopped
        }
      }
    }, 500);

    // Log stream state
    const videoTracks = mediaStream.getVideoTracks();
    const audioTracks = mediaStream.getAudioTracks();
    console.log('[Offscreen] Recording started, video tracks:', videoTracks.length, 'audio tracks:', audioTracks.length);
    console.log('[Offscreen] Video track state:', videoTracks[0]?.readyState, 'enabled:', videoTracks[0]?.enabled);
    console.log('[Offscreen] MediaRecorder state:', mediaRecorder.state);
    console.log('[Offscreen] AudioContext state:', audioContext.state);

    // Send diagnostic info back to background
    chrome.runtime.sendMessage({
      action: 'offscreen:recordingDiagnostic',
      videoTracks: videoTracks.length,
      audioTracks: audioTracks.length,
      videoTrackState: videoTracks[0]?.readyState,
      audioContextState: audioContext.state,
      recorderState: mediaRecorder.state,
      mimeType: selectedMimeType,
    }).catch(() => {});

    // Periodic status check every 2 seconds
    statusInterval = setInterval(() => {
      if (!isRecording) {
        clearInterval(statusInterval);
        statusInterval = null;
        return;
      }
      const vt = mediaStream?.getVideoTracks()[0];
      console.log('[Offscreen] Status check - recorder:', mediaRecorder?.state,
        'chunks:', recordedChunks.length,
        'videoTrack:', vt?.readyState, vt?.enabled ? 'enabled' : 'disabled',
        'audioContext:', audioContext?.state);
    }, 2000);

    isStartingRecording = false;
    return { success: true };
  } catch (error) {
    console.error('[Offscreen] Failed to start recording:', error);
    isStartingRecording = false;
    await cleanup();
    return { success: false, error: String(error) };
  }
}

// Stop recording
async function stopRecording() {
  try {
    console.log('[Offscreen] Stopping recording, current state:', mediaRecorder?.state, 'chunks:', recordedChunks.length);

    if (!mediaRecorder || mediaRecorder.state === 'inactive') {
      console.log('[Offscreen] No active recording to stop');
      return { success: false, error: 'No active recording' };
    }

    // Request final data before stopping
    if (mediaRecorder.state === 'recording') {
      console.log('[Offscreen] Requesting final data chunk...');
      mediaRecorder.requestData();
    }

    // Create a promise that resolves when the recorder actually stops
    const stopPromise = new Promise((resolve) => {
      if (mediaRecorder) {
        const originalOnStop = mediaRecorder.onstop;
        mediaRecorder.onstop = (event) => {
          if (originalOnStop) {
            originalOnStop.call(mediaRecorder, event);
          }
          resolve();
        };
      } else {
        resolve();
      }
    });

    mediaRecorder.stop();
    await stopPromise;

    console.log('[Offscreen] Recording stopped, final chunks:', recordedChunks.length);
    return { success: true };
  } catch (error) {
    console.error('[Offscreen] Failed to stop recording:', error);
    return { success: false, error: String(error) };
  }
}

// Pause recording
function pauseRecording() {
  try {
    if (!mediaRecorder || mediaRecorder.state !== 'recording') {
      return { success: false, error: 'No active recording to pause' };
    }

    mediaRecorder.pause();
    isPaused = true;
    lastPauseTime = Date.now();

    console.log('[Offscreen] Recording paused');
    return { success: true };
  } catch (error) {
    console.error('[Offscreen] Failed to pause recording:', error);
    return { success: false, error: String(error) };
  }
}

// Resume recording
function resumeRecording() {
  try {
    if (!mediaRecorder || mediaRecorder.state !== 'paused') {
      return { success: false, error: 'No paused recording to resume' };
    }

    // Add the paused duration
    if (lastPauseTime) {
      pausedDuration += Date.now() - lastPauseTime;
      lastPauseTime = null;
    }

    mediaRecorder.resume();
    isPaused = false;

    console.log('[Offscreen] Recording resumed');
    return { success: true };
  } catch (error) {
    console.error('[Offscreen] Failed to resume recording:', error);
    return { success: false, error: String(error) };
  }
}

// Get current state
function getState() {
  return {
    isRecording,
    isPaused,
    duration: getRecordingDuration(),
    chunksCount: recordedChunks.length,
    micEnabled: isMicEnabled,
  };
}

// Helper function to add timeout to promises
function withTimeout(promise, timeoutMs, errorMessage) {
  let timeoutId;
  const timeoutPromise = new Promise((_, reject) => {
    timeoutId = setTimeout(() => reject(new Error(errorMessage)), timeoutMs);
  });
  return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
}

// Get video blob as Uint8Array (for transfer via messaging)
async function getVideoBlob() {
  try {
    console.log('[Offscreen] getVideoBlob called, chunks:', recordedChunks.length);

    if (recordedChunks.length === 0) {
      console.error('[Offscreen] No recorded chunks available');
      return { success: false, error: 'No recorded data available' };
    }

    const blob = new Blob(recordedChunks, { type: 'video/webm' });
    const duration = getRecordingDuration();

    console.log('[Offscreen] Original blob size:', blob.size, 'bytes, duration:', duration);

    // Fix WebM duration metadata with timeout (max 10 seconds)
    let fixedBlob = blob;
    if (typeof ysFixWebmDuration !== 'undefined') {
      try {
        fixedBlob = await withTimeout(
          ysFixWebmDuration(blob, duration, { logger: false }),
          10000,
          'ysFixWebmDuration timeout'
        );
        console.log('[Offscreen] Fixed blob size:', fixedBlob.size, 'bytes');
      } catch (fixError) {
        console.warn('[Offscreen] Failed to fix WebM duration (using original):', fixError);
        fixedBlob = blob; // Use original blob if fix fails
      }
    } else {
      console.warn('[Offscreen] ysFixWebmDuration not available');
    }

    console.log('[Offscreen] Converting blob to array...');
    const arrayBuffer = await fixedBlob.arrayBuffer();
    const uint8Array = new Uint8Array(arrayBuffer);

    console.log('[Offscreen] Video blob ready, size:', fixedBlob.size, 'bytes');

    return {
      success: true,
      data: Array.from(uint8Array),
      mimeType: 'video/webm',
      duration: duration,
    };
  } catch (error) {
    console.error('[Offscreen] Failed to get video blob:', error);
    return { success: false, error: String(error) };
  }
}

// Clean up resources
async function cleanup() {
  console.log('[Offscreen] Cleaning up resources');

  // Clear intervals first
  if (dataFlushInterval) {
    clearInterval(dataFlushInterval);
    dataFlushInterval = null;
  }
  if (statusInterval) {
    clearInterval(statusInterval);
    statusInterval = null;
  }

  if (mediaRecorder && mediaRecorder.state !== 'inactive') {
    try {
      mediaRecorder.stop();
    } catch (e) {
      // Ignore
    }
  }

  if (mediaStream) {
    mediaStream.getTracks().forEach((track) => track.stop());
  }

  // Clean up mic audio
  cleanupMicAudio();
  if (systemAudioSource) {
    try {
      systemAudioSource.disconnect();
    } catch (e) {
      // Ignore
    }
    systemAudioSource = null;
  }

  // Clean up silent oscillator
  if (silentOscillator) {
    try {
      silentOscillator.stop();
      silentOscillator.disconnect();
    } catch (e) {
      // Ignore
    }
    silentOscillator = null;
  }
  if (silentGainNode) {
    try {
      silentGainNode.disconnect();
    } catch (e) {
      // Ignore
    }
    silentGainNode = null;
  }

  mixedAudioDestination = null;

  // Close audio context
  if (audioContext) {
    try {
      await audioContext.close();
    } catch (e) {
      // Ignore
    }
    audioContext = null;
  }

  mediaRecorder = null;
  mediaStream = null;
  displayStreamRef = null;
  recordedChunks = [];
  drainedUpTo = 0;
  recordingStartTime = null;
  pausedDuration = 0;
  lastPauseTime = null;
  isRecording = false;
  isPaused = false;
  isStartingRecording = false;
}

// ============================================================================
// Chunked Video Transfer
// ============================================================================

const VIDEO_CHUNK_SIZE = 8 * 1024 * 1024; // 8MB per chunk
let cachedVideoBlob = null;

// Convert Uint8Array to base64 in batches to avoid call stack overflow
function uint8ArrayToBase64(uint8Array) {
  const BATCH_SIZE = 8192;
  let binary = '';
  for (let i = 0; i < uint8Array.length; i += BATCH_SIZE) {
    const slice = uint8Array.subarray(i, Math.min(i + BATCH_SIZE, uint8Array.length));
    binary += String.fromCharCode.apply(null, slice);
  }
  return btoa(binary);
}

// Prepare video blob: merge chunks, fix duration, cache for chunked transfer
async function prepareVideoBlob() {
  try {
    console.log('[Offscreen] prepareVideoBlob called, chunks:', recordedChunks.length);

    if (recordedChunks.length === 0) {
      return { success: false, error: 'No recorded data available' };
    }

    const blob = new Blob(recordedChunks, { type: 'video/webm' });
    const duration = getRecordingDuration();

    console.log('[Offscreen] Original blob size:', blob.size, 'bytes, duration:', duration);

    // Fix WebM duration metadata
    let fixedBlob = blob;
    if (typeof ysFixWebmDuration !== 'undefined') {
      try {
        fixedBlob = await withTimeout(
          ysFixWebmDuration(blob, duration, { logger: false }),
          60000,
          'ysFixWebmDuration timeout'
        );
        console.log('[Offscreen] Fixed blob size:', fixedBlob.size, 'bytes');
      } catch (fixError) {
        console.warn('[Offscreen] Failed to fix WebM duration (using original):', fixError);
        fixedBlob = blob;
      }
    }

    cachedVideoBlob = fixedBlob;
    // Free raw MediaRecorder chunks
    recordedChunks = [];

    const totalChunks = Math.ceil(fixedBlob.size / VIDEO_CHUNK_SIZE);
    console.log('[Offscreen] Video prepared, size:', fixedBlob.size, 'totalChunks:', totalChunks);

    return {
      success: true,
      totalSize: fixedBlob.size,
      totalChunks,
      mimeType: 'video/webm',
      duration,
    };
  } catch (error) {
    console.error('[Offscreen] Failed to prepare video blob:', error);
    return { success: false, error: String(error) };
  }
}

// Get a single chunk of the cached video blob as base64
async function getVideoChunk(chunkIndex) {
  try {
    if (!cachedVideoBlob) {
      return { success: false, error: 'No cached video blob. Call prepareVideoBlob first.' };
    }

    const start = chunkIndex * VIDEO_CHUNK_SIZE;
    const end = Math.min(start + VIDEO_CHUNK_SIZE, cachedVideoBlob.size);
    const slice = cachedVideoBlob.slice(start, end);

    const arrayBuffer = await slice.arrayBuffer();
    const uint8Array = new Uint8Array(arrayBuffer);
    const base64Data = uint8ArrayToBase64(uint8Array);

    console.log('[Offscreen] Chunk', chunkIndex, 'ready, size:', uint8Array.length, 'base64Len:', base64Data.length);

    return {
      success: true,
      base64Data,
      chunkIndex,
    };
  } catch (error) {
    console.error('[Offscreen] Failed to get video chunk:', error);
    return { success: false, error: String(error) };
  }
}

// Drain current recording chunks as raw ArrayBuffer for progressive upload.
// Non-destructive: keeps all chunks in recordedChunks for blob transfer fallback.
async function drainChunks() {
  try {
    if (drainedUpTo >= recordedChunks.length) {
      return { success: true, data: new ArrayBuffer(0), size: 0 };
    }

    const newChunks = recordedChunks.slice(drainedUpTo);
    const blob = new Blob(newChunks, { type: 'video/webm' });
    const arrayBuffer = await blob.arrayBuffer();
    const size = arrayBuffer.byteLength;

    // Track position but DON'T clear recordedChunks — blob transfer needs them
    drainedUpTo = recordedChunks.length;

    console.log('[Offscreen] Drained chunks, size:', size, 'drainedUpTo:', drainedUpTo);
    return { success: true, data: arrayBuffer, size };
  } catch (error) {
    console.error('[Offscreen] Failed to drain chunks:', error);
    return { success: false, error: String(error), size: 0 };
  }
}

// =====================================================================
// Image-mode Instant Replay GIF encoding
// ---------------------------------------------------------------------
// Runs in the offscreen document (not the content script) so the GIF
// worker can be loaded from a chrome-extension:// URL — same-origin to
// the offscreen document, which sidesteps the host page's CSP. Strict
// CSP sites (e.g. hyblockcapital.com) block blob:/extension workers
// when spawned from a content script's Worker constructor; doing the
// encode here lets every site work regardless of its CSP.
//
// gif.js is loaded via <script src="./gif.js"></script> in offscreen.html
// and exposes the GIF class on window.
// =====================================================================

function loadImageInOffscreen(dataUrl) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = (err) => reject(err instanceof Error ? err : new Error('Image decode failed'));
    img.src = dataUrl;
  });
}

async function encodeReplayGif({ frames, maxWidth, frameDelayMs }) {
  if (!frames || frames.length === 0) {
    return { success: false, error: 'no frames' };
  }
  if (typeof GIF === 'undefined') {
    return { success: false, error: 'gif.js not loaded' };
  }

  try {
    const images = await Promise.all(frames.map((f) => loadImageInOffscreen(f.dataUrl)));
    const firstImg = images[0];
    const sourceWidth = firstImg.naturalWidth || firstImg.width;
    const sourceHeight = firstImg.naturalHeight || firstImg.height;
    if (sourceWidth === 0 || sourceHeight === 0) {
      return { success: false, error: 'first frame has zero dimensions' };
    }

    const cap = maxWidth || 960;
    const scale = Math.min(1, cap / sourceWidth);
    const targetWidth = Math.max(1, Math.round(sourceWidth * scale));
    const targetHeight = Math.max(1, Math.round(sourceHeight * scale));

    const canvas = document.createElement('canvas');
    canvas.width = targetWidth;
    canvas.height = targetHeight;
    // willReadFrequently silences the Canvas2D perf warning gif.js
    // triggers via getImageData() — and switches Chrome to a software
    // raster path that's faster for repeated readbacks.
    const ctx = canvas.getContext('2d', { willReadFrequently: true });

    const gif = new GIF({
      workers: 2,
      // Same-origin URL because the offscreen document IS the extension
      // origin. The host-page CSP that broke the content-script path
      // does not apply here.
      workerScript: chrome.runtime.getURL('gif.worker.js'),
      quality: 10,
      width: targetWidth,
      height: targetHeight,
      background: '#000',
    });

    for (const img of images) {
      ctx.clearRect(0, 0, targetWidth, targetHeight);
      ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
      gif.addFrame(ctx, { delay: frameDelayMs || 1000, copy: true });
    }

    return await new Promise((resolve) => {
      gif.on('finished', async (blob) => {
        try {
          const buffer = await blob.arrayBuffer();
          // Return as a Uint8Array — structured-clone-friendly across
          // chrome.runtime.sendMessage hops.
          resolve({ success: true, buffer: new Uint8Array(buffer) });
        } catch (err) {
          resolve({ success: false, error: String(err) });
        }
      });
      try {
        gif.render();
      } catch (err) {
        resolve({ success: false, error: String(err) });
      }
    });
  } catch (err) {
    console.error('[Offscreen] encodeReplayGif failed:', err);
    return { success: false, error: String(err) };
  }
}

// Listen for messages from background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  console.log('[Offscreen] Received message:', message.action);

  switch (message.action) {
    case 'offscreen:startRecording':
      startRecording(
        message.streamId,
        message.enableSystemAudio,
        message.enableMicAudio
      ).then(sendResponse);
      return true; // Will respond asynchronously

    case 'offscreen:enableMicAudio':
      enableMicAudioDuringRecording().then(sendResponse);
      return true; // Will respond asynchronously

    case 'offscreen:stopRecording':
      stopRecording().then(sendResponse);
      return true;

    case 'offscreen:pauseRecording':
      sendResponse(pauseRecording());
      return false;

    case 'offscreen:resumeRecording':
      sendResponse(resumeRecording());
      return false;

    case 'offscreen:getState':
      sendResponse(getState());
      return false;

    case 'offscreen:getVideoBlob':
      getVideoBlob().then(sendResponse);
      return true;

    case 'offscreen:cleanup':
      cleanup().then(() => sendResponse({ success: true }));
      return true;

    // Mic permission and control handlers
    case 'offscreen:checkMicPermission':
      checkMicPermission().then(sendResponse);
      return true;

    case 'offscreen:toggleMicMute':
      toggleMicMute(message.muted).then(sendResponse);
      return true;

    // Chunked video transfer handlers
    case 'offscreen:prepareVideoBlob':
      prepareVideoBlob().then(sendResponse);
      return true;

    case 'offscreen:getVideoChunk':
      getVideoChunk(message.chunkIndex).then(sendResponse);
      return true;

    // Progressive upload drain handlers
    case 'offscreen:getBufferSize':
      const undrainedChunks = recordedChunks.slice(drainedUpTo);
      sendResponse({
        success: true,
        size: undrainedChunks.reduce((sum, chunk) => sum + chunk.size, 0),
      });
      return false;

    case 'offscreen:drainChunks':
      drainChunks().then(sendResponse);
      return true;

    case 'offscreen:encodeReplayGif':
      encodeReplayGif({
        frames: message.frames,
        maxWidth: message.maxWidth,
        frameDelayMs: message.frameDelayMs,
      }).then(sendResponse);
      return true;

    default:
      return false;
  }
});
