/// Publish actual-size local preview frames so the launcher mirrors uplink quality.
fn spawn_camera_preview_tap(sink: gst_app::AppSink, path: PathBuf) -> Arc<AtomicBool> {
    let running = Arc::new(AtomicBool::new(true));
    let thread_running = Arc::clone(&running);
    thread::spawn(move || {
        let mut wrote_first = false;
        let mut empty_polls = 0_u64;
        while thread_running.load(Ordering::Acquire) {
            if let Some(sample) = sink.try_pull_sample(gst::ClockTime::from_mseconds(250)) {
                empty_polls = 0;
                match write_camera_preview_tap(&path, &sample) {
                    Ok(info) => {
                        if !wrote_first {
                            wrote_first = true;
                            #[cfg(not(coverage))]
                            log_camera_preview_tap_started(&path, &info);
                        }
                    }
                    Err(err) => {
                        tracing::debug!("📸 local uplink preview tap write failed: {err:#}");
                        thread::sleep(Duration::from_millis(100));
                    }
                }
            } else if !wrote_first {
                note_camera_preview_tap_empty_poll(&path, &mut empty_polls);
            }
        }
    });
    running
}

/// Publish raw RGBA frames emitted by the FFmpeg/NVENC route.
///
/// Inputs: FFmpeg's rawvideo stream plus the expected frame geometry. Output:
/// a cancellation flag for the reader thread. Why: the NVENC path cannot use
/// the GStreamer tee preview branch, but the launcher still needs to show the
/// real upstream camera image while HEVC is active.
#[cfg(not(coverage))]
fn spawn_ffmpeg_raw_preview_tap<R>(
    mut reader: R,
    path: PathBuf,
    width: u32,
    height: u32,
) -> Arc<AtomicBool>
where
    R: Read + Send + 'static,
{
    let running = Arc::new(AtomicBool::new(true));
    let thread_running = Arc::clone(&running);
    thread::spawn(move || {
        let width_i32 = i32::try_from(width).unwrap_or(i32::MAX).max(1);
        let height_i32 = i32::try_from(height).unwrap_or(i32::MAX).max(1);
        let stride = usize::try_from(width)
            .ok()
            .and_then(|width| width.checked_mul(4))
            .unwrap_or(4);
        let frame_len = stride
            .checked_mul(usize::try_from(height).unwrap_or(1).max(1))
            .unwrap_or(stride);
        let mut frame = vec![0_u8; frame_len];
        let mut wrote_first = false;
        while thread_running.load(Ordering::Acquire) {
            match reader.read_exact(&mut frame) {
                Ok(()) => match write_camera_preview_rgba(
                    &path, width_i32, height_i32, stride, &frame,
                ) {
                    Ok(info) => {
                        if !wrote_first {
                            wrote_first = true;
                            log_camera_preview_tap_started(&path, &info);
                        }
                    }
                    Err(err) => {
                        tracing::debug!("📸 FFmpeg preview tap write failed: {err:#}");
                        thread::sleep(Duration::from_millis(100));
                    }
                },
                Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
                Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => break,
                Err(err) => {
                    tracing::debug!("📸 FFmpeg preview tap stopped: {err:#}");
                    break;
                }
            }
        }
    });
    running
}

#[cfg(coverage)]
fn spawn_ffmpeg_raw_preview_tap<R>(
    _reader: R,
    _path: PathBuf,
    _width: u32,
    _height: u32,
) -> Arc<AtomicBool>
where
    R: Read + Send + 'static,
{
    Arc::new(AtomicBool::new(false))
}

#[cfg(not(coverage))]
fn log_camera_preview_tap_started(path: &Path, info: &CameraPreviewTapInfo) {
    tracing::info!(
        path = %path.display(),
        width = info.width,
        height = info.height,
        stride = info.stride,
        "📸 local uplink preview tap publishing frames"
    );
}

#[cfg(not(coverage))]
/// Tracks empty preview polls and periodically logs that the tap is alive.
///
/// Inputs: preview path and mutable empty-poll counter. Output: counter/log side
/// effect only. Why: startup stalls should be visible in real sessions, but the
/// coverage build should not need to spend seconds waiting for a warning-only
/// branch.
fn note_camera_preview_tap_empty_poll(path: &Path, empty_polls: &mut u64) {
    *empty_polls += 1;
    if *empty_polls == 20 || empty_polls.is_multiple_of(120) {
        log_camera_preview_tap_waiting(path);
    }
}

#[cfg(coverage)]
/// Keeps preview-tap empty polling coverage fast.
///
/// Inputs: same as the production helper. Output: none.
/// Why: the wait-log branch exists only for operator visibility during slow
/// camera startup; coverage should validate frame publishing without waiting
/// five seconds for the first warning threshold.
fn note_camera_preview_tap_empty_poll(_path: &Path, _empty_polls: &mut u64) {}

#[cfg(not(coverage))]
/// Log that the preview tap is still alive while waiting for first frames.
fn log_camera_preview_tap_waiting(path: &Path) {
    tracing::warn!(
        path = %path.display(),
        "📸 local uplink preview tap is still waiting for webcam frames"
    );
}

struct CameraPreviewTapInfo {
    width: i32,
    height: i32,
    stride: usize,
}

/// Atomically write one RGBA preview sample for the launcher status pane.
fn write_camera_preview_tap(
    path: &Path,
    sample: &gst::Sample,
) -> anyhow::Result<CameraPreviewTapInfo> {
    let caps = sample.caps().context("preview tap sample missing caps")?;
    let structure = caps
        .structure(0)
        .context("preview tap caps missing structure")?;
    let width = structure
        .get::<i32>("width")
        .context("preview tap caps missing width")?;
    let height = structure
        .get::<i32>("height")
        .context("preview tap caps missing height")?;
    let buffer = sample
        .buffer()
        .context("preview tap sample missing buffer")?;
    let map = buffer
        .map_readable()
        .context("preview tap buffer unreadable")?;
    let row_count = usize::try_from(height)
        .ok()
        .filter(|height| *height > 0)
        .unwrap_or(1);
    let stride = map.as_slice().len() / row_count;
    write_camera_preview_rgba(path, width, height, stride, map.as_slice())
}

fn write_camera_preview_rgba(
    path: &Path,
    width: i32,
    height: i32,
    stride: usize,
    rgba: &[u8],
) -> anyhow::Result<CameraPreviewTapInfo> {
    let tmp_path = path.with_extension("tmp");
    let mut file = std::fs::File::create(&tmp_path)
        .with_context(|| format!("creating {}", tmp_path.display()))?;
    writeln!(file, "LESAVKA_RGBA {width} {height} {stride}")?;
    file.write_all(rgba)?;
    file.sync_all().ok();
    std::fs::rename(&tmp_path, path).with_context(|| format!("publishing {}", path.display()))?;
    Ok(CameraPreviewTapInfo {
        width,
        height,
        stride,
    })
}
