//! Shared-clock synthetic A/V source for the upstream sync probe.

#[cfg(any(not(coverage), test))]
use anyhow::{Context, Result, bail};
#[cfg(any(not(coverage), test))]
use gst::prelude::*;
#[cfg(any(not(coverage), test))]
use gstreamer as gst;
#[cfg(any(not(coverage), test))]
use gstreamer_app as gst_app;
#[cfg(any(not(coverage), test))]
use lesavka_common::lesavka::{AudioPacket, VideoPacket};
#[cfg(any(not(coverage), test))]
use std::collections::BTreeMap;
#[cfg(any(not(coverage), test))]
use std::sync::{
    Arc,
    atomic::{AtomicBool, Ordering},
};
#[cfg(any(not(coverage), test))]
use std::thread::{self, JoinHandle};
#[cfg(any(not(coverage), test))]
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
#[cfg(any(not(coverage), test))]
use std::{f64::consts::TAU, mem::size_of};

#[cfg(any(not(coverage), test))]
use crate::input::camera::{CameraCodec, CameraConfig};
#[cfg(any(not(coverage), test))]
use crate::sync_probe::schedule::PulseSchedule;
#[cfg(any(not(coverage), test))]
use crate::sync_probe::signature::{
    ProbeColor, probe_audio_frequency_for_event_code, probe_color_for_event_code,
};
#[cfg(any(not(coverage), test))]
use crate::uplink_fresh_queue::{FreshPacketQueue, FreshQueueConfig};

#[cfg(coverage)]
mod coverage_stub;
#[cfg(not(coverage))]
mod runtime;
#[cfg(test)]
mod tests;
#[cfg(not(coverage))]
mod video_packets;

#[cfg(coverage)]
pub use coverage_stub::SyncProbeCapture;
#[cfg(not(coverage))]
pub use runtime::SyncProbeCapture;

#[cfg(any(not(coverage), test))]
const PROBE_VIDEO_QUEUE: FreshQueueConfig = FreshQueueConfig {
    capacity: 32,
    max_age: Duration::from_millis(350),
    policy: crate::uplink_fresh_queue::FreshQueuePolicy::LatestOnly,
};

#[cfg(any(not(coverage), test))]
const PROBE_AUDIO_QUEUE: FreshQueueConfig = FreshQueueConfig {
    capacity: 64,
    max_age: Duration::from_millis(400),
    policy: crate::uplink_fresh_queue::FreshQueuePolicy::DrainOldest,
};

#[cfg(any(not(coverage), test))]
const AUDIO_SAMPLE_RATE: i32 = 48_000;
#[cfg(any(not(coverage), test))]
const AUDIO_CHANNELS: usize = 2;
#[cfg(any(not(coverage), test))]
const AUDIO_CHUNK_MS: u64 = 20;
#[cfg(any(not(coverage), test))]
const AUDIO_PULSE_FREQUENCY_HZ: f64 = 1_800.0;
#[cfg(any(not(coverage), test))]
const AUDIO_PULSE_AMPLITUDE: f64 = 24_000.0;

#[cfg(any(not(coverage), test))]
/// Build the dark RGB frame used outside active probe pulses.
///
/// Inputs: frame width and height in pixels.
/// Outputs: raw RGB bytes.
/// Why: idle frames need stable low luma so the RCT analyzer can separate
/// transport pulses from background video.
fn build_dark_probe_frame(width: usize, height: usize) -> Vec<u8> {
    build_solid_rgb_probe_frame(
        width,
        height,
        ProbeColor {
            r: 16,
            g: 16,
            b: 16,
        },
    )
}

#[cfg(any(not(coverage), test))]
/// Build the uncoded bright RGB pulse frame.
///
/// Inputs: frame width and height in pixels.
/// Outputs: raw RGB bytes.
/// Why: the legacy marker-mode probe still needs a high-contrast frame when
/// event identity is not being carried by color.
fn build_regular_probe_frame(width: usize, height: usize) -> Vec<u8> {
    build_solid_rgb_probe_frame(
        width,
        height,
        ProbeColor {
            r: 240,
            g: 240,
            b: 240,
        },
    )
}

#[cfg(any(not(coverage), test))]
/// Build a color-coded RGB pulse frame for one event identity.
///
/// Inputs: frame dimensions and one-based event code.
/// Outputs: raw RGB bytes using the shared probe palette.
/// Why: client-to-RCT transport tests can lose startup cadence, so each flash
/// carries identity in color before it is bundled for upstream transport.
fn build_coded_probe_frame(width: usize, height: usize, code: u32) -> Vec<u8> {
    build_solid_rgb_probe_frame(
        width,
        height,
        probe_color_for_event_code(code).unwrap_or(ProbeColor {
            r: 240,
            g: 240,
            b: 240,
        }),
    )
}

#[cfg(any(not(coverage), test))]
/// Fill a whole raw RGB frame with one color.
///
/// Inputs: frame dimensions and RGB color.
/// Outputs: raw RGB bytes.
/// Why: generating simple solid frames keeps the synthetic source deterministic
/// and cheap to compare in tests.
fn build_solid_rgb_probe_frame(width: usize, height: usize, color: ProbeColor) -> Vec<u8> {
    let mut frame = Vec::with_capacity(width.saturating_mul(height).saturating_mul(3));
    for _ in 0..width.saturating_mul(height) {
        frame.extend_from_slice(&[color.r, color.g, color.b]);
    }
    frame
}

#[cfg(any(not(coverage), test))]
/// Build the old marker pulse frame with a visible cross.
///
/// Inputs: frame width and height in pixels.
/// Outputs: raw RGB bytes.
/// Why: marker-mode analysis uses pulse width and shape to break cadence
/// aliases when coded event identity is disabled.
fn build_marker_probe_frame(width: usize, height: usize) -> Vec<u8> {
    let mut frame = build_regular_probe_frame(width, height);

    let cross_half_w = (width / 48).max(6);
    let cross_half_h = (height / 48).max(6);
    let cx = width / 2;
    let cy = height / 2;
    fill_rect(
        &mut frame,
        width,
        cx.saturating_sub(cross_half_w),
        0,
        (cx + cross_half_w).min(width),
        height,
        ProbeColor {
            r: 16,
            g: 16,
            b: 16,
        },
    );
    fill_rect(
        &mut frame,
        width,
        0,
        cy.saturating_sub(cross_half_h),
        width,
        (cy + cross_half_h).min(height),
        ProbeColor {
            r: 16,
            g: 16,
            b: 16,
        },
    );
    frame
}

#[cfg(any(not(coverage), test))]
/// Fill a rectangular region in a raw RGB frame.
///
/// Inputs: mutable frame bytes, image width, rectangle bounds, and RGB color.
/// Outputs: in-place mutation of the requested region.
/// Why: marker frames need a deterministic visual feature without pulling in a
/// heavier image dependency.
fn fill_rect(
    frame: &mut [u8],
    width: usize,
    x0: usize,
    y0: usize,
    x1: usize,
    y1: usize,
    color: ProbeColor,
) {
    let height = frame.len() / width.max(1) / 3;
    let x1 = x1.min(width);
    let y1 = y1.min(height);
    for y in y0.min(height)..y1 {
        for x in x0.min(width)..x1 {
            let offset = (y * width + x) * 3;
            frame[offset..offset + 3].copy_from_slice(&[color.r, color.g, color.b]);
        }
    }
}

#[cfg(any(not(coverage), test))]
/// Render one interleaved PCM audio chunk for the synthetic probe.
///
/// Inputs: pulse schedule, chunk PTS, and mono samples per channel.
/// Outputs: stereo little-endian PCM bytes.
/// Why: the client transport test must inject audio exactly where physical
/// capture would feed the uplink, but with deterministic tone identities.
fn render_audio_chunk(
    schedule: &PulseSchedule,
    chunk_pts: Duration,
    samples_per_chunk: usize,
) -> Vec<u8> {
    let sample_step = Duration::from_nanos(1_000_000_000u64 / AUDIO_SAMPLE_RATE as u64);
    let mut pcm = Vec::with_capacity(samples_per_chunk * AUDIO_CHANNELS * size_of::<i16>());
    for sample_index in 0..samples_per_chunk {
        let sample_pts = chunk_pts + sample_step.saturating_mul(sample_index as u32);
        let amplitude = if let Some(frequency_hz) = probe_audio_frequency(schedule, sample_pts) {
            let phase = TAU * frequency_hz * sample_pts.as_secs_f64();
            (phase.sin() * AUDIO_PULSE_AMPLITUDE) as i16
        } else {
            0
        };
        for _ in 0..AUDIO_CHANNELS {
            pcm.extend_from_slice(&amplitude.to_le_bytes());
        }
    }
    pcm
}

#[cfg(any(not(coverage), test))]
/// Select the tone frequency active at a sample timestamp.
///
/// Inputs: pulse schedule and sample PTS.
/// Outputs: tone frequency when the pulse gate is active, otherwise `None`.
/// Why: coded probes need audio identity to survive transport separately from
/// the video color identity, while legacy probes keep the original single tone.
fn probe_audio_frequency(schedule: &PulseSchedule, sample_pts: Duration) -> Option<f64> {
    if !schedule.flash_active(sample_pts) {
        return None;
    }
    schedule
        .event_code(sample_pts)
        .and_then(probe_audio_frequency_for_event_code)
        .or(Some(AUDIO_PULSE_FREQUENCY_HZ))
}

#[cfg(test)]
fn probe_pts_exceeds_duration(pts_usecs: u64, duration: std::time::Duration) -> bool {
    pts_usecs > duration.as_micros() as u64
}

#[cfg(any(not(coverage), test))]
/// Capture the local Unix clock in nanoseconds.
///
/// Inputs: none.
/// Outputs: best-effort Unix timestamp.
/// Why: the RCT freshness summary needs a client-origin timestamp aligned with
/// synthetic PTS zero before media enters the bundled transport path.
fn unix_now_ns() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos()
        .min(u64::MAX as u128) as u64
}
