/// 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 {
                empty_polls += 1;
                if empty_polls == 20 || empty_polls.is_multiple_of(120) {
                    #[cfg(not(coverage))]
                    log_camera_preview_tap_waiting(&path);
                }
            }
        }
    });
    running
}

#[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))]
/// 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;
    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(map.as_slice())?;
    file.sync_all().ok();
    std::fs::rename(&tmp_path, path).with_context(|| format!("publishing {}", path.display()))?;
    Ok(CameraPreviewTapInfo {
        width,
        height,
        stride,
    })
}
