impl MicrophoneCapture {
    pub fn new() -> Result<Self> {
        Self::new_with_source_and_env(None, true)
    }

    pub fn new_with_source(source_override: Option<&str>) -> Result<Self> {
        Self::new_with_source_and_env(source_override, true)
    }

    pub fn new_default_source() -> Result<Self> {
        Self::new_with_source_and_env(None, false)
    }

    pub fn new_with_source_options(
        source_override: Option<&str>,
        codec: UpstreamAudioCodec,
        noise_suppression: bool,
    ) -> Result<Self> {
        Self::new_with_source_env_and_options(source_override, true, codec, noise_suppression)
    }

    pub fn new_default_source_options(
        codec: UpstreamAudioCodec,
        noise_suppression: bool,
    ) -> Result<Self> {
        Self::new_with_source_env_and_options(None, false, codec, noise_suppression)
    }

    /// Keeps `new_with_source_and_env` explicit because it sits on microphone capture setup, where host audio stacks expose different source names and latency controls.
    /// Inputs are the typed parameters; output is the return value or side effect.
    fn new_with_source_and_env(
        source_override: Option<&str>,
        allow_env_source: bool,
    ) -> Result<Self> {
        Self::new_with_source_env_and_options(
            source_override,
            allow_env_source,
            crate::input::audio_codec::requested_upstream_audio_codec_from_env(),
            mic_noise_suppression_from_env(),
        )
    }

    /// Build a microphone capture pipeline with explicit uplink codec/noise choices.
    ///
    /// Inputs: selected source, audio codec, and noise suppression flag. Output:
    /// a live capture object. Why: live UI changes must rebuild only the mic
    /// path while preserving the established camera/session transport.
    fn new_with_source_env_and_options(
        source_override: Option<&str>,
        allow_env_source: bool,
        codec: UpstreamAudioCodec,
        noise_suppression: bool,
    ) -> Result<Self> {
        gst::init().ok(); // idempotent

        /* preferred path: pipewiresrc; fallback: pulsesrc ----------------*/
        let selected_source = source_override.map(str::to_string).or_else(|| {
            allow_env_source
                .then(|| std::env::var("LESAVKA_MIC_SOURCE").ok())
                .flatten()
        });
        let source_desc = match selected_source {
            Some(s) if !s.is_empty() => match Self::resolve_source_desc(&s) {
                Some(desc) => desc,
                None => {
                    if explicit_media_sources_required() {
                        bail!(
                            "requested mic '{s}' was not found; refusing to use default because {REQUIRE_EXPLICIT_MEDIA_SOURCES_ENV}=1"
                        );
                    }
                    warn!("🎤 requested mic '{s}' not found; using default");
                    Self::default_source_desc()
                }
            },
            _ => Self::default_source_desc(),
        };
        debug!("🎤 source: {source_desc}");
        let gain = mic_gain_from_env();
        let level_tap_path = mic_level_tap_path();
        let desc =
            microphone_pipeline_desc(&source_desc, gain, level_tap_path.is_some(), noise_suppression);

        let pipeline: gst::Pipeline = gst::parse::launch(&desc)?.downcast().expect("pipeline");
        let sink: gst_app::AppSink = pipeline.by_name("asink").unwrap().downcast().unwrap();
        let volume = pipeline
            .by_name("mic_input_gain")
            .context("missing mic_input_gain volume")?;

        #[cfg(not(coverage))]
        {
            /* ─── bus for diagnostics ───────────────────────────────────────*/
            let bus = pipeline.bus().unwrap();
            std::thread::spawn(move || {
                use gst::MessageView::*;
                for msg in bus.iter_timed(gst::ClockTime::NONE) {
                    match msg.view() {
                        StateChanged(s)
                            if s.current() == gst::State::Playing
                                && msg.src().map(|s| s.is::<gst::Pipeline>()).unwrap_or(false) =>
                        {
                            info!("🎤 mic pipeline ▶️")
                        }
                        Error(e) => error!(
                            "🎤💥 mic: {} ({})",
                            e.error(),
                            e.debug().unwrap_or_default()
                        ),
                        Warning(w) => warn!(
                            "🎤⚠️ mic: {} ({})",
                            w.error(),
                            w.debug().unwrap_or_default()
                        ),
                        _ => {}
                    }
                }
            });
        }

        if let Err(err) = pipeline.set_state(gst::State::Playing) {
            let _ = pipeline.set_state(gst::State::Null);
            return Err(err).context("start mic pipeline");
        }
        maybe_spawn_mic_gain_control(volume);
        let level_tap_running = if let Some(path) = level_tap_path {
            let level_sink = pipeline
                .by_name("level_sink")
                .context("missing microphone level tap appsink")?
                .downcast::<gst_app::AppSink>()
                .expect("microphone level tap appsink");
            Some(spawn_mic_level_tap(level_sink, path))
        } else {
            None
        };

        let audio_encoder = match codec {
            UpstreamAudioCodec::PcmS16le => None,
            UpstreamAudioCodec::Opus => {
                match crate::input::audio_codec::OpusPacketEncoder::new() {
                    Ok(encoder) => {
                        info!("🎤 upstream microphone Opus encoder enabled");
                        Some(encoder)
                    }
                    Err(err) => {
                        warn!(
                            "🎤⚠️ Opus requested but unavailable ({err:#}); falling back to PCM"
                        );
                        None
                    }
                }
            }
        };

        Ok(Self {
            pipeline,
            sink,
            level_tap_running,
            pts_rebaser: crate::live_capture_clock::DurationPacedSourcePtsRebaser::default(),
            pending_packets: Mutex::default(),
            audio_encoder: Mutex::new(audio_encoder),
        })
    }

    /// Blocking pull; call from an async wrapper
    pub fn pull(&self) -> Option<AudioPacket> {
        if let Some(packet) = self.pending_packets.lock().ok()?.pop_front() {
            return self.encode_for_transport(packet);
        }
        match self.sink.pull_sample() {
            Ok(sample) => {
                let buf = sample.buffer().unwrap();
                let map = buf.map_readable().unwrap();
                let source_pts_us = buf.pts().map(|ts| ts.nseconds() / 1_000);
                let packet_duration_us = buffer_duration_us(buf, map.len());
                let timing = self.pts_rebaser.rebase_with_packet_duration(
                    source_pts_us,
                    packet_duration_us,
                    crate::live_capture_clock::upstream_source_lag_cap(),
                );
                if timing.lag_clamped {
                    log_microphone_lag_clamped_source(timing, map.len());
                }
                let pts = timing.packet_pts_us;
                let target_bytes = mic_packet_target_bytes();
                let mut packets = split_audio_sample(pts, map.as_slice(), target_bytes);
                let packet_count = packets.len();
                let first_packet = packets.pop_front();
                #[cfg(not(coverage))]
                {
                    static CNT: AtomicU64 = AtomicU64::new(0);
                    let n = CNT.fetch_add(1, Ordering::Relaxed);
                    if crate::live_capture_clock::upstream_timing_trace_enabled()
                        && (n < 10 || n.is_multiple_of(300))
                    {
                        info!(
                            packet_index = n,
                            source_pts_us = timing.source_pts_us.unwrap_or_default(),
                            source_base_us = timing.source_base_us.unwrap_or_default(),
                            capture_base_us = timing.capture_base_us.unwrap_or_default(),
                            capture_now_us = timing.capture_now_us,
                            packet_pts_us = timing.packet_pts_us,
                            pull_path_delay_us =
                                timing.capture_now_us as i128 - timing.packet_pts_us as i128,
                            used_source_pts = timing.used_source_pts,
                            lag_clamped = timing.lag_clamped,
                            lead_clamped = timing.lead_clamped,
                            bytes = map.len(),
                            packet_duration_us,
                            split_packets = packet_count,
                            target_packet_bytes = target_bytes,
                            "🎤 upstream microphone timing sample"
                        );
                    }
                    if n < 10 || n.is_multiple_of(300) {
                        trace!(
                            "🎤⇧ cli sample#{n} {} bytes -> {} packet(s)",
                            map.len(),
                            packet_count
                        );
                    }
                }
                if !packets.is_empty()
                    && let Ok(mut pending) = self.pending_packets.lock()
                {
                    pending.extend(packets);
                }
                first_packet.and_then(|packet| self.encode_for_transport(packet))
            }
            Err(_) => None,
        }
    }

    fn encode_for_transport(&self, packet: AudioPacket) -> Option<AudioPacket> {
        let Some(mut guard) = self.audio_encoder.lock().ok() else {
            return Some(packet);
        };
        let Some(encoder) = guard.as_mut() else {
            return Some(packet);
        };
        match encoder.encode_packet(packet.clone()) {
            Ok(Some(encoded)) => Some(encoded),
            Ok(None) => None,
            Err(err) => {
                warn!("🎤⚠️ Opus encode failed; sending PCM fallback for this packet: {err:#}");
                Some(packet)
            }
        }
    }

    /// Resolve launcher-selected mic names while preserving Pulse catalog routing.
    fn resolve_source_desc(fragment: &str) -> Option<String> {
        if looks_like_pulse_source_name(fragment)
            && let Some(full) = Self::pulse_source_by_substr(fragment)
        {
            return Some(Self::pulse_source_desc(Some(&full)));
        }
        if Self::pipewire_source_available()
            && let Some(full) = Self::pipewire_source_by_substr(fragment)
        {
            return Some(Self::pipewire_source_desc(Some(&full)));
        }
        Self::pulse_source_by_substr(fragment).map(|full| Self::pulse_source_desc(Some(&full)))
    }

    /// Keeps `pipewire_source_available` explicit because it sits on microphone capture setup, where host audio stacks expose different source names and latency controls.
    /// Inputs are the typed parameters; output is the return value or side effect.
    fn pipewire_source_available() -> bool {
        #[cfg(coverage)]
        if std::env::var("LESAVKA_MIC_DISABLE_PIPEWIRE").is_ok() {
            return false;
        }
        gst::ElementFactory::find("pipewiresrc").is_some()
    }

    /// Keeps `pipewire_source_desc` explicit because it sits on microphone capture setup, where host audio stacks expose different source names and latency controls.
    /// Inputs are the typed parameters; output is the return value or side effect.
    fn pipewire_source_desc(source: Option<&str>) -> String {
        match source {
            Some(source) if !source.trim().is_empty() => {
                format!(
                    "pipewiresrc target-object={} do-timestamp=true",
                    escape(source.to_string().into())
                )
            }
            _ => "pipewiresrc do-timestamp=true".to_string(),
        }
    }

    /// Keeps `pulse_source_desc` explicit because it sits on microphone capture setup, where host audio stacks expose different source names and latency controls.
    /// Inputs are the typed parameters; output is the return value or side effect.
    fn pulse_source_desc(source: Option<&str>) -> String {
        let buffer_time_us = mic_pulse_buffer_time_us();
        let latency_time_us = mic_pulse_latency_time_us().min(buffer_time_us);
        match source {
            Some(source) if !source.trim().is_empty() => {
                format!(
                    "pulsesrc device={} do-timestamp=true buffer-time={buffer_time_us} latency-time={latency_time_us}",
                    escape(source.to_string().into())
                )
            }
            _ => {
                format!(
                    "pulsesrc do-timestamp=true buffer-time={buffer_time_us} latency-time={latency_time_us}"
                )
            }
        }
    }

    /// Keeps `pipewire_source_by_substr` explicit because it sits on microphone capture setup, where host audio stacks expose different source names and latency controls.
    /// Inputs are the typed parameters; output is the return value or side effect.
    fn pipewire_source_by_substr(fragment: &str) -> Option<String> {
        let out = std::process::Command::new("pw-dump").output().ok()?;
        let list = serde_json::from_slice::<serde_json::Value>(&out.stdout).ok()?;
        let objects = list.as_array()?;
        objects.iter().find_map(|object| {
            let props = object.get("info")?.get("props")?.as_object()?;
            if props.get("media.class")?.as_str()? != "Audio/Source" {
                return None;
            }
            let name = props
                .get("node.name")
                .or_else(|| props.get("node.nick"))?
                .as_str()?;
            if name.contains(fragment) && !name.ends_with(".monitor") {
                Some(name.to_owned())
            } else {
                None
            }
        })
    }

    /// Keeps `pulse_source_by_substr` explicit because it sits on microphone capture setup, where host audio stacks expose different source names and latency controls.
    /// Inputs are the typed parameters; output is the return value or side effect.
    fn pulse_source_by_substr(fragment: &str) -> Option<String> {
        use std::process::Command;
        let out = Command::new("pactl")
            .args(["list", "short", "sources"])
            .output()
            .ok()?;
        let list = String::from_utf8_lossy(&out.stdout);
        list.lines().find_map(|ln| {
            let mut cols = ln.split_whitespace();
            let _id = cols.next()?;
            let name = cols.next()?; // column #1
            if name.contains(fragment) {
                Some(name.to_owned())
            } else {
                None
            }
        })
    }

    /// Keeps `default_source_desc` explicit because it sits on microphone capture setup, where host audio stacks expose different source names and latency controls.
    /// Inputs are the typed parameters; output is the return value or side effect.
    fn default_source_desc() -> String {
        #[cfg(coverage)]
        if let Ok(source) = std::env::var("LESAVKA_MIC_TEST_SOURCE_DESC")
            && !source.trim().is_empty()
        {
            return source;
        }
        if Self::pipewire_source_available() {
            return Self::pipewire_source_desc(None);
        }
        Self::pulse_source_desc(None)
    }
}
