//! Frame encoding module
//!
//! Encodes captured frames for transmission. Supports:
//! - Raw BGRA + Zstd compression (lowest latency, LAN mode; the guaranteed
//!   fallback and the current default).
//! - H.264 hardware encoding via Windows Media Foundation (Task 7) — the
//!   negotiated upgrade. Compile-verified; validated on real hardware in plan
//!   Task 8. On any init/feed failure the factory or encoder falls back to raw.
//!
//! Codec selection is driven by the negotiated `VideoCodec` the server sends on
//! `StartStream` (see `select_codec` / `create_encoder_for`). The capability the
//! agent advertises to the server is detected by `capability::supports_hardware_h264`.

mod capability;
pub(crate) mod color;
#[cfg(windows)]
mod h264;
mod raw;

pub use capability::supports_hardware_h264;
pub use raw::RawEncoder;

use crate::capture::CapturedFrame;
use crate::proto::{video_frame, VideoCodec, VideoFrame};
use anyhow::Result;

/// Encoded frame ready for transmission
#[derive(Debug)]
pub struct EncodedFrame {
    /// Protobuf video frame message
    pub frame: VideoFrame,

    /// Size in bytes after encoding
    pub size: usize,

    /// Whether this is a keyframe (full frame)
    // Set by encoders; not yet read by the transmit path.
    #[allow(dead_code)]
    pub is_keyframe: bool,
}

/// Frame encoder trait.
///
/// Every implementor turns a `CapturedFrame` (BGRA) into a wire `VideoFrame`
/// using one `video_frame::Encoding` variant. `RawEncoder` emits the `Raw`
/// variant; the H.264 encoder emits the `H264` variant. The factory
/// (`create_encoder_for`) selects the implementor from the negotiated codec.
pub trait Encoder: Send {
    /// Encode a captured frame
    fn encode(&mut self, frame: &CapturedFrame) -> Result<EncodedFrame>;

    /// Request a keyframe on next encode
    #[allow(dead_code)]
    fn request_keyframe(&mut self);

    /// Get encoder name/type
    #[allow(dead_code)]
    fn name(&self) -> &str;
}

/// Map a configured/negotiated codec string to a `VideoCodec`.
///
/// Used when constructing an encoder from the agent's own `EncodingConfig`
/// (before any server negotiation). Unknown / "auto" / "raw" all resolve to raw
/// — the safe default. "h264" resolves to H.264 (which itself falls back to raw
/// if MF init fails).
///
/// Retained as the config-string entry point (used by `create_encoder` and the
/// unit tests); the live session negotiates via `select_codec` on a `VideoCodec`.
#[allow(dead_code)]
pub fn codec_from_str(codec: &str) -> VideoCodec {
    match codec.to_lowercase().as_str() {
        "h264" => VideoCodec::H264,
        // "h265"/"hevc" are future opt-in (TODO) — treat as raw for now so we
        // never select an unimplemented codec.
        _ => VideoCodec::Raw,
    }
}

/// Choose the codec the agent will actually use for a stream, given the codec
/// the server negotiated and the agent's own hardware capability.
///
/// This is the agent-side guard that keeps the raw fallback authoritative:
/// - The server only negotiates H.264 when the agent advertised support, but we
///   re-check `supports_hardware_h264()` here so a stale/misconfigured server
///   selection can never force an unsupported codec.
/// - H.265 is not implemented; it degrades to raw.
/// - Anything else is raw.
pub fn select_codec(negotiated: VideoCodec, hardware_h264_available: bool) -> VideoCodec {
    match negotiated {
        VideoCodec::H264 if hardware_h264_available => VideoCodec::H264,
        // Server asked for H.264 but we have no HW encoder -> raw.
        VideoCodec::H264 => VideoCodec::Raw,
        // HEVC not implemented yet (TODO: Task 7 opt-in / future).
        VideoCodec::H265 => VideoCodec::Raw,
        VideoCodec::Raw => VideoCodec::Raw,
    }
}

/// Create an encoder for an explicit `VideoCodec`, with a transparent fallback
/// to raw if a hardware encoder cannot be constructed.
///
/// `quality` is the 1-100 quality knob (mapped per-codec). On H.264 init failure
/// this logs and returns a raw encoder so the session keeps working.
pub fn create_encoder_for(codec: VideoCodec, quality: u32) -> Result<Box<dyn Encoder>> {
    match codec {
        VideoCodec::H264 => {
            #[cfg(windows)]
            {
                match h264::H264Encoder::new(quality) {
                    Ok(enc) => {
                        tracing::info!("Using hardware H.264 encoder (Media Foundation)");
                        Ok(Box::new(enc))
                    }
                    Err(e) => {
                        tracing::warn!(
                            "H.264 encoder init failed ({e:#}); falling back to raw+Zstd"
                        );
                        Ok(Box::new(RawEncoder::new(quality)?))
                    }
                }
            }
            #[cfg(not(windows))]
            {
                tracing::warn!("H.264 unsupported on this platform; using raw+Zstd");
                Ok(Box::new(RawEncoder::new(quality)?))
            }
        }
        // Raw (and anything that resolved to raw) uses the salvaged encoder.
        VideoCodec::Raw | VideoCodec::H265 => Ok(Box::new(RawEncoder::new(quality)?)),
    }
}

/// Create an encoder based on a codec string (agent config path).
///
/// Backwards-compatible entry point that builds an encoder from a codec STRING
/// (e.g. `EncodingConfig.codec`). Resolves the string to a `VideoCodec`, applies
/// the hardware-availability guard, then builds the encoder. The live session
/// uses `select_codec` + `create_encoder_for` (negotiated `VideoCodec`) instead;
/// this remains for the config path and is covered by unit tests.
#[allow(dead_code)]
pub fn create_encoder(codec: &str, quality: u32) -> Result<Box<dyn Encoder>> {
    let requested = codec_from_str(codec);
    let chosen = select_codec(requested, supports_hardware_h264());
    create_encoder_for(chosen, quality)
}

/// Build an `EncodedFrame` carrying a single `video_frame::Encoding` payload.
/// Shared helper so encoders don't each repeat the `VideoFrame` wrapper.
#[allow(dead_code)]
pub(crate) fn wrap_video_frame(
    timestamp_ms: i64,
    display_id: i32,
    sequence: i32,
    encoding: video_frame::Encoding,
    size: usize,
    is_keyframe: bool,
) -> EncodedFrame {
    EncodedFrame {
        frame: VideoFrame {
            timestamp: timestamp_ms,
            display_id,
            sequence,
            encoding: Some(encoding),
        },
        size,
        is_keyframe,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn codec_from_str_maps_known_and_unknown() {
        assert_eq!(codec_from_str("h264"), VideoCodec::H264);
        assert_eq!(codec_from_str("H264"), VideoCodec::H264);
        assert_eq!(codec_from_str("raw"), VideoCodec::Raw);
        assert_eq!(codec_from_str("zstd"), VideoCodec::Raw);
        assert_eq!(codec_from_str("auto"), VideoCodec::Raw);
        assert_eq!(codec_from_str("vp9"), VideoCodec::Raw);
        // HEVC not implemented -> raw, never H265.
        assert_eq!(codec_from_str("h265"), VideoCodec::Raw);
        assert_eq!(codec_from_str("hevc"), VideoCodec::Raw);
        assert_eq!(codec_from_str(""), VideoCodec::Raw);
    }

    #[test]
    fn select_codec_honors_hardware_guard() {
        // Server negotiated H.264 and HW is present -> H.264.
        assert_eq!(select_codec(VideoCodec::H264, true), VideoCodec::H264);
        // Server negotiated H.264 but no HW -> raw (never forced).
        assert_eq!(select_codec(VideoCodec::H264, false), VideoCodec::Raw);
        // Raw stays raw regardless of HW.
        assert_eq!(select_codec(VideoCodec::Raw, true), VideoCodec::Raw);
        assert_eq!(select_codec(VideoCodec::Raw, false), VideoCodec::Raw);
        // HEVC always degrades to raw (unimplemented).
        assert_eq!(select_codec(VideoCodec::H265, true), VideoCodec::Raw);
    }

    #[test]
    fn raw_factory_always_succeeds() {
        // Raw must always construct (the guaranteed fallback).
        let enc = create_encoder_for(VideoCodec::Raw, 75).unwrap();
        assert_eq!(enc.name(), "raw+zstd");
    }

    #[test]
    fn create_encoder_string_path_resolves_to_raw_without_hw() {
        // On a machine without a HW encoder (CI / non-Windows), "h264" must
        // resolve to a working raw encoder, not an error.
        let enc = create_encoder("h264", 75).unwrap();
        // Without HW it is raw; with HW it would be the H.264 encoder. We only
        // assert it constructed.
        let _ = enc.name();
    }

    #[test]
    fn create_encoder_auto_is_raw() {
        let enc = create_encoder("auto", 75).unwrap();
        assert_eq!(enc.name(), "raw+zstd");
    }
}
