impl OutputDelayProbeTimeline {
    fn new(config: &ProbeConfig, camera: &CameraConfig, server_start_unix_ns: u128) -> Self {
        let event_count = config.event_count();
        let events = (0..event_count)
            .map(|event_id| {
                let slot = config.event_slot_by_id(event_id as usize);
                OutputDelayProbeEventTimeline {
                    event_id: event_id as usize,
                    code: slot.code,
                    planned_start_us: slot.planned_start_us,
                    planned_end_us: slot.planned_end_us,
                    video_seq: None,
                    audio_seq: None,
                    video_feed_monotonic_us: None,
                    audio_push_monotonic_us: None,
                    video_feed_unix_ns: None,
                    audio_push_unix_ns: None,
                    server_feed_delta_ms: None,
                }
            })
            .collect();
        Self {
            schema: "lesavka.output-delay-server-timeline.v1",
            origin: "theia-server-generated",
            media_path: "server generator -> UVC/UAC sinks",
            injection_scope: "server-final-output-handoff",
            server_pipeline_reference: "generated media PTS before intentional sync delay",
            sink_handoff_path: "video CameraRelay::feed; audio Voice::push",
            client_uplink_included: false,
            camera_width: camera.width,
            camera_height: camera.height,
            camera_fps: camera.fps,
            audio_sample_rate: AUDIO_SAMPLE_RATE,
            audio_channels: AUDIO_CHANNELS,
            audio_chunk_ms: AUDIO_CHUNK_MS,
            audio_delay_us: duration_us(config.audio_delay),
            video_delay_us: duration_us(config.video_delay),
            server_start_unix_ns,
            pulse_period_ms: config.pulse_period.as_millis() as u64,
            pulse_width_ms: config.pulse_width.as_millis() as u64,
            warmup_us: duration_us(config.warmup),
            duration_us: duration_us(config.duration),
            events,
        }
    }

    fn mark_audio(&mut self, slot: ProbeEventSlot, seq: u64, monotonic_us: u64, unix_ns: u128) {
        let Some(event) = self.events.get_mut(slot.event_id) else {
            return;
        };
        if event.audio_push_monotonic_us.is_none() {
            event.audio_seq = Some(seq);
            event.audio_push_monotonic_us = Some(monotonic_us);
            event.audio_push_unix_ns = Some(unix_ns);
            event.update_delta();
        }
    }

    fn mark_video(&mut self, slot: ProbeEventSlot, seq: u64, monotonic_us: u64, unix_ns: u128) {
        let Some(event) = self.events.get_mut(slot.event_id) else {
            return;
        };
        if event.video_feed_monotonic_us.is_none() {
            event.video_seq = Some(seq);
            event.video_feed_monotonic_us = Some(monotonic_us);
            event.video_feed_unix_ns = Some(unix_ns);
            event.update_delta();
        }
    }
}

impl OutputDelayProbeEventTimeline {
    fn update_delta(&mut self) {
        let (Some(audio_us), Some(video_us)) =
            (self.audio_push_monotonic_us, self.video_feed_monotonic_us)
        else {
            return;
        };
        self.server_feed_delta_ms = Some((audio_us as f64 - video_us as f64) / 1000.0);
    }
}

impl ProbeConfig {
    fn from_request(request: &OutputDelayProbeRequest) -> Result<Self> {
        let duration_seconds = non_zero_or_default(
            request.duration_seconds,
            DEFAULT_DURATION_SECONDS,
            "duration_seconds",
        )?;
        let warmup_seconds = if request.warmup_seconds == 0 {
            DEFAULT_WARMUP_SECONDS
        } else {
            request.warmup_seconds
        };
        let pulse_period_ms = non_zero_or_default(
            request.pulse_period_ms,
            DEFAULT_PULSE_PERIOD_MS,
            "pulse_period_ms",
        )?;
        let pulse_width_ms = non_zero_or_default(
            request.pulse_width_ms,
            DEFAULT_PULSE_WIDTH_MS,
            "pulse_width_ms",
        )?;
        if pulse_width_ms >= pulse_period_ms {
            bail!("pulse_width_ms must stay smaller than pulse_period_ms");
        }

        let event_width_codes = parse_event_width_codes(&request.event_width_codes)?;
        Ok(Self {
            duration: Duration::from_secs(u64::from(duration_seconds)),
            warmup: Duration::from_secs(u64::from(warmup_seconds)),
            pulse_period: Duration::from_millis(u64::from(pulse_period_ms)),
            pulse_width: Duration::from_millis(u64::from(pulse_width_ms)),
            event_width_codes,
            audio_delay: positive_delay(request.audio_delay_us, "audio_delay_us")?,
            video_delay: positive_delay(request.video_delay_us, "video_delay_us")?,
        })
    }

    fn event_code_at(&self, pts: Duration) -> Option<u32> {
        self.event_slot_at(pts).map(|slot| slot.code)
    }

    fn event_slot_at(&self, pts: Duration) -> Option<ProbeEventSlot> {
        if pts < self.warmup {
            return None;
        }
        let since_warmup = pts.saturating_sub(self.warmup);
        let period_ns = self.pulse_period.as_nanos().max(1);
        let pulse_index = (since_warmup.as_nanos() / period_ns) as usize;
        let pulse_offset_ns = since_warmup.as_nanos() % period_ns;
        let active_ns = self.pulse_width.as_nanos();
        (pulse_offset_ns < active_ns).then(|| self.event_slot_by_id(pulse_index))
    }

    fn event_slot_by_id(&self, event_id: usize) -> ProbeEventSlot {
        let code = self.event_width_codes[event_id % self.event_width_codes.len()];
        let planned_start = self
            .warmup
            .saturating_add(duration_mul(self.pulse_period, event_id as u64));
        let planned_end = planned_start.saturating_add(self.pulse_width);
        ProbeEventSlot {
            event_id,
            code,
            planned_start_us: duration_us(planned_start),
            planned_end_us: duration_us(planned_end),
        }
    }

    fn event_count(&self) -> u64 {
        if self.duration <= self.warmup {
            return 0;
        }
        let active = self.duration - self.warmup;
        let count = active.as_nanos() / self.pulse_period.as_nanos().max(1);
        count.try_into().unwrap_or(u64::MAX)
    }
}

fn non_zero_or_default(value: u32, default: u32, name: &str) -> Result<u32> {
    if value == 0 {
        return Ok(default);
    }
    if value == u32::MAX {
        bail!("{name} is too large");
    }
    Ok(value)
}

fn positive_delay(value_us: i64, name: &str) -> Result<Duration> {
    if value_us < 0 {
        bail!("{name} must be zero or positive for the direct output-delay probe");
    }
    Ok(Duration::from_micros(value_us as u64))
}

fn parse_event_width_codes(raw: &str) -> Result<Vec<u32>> {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return Ok(DEFAULT_EVENT_WIDTH_CODES.to_vec());
    }
    let codes = trimmed
        .split(',')
        .filter_map(|part| {
            let part = part.trim();
            (!part.is_empty()).then_some(part)
        })
        .map(|part| {
            let code = part
                .parse::<u32>()
                .with_context(|| format!("parsing event width code `{part}`"))?;
            if !(1..=16).contains(&code) {
                bail!("event signature code {code} is unsupported; use values 1..16");
            }
            Ok(code)
        })
        .collect::<Result<Vec<_>>>()?;
    if codes.is_empty() {
        bail!("event_width_codes must contain at least one code");
    }
    Ok(codes)
}
