pub fn quality_probe_command() -> &'static str {
    "scripts/ci/platform_quality_gate.sh"
}

fn capture_profile_label(capture: &CaptureSizeChoice, stream_caps_label: &str) -> String {
    if let Some((width, height, fps)) = parse_stream_caps_profile(stream_caps_label) {
        return format!(
            "{} | observed {}x{} @ {} fps | bitrate est ~{} kbit",
            capture.preset.label(),
            width,
            height,
            fps,
            capture.max_bitrate_kbit
        );
    }
    format!(
        "{} | {}x{} | {} fps | bitrate est ~{} kbit",
        capture.preset.label(),
        capture.width,
        capture.height,
        capture.fps,
        capture.max_bitrate_kbit
    )
}

fn parse_stream_caps_profile(caps: &str) -> Option<(u32, u32, u32)> {
    let width = parse_caps_u32(caps, "width=(int)")?;
    let height = parse_caps_u32(caps, "height=(int)")?;
    let fps = parse_caps_fraction_numerator(caps, "framerate=(fraction)")?;
    Some((width, height, fps))
}

fn parse_caps_u32(caps: &str, needle: &str) -> Option<u32> {
    let start = caps.find(needle)? + needle.len();
    let tail = &caps[start..];
    let end = tail.find([',', ';']).unwrap_or(tail.len());
    tail[..end].trim().parse::<u32>().ok()
}

fn parse_caps_fraction_numerator(caps: &str, needle: &str) -> Option<u32> {
    let start = caps.find(needle)? + needle.len();
    let tail = &caps[start..];
    let end = tail.find([',', ';']).unwrap_or(tail.len());
    let value = tail[..end].trim();
    let numerator = value.split('/').next()?;
    numerator.parse::<u32>().ok()
}

fn recommendations_for(state: &LauncherState, log: &DiagnosticsLog) -> Vec<String> {
    let mut items = Vec::new();
    let hardware_decode_active = log.latest().is_some_and(sample_uses_hardware_decode);
    let software_decode_active = log.latest().is_some_and(sample_uses_software_decode);
    if !state.server_available {
        items.push(
            "The server is not reachable from this launcher yet, so stream-quality results would not be meaningful."
                .to_string(),
        );
    }
    if log.is_empty() {
        items.push(
            "Live stream samples will appear here after the launcher collects a few probe windows. Leave the relay up for a few seconds to populate RTT, probe spread, loss, and fps."
                .to_string(),
        );
    }
    if let Some(sample) = log.latest() {
        if sample.probe_loss_pct >= 3.0 || sample.probe_spread_ms >= 18.0 {
            items.push(
                "Control-plane probe spread or loss is elevated. That can come from the network or from server stalls, so compare it against the eye fps before blaming the WAN."
                    .to_string(),
            );
        }
        if sample.video_loss_pct >= 2.0 || sample.dropped_frames > 0 {
            items.push(
                "Video packets are arriving with gaps or server-side drops. Stay on device H.264 pass-through for now and reduce concurrent load before trying more invasive changes."
                    .to_string(),
            );
        }
        if sample.left_present_fps + 1.0 < sample.left_receive_fps
            || sample.right_present_fps + 1.0 < sample.right_receive_fps
        {
            items.push(if hardware_decode_active {
                "The client is receiving more frames than it is presenting. That points at local decode/render pressure, so prefer lighter breakout sizes or a cheaper source mode before adding bitrate."
                    .to_string()
            } else {
                "The client is receiving more frames than it is presenting. That points at local decode/render pressure, so prefer lighter breakout sizes or hardware decode."
                    .to_string()
            });
        }
        if (sample.left_present_gap_peak_ms - sample.left_packet_gap_peak_ms) > 40.0
            || (sample.right_present_gap_peak_ms - sample.right_packet_gap_peak_ms) > 40.0
        {
            items.push(
                "Present-gap spikes are materially larger than packet-gap spikes. That usually means the client decode/render path is stalling after packets arrive."
                    .to_string(),
            );
        }
        if sample.queue_depth > 8 {
            items.push(
                "The preview queue is backing up. When queue depth climbs, expect laggy mouse feel and delayed visual response even if raw fps still looks okay."
                    .to_string(),
            );
        }
        if sample.left_queue_peak >= 4 || sample.right_queue_peak >= 4 {
            items.push(
                "Queue depth is spiking even if the latest sample looks calm. That points at bursty backpressure rather than steady-state overload."
                    .to_string(),
            );
        }
        if (sample.left_packet_gap_peak_ms - sample.left_server_send_gap_peak_ms) > 60.0
            || (sample.right_packet_gap_peak_ms - sample.right_server_send_gap_peak_ms) > 60.0
        {
            items.push(
                "Client packet-gap spikes are much larger than the server's send-gap peaks. That points away from the server pipeline and toward network burstiness or client-side receive scheduling."
                    .to_string(),
            );
        }
        if sample.left_server_source_gap_peak_ms >= 120.0
            || sample.right_server_source_gap_peak_ms >= 120.0
        {
            items.push(
                "The server is seeing large source-frame gaps before packets even leave the box. That points at capture cadence or server-side pipeline stalls more than WAN loss."
                    .to_string(),
            );
        }
        if sample.left_server_queue_peak >= 4 || sample.right_server_queue_peak >= 4 {
            items.push(
                "The server-side stream queue is peaking above its steady state. That suggests bursty backpressure is already forming before the client sees it."
                    .to_string(),
            );
        }
        if sample.upstream_camera.enabled
            && (sample.upstream_camera.latest_enqueue_age_ms >= 120.0
                || sample.upstream_camera.enqueue_age_peak_ms >= 250.0
                || sample.upstream_camera.enqueue_block_peak_ms >= 40.0)
        {
            items.push(
                "The webcam uplink queue is aging packets before they even leave the client. That points at backlog inside the relay child, so freshness-first dropping should help more than extra bitrate."
                    .to_string(),
            );
        }
        if sample.upstream_microphone.enabled
            && (sample.upstream_microphone.latest_enqueue_age_ms >= 80.0
                || sample.upstream_microphone.enqueue_age_peak_ms >= 180.0
                || sample.upstream_microphone.enqueue_block_peak_ms >= 25.0)
        {
            items.push(
                "The microphone uplink queue is aging live audio before transport. That is a direct path to lip-sync drift, so stale audio needs to be dropped instead of drained."
                    .to_string(),
            );
        }
        if sample.upstream_camera.reconnect_count >= 3 || sample.upstream_microphone.reconnect_count >= 3
        {
            items.push(
                "The upstream media loops have already reconnected several times during this session. Treat repeated connect churn as part of the lag budget, not just a cosmetic log problem."
                    .to_string(),
            );
        }
        if sample.upstream_camera.enabled
            && (sample.upstream_camera.delivery_age_peak_ms >= 250.0
                || sample.upstream_camera.dropped_queue_full_packets > 0
                || sample.upstream_camera.dropped_stale_packets > 0)
        {
            items.push(
                "The webcam uplink is now choosing freshness over completeness. If delivery age or stale-drop counts keep climbing, the source path is outrunning the live-call budget and needs more than bitrate tweaks."
                    .to_string(),
            );
        }
        if sample.upstream_microphone.enabled
            && (sample.upstream_microphone.delivery_age_peak_ms >= 180.0
                || sample.upstream_microphone.dropped_queue_full_packets > 0
                || sample.upstream_microphone.dropped_stale_packets > 0)
        {
            items.push(
                "The microphone uplink is dropping or aging live chunks to stay current. A few drops are healthier than minutes of lag, but persistent churn means the upstream path is still unstable."
                    .to_string(),
            );
        }
        if sample.client_process_cpu_pct >= 85.0 {
            items.push(if hardware_decode_active {
                "Client process CPU is high even though hardware decode is active. If motion still looks rough, favor lighter breakout layouts or a cheaper source mode before adding more bitrate."
                    .to_string()
            } else {
                "Client process CPU is high. If motion still looks rough, favor lighter breakout layouts or a hardware decoder before adding more bitrate."
                    .to_string()
            });
        }
        if sample.server_process_cpu_pct >= 85.0 {
            items.push(
                "Server process CPU is high. On current hardware that is a strong reason to stay on device H.264 pass-through and avoid any server-side eye transcoding."
                    .to_string(),
            );
        }
    }
    let source_passthrough = state
        .feed_sources
        .iter()
        .any(|preset| !matches!(preset, FeedSourcePreset::Off));
    if source_passthrough {
        items.push(
            "Device H.264 pass-through is active. On current GC311 hardware, prefer the real 1080p/720p source modes. The lower SD/VGA modes are intentionally retired because they center-cut widescreen HDMI sources."
                .to_string(),
        );
    }
    if let Some(sample) = log.latest()
        && sample.video_loss_pct < 0.5
        && sample.dropped_frames == 0
        && ((sample.left_server_fps - sample.left_receive_fps) > 6.0
            || (sample.right_server_fps - sample.right_receive_fps) > 6.0)
    {
        items.push(
            "Receive fps is well below the target without packet loss. That usually points at source cadence or local decode pressure more than WAN loss."
                .to_string(),
        );
    }
    if let Some(sample) = log.latest()
        && sample.video_loss_pct < 0.5
        && sample.dropped_frames == 0
        && (sample.left_packet_gap_peak_ms >= 140.0 || sample.right_packet_gap_peak_ms >= 140.0)
    {
        items.push(
            "Packet-gap spikes are high without packet loss. That means the stream is arriving in bursts, which usually points at source cadence, encoder stalls, or local decoder starvation more than raw WAN loss."
                .to_string(),
        );
    }
    if let Some(sample) = log.latest()
        && software_decode_active
        && ((sample.left_decoder_label.contains("avdec")
            && sample.left_present_fps + 1.0 < sample.left_receive_fps)
            || (sample.right_decoder_label.contains("avdec")
                && sample.right_present_fps + 1.0 < sample.right_receive_fps))
    {
        items.push(
            "At least one eye is falling back to `avdec_*` while presentation lags behind receive. A hardware decode path would likely help more than extra bitrate."
                .to_string(),
        );
    }
    if let Some(sample) = log.latest()
        && sample.server_process_cpu_pct >= 70.0
        && (sample.left_server_encoder_label.contains("x264")
            || sample.right_server_encoder_label.contains("x264"))
    {
        items.push(
            "At least one eye is still leaning on `x264enc`. That is now unexpected on the source-first path, so treat it as a bug or stale install rather than a normal operating mode."
                .to_string(),
        );
    }
    if state.breakout_count() == 2 {
        items.push(
            "Both eye feeds are broken out right now. If the client starts struggling, compare in-launcher preview smoothness against full-window decode."
                .to_string(),
        );
    }
    if items.is_empty() {
        items.push("Session state looks stable. Collect a few real samples before changing capture settings.".to_string());
    }
    items
}

fn sample_uses_hardware_decode(sample: &PerformanceSample) -> bool {
    decoder_label_is_hardware(&sample.left_decoder_label)
        || decoder_label_is_hardware(&sample.right_decoder_label)
}

fn sample_uses_software_decode(sample: &PerformanceSample) -> bool {
    sample.left_decoder_label.contains("avdec") || sample.right_decoder_label.contains("avdec")
}

fn decoder_label_is_hardware(label: &str) -> bool {
    let lower = label.to_ascii_lowercase();
    lower.contains("nvh264dec")
        || lower.contains("nvdec")
        || lower.contains("vulkanh264dec")
        || lower.contains("vah264dec")
        || lower.contains("vaapih264dec")
        || lower.contains("v4l2slh264dec")
        || lower.contains("d3d11")
        || lower.contains("vtdec")
}
