enum CameraSink {
    Uvc(WebcamSink),
    Hdmi(HdmiSink),
    #[cfg(coverage)]
    Noop,
}

impl CameraSink {
    fn push(&self, pkt: VideoPacket) {
        match self {
            CameraSink::Uvc(sink) => sink.push(pkt),
            CameraSink::Hdmi(sink) => sink.push(pkt),
            #[cfg(coverage)]
            CameraSink::Noop => {
                let _ = pkt;
            }
        }
    }
}

/// Forward camera packets from gRPC into either a UVC or HDMI sink.
///
/// Inputs: packets received from the client camera stream.
/// Outputs: none; packets are forwarded to the configured sink.
/// Why: camera sessions share the same logging and dev-mode dump behavior even
/// though their physical sinks differ.
pub struct CameraRelay {
    sink: CameraSink,
    id: u32,
    frames: AtomicU64,
}

impl CameraRelay {
    /// Build a relay that targets the USB UVC gadget.
    ///
    /// Inputs: the logical camera id, UVC device node, and camera config.
    /// Outputs: a relay that writes frames into the gadget pipeline.
    /// Why: keeping constructors explicit avoids accidental sink mismatches.
    pub fn new_uvc(id: u32, uvc_dev: &str, cfg: &CameraConfig) -> anyhow::Result<Self> {
        Ok(Self {
            sink: CameraSink::Uvc(WebcamSink::new(uvc_dev, cfg)?),
            id,
            frames: AtomicU64::new(0),
        })
    }

    /// Build a relay that targets the HDMI output pipeline.
    ///
    /// Inputs: the logical camera id plus the camera config.
    /// Outputs: a relay that writes frames into the display pipeline.
    /// Why: the camera runtime reuses this constructor when the negotiated
    /// output mode selects HDMI instead of UVC.
    pub fn new_hdmi(id: u32, cfg: &CameraConfig) -> anyhow::Result<Self> {
        Ok(Self {
            sink: CameraSink::Hdmi(HdmiSink::new(cfg)?),
            id,
            frames: AtomicU64::new(0),
        })
    }

    #[cfg(coverage)]
    pub fn new_noop(id: u32) -> Self {
        Self {
            sink: CameraSink::Noop,
            id,
            frames: AtomicU64::new(0),
        }
    }

    /// Push one `VideoPacket` coming from the client.
    ///
    /// Inputs: the next packet from the camera stream.
    /// Outputs: none; the packet is logged and forwarded to the sink.
    /// Why: centralizing frame logging and dev-mode dump behavior keeps the
    /// transport session logic separate from media sink mechanics.
    #[cfg(coverage)]
    pub fn feed(&self, pkt: VideoPacket) {
        let frame = self
            .frames
            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);

        if dev_mode_enabled() && contains_idr(&pkt.data) {
            let path = format!("/tmp/eye3-cli-{frame:05}.h264");
            let _ = std::fs::write(&path, &pkt.data);
        }

        self.sink.push(pkt);
    }

    #[cfg(not(coverage))]
    pub fn feed(&self, pkt: VideoPacket) {
        let frame = self
            .frames
            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        if frame < 10 || frame.is_multiple_of(60) {
            tracing::debug!(
                target:"lesavka_server::video",
                cam_id = self.id,
                frame,
                bytes = pkt.data.len(),
                pts = pkt.pts,
                "📸 srv  webcam frame"
            );
        } else if frame.is_multiple_of(10) {
            tracing::trace!(
                target:"lesavka_server::video",
                cam_id = self.id,
                bytes = pkt.data.len(),
                "📸📥 srv  pkt"
            );
        }

        if dev_mode_enabled()
            && (cfg!(debug_assertions) || tracing::enabled!(tracing::Level::TRACE))
            && contains_idr(&pkt.data)
        {
            let path = format!("/tmp/eye3-cli-{frame:05}.h264");
            if let Err(error) = std::fs::write(&path, &pkt.data) {
                warn!("📸💾 dump failed: {error}");
            } else {
                tracing::debug!("📸💾 wrote {}", path);
            }
        }

        self.sink.push(pkt);
    }
}
