fn spawn_raw_video_encoder(
    width: i32,
    height: i32,
    output_path: &Path,
    encode_fps: u32,
    encode_bitrate_kbit: u32,
) -> Result<std::process::Child, String> {
    let bitrate_arg = format!("{}k", encode_bitrate_kbit.max(800));
    let fps_arg = encode_fps.max(1).to_string();
    let video_size = format!("{}x{}", width.max(1), height.max(1));
    let output_arg = output_path.to_string_lossy().into_owned();
    Command::new("ffmpeg")
        .args([
            "-hide_banner",
            "-loglevel",
            "error",
            "-y",
            "-f",
            "rawvideo",
            "-pix_fmt",
            "rgba",
            "-video_size",
            &video_size,
            "-framerate",
            &fps_arg,
            "-i",
            "-",
            "-c:v",
            "libx264",
            "-pix_fmt",
            "yuv420p",
            "-r",
            &fps_arg,
            "-b:v",
            &bitrate_arg,
        ])
        .arg(&output_arg)
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()
        .map_err(|err| format!("ffmpeg video encoder is unavailable: {err}"))
}

fn normalize_recording_frame(frame: PreviewFrameSnapshot) -> Result<(i32, i32, Vec<u8>), String> {
    let width = frame.width.max(0) as usize;
    let height = frame.height.max(0) as usize;
    if width == 0 || height == 0 {
        return Err("decoded preview frame had zero size".to_string());
    }
    let row_bytes = width.saturating_mul(4);
    let needed = row_bytes.saturating_mul(height);
    if frame.rgba.len() < needed && frame.stride == row_bytes {
        return Err("decoded preview frame was shorter than its declared size".to_string());
    }
    if frame.stride == row_bytes && frame.rgba.len() >= needed {
        return Ok((frame.width, frame.height, frame.rgba[..needed].to_vec()));
    }
    if frame.stride < row_bytes || frame.rgba.len() < frame.stride.saturating_mul(height) {
        return Err("decoded preview frame stride was inconsistent".to_string());
    }
    let mut rgba = Vec::with_capacity(needed);
    for row in 0..height {
        let start = row.saturating_mul(frame.stride);
        rgba.extend_from_slice(&frame.rgba[start..start + row_bytes]);
    }
    Ok((frame.width, frame.height, rgba))
}

fn finish_raw_video_encoder(
    child: &mut std::process::Child,
    frame_dir: &Path,
    output_path: &Path,
) -> Result<(), String> {
    let _ = child.stdin.take();
    let status = child
        .wait()
        .map_err(|err| format!("ffmpeg video encoder wait failed: {err}"))?;
    if !status.success() {
        return Err(format!(
            "ffmpeg failed while encoding {}; temporary data is still in {}",
            output_path.display(),
            frame_dir.display()
        ));
    }
    Ok(())
}

fn mux_recording_audio(
    video_path: &Path,
    output_path: &Path,
    audio_mode: EyeRecordAudioMode,
    audio_paths: &[PathBuf],
) -> Result<(), String> {
    let usable_audio_paths = validated_audio_paths(audio_mode, audio_paths)?;
    if usable_audio_paths.is_empty() {
        return Ok(());
    }
    let video_arg = video_path.to_string_lossy().into_owned();
    let output_arg = output_path.to_string_lossy().into_owned();
    let mut command = Command::new("ffmpeg");
    command.args([
        "-hide_banner",
        "-loglevel",
        "error",
        "-y",
        "-i",
        &video_arg,
    ]);
    for audio_path in &usable_audio_paths {
        command.arg("-i").arg(audio_path);
    }
    if usable_audio_paths.len() > 1 {
        command.args([
            "-filter_complex",
            "[1:a][2:a]amix=inputs=2:duration=shortest:normalize=0[a]",
            "-map",
            "0:v:0",
            "-map",
            "[a]",
        ]);
    } else {
        command.args(["-map", "0:v:0", "-map", "1:a:0"]);
    }
    command.args(["-c:v", "copy", "-c:a", "aac", "-b:a", "160k", "-shortest"]);
    let mux = command
        .arg(&output_arg)
        .status()
        .map_err(|err| format!("ffmpeg is unavailable: {err}"))?;
    if !mux.success() {
        return Err(format!(
            "ffmpeg failed while adding audio to {}",
            output_path.display()
        ));
    }
    Ok(())
}

struct EyeRecordingWorkerConfig {
    frame_dir: PathBuf,
    output_path: PathBuf,
    encode_fps: u32,
    encode_bitrate_kbit: u32,
    audio_mode: EyeRecordAudioMode,
    audio_paths: Vec<PathBuf>,
}

fn run_recording_worker(
    frame_tap: PreviewRecordingTap,
    control_rx: std::sync::mpsc::Receiver<RecordFrameTask>,
    config: EyeRecordingWorkerConfig,
) -> Result<PathBuf, String> {
    let EyeRecordingWorkerConfig {
        frame_dir,
        output_path,
        encode_fps,
        encode_bitrate_kbit,
        audio_mode,
        audio_paths,
    } = config;
    let needs_audio_mux = audio_mode != EyeRecordAudioMode::NoAudio;
    let video_output_path = if needs_audio_mux {
        frame_dir.join("recording-video.mp4")
    } else {
        output_path.clone()
    };
    let mut encoder: Option<std::process::Child> = None;
    let mut frame_size: Option<(i32, i32)> = None;
    let mut captured_frames = 0_u32;

    loop {
        match control_rx.try_recv() {
            Ok(RecordFrameTask::Finish) | Err(std::sync::mpsc::TryRecvError::Disconnected) => {
                break;
            }
            Err(std::sync::mpsc::TryRecvError::Empty) => {}
        }

        match frame_tap.recv_timeout(Duration::from_millis(50)) {
            Ok(frame) => {
                let (width, height, rgba) = normalize_recording_frame(frame)?;
                if let Some((expected_width, expected_height)) = frame_size {
                    if width != expected_width || height != expected_height {
                        continue;
                    }
                } else {
                    frame_size = Some((width, height));
                    encoder = Some(spawn_raw_video_encoder(
                        width,
                        height,
                        &video_output_path,
                        encode_fps,
                        encode_bitrate_kbit,
                    )?);
                }
                let encoder = encoder
                    .as_mut()
                    .ok_or_else(|| "recording encoder did not start".to_string())?;
                let stdin = encoder
                    .stdin
                    .as_mut()
                    .ok_or_else(|| "recording encoder stdin is closed".to_string())?;
                std::io::Write::write_all(stdin, &rgba)
                    .map_err(|err| format!("recording encoder write failed: {err}"))?;
                captured_frames = captured_frames.saturating_add(1);
            }
            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
        }
    }

    if captured_frames < 2 {
        if let Some(mut child) = encoder {
            let _ = child.kill();
            let _ = child.wait();
        }
        let _ = std::fs::remove_dir_all(&frame_dir);
        return Err("need at least two captured frames to build a recording".to_string());
    }

    if let Some(mut child) = encoder {
        finish_raw_video_encoder(&mut child, &frame_dir, &video_output_path)?;
    }
    mux_recording_audio(
        &video_output_path,
        &output_path,
        audio_mode,
        &audio_paths,
    )?;
    if needs_audio_mux {
        let _ = std::fs::remove_file(&video_output_path);
    }
    let _ = std::fs::remove_dir_all(&frame_dir);
    Ok(output_path)
}
