#[cfg(not(coverage))]
struct EncodedProbeFrames {
    frames: Vec<Vec<u8>>,
}

#[cfg(not(coverage))]
impl EncodedProbeFrames {
    fn new(camera: &CameraConfig, config: &ProbeConfig, frame_step: Duration) -> Result<Self> {
        let mut encoder = ProbeFrameEncoder::new(camera)?;
        let mut frames = Vec::new();
        let mut frame_index = 0u64;
        loop {
            let pts = duration_mul(frame_step, frame_index);
            if pts > config.duration {
                break;
            }
            let code = config.event_code_at(pts);
            frames.push(encoder.encode_probe_frame(probe_color_for_code(code), frame_index)?);
            frame_index = frame_index.saturating_add(1);
        }
        Ok(Self { frames })
    }

    fn packet_for_frame(&self, frame_index: u64) -> Result<&[u8]> {
        self.frames
            .get(usize::try_from(frame_index).unwrap_or(usize::MAX))
            .map(Vec::as_slice)
            .with_context(|| format!("missing pre-encoded probe frame {frame_index}"))
    }
}

#[cfg(not(coverage))]
enum ProbeFrameEncoder {
    Mjpeg(MjpegFrameEncoder),
    Hevc(HevcFrameEncoder),
}

#[cfg(not(coverage))]
impl ProbeFrameEncoder {
    /// Build the encoder that matches the ingress profile under calibration.
    ///
    /// Inputs: camera codec, dimensions, and frame rate. Output: an encoder
    /// capable of producing packets for the server-generated probe. Why:
    /// profile-specific server-to-RCT calibration must include the same decode
    /// work that real client-origin media will impose on the server.
    fn new(camera: &CameraConfig) -> Result<Self> {
        match camera.codec {
            CameraCodec::Mjpeg => Ok(Self::Mjpeg(MjpegFrameEncoder::new(camera)?)),
            CameraCodec::Hevc => Ok(Self::Hevc(HevcFrameEncoder::new(camera)?)),
            CameraCodec::H264 => bail!(
                "server-generated output-delay probe currently supports MJPEG or HEVC UVC ingress profiles, got {}",
                camera.codec.as_str()
            ),
        }
    }

    /// Encode one visual probe frame for the active profile.
    ///
    /// Inputs: the event color and monotonically increasing frame sequence.
    /// Output: one compressed video access unit. Why: the analyzer can only
    /// compare MJPEG and HEVC paths fairly when both originate from the same
    /// coded flash schedule.
    fn encode_probe_frame(&mut self, color: Rgb, sequence: u64) -> Result<Vec<u8>> {
        match self {
            Self::Mjpeg(encoder) => encoder.encode_probe_frame(color, sequence),
            Self::Hevc(encoder) => encoder.encode_probe_frame(color, sequence),
        }
    }
}

/// Map an optional event identity to the RGB color emitted in the probe frame.
///
/// Inputs: an event code from the probe schedule. Output: the matching color or
/// the dark idle frame. Why: keeping the mapping centralized preserves the
/// analyzer's coded-pulse identity contract across MJPEG and HEVC encoders.
fn probe_color_for_code(code: Option<u32>) -> Rgb {
    code.and_then(|code| EVENT_COLORS.get(code.checked_sub(1)? as usize).copied())
        .unwrap_or(DARK_FRAME_RGB)
}

#[cfg(not(coverage))]
struct MjpegFrameEncoder {
    src: gst_app::AppSrc,
    sink: gst_app::AppSink,
    pipeline: gst::Pipeline,
    width: usize,
    height: usize,
    frame_step_us: u64,
}

#[cfg(not(coverage))]
impl MjpegFrameEncoder {
    fn new(camera: &CameraConfig) -> Result<Self> {
        gst::init().context("gst init")?;
        let width = camera.width as i32;
        let height = camera.height as i32;
        let fps = camera.fps.max(1) as i32;
        let raw_caps = gst::Caps::builder("video/x-raw")
            .field("format", "RGB")
            .field("width", width)
            .field("height", height)
            .field("framerate", gst::Fraction::new(fps, 1))
            .build();
        let jpeg_caps = gst::Caps::builder("image/jpeg")
            .field("parsed", true)
            .field("width", width)
            .field("height", height)
            .field("framerate", gst::Fraction::new(fps, 1))
            .build();
        let pipeline = gst::Pipeline::new();
        let src = gst::ElementFactory::make("appsrc")
            .name("output_delay_probe_src")
            .build()?
            .downcast::<gst_app::AppSrc>()
            .expect("appsrc");
        src.set_is_live(false);
        src.set_format(gst::Format::Time);
        src.set_property("do-timestamp", false);
        src.set_caps(Some(&raw_caps));
        let convert = gst::ElementFactory::make("videoconvert").build()?;
        let encoder = gst::ElementFactory::make("jpegenc")
            .property("quality", 95i32)
            .build()?;
        let capsfilter = gst::ElementFactory::make("capsfilter")
            .property("caps", &jpeg_caps)
            .build()?;
        let sink = gst::ElementFactory::make("appsink")
            .name("output_delay_probe_sink")
            .property("sync", false)
            .property("emit-signals", false)
            .property("max-buffers", 8u32)
            .build()?
            .downcast::<gst_app::AppSink>()
            .expect("appsink");
        pipeline.add_many([
            src.upcast_ref(),
            &convert,
            &encoder,
            &capsfilter,
            sink.upcast_ref(),
        ])?;
        gst::Element::link_many([
            src.upcast_ref(),
            &convert,
            &encoder,
            &capsfilter,
            sink.upcast_ref(),
        ])?;
        pipeline
            .set_state(gst::State::Playing)
            .context("starting output-delay probe MJPEG encoder")?;

        Ok(Self {
            src,
            sink,
            pipeline,
            width: camera.width as usize,
            height: camera.height as usize,
            frame_step_us: (1_000_000u64 / u64::from(camera.fps.max(1))).max(1),
        })
    }

    fn encode_probe_frame(&mut self, color: Rgb, sequence: u64) -> Result<Vec<u8>> {
        let pts_us = sequence.saturating_mul(self.frame_step_us);
        let frame = probe_rgb_frame(self.width, self.height, color, sequence);
        let mut buffer = gst::Buffer::from_slice(frame);
        if let Some(meta) = buffer.get_mut() {
            let pts = gst::ClockTime::from_useconds(pts_us);
            meta.set_pts(Some(pts));
            meta.set_dts(Some(pts));
            meta.set_duration(Some(gst::ClockTime::from_useconds(self.frame_step_us)));
        }
        self.src
            .push_buffer(buffer)
            .context("encoding output-delay probe frame")?;
        let sample = self
            .sink
            .pull_sample()
            .context("pulling encoded output-delay probe frame")?;
        let buffer = sample.buffer().context("encoded frame had no buffer")?;
        let map = buffer
            .map_readable()
            .context("mapping encoded output-delay probe frame")?;
        Ok(map.as_slice().to_vec())
    }
}

#[cfg(not(coverage))]
impl Drop for MjpegFrameEncoder {
    fn drop(&mut self) {
        let _ = self.src.end_of_stream();
        let _ = self.pipeline.set_state(gst::State::Null);
    }
}

#[cfg(not(coverage))]
struct HevcFrameEncoder {
    src: gst_app::AppSrc,
    sink: gst_app::AppSink,
    pipeline: gst::Pipeline,
    width: usize,
    height: usize,
    frame_step_us: u64,
}

#[cfg(not(coverage))]
impl HevcFrameEncoder {
    /// Create the HEVC probe encoder used by profile calibration.
    ///
    /// Inputs: camera dimensions and frame rate. Output: a GStreamer encoder
    /// pipeline that emits byte-stream HEVC access units. Why: server-side HEVC
    /// calibration needs synthetic media to enter the same compressed ingress
    /// profile that client transport will use.
    fn new(camera: &CameraConfig) -> Result<Self> {
        gst::init().context("gst init")?;
        let width = camera.width as i32;
        let height = camera.height as i32;
        let fps = camera.fps.max(1) as i32;
        let raw_caps = gst::Caps::builder("video/x-raw")
            .field("format", "RGB")
            .field("width", width)
            .field("height", height)
            .field("framerate", gst::Fraction::new(fps, 1))
            .build();
        let hevc_caps = gst::Caps::builder("video/x-h265")
            .field("stream-format", "byte-stream")
            .field("alignment", "au")
            .build();
        let pipeline = gst::Pipeline::new();
        let src = gst::ElementFactory::make("appsrc")
            .name("output_delay_probe_hevc_src")
            .build()?
            .downcast::<gst_app::AppSrc>()
            .expect("appsrc");
        src.set_is_live(false);
        src.set_format(gst::Format::Time);
        src.set_property("do-timestamp", false);
        src.set_caps(Some(&raw_caps));
        let convert = gst::ElementFactory::make("videoconvert").build()?;
        let raw_i420 = gst::Caps::builder("video/x-raw")
            .field("format", "I420")
            .field("width", width)
            .field("height", height)
            .field("framerate", gst::Fraction::new(fps, 1))
            .build();
        let raw_capsfilter = gst::ElementFactory::make("capsfilter")
            .property("caps", &raw_i420)
            .build()?;
        let encoder = gst::ElementFactory::make("x265enc")
            .property_from_str("tune", "zerolatency")
            .property_from_str("speed-preset", "ultrafast")
            .property("bitrate", 2500u32)
            .property("key-int-max", 1i32)
            .build()
            .context("building HEVC probe encoder x265enc")?;
        let parser = gst::ElementFactory::make("h265parse")
            .property("config-interval", -1i32)
            .build()?;
        let hevc_capsfilter = gst::ElementFactory::make("capsfilter")
            .property("caps", &hevc_caps)
            .build()?;
        let sink = gst::ElementFactory::make("appsink")
            .name("output_delay_probe_hevc_sink")
            .property("sync", false)
            .property("emit-signals", false)
            .property("max-buffers", 8u32)
            .build()?
            .downcast::<gst_app::AppSink>()
            .expect("appsink");
        pipeline.add_many([
            src.upcast_ref(),
            &convert,
            &raw_capsfilter,
            &encoder,
            &parser,
            &hevc_capsfilter,
            sink.upcast_ref(),
        ])?;
        gst::Element::link_many([
            src.upcast_ref(),
            &convert,
            &raw_capsfilter,
            &encoder,
            &parser,
            &hevc_capsfilter,
            sink.upcast_ref(),
        ])?;
        pipeline
            .set_state(gst::State::Playing)
            .context("starting output-delay probe HEVC encoder")?;

        Ok(Self {
            src,
            sink,
            pipeline,
            width: camera.width as usize,
            height: camera.height as usize,
            frame_step_us: (1_000_000u64 / u64::from(camera.fps.max(1))).max(1),
        })
    }

    /// Encode one RGB probe frame as an HEVC access unit.
    ///
    /// Inputs: the rendered color and sequence number. Output: the compressed
    /// HEVC packet to feed into the server camera path. Why: sequence-stamped
    /// HEVC frames let the final RCT capture prove sync after decode and MJPEG
    /// re-emission, not just before transport.
    fn encode_probe_frame(&mut self, color: Rgb, sequence: u64) -> Result<Vec<u8>> {
        let pts_us = sequence.saturating_mul(self.frame_step_us);
        let frame = probe_rgb_frame(self.width, self.height, color, sequence);
        let mut buffer = gst::Buffer::from_slice(frame);
        if let Some(meta) = buffer.get_mut() {
            let pts = gst::ClockTime::from_useconds(pts_us);
            meta.set_pts(Some(pts));
            meta.set_dts(Some(pts));
            meta.set_duration(Some(gst::ClockTime::from_useconds(self.frame_step_us)));
        }
        self.src
            .push_buffer(buffer)
            .context("encoding output-delay probe HEVC frame")?;
        let sample = self
            .sink
            .pull_sample()
            .context("pulling encoded output-delay probe HEVC frame")?;
        let buffer = sample.buffer().context("encoded HEVC frame had no buffer")?;
        let map = buffer
            .map_readable()
            .context("mapping encoded output-delay probe HEVC frame")?;
        Ok(map.as_slice().to_vec())
    }
}

#[cfg(not(coverage))]
impl Drop for HevcFrameEncoder {
    fn drop(&mut self) {
        let _ = self.src.end_of_stream();
        let _ = self.pipeline.set_state(gst::State::Null);
    }
}

#[cfg(not(coverage))]
fn probe_rgb_frame(width: usize, height: usize, color: Rgb, sequence: u64) -> Vec<u8> {
    let mut frame = vec![0u8; width.saturating_mul(height).saturating_mul(3)];
    for pixel in frame.chunks_exact_mut(3) {
        pixel[0] = color.r;
        pixel[1] = color.g;
        pixel[2] = color.b;
    }
    draw_frame_continuity_watermark(&mut frame, width, height, sequence);
    frame
}

fn draw_frame_continuity_watermark(frame: &mut [u8], width: usize, height: usize, sequence: u64) {
    if width < VIDEO_CONTINUITY_BLOCKS || height < 8 {
        return;
    }
    let stripe_height = (height / 18).clamp(8, 48);
    let stripe_top = height.saturating_sub(stripe_height);
    let block_width = (width / VIDEO_CONTINUITY_BLOCKS).max(1);
    let seq = (sequence & 0xffff) as u16;
    let parity = (seq.count_ones() & 1) != 0;
    for block in 0..VIDEO_CONTINUITY_BLOCKS {
        let white = match block {
            0 => true,
            1 => false,
            2..=17 => {
                let bit = VIDEO_CONTINUITY_DATA_BITS - 1 - (block - 2);
                ((seq >> bit) & 1) != 0
            }
            18 => parity,
            _ => !parity,
        };
        let value = if white { 255 } else { 0 };
        let x_start = block * block_width;
        let x_end = if block + 1 == VIDEO_CONTINUITY_BLOCKS {
            width
        } else {
            ((block + 1) * block_width).min(width)
        };
        for y in stripe_top..height {
            for x in x_start..x_end {
                let offset = (y * width + x) * 3;
                if let Some(pixel) = frame.get_mut(offset..offset + 3) {
                    pixel[0] = value;
                    pixel[1] = value;
                    pixel[2] = value;
                }
            }
        }
    }
}

fn render_audio_chunk(
    config: &ProbeConfig,
    chunk_pts: Duration,
    samples_per_chunk: usize,
) -> Vec<u8> {
    let sample_step = Duration::from_nanos(1_000_000_000u64 / u64::from(AUDIO_SAMPLE_RATE));
    let mut pcm =
        Vec::with_capacity(samples_per_chunk * AUDIO_CHANNELS * std::mem::size_of::<i16>());
    for sample_index in 0..samples_per_chunk {
        let sample_pts = chunk_pts + duration_mul(sample_step, sample_index as u64);
        let pilot_phase = TAU * AUDIO_PILOT_FREQUENCY_HZ * sample_pts.as_secs_f64();
        let pilot = pilot_phase.sin() * AUDIO_PILOT_AMPLITUDE;
        let event = config
            .event_code_at(sample_pts)
            .and_then(event_frequency_hz)
            .map(|frequency| {
                let phase = TAU * frequency * sample_pts.as_secs_f64();
                phase.sin() * AUDIO_AMPLITUDE
            })
            .unwrap_or(0.0);
        let sample = (pilot + event).clamp(f64::from(i16::MIN), f64::from(i16::MAX)) as i16;
        for _ in 0..AUDIO_CHANNELS {
            pcm.extend_from_slice(&sample.to_le_bytes());
        }
    }
    pcm
}

fn event_frequency_hz(code: u32) -> Option<f64> {
    EVENT_FREQUENCIES_HZ
        .get(code.checked_sub(1)? as usize)
        .copied()
}

fn duration_us(duration: Duration) -> u64 {
    duration.as_micros().min(u128::from(u64::MAX)) as u64
}

fn unix_ns_now() -> u128 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos()
}

fn unix_ns_from_start(server_start_unix_ns: u128, monotonic_us: u64) -> u128 {
    server_start_unix_ns.saturating_add(u128::from(monotonic_us).saturating_mul(1_000))
}

fn monotonic_us_since(start: tokio::time::Instant) -> u64 {
    duration_us(tokio::time::Instant::now().saturating_duration_since(start))
}

fn duration_mul(duration: Duration, count: u64) -> Duration {
    let nanos = duration
        .as_nanos()
        .saturating_mul(u128::from(count))
        .min(u128::from(u64::MAX));
    Duration::from_nanos(nanos as u64)
}
