//! Client-origin timeline artifacts for synthetic upstream transport probes.
//!
//! The external RCT capture is the truth source for final sync, but freshness
//! needs a client-side origin clock. This module writes that compact schedule
//! before transport starts so manual harnesses can correlate observed flashes
//! and tones against when the client generated them.

use anyhow::{Context, Result};
use serde::Serialize;
use std::path::Path;
use std::time::Duration;

use crate::input::camera::{CameraCodec, CameraConfig};
use crate::sync_probe::schedule::PulseSchedule;

const AUDIO_SAMPLE_RATE_HZ: u32 = 48_000;
const AUDIO_CHANNEL_COUNT: u32 = 2;
const AUDIO_CHUNK_MS: u32 = 20;

#[derive(Debug, Serialize)]
pub struct ProbeTimeline {
    schema: &'static str,
    origin: &'static str,
    media_path: &'static str,
    injection_scope: &'static str,
    client_uplink_included: bool,
    client_start_unix_ns: u64,
    camera_width: u32,
    camera_height: u32,
    camera_fps: u32,
    camera_codec: &'static str,
    audio_sample_rate: u32,
    audio_channels: u32,
    audio_chunk_ms: u32,
    warmup_us: u64,
    duration_us: u64,
    pulse_period_ms: u64,
    pulse_width_ms: u64,
    marker_tick_period: u32,
    event_width_codes: Vec<u32>,
    events: Vec<ProbeTimelineEvent>,
}

#[derive(Debug, Serialize)]
pub struct ProbeTimelineEvent {
    event_id: usize,
    code: u32,
    planned_start_us: u64,
    planned_end_us: u64,
    client_capture_unix_ns: u64,
}

impl ProbeTimeline {
    /// Build a client-origin event schedule for a synthetic bundled probe.
    ///
    /// Inputs: negotiated media profile, deterministic pulse schedule, total
    /// probe duration, and the Unix timestamp associated with local PTS zero.
    /// Outputs: a serializable timeline artifact.
    /// Why: the RCT analyzer can prove final A/V sync by itself, but end-to-end
    /// freshness needs to know when each flash/tone was generated on the client.
    #[must_use]
    pub fn new(
        camera: CameraConfig,
        schedule: &PulseSchedule,
        duration: Duration,
        client_start_unix_ns: u64,
    ) -> Self {
        Self {
            schema: "lesavka.client-transport-probe-timeline.v1",
            origin: "client-generated",
            media_path: "client synthetic capture -> bundled upstream media -> server UVC/UAC sinks -> RCT capture",
            injection_scope: "client-post-capture-uplink-bundle",
            client_uplink_included: true,
            client_start_unix_ns,
            camera_width: camera.width,
            camera_height: camera.height,
            camera_fps: camera.fps,
            camera_codec: codec_label(camera.codec),
            audio_sample_rate: AUDIO_SAMPLE_RATE_HZ,
            audio_channels: AUDIO_CHANNEL_COUNT,
            audio_chunk_ms: AUDIO_CHUNK_MS,
            warmup_us: micros(schedule.warmup_boundary()),
            duration_us: micros(duration),
            pulse_period_ms: millis(schedule.pulse_period()),
            pulse_width_ms: millis(schedule.pulse_width()),
            marker_tick_period: schedule.marker_tick_period(),
            event_width_codes: schedule.event_width_codes().to_vec(),
            events: timeline_events(schedule, duration, client_start_unix_ns),
        }
    }

    /// Write the timeline as pretty JSON.
    ///
    /// Inputs: destination path.
    /// Outputs: a filesystem artifact consumed by manual RCT probe scripts.
    /// Why: keeping the artifact structured avoids scraping probe logs for
    /// timing truth during transport tuning.
    pub fn write_to(&self, path: &Path) -> Result<()> {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)
                .with_context(|| format!("creating timeline directory {}", parent.display()))?;
        }
        let json = serde_json::to_string_pretty(self).context("serializing probe timeline")?;
        std::fs::write(path, format!("{json}\n"))
            .with_context(|| format!("writing {}", path.display()))
    }
}

/// Derive client-origin event rows from the pulse schedule.
///
/// Inputs: schedule, total probe duration, and Unix timestamp for PTS zero.
/// Outputs: timeline events with planned PTS and client clock timestamps.
/// Why: the RCT analyzer reports observed event identity, but freshness needs
/// the client-side generation time for the same event.
fn timeline_events(
    schedule: &PulseSchedule,
    duration: Duration,
    client_start_unix_ns: u64,
) -> Vec<ProbeTimelineEvent> {
    let mut events = Vec::new();
    let mut start = schedule.warmup_boundary();
    let period = schedule.pulse_period();
    while start < duration {
        let width = schedule.active_pulse_width(start);
        if width.is_zero() {
            break;
        }
        let code = if let Some(code) = schedule.event_code(start) {
            code
        } else if schedule.pulse_is_marker(start) {
            2
        } else {
            1
        };
        let planned_start_us = micros(start);
        events.push(ProbeTimelineEvent {
            event_id: events.len(),
            code,
            planned_start_us,
            planned_end_us: micros((start + width).min(duration)),
            client_capture_unix_ns: client_start_unix_ns
                .saturating_add(planned_start_us.saturating_mul(1_000)),
        });
        start += period;
    }
    events
}

fn codec_label(codec: CameraCodec) -> &'static str {
    match codec {
        CameraCodec::H264 => "h264",
        CameraCodec::Hevc => "hevc",
        CameraCodec::Mjpeg => "mjpeg",
    }
}

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

fn millis(duration: Duration) -> u64 {
    duration.as_millis().min(u64::MAX as u128) as u64
}

#[cfg(test)]
mod tests {
    use super::ProbeTimeline;
    use crate::input::camera::{CameraCodec, CameraConfig};
    use crate::sync_probe::schedule::PulseSchedule;
    use std::time::Duration;

    #[test]
    fn timeline_lists_client_origin_events_after_rounded_warmup() {
        let camera = CameraConfig {
            codec: CameraCodec::Mjpeg,
            width: 1280,
            height: 720,
            fps: 30,
        };
        let schedule = PulseSchedule::new(
            Duration::from_millis(3_500),
            Duration::from_millis(1_000),
            Duration::from_millis(120),
            5,
        );

        let timeline = ProbeTimeline::new(camera, &schedule, Duration::from_secs(8), 1_000);

        assert_eq!(
            timeline.schema,
            "lesavka.client-transport-probe-timeline.v1"
        );
        assert_eq!(timeline.warmup_us, 4_000_000);
        assert_eq!(timeline.camera_codec, "mjpeg");
        assert_eq!(timeline.audio_sample_rate, 48_000);
        assert_eq!(timeline.audio_channels, 2);
        assert_eq!(timeline.audio_chunk_ms, 20);
        assert!(timeline.event_width_codes.is_empty());
        assert_eq!(timeline.events.len(), 4);
        assert_eq!(timeline.events[0].event_id, 0);
        assert_eq!(timeline.events[0].code, 2);
        assert_eq!(timeline.events[0].planned_start_us, 4_000_000);
        assert_eq!(timeline.events[0].client_capture_unix_ns, 4_000_001_000);
        assert_eq!(timeline.events[1].code, 1);
    }

    #[test]
    fn timeline_preserves_explicit_event_codes() {
        let camera = CameraConfig {
            codec: CameraCodec::Mjpeg,
            width: 1280,
            height: 720,
            fps: 30,
        };
        let schedule = PulseSchedule::with_event_width_codes(
            Duration::from_secs(4),
            Duration::from_millis(1_000),
            Duration::from_millis(120),
            5,
            vec![1, 3],
        );

        let timeline = ProbeTimeline::new(camera, &schedule, Duration::from_secs(8), 1_000);

        assert_eq!(timeline.event_width_codes, vec![1, 3]);
        assert_eq!(timeline.events.len(), 2);
        assert_eq!(timeline.events[0].code, 1);
        assert_eq!(timeline.events[0].planned_end_us, 4_120_000);
        assert_eq!(timeline.events[1].code, 3);
        assert_eq!(timeline.events[1].planned_end_us, 5_120_000);
    }

    #[test]
    /// Timeline files should be durable artifacts for end-to-end freshness analysis.
    fn timeline_writes_json_and_labels_hevc_origin_profile() {
        let camera = CameraConfig {
            codec: CameraCodec::Hevc,
            width: 1920,
            height: 1080,
            fps: 30,
        };
        let schedule = PulseSchedule::new(
            Duration::from_secs(1),
            Duration::from_secs(1),
            Duration::from_millis(120),
            4,
        );
        let temp_dir = tempfile::tempdir().expect("tempdir");
        let path = temp_dir.path().join("nested/timeline.json");

        ProbeTimeline::new(camera, &schedule, Duration::from_secs(3), 123)
            .write_to(&path)
            .expect("write timeline");

        let json: serde_json::Value =
            serde_json::from_str(&std::fs::read_to_string(path).expect("timeline")).expect("json");
        assert_eq!(json["camera_codec"], "hevc");
        assert_eq!(json["events"][0]["code"], 2);
    }

    #[test]
    /// Codec labeling stays independent from GStreamer so transport reports stay cheap.
    fn timeline_labels_h264_without_needing_transport_runtime() {
        let camera = CameraConfig {
            codec: CameraCodec::H264,
            width: 640,
            height: 360,
            fps: 20,
        };
        let schedule = PulseSchedule::new(
            Duration::from_secs(1),
            Duration::from_secs(1),
            Duration::from_millis(120),
            4,
        );

        let timeline = ProbeTimeline::new(camera, &schedule, Duration::from_secs(2), 0);

        assert_eq!(timeline.camera_codec, "h264");
    }
}
