fn normalize_camera_selection(camera: Option<&str>) -> Option<String> {
    camera
        .map(str::trim)
        .filter(|value| !value.is_empty() && !value.eq_ignore_ascii_case("auto"))
        .map(ToOwned::to_owned)
}

fn resolve_camera_device(camera: &str) -> String {
    if camera.starts_with("/dev/") {
        camera.to_string()
    } else {
        format!("/dev/v4l/by-id/{camera}")
    }
}

fn run_microphone_monitor_feed(
    source: &str,
    sink: Option<&str>,
    token: u64,
    recent_audio: Arc<Mutex<Vec<u8>>>,
    level: Arc<Mutex<f64>>,
    generation: Arc<AtomicU64>,
    running: Arc<AtomicBool>,
) -> Result<()> {
    let (pipeline, appsink) = build_microphone_monitor_pipeline(source, sink)?;
    pipeline
        .set_state(gst::State::Playing)
        .context("starting microphone preview pipeline")?;

    while running.load(Ordering::Acquire) && generation.load(Ordering::Acquire) == token {
        if let Some(sample) = appsink.try_pull_sample(gst::ClockTime::from_mseconds(250)) {
            if let Some(buffer) = sample.buffer()
                && let Ok(map) = buffer.map_readable()
            {
                let bytes = map.as_slice();
                push_recent_audio(&recent_audio, bytes);
                update_microphone_level(&level, bytes);
            }
        } else if let Ok(mut meter) = level.lock() {
            *meter = (*meter * 0.8).clamp(0.0, 1.0);
        }
    }

    let _ = pipeline.set_state(gst::State::Null);
    if let Ok(mut meter) = level.lock() {
        *meter = 0.0;
    }
    running.store(false, Ordering::Release);
    Ok(())
}

#[allow(clippy::too_many_arguments)]
fn run_camera_preview_feed(
    selected: String,
    device: String,
    mode: Option<CameraMode>,
    token: u64,
    latest: Arc<Mutex<Option<PreviewFrame>>>,
    status_text: Arc<Mutex<String>>,
    generation: Arc<AtomicU64>,
    running: Arc<AtomicBool>,
) -> Result<()> {
    let (pipeline, appsink) = build_camera_preview_pipeline(&device, mode)?;
    pipeline
        .set_state(gst::State::Playing)
        .context("starting in-launcher camera preview pipeline")?;

    if let Ok(mut status) = status_text.lock() {
        let quality = mode
            .map(CameraMode::short_label)
            .unwrap_or_else(|| "default quality".to_string());
        *status = format!("Local preview live for {selected} at {quality}; waiting for frames...");
    }

    let mut announced_size = None::<(i32, i32)>;
    while running.load(Ordering::Acquire) && generation.load(Ordering::Acquire) == token {
        if let Some(sample) = appsink.try_pull_sample(gst::ClockTime::from_mseconds(250))
            && let Some(frame) = sample_to_frame(&sample)
        {
            let size = (frame.width, frame.height);
            if announced_size != Some(size) {
                announced_size = Some(size);
                if let Ok(mut status) = status_text.lock() {
                    let quality = mode
                        .map(CameraMode::short_label)
                        .unwrap_or_else(|| "default quality".to_string());
                    *status = format!(
                        "Local preview live for {selected} at {quality}; showing {}x{}.",
                        size.0, size.1
                    );
                }
            }
            if let Ok(mut slot) = latest.lock() {
                *slot = Some(frame);
            }
        }
    }

    let _ = pipeline.set_state(gst::State::Null);
    Ok(())
}

fn run_camera_file_preview_feed(
    path: PathBuf,
    selected: String,
    token: u64,
    latest: Arc<Mutex<Option<PreviewFrame>>>,
    status_text: Arc<Mutex<String>>,
    generation: Arc<AtomicU64>,
    running: Arc<AtomicBool>,
) {
    let mut has_frame = false;
    let mut announced_size = None::<(i32, i32)>;
    while running.load(Ordering::Acquire) && generation.load(Ordering::Acquire) == token {
        match read_camera_preview_tap(&path) {
            Ok(frame) => {
                let size = (frame.width, frame.height);
                if let Ok(mut slot) = latest.lock() {
                    *slot = Some(frame);
                }
                if !has_frame || announced_size != Some(size) {
                    has_frame = true;
                    announced_size = Some(size);
                    if let Ok(mut status) = status_text.lock() {
                        *status = format!(
                            "Relay webcam preview live for {selected}; showing {}x{}.",
                            size.0, size.1
                        );
                    }
                }
            }
            Err(err) => {
                if !has_frame && let Ok(mut status) = status_text.lock() {
                    *status = format!("Waiting for relay webcam frames from {selected}: {err}");
                }
            }
        }
        std::thread::sleep(Duration::from_millis(120));
    }
    running.store(false, Ordering::Release);
}

fn run_microphone_level_probe(
    path: PathBuf,
    token: u64,
    level: Arc<Mutex<f64>>,
    generation: Arc<AtomicU64>,
    running: Arc<AtomicBool>,
) {
    while running.load(Ordering::Acquire) && generation.load(Ordering::Acquire) == token {
        let next = read_microphone_level_tap(&path).unwrap_or_else(|| {
            level
                .lock()
                .map(|current| (*current * 0.8).clamp(0.0, 1.0))
                .unwrap_or(0.0)
        });
        if let Ok(mut meter) = level.lock() {
            *meter = next.clamp(0.0, 1.0);
        }
        std::thread::sleep(Duration::from_millis(100));
    }
    if let Ok(mut meter) = level.lock() {
        *meter = 0.0;
    }
    running.store(false, Ordering::Release);
}

fn build_camera_preview_pipeline(
    device: &str,
    mode: Option<CameraMode>,
) -> Result<(gst::Pipeline, gst_app::AppSink)> {
    let desc = camera_preview_pipeline_desc(device, mode);
    let (width, height, _fps) = camera_preview_mode(mode);
    let pipeline = gst::parse::launch(&desc)?
        .downcast::<gst::Pipeline>()
        .expect("camera preview pipeline");
    let appsink = pipeline
        .by_name("sink")
        .context("missing in-launcher camera preview appsink")?
        .downcast::<gst_app::AppSink>()
        .expect("camera preview appsink");
    appsink.set_caps(Some(
        &gst::Caps::builder("video/x-raw")
            .field("format", "RGBA")
            .field("width", width)
            .field("height", height)
            .build(),
    ));
    Ok((pipeline, appsink))
}

fn build_microphone_monitor_pipeline(
    source: &str,
    sink: Option<&str>,
) -> Result<(gst::Pipeline, gst_app::AppSink)> {
    let desc = microphone_monitor_pipeline_desc(source, sink);
    let pipeline = gst::parse::launch(&desc)?
        .downcast::<gst::Pipeline>()
        .expect("microphone monitor pipeline");
    let appsink = pipeline
        .by_name("mic_preview_sink")
        .context("missing microphone preview appsink")?
        .downcast::<gst_app::AppSink>()
        .expect("microphone preview appsink");
    appsink.set_caps(Some(
        &gst::Caps::builder("audio/x-raw")
            .field("format", "S16LE")
            .field("rate", MIC_MONITOR_RATE)
            .field("channels", MIC_MONITOR_CHANNELS)
            .build(),
    ));
    Ok((pipeline, appsink))
}

fn camera_preview_pipeline_desc(device: &str, mode: Option<CameraMode>) -> String {
    let device = gst_quote(device);
    let (preview_width, preview_height, preview_fps) = camera_preview_mode(mode);
    let source_caps = mode
        .map(|mode| {
            format!(
                "capsfilter caps=\"video/x-raw,width=(int){},height=(int){},framerate=(fraction){}/1;image/jpeg,width=(int){},height=(int){},framerate=(fraction){}/1\" ! decodebin ! ",
                mode.width, mode.height, mode.fps, mode.width, mode.height, mode.fps
            )
        })
        .unwrap_or_default();
    format!(
        "v4l2src device=\"{device}\" do-timestamp=true ! \
         {source_caps}videoconvert ! videoscale ! videorate ! \
         video/x-raw,format=RGBA,width={preview_width},height={preview_height},framerate={preview_fps}/1,pixel-aspect-ratio=1/1 ! \
         appsink name=sink emit-signals=false sync=false max-buffers=1 drop=true"
    )
}

fn camera_preview_mode(mode: Option<CameraMode>) -> (i32, i32, u32) {
    mode.map(|mode| {
        (
            i32::try_from(mode.width).unwrap_or(i32::MAX).max(1),
            i32::try_from(mode.height).unwrap_or(i32::MAX).max(1),
            mode.fps.max(1),
        )
    })
    .unwrap_or((
        CAMERA_PREVIEW_DEFAULT_WIDTH,
        CAMERA_PREVIEW_DEFAULT_HEIGHT,
        CAMERA_PREVIEW_DEFAULT_FPS,
    ))
}

fn microphone_monitor_pipeline_desc(source: &str, sink: Option<&str>) -> String {
    let source_element = if looks_like_pulse_source_name(source)
        || gst::ElementFactory::find("pipewiresrc").is_none()
    {
        let source = gst_quote(source);
        format!("pulsesrc device=\"{source}\" do-timestamp=true")
    } else {
        let source = gst_quote(source);
        format!("pipewiresrc target-object=\"{source}\" do-timestamp=true")
    };
    let sink_prop = sink
        .map(gst_quote)
        .map(|value| format!(" device=\"{value}\""))
        .unwrap_or_default();
    format!(
        "{source_element} ! \
         audioconvert ! audioresample ! \
         audio/x-raw,format=S16LE,rate={MIC_MONITOR_RATE},channels={MIC_MONITOR_CHANNELS} ! \
         tee name=t \
         t. ! queue ! pulsesink{sink_prop} \
         t. ! queue ! appsink name=mic_preview_sink emit-signals=false sync=false max-buffers=8 drop=true"
    )
}

fn looks_like_pulse_source_name(source: &str) -> bool {
    let source = source.trim();
    source.starts_with("alsa_input.")
        || source.starts_with("bluez_input.")
        || source.starts_with("input.")
}

fn sample_to_frame(sample: &gst::Sample) -> Option<PreviewFrame> {
    let caps = sample.caps()?;
    let structure = caps.structure(0)?;
    let width = structure.get::<i32>("width").ok()?;
    let height = structure.get::<i32>("height").ok()?;
    let buffer = sample.buffer()?;
    let map = buffer.map_readable().ok()?;
    let rgba = map.as_slice().to_vec();
    let stride = rgba.len() / height.max(1) as usize;
    Some(PreviewFrame {
        width,
        height,
        stride,
        rgba,
    })
}

fn read_camera_preview_tap(path: &Path) -> Result<PreviewFrame> {
    let bytes = fs::read(path).with_context(|| format!("{} is not ready", path.display()))?;
    let header_end = bytes
        .iter()
        .position(|byte| *byte == b'\n')
        .ok_or_else(|| anyhow!("preview frame header is incomplete"))?;
    let header =
        std::str::from_utf8(&bytes[..header_end]).context("preview frame header is not UTF-8")?;
    let mut fields = header.split_whitespace();
    if fields.next() != Some("LESAVKA_RGBA") {
        return Err(anyhow!("preview frame has an unknown format"));
    }
    let width = fields
        .next()
        .and_then(|value| value.parse::<i32>().ok())
        .filter(|value| *value > 0)
        .ok_or_else(|| anyhow!("preview frame width is invalid"))?;
    let height = fields
        .next()
        .and_then(|value| value.parse::<i32>().ok())
        .filter(|value| *value > 0)
        .ok_or_else(|| anyhow!("preview frame height is invalid"))?;
    let stride = fields
        .next()
        .and_then(|value| value.parse::<usize>().ok())
        .filter(|value| *value > 0)
        .ok_or_else(|| anyhow!("preview frame stride is invalid"))?;
    let rgba = bytes[header_end + 1..].to_vec();
    let expected_min = stride.saturating_mul(height as usize);
    if rgba.len() < expected_min {
        return Err(anyhow!("preview frame payload is incomplete"));
    }
    Ok(PreviewFrame {
        width,
        height,
        stride,
        rgba,
    })
}

fn read_microphone_level_tap(path: &Path) -> Option<f64> {
    fs::read_to_string(path)
        .ok()
        .and_then(|raw| raw.split_ascii_whitespace().next()?.parse::<f64>().ok())
        .filter(|value| value.is_finite())
        .map(|value| value.clamp(0.0, 1.0))
}

fn gst_quote(value: &str) -> String {
    value.replace('\\', "\\\\").replace('"', "\\\"")
}

fn build_speaker_test(sink: Option<&str>) -> Result<Command> {
    let sink_prop = sink
        .filter(|value| !value.trim().is_empty())
        .map(|value| format!("device={}", quote(value)))
        .unwrap_or_default();
    Ok(shell_command(format!(
        "gst-launch-1.0 -q audiotestsrc is-live=true wave=sine freq=880 volume=0.25 ! audioconvert ! audioresample ! queue ! pulsesink {}",
        sink_prop
    )))
}

fn build_microphone_replay_test(path: &str, sink: Option<&str>) -> Result<Command> {
    let sink_prop = sink
        .filter(|value| !value.trim().is_empty())
        .map(|value| format!("device={}", quote(value)))
        .unwrap_or_default();
    Ok(shell_command(format!(
        "gst-launch-1.0 -q filesrc location={} ! wavparse ! audioconvert ! audioresample ! queue ! pulsesink {}",
        quote(path),
        sink_prop
    )))
}

fn shell_command(command: String) -> Command {
    let mut child = Command::new("bash");
    child.args(["-lc", &command]);
    child
}

fn quote(value: impl Into<String>) -> String {
    escape(Cow::Owned(value.into())).into_owned()
}

fn push_recent_audio(buffer: &Arc<Mutex<Vec<u8>>>, bytes: &[u8]) {
    if let Ok(mut ring) = buffer.lock() {
        ring.extend_from_slice(bytes);
        if ring.len() > MIC_REPLAY_MAX_BYTES {
            let overflow = ring.len() - MIC_REPLAY_MAX_BYTES;
            ring.drain(0..overflow);
        }
    }
}

fn update_microphone_level(level: &Arc<Mutex<f64>>, bytes: &[u8]) {
    let peak = bytes
        .chunks_exact(2)
        .map(|chunk| i16::from_le_bytes([chunk[0], chunk[1]]).unsigned_abs() as f64)
        .fold(0.0, f64::max)
        / i16::MAX as f64;
    if let Ok(mut meter) = level.lock() {
        *meter = peak.clamp(0.0, 1.0);
    }
}

fn build_wav_bytes(audio: &[u8], sample_rate: u32, channels: u16, bits_per_sample: u16) -> Vec<u8> {
    let block_align = channels * (bits_per_sample / 8);
    let byte_rate = sample_rate * block_align as u32;
    let data_len = audio.len() as u32;
    let riff_len = 36 + data_len;

    let mut wav = Vec::with_capacity(44 + audio.len());
    wav.extend_from_slice(b"RIFF");
    wav.extend_from_slice(&riff_len.to_le_bytes());
    wav.extend_from_slice(b"WAVE");
    wav.extend_from_slice(b"fmt ");
    wav.extend_from_slice(&16u32.to_le_bytes());
    wav.extend_from_slice(&1u16.to_le_bytes());
    wav.extend_from_slice(&channels.to_le_bytes());
    wav.extend_from_slice(&sample_rate.to_le_bytes());
    wav.extend_from_slice(&byte_rate.to_le_bytes());
    wav.extend_from_slice(&block_align.to_le_bytes());
    wav.extend_from_slice(&bits_per_sample.to_le_bytes());
    wav.extend_from_slice(b"data");
    wav.extend_from_slice(&data_len.to_le_bytes());
    wav.extend_from_slice(audio);
    wav
}
