const MEDIA_V2_DEFAULT_PLAYOUT_DELAY_MS: u64 = 20;
const MEDIA_V2_DEFAULT_MAX_LIVE_AGE_MS: u64 = 1_000;
const MEDIA_V2_DEFAULT_UAC_START_TIMEOUT_MS: u64 = 750;
const MEDIA_V2_DEFAULT_STREAM_IDLE_TIMEOUT_MS: u64 = 5_000;
const MEDIA_V2_DEFAULT_AUDIO_STARVATION_HEAL_MS: u64 = 1_500;
const MEDIA_V2_DEFAULT_AV_SKEW_HEAL_MS: u64 = 1_500;
const MEDIA_V2_MAX_MIXED_CAPTURE_SPAN_US: u64 = 250_000;

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
struct MediaV2BundleFacts {
    has_audio: bool,
    has_video: bool,
    capture_start_us: u64,
    capture_end_us: u64,
    capture_span_us: u64,
    max_queue_age_ms: u32,
}

#[derive(Clone, Copy, Debug)]
struct MediaV2HandoffSchedule {
    audio_due_at: Option<tokio::time::Instant>,
    video_due_at: Option<tokio::time::Instant>,
    common_delay: Duration,
    relative_audio_delay: Duration,
    relative_video_delay: Duration,
}

#[cfg(not(coverage))]
struct MediaV2ScheduledAudio {
    packets: Vec<AudioPacket>,
    due_at: tokio::time::Instant,
}

#[cfg(not(coverage))]
#[derive(Clone, Debug)]
struct MediaV2ScheduledVideo {
    packet: VideoPacket,
    due_at: tokio::time::Instant,
    received_at: tokio::time::Instant,
}

/// Keeps `summarize_media_v2_bundle` explicit because it sits on relay RPC orchestration, where hardware failures must surface without stopping the server.
/// Inputs are the typed parameters; output is the return value or side effect.
fn summarize_media_v2_bundle(bundle: &UpstreamMediaBundle) -> Option<MediaV2BundleFacts> {
    let mut capture_start_us = u64::MAX;
    let mut capture_end_us = 0_u64;
    let mut max_queue_age_ms = 0_u32;
    let has_video = bundle.video.is_some();
    let has_audio = !bundle.audio.is_empty();
    if let Some(video) = bundle.video.as_ref() {
        let pts = packet_video_capture_pts_us(video);
        capture_start_us = capture_start_us.min(pts);
        capture_end_us = capture_end_us.max(pts);
        max_queue_age_ms = max_queue_age_ms.max(video.client_queue_age_ms);
    }
    for audio in &bundle.audio {
        let pts = packet_audio_capture_pts_us(audio);
        capture_start_us = capture_start_us.min(pts);
        capture_end_us = capture_end_us.max(pts);
        max_queue_age_ms = max_queue_age_ms.max(audio.client_queue_age_ms);
    }
    if !has_audio && !has_video {
        return None;
    }
    if capture_start_us == u64::MAX {
        capture_start_us = 0;
    }
    Some(MediaV2BundleFacts {
        has_audio,
        has_video,
        capture_start_us,
        capture_end_us,
        capture_span_us: capture_end_us.saturating_sub(capture_start_us),
        max_queue_age_ms,
    })
}

/// Keeps `packet_audio_capture_pts_us` explicit because it sits on relay RPC orchestration, where hardware failures must surface without stopping the server.
/// Inputs are the typed parameters; output is the return value or side effect.
fn packet_audio_capture_pts_us(packet: &AudioPacket) -> u64 {
    if packet.client_capture_pts_us == 0 {
        packet.pts
    } else {
        packet.client_capture_pts_us
    }
}

/// Keeps `packet_video_capture_pts_us` explicit because it sits on relay RPC orchestration, where hardware failures must surface without stopping the server.
/// Inputs are the typed parameters; output is the return value or side effect.
fn packet_video_capture_pts_us(packet: &VideoPacket) -> u64 {
    if packet.client_capture_pts_us == 0 {
        packet.pts
    } else {
        packet.client_capture_pts_us
    }
}

fn media_v2_playout_delay() -> Duration {
    std::env::var("LESAVKA_UPSTREAM_V2_PLAYOUT_DELAY_MS")
        .or_else(|_| std::env::var("LESAVKA_UPSTREAM_BUNDLED_PLAYOUT_DELAY_MS"))
        .ok()
        .and_then(|value| value.trim().parse::<u64>().ok())
        .map(Duration::from_millis)
        .unwrap_or_else(|| Duration::from_millis(MEDIA_V2_DEFAULT_PLAYOUT_DELAY_MS))
}

fn media_v2_max_live_age() -> Duration {
    std::env::var("LESAVKA_UPSTREAM_V2_MAX_LIVE_AGE_MS")
        .or_else(|_| std::env::var("LESAVKA_UPSTREAM_MAX_LIVE_LAG_MS"))
        .ok()
        .and_then(|value| value.trim().parse::<u64>().ok())
        .map(Duration::from_millis)
        .unwrap_or_else(|| Duration::from_millis(MEDIA_V2_DEFAULT_MAX_LIVE_AGE_MS))
}

fn media_v2_uac_start_timeout() -> Duration {
    std::env::var("LESAVKA_UPSTREAM_V2_UAC_START_TIMEOUT_MS")
        .ok()
        .and_then(|value| value.trim().parse::<u64>().ok())
        .map(Duration::from_millis)
        .unwrap_or_else(|| Duration::from_millis(MEDIA_V2_DEFAULT_UAC_START_TIMEOUT_MS))
}

fn media_v2_stream_idle_timeout() -> Duration {
    std::env::var("LESAVKA_UPSTREAM_V2_STREAM_IDLE_TIMEOUT_MS")
        .ok()
        .and_then(|value| value.trim().parse::<u64>().ok())
        .map(Duration::from_millis)
        .unwrap_or_else(|| Duration::from_millis(MEDIA_V2_DEFAULT_STREAM_IDLE_TIMEOUT_MS))
}

fn media_v2_audio_starvation_heal_after() -> Option<Duration> {
    let millis = std::env::var("LESAVKA_UPSTREAM_V2_AUDIO_STARVATION_HEAL_MS")
        .ok()
        .and_then(|value| value.trim().parse::<u64>().ok())
        .unwrap_or(MEDIA_V2_DEFAULT_AUDIO_STARVATION_HEAL_MS);
    (millis > 0).then(|| Duration::from_millis(millis))
}

fn media_v2_av_skew_heal_after() -> Option<Duration> {
    let millis = std::env::var("LESAVKA_UPSTREAM_V2_AV_SKEW_HEAL_MS")
        .ok()
        .and_then(|value| value.trim().parse::<u64>().ok())
        .unwrap_or(MEDIA_V2_DEFAULT_AV_SKEW_HEAL_MS);
    (millis > 0).then(|| Duration::from_millis(millis))
}

/// Track whether a bundled stream has lost audio while video is still live.
///
/// Inputs are the latest bundle facts plus the current starvation marker.
/// Output is true once the audio-free video run exceeds the heal window. Why:
/// a stuck mic capture path can leave the RCT hearing a stale UAC epoch while
/// fresh video keeps flowing; retiring the mic generation forces the client to
/// rebuild the bundled stream without touching USB descriptors.
fn media_v2_audio_starvation_elapsed(
    audio_enabled: bool,
    facts: MediaV2BundleFacts,
    now: tokio::time::Instant,
    starved_since: &mut Option<tokio::time::Instant>,
    heal_after: Option<Duration>,
) -> bool {
    if !audio_enabled || !facts.has_video || facts.has_audio {
        *starved_since = None;
        return false;
    }
    let Some(heal_after) = heal_after else {
        return false;
    };
    let since = starved_since.get_or_insert(now);
    now.saturating_duration_since(*since) >= heal_after
}

/// Track sustained impossible A/V capture skew inside otherwise live bundles.
///
/// Inputs are the latest bundle facts plus the current skew marker. Output is
/// true once skewed bundles persist beyond the heal window. Why: the mic epoch
/// can keep producing packets while its timestamp base has drifted far away
/// from video; soft UAC recovery is the known-safe repair for that state.
fn media_v2_av_skew_elapsed(
    facts: MediaV2BundleFacts,
    now: tokio::time::Instant,
    skewed_since: &mut Option<tokio::time::Instant>,
    heal_after: Option<Duration>,
) -> bool {
    if !facts.has_audio
        || !facts.has_video
        || facts.capture_span_us <= MEDIA_V2_MAX_MIXED_CAPTURE_SPAN_US
    {
        *skewed_since = None;
        return false;
    }
    let Some(heal_after) = heal_after else {
        return false;
    };
    let since = skewed_since.get_or_insert(now);
    now.saturating_duration_since(*since) >= heal_after
}

/// Keeps `media_v2_handoff_schedule` explicit because it sits on relay RPC orchestration, where hardware failures must surface without stopping the server.
/// Inputs are the typed parameters; output is the return value or side effect.
fn media_v2_handoff_schedule(
    facts: MediaV2BundleFacts,
    audio_offset_us: i64,
    video_offset_us: i64,
) -> Option<MediaV2HandoffSchedule> {
    let max_live_age = media_v2_max_live_age();
    let queue_age = Duration::from_millis(u64::from(facts.max_queue_age_ms));
    if queue_age >= max_live_age {
        return None;
    }

    let mut relative_audio_delay = Duration::ZERO;
    let mut relative_video_delay = Duration::ZERO;
    if facts.has_audio && facts.has_video {
        let base_offset_us = audio_offset_us.min(video_offset_us);
        relative_audio_delay =
            Duration::from_micros(audio_offset_us.saturating_sub(base_offset_us) as u64);
        relative_video_delay =
            Duration::from_micros(video_offset_us.saturating_sub(base_offset_us) as u64);
    }
    let relative_span = relative_audio_delay.max(relative_video_delay);
    let remaining_after_offset = max_live_age
        .saturating_sub(queue_age)
        .saturating_sub(relative_span);
    let common_delay = media_v2_playout_delay().min(remaining_after_offset);
    let now = tokio::time::Instant::now();
    Some(MediaV2HandoffSchedule {
        audio_due_at: facts
            .has_audio
            .then_some(now + common_delay + relative_audio_delay),
        video_due_at: facts
            .has_video
            .then_some(now + common_delay + relative_video_delay),
        common_delay,
        relative_audio_delay,
        relative_video_delay,
    })
}

/// Convert a negotiated video FPS into a microsecond frame step.
///
/// Inputs: frames per second from the camera config. Output: at least one
/// microsecond per frame. Why: recovery keyframe cadence and audio/video
/// bundle planning both need a bounded step even when a bad mode reports zero.
fn media_v2_frame_step_us(fps: u32) -> u64 {
    if fps == 0 {
        return 1;
    }
    (1_000_000_u64 / u64::from(fps)).max(1)
}

#[cfg(not(coverage))]
fn media_v2_drop_late_plan(plan: &PlannedUpstreamPacket) -> bool {
    plan.late_by >= media_v2_max_live_age()
}

/// Decide whether server-side HEVC recovery should hold a video packet.
///
/// Inputs: current recovery state, camera codec, and candidate video packet.
/// Output: true when the packet should not be handed to the decoder yet. Why:
/// server freshness drops can create the same HEVC reference gap as client
/// queue drops, and feeding the next delta frame produces block tearing.
fn media_v2_should_hold_hevc_video_for_recovery(
    waiting_for_keyframe: bool,
    codec: camera::CameraCodec,
    video: Option<&VideoPacket>,
) -> bool {
    waiting_for_keyframe
        && matches!(codec, camera::CameraCodec::Hevc)
        && video.is_some_and(|packet| {
            !lesavka_server::video_support::contains_hevc_irap(&packet.data)
        })
}

/// Report whether a video packet can repair a server-side HEVC reference gap.
///
/// Inputs: camera codec and candidate video packet. Output: true for HEVC IRAP
/// frames. Why: after the server drops a late HEVC packet for freshness, this
/// tells the relay when it is safe to resume motion without visible corruption.
fn media_v2_has_hevc_recovery_keyframe(
    codec: camera::CameraCodec,
    video: Option<&VideoPacket>,
) -> bool {
    matches!(codec, camera::CameraCodec::Hevc)
        && video.is_some_and(|packet| {
            lesavka_server::video_support::contains_hevc_irap(&packet.data)
        })
}

#[cfg(not(coverage))]
/// Keeps `sleep_until_media_v2` explicit because it sits on relay RPC orchestration, where hardware failures must surface without stopping the server.
/// Inputs are the typed parameters; output is the return value or side effect.
async fn sleep_until_media_v2(due_at: tokio::time::Instant) {
    if due_at > tokio::time::Instant::now() {
        tokio::time::sleep_until(due_at).await;
    }
}

#[cfg(not(coverage))]
/// Rebase audio packet PTS before scheduling UAC handoff.
///
/// Inputs: bundle audio packets and the shared v2 media clock.
/// Outputs: audio packets with server-local PTS.
/// Why: ingress must keep draining gRPC while a separate handoff worker sleeps,
/// so clock rebasing has to happen before packets leave the receive loop.
fn prepare_media_v2_audio(
    audio_packets: &mut Vec<AudioPacket>,
    upstream_media_rt: &UpstreamMediaRuntime,
    bundle_base_remote_pts_us: u64,
    bundle_epoch: tokio::time::Instant,
) -> Option<MediaV2ScheduledAudio> {
    let mut due_at: Option<tokio::time::Instant> = None;
    let packets: Vec<AudioPacket> = audio_packets
        .drain(..)
        .filter_map(|mut audio| {
            let capture_pts_us = packet_audio_capture_pts_us(&audio);
            match upstream_media_rt.plan_bundled_pts(
                UpstreamMediaKind::Microphone,
                capture_pts_us,
                1,
                bundle_base_remote_pts_us,
                bundle_epoch,
            ) {
                UpstreamPlanDecision::Play(plan) if !media_v2_drop_late_plan(&plan) => {
                    due_at = Some(due_at.map_or(plan.due_at, |current| current.min(plan.due_at)));
                    audio.pts = plan.local_pts_us;
                    Some(audio)
                }
                _ => None,
            }
        })
        .collect();
    Some(MediaV2ScheduledAudio {
        packets,
        due_at: due_at?,
    })
    .filter(|scheduled| !scheduled.packets.is_empty())
}

#[cfg(not(coverage))]
/// Rebase one video packet before scheduling UVC handoff.
///
/// Inputs: optional bundle video packet and the shared v2 media clock.
/// Outputs: video packet with server-local PTS.
/// Why: the receive loop should never wait for presentation timing; it only
/// prepares media for the handoff worker.
fn prepare_media_v2_video(
    video: Option<VideoPacket>,
    upstream_media_rt: &UpstreamMediaRuntime,
    bundle_base_remote_pts_us: u64,
    bundle_epoch: tokio::time::Instant,
    received_at: tokio::time::Instant,
    frame_step_us: u64,
) -> Option<MediaV2ScheduledVideo> {
    let mut video = video?;
    let capture_pts_us = packet_video_capture_pts_us(&video);
    match upstream_media_rt.plan_bundled_pts(
        UpstreamMediaKind::Camera,
        capture_pts_us,
        frame_step_us,
        bundle_base_remote_pts_us,
        bundle_epoch,
    ) {
        UpstreamPlanDecision::Play(plan) if !media_v2_drop_late_plan(&plan) => {
            video.pts = plan.local_pts_us;
            Some(MediaV2ScheduledVideo {
                packet: video,
                due_at: plan.due_at,
                received_at,
            })
        }
        _ => None,
    }
}

#[cfg(not(coverage))]
/// Push scheduled audio packets to UAC without blocking gRPC ingress.
///
/// Inputs: scheduled audio queue, UAC sink, and upstream timing runtime.
/// Outputs: sink writes and presentation telemetry.
/// Why: sleeping in the receive loop created HTTP/2 backlog, so handoff timing
/// belongs in a worker that can lag or catch up independently.
async fn run_media_v2_audio_handoff(
    mut rx: tokio::sync::mpsc::Receiver<MediaV2ScheduledAudio>,
    mut sink: lesavka_server::audio::Voice,
    upstream_media_rt: Arc<UpstreamMediaRuntime>,
) {
    while let Some(item) = rx.recv().await {
        sleep_until_media_v2(item.due_at).await;
        for audio in item.packets {
            let pts = audio.pts;
            sink.push(&audio);
            upstream_media_rt.mark_audio_presented(pts, item.due_at);
        }
    }
    sink.finish();
}

#[cfg(not(coverage))]
/// Feed scheduled video packets to UVC without blocking gRPC ingress.
///
/// Inputs: scheduled video queue, camera relay, timing runtime, and log IDs.
/// Outputs: relay feeds and presentation telemetry.
/// Why: UVC sync offsets still require sleeping, but that sleep must not slow
/// the network receive loop.
async fn run_media_v2_video_handoff(
    mut rx: tokio::sync::watch::Receiver<Option<MediaV2ScheduledVideo>>,
    relay: Arc<lesavka_server::video::CameraRelay>,
    upstream_media_rt: Arc<UpstreamMediaRuntime>,
    rpc_id: u64,
    session_id: u64,
    camera_session_id: u64,
) {
    let mut video_presented_once = false;
    while rx.changed().await.is_ok() {
        let Some(item) = rx.borrow_and_update().clone() else {
            continue;
        };
        sleep_until_media_v2(item.due_at).await;
        let presented_pts = item.packet.pts;
        let live_lag_ms = f64::from(item.packet.client_queue_age_ms)
            + tokio::time::Instant::now()
                .saturating_duration_since(item.received_at)
                .as_secs_f64()
                * 1_000.0;
        relay.feed(item.packet);
        if !video_presented_once {
            info!(
                rpc_id,
                session_id,
                camera_session_id,
                pts = presented_pts,
                "📦 first v2 bundled video frame fed to camera sink"
            );
            video_presented_once = true;
        }
        upstream_media_rt.mark_video_presented_with_live_lag(
            presented_pts,
            item.due_at,
            live_lag_ms,
        );
    }
}
