use super::*;

/// Return elapsed probe time in microseconds on the synthetic capture clock.
///
/// Inputs: probe monotonic start instant.
/// Outputs: saturating microsecond timestamp.
/// Why: send-age telemetry must share the same origin as the generated media
/// PTS so local queue age is not inflated by setup work.
pub(super) fn probe_elapsed_us(probe_start: Instant) -> u64 {
    probe_start.elapsed().as_micros().min(u64::MAX as u128) as u64
}

/// Calculate client-local queue age for one packet.
///
/// Inputs: packet capture PTS and send PTS in microseconds.
/// Outputs: saturating age in milliseconds.
/// Why: the server freshness budget drops already-stale bundles before they can
/// distort RCT sync evidence.
pub(super) fn packet_age_ms(capture_pts_us: u64, send_pts_us: u64) -> u32 {
    (send_pts_us.saturating_sub(capture_pts_us) / 1_000)
        .try_into()
        .unwrap_or(u32::MAX)
}

/// Append raw audio bytes when a debug dump is requested.
///
/// Inputs: optional dump file and one stamped audio packet.
/// Outputs: best-effort write to the dump file.
/// Why: audio decoding failures in the RCT analyzer are easier to debug when
/// the exact sent PCM can be inspected.
pub(super) fn write_probe_audio_dump(file: Option<&mut File>, packet: &AudioPacket) {
    if let Some(file) = file {
        let _ = file.write_all(&packet.data);
    }
}

/// Write one client-side bundled send telemetry row.
///
/// Inputs: optional log file, bundle sequence, probe clock, and media packets.
/// Outputs: JSONL sidecar used by manual transport probes.
/// Why: when RCT freshness fails, this shows whether the client yielded stale
/// synthetic media or whether delay accumulated after gRPC accepted the bundle.
pub(super) fn write_probe_send_log(
    file: Option<&mut File>,
    bundle_seq: u64,
    probe_start: Instant,
    video: Option<&VideoPacket>,
    audio: &[AudioPacket],
) {
    let Some(file) = file else {
        return;
    };
    let send_elapsed_us = probe_elapsed_us(probe_start);
    let unix_ns = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|duration| duration.as_nanos().min(u64::MAX as u128) as u64)
        .unwrap_or_default();
    let video_pts_us = video.map(packet_video_capture_pts_us);
    let video_bytes = video.map(|packet| packet.data.len()).unwrap_or_default();
    let audio_first_pts_us = audio.first().map(packet_audio_capture_pts_us);
    let audio_last_pts_us = audio.last().map(packet_audio_capture_pts_us);
    let audio_bytes: usize = audio.iter().map(|packet| packet.data.len()).sum();
    let max_capture_pts_us = video_pts_us
        .into_iter()
        .chain(audio_last_pts_us)
        .max()
        .unwrap_or_default();
    let local_age_ms = send_elapsed_us.saturating_sub(max_capture_pts_us) as f64 / 1000.0;
    let _ = writeln!(
        file,
        "{{\"schema\":\"lesavka.sync-probe-send.v1\",\"bundle_seq\":{},\"send_unix_ns\":{},\"send_elapsed_us\":{},\"video_capture_pts_us\":{},\"video_bytes\":{},\"audio_packets\":{},\"audio_first_capture_pts_us\":{},\"audio_last_capture_pts_us\":{},\"audio_bytes\":{},\"local_age_ms\":{:.3}}}",
        bundle_seq,
        unix_ns,
        send_elapsed_us,
        optional_u64_json(video_pts_us),
        video_bytes,
        audio.len(),
        optional_u64_json(audio_first_pts_us),
        optional_u64_json(audio_last_pts_us),
        audio_bytes,
        local_age_ms,
    );
}

fn optional_u64_json(value: Option<u64>) -> String {
    value
        .map(|value| value.to_string())
        .unwrap_or_else(|| "null".to_string())
}

/// Keep only the newest bounded audio packets for the next video bundle.
///
/// Inputs: mutable pending audio packet list.
/// Outputs: list trimmed in place.
/// Why: stale audio continuity is less useful than preserving a live capture
/// relationship with the next flash frame.
pub(super) fn retain_newest_probe_audio(pending_audio: &mut Vec<AudioPacket>) {
    if pending_audio.len() > PROBE_BUNDLE_MAX_AUDIO_PACKETS {
        let dropped = pending_audio.len() - PROBE_BUNDLE_MAX_AUDIO_PACKETS;
        pending_audio.drain(..dropped);
    }
}

/// Retain only audio packets close enough to travel with one video frame.
///
/// Inputs: pending audio packet list and video capture PTS.
/// Outputs: list filtered and capped in place.
/// Why: client->server sync testing should send physically plausible bundles,
/// not arbitrary pending audio that only happened to be in memory.
pub(super) fn retain_probe_audio_for_video(
    pending_audio: &mut Vec<AudioPacket>,
    video_pts_us: u64,
) {
    let min_pts = video_pts_us.saturating_sub(PROBE_BUNDLE_AUDIO_WINDOW_BEFORE_US);
    let max_pts = video_pts_us.saturating_add(PROBE_BUNDLE_AUDIO_WINDOW_AFTER_US);
    pending_audio.retain(|packet| {
        let pts = packet_audio_capture_pts_us(packet);
        pts >= min_pts && pts <= max_pts
    });
    retain_newest_probe_audio(pending_audio);
}

/// Return capture PTS bounds for one outgoing bundle.
///
/// Inputs: optional video packet and audio packets.
/// Outputs: `(start_us, end_us)` capture span for the bundle message.
/// Why: the server uses this span as a plausibility check before trusting the
/// client capture clock for lip-sync scheduling.
pub(super) fn probe_bundle_capture_bounds(
    video: Option<&VideoPacket>,
    audio: &[AudioPacket],
) -> (u64, u64) {
    let mut start = u64::MAX;
    let mut end = 0_u64;
    if let Some(video) = video {
        let pts = packet_video_capture_pts_us(video);
        start = start.min(pts);
        end = end.max(pts);
    }
    for packet in audio {
        let pts = packet_audio_capture_pts_us(packet);
        start = start.min(pts);
        end = end.max(pts);
    }
    if start == u64::MAX {
        let now = crate::live_capture_clock::capture_pts_us();
        return (now, now);
    }
    (start, end.max(start))
}

/// Read the capture PTS from an audio packet sidecar or legacy PTS field.
///
/// Inputs: one audio packet.
/// Outputs: capture timestamp in microseconds.
/// Why: old callers may only populate `pts`, while the bundled path prefers the
/// explicit client capture sidecar.
pub(super) fn packet_audio_capture_pts_us(packet: &AudioPacket) -> u64 {
    if packet.client_capture_pts_us == 0 {
        packet.pts
    } else {
        packet.client_capture_pts_us
    }
}

/// Read the capture PTS from a video packet sidecar or legacy PTS field.
///
/// Inputs: one video packet.
/// Outputs: capture timestamp in microseconds.
/// Why: the probe sender and server both need one stable clock field while
/// older test packets may still use `pts` directly.
pub(super) fn packet_video_capture_pts_us(packet: &VideoPacket) -> u64 {
    if packet.client_capture_pts_us == 0 {
        packet.pts
    } else {
        packet.client_capture_pts_us
    }
}

/// Open an optional debug sidecar path named by an environment variable.
///
/// Inputs: environment variable name.
/// Outputs: `None` when unset, otherwise a writable file handle.
/// Why: manual transport probes need rich artifacts, but normal probe runs
/// should not pay for debug file creation unless explicitly requested.
pub(super) fn open_debug_dump(env_var: &str) -> Result<Option<File>> {
    let Some(path) = std::env::var_os(env_var) else {
        return Ok(None);
    };
    let path = PathBuf::from(path);
    let file = File::create(&path)
        .with_context(|| format!("creating debug dump at {}", path.display()))?;
    Ok(Some(file))
}
