#![forbid(unsafe_code)]

use gstreamer as gst;

pub const SOFTWARE_VIDEO_FALLBACK_ENV: &str = "LESAVKA_ALLOW_SOFTWARE_VIDEO";
pub const ALLOW_VULKAN_H264_DECODER_ENV: &str = "LESAVKA_ALLOW_VULKAN_H264_DECODER";

/// Return whether software video fallback is explicitly allowed.
///
/// Inputs: `LESAVKA_ALLOW_SOFTWARE_VIDEO`.
/// Outputs: `true` only for intentional opt-in values.
/// Why: production Lesavka should fail loudly when GPU decode is unavailable,
/// instead of silently shifting downstream video onto the CPU.
#[must_use]
pub fn software_video_fallback_allowed() -> bool {
    env_flag_enabled(SOFTWARE_VIDEO_FALLBACK_ENV)
}

fn env_flag_enabled(name: &str) -> bool {
    std::env::var(name).ok().is_some_and(|value| {
        let trimmed = value.trim();
        !(trimmed.is_empty()
            || trimmed.eq_ignore_ascii_case("0")
            || trimmed.eq_ignore_ascii_case("false")
            || trimmed.eq_ignore_ascii_case("no")
            || trimmed.eq_ignore_ascii_case("off"))
    })
}

fn stability_software_decode_allowed() -> bool {
    software_video_fallback_allowed()
}

fn vulkan_h264_decoder_allowed() -> bool {
    env_flag_enabled(ALLOW_VULKAN_H264_DECODER_ENV)
}

#[must_use]
pub fn is_hardware_h264_decoder(name: &str) -> bool {
    matches!(
        name,
        "nvh264dec"
            | "nvh264sldec"
            | "vulkanh264dec"
            | "vah264dec"
            | "vaapih264dec"
            | "v4l2h264dec"
            | "v4l2slh264dec"
    )
}

/// Pick the client-side H.264 decoder in a predictable preference order.
///
/// Inputs: none, though operators may override the choice with
/// `LESAVKA_H264_DECODER=<element>` or bias automatic fallback order with
/// `LESAVKA_H264_DECODER_PREFERENCE=hardware|software`.
/// Outputs: the chosen decoder element name, or `decodebin` as a last-resort
/// error when no hardware decoder is present.
/// Why: Lesavka should use GPU decode on NVIDIA/VAAPI/V4L2-capable clients
/// and should not hide hardware failures behind CPU decode.
#[must_use]
#[allow(dead_code)] // retained for include-based tests and diagnostics.
pub fn pick_h264_decoder() -> String {
    require_h264_decoder().unwrap_or_else(|_| "missing-hardware-h264dec".to_string())
}

/// Require a buildable H.264 decoder that satisfies the production policy.
///
/// Inputs: optional decoder override plus local GStreamer registry.
/// Outputs: a selected decoder or a human-readable error.
/// Why: callers that create live downstream video must fail before constructing
/// a CPU-bound pipeline when hardware decode is unavailable.
pub fn require_h264_decoder() -> Result<String, String> {
    if let Ok(raw) = std::env::var("LESAVKA_H264_DECODER") {
        let name = raw.trim();
        if !name.is_empty() {
            if name.eq_ignore_ascii_case("decodebin") {
                if software_video_fallback_allowed() {
                    return Ok("decodebin".to_string());
                }
                return Err(format!(
                    "requested H.264 decoder '{name}' is not hardware-specific; set {SOFTWARE_VIDEO_FALLBACK_ENV}=1 only for lab fallback"
                ));
            }
            if !buildable_decoder(name) {
                return Err(format!("requested H.264 decoder '{name}' is not buildable"));
            }
            if is_hardware_h264_decoder(name) || software_video_fallback_allowed() {
                return Ok(name.to_string());
            }
            return Err(format!(
                "requested H.264 decoder '{name}' is not a hardware decoder; set {SOFTWARE_VIDEO_FALLBACK_ENV}=1 only for lab fallback"
            ));
        }
    }

    for name in h264_decoder_preference_order() {
        if buildable_decoder(name) {
            return Ok(name.to_string());
        }
    }

    Err("hardware H.264 decoder required, but no buildable NVIDIA/VAAPI/V4L2 decoder was found; install gst-plugin-va for the libva-nvidia NVDEC route, or set LESAVKA_ALLOW_VULKAN_H264_DECODER=1 only for Vulkan diagnostics".to_string())
}

/// Return automatic H.264 decoder candidates in selection order.
///
/// Inputs: `LESAVKA_H264_DECODER_PREFERENCE`, if set. Output: ordered decoder
/// element names. Why: tests and diagnostics need to prove proprietary
/// NVIDIA and VAAPI/V4L2 routes stay ahead of explicit lab fallback; Vulkan is
/// opt-in because it has been choppy on the NVIDIA desktop path.
#[must_use]
pub fn h264_decoder_preference_order() -> Vec<&'static str> {
    const PRIMARY_HARDWARE: &[&str] = &[
        "nvh264dec",
        "nvh264sldec",
        "vah264dec",
        "vaapih264dec",
        "v4l2h264dec",
        "v4l2slh264dec",
    ];
    const VULKAN_HARDWARE: &[&str] = &["vulkanh264dec"];
    const SOFTWARE: &[&str] = &["avdec_h264", "openh264dec"];

    let auto_software_allowed = stability_software_decode_allowed();
    let prefer_software = auto_software_allowed
        && std::env::var("LESAVKA_H264_DECODER_PREFERENCE")
            .ok()
            .map(|value| {
                matches!(
                    value.trim().to_ascii_lowercase().as_str(),
                    "software" | "sw" | "cpu"
                )
            })
            .unwrap_or(false);

    let mut candidates =
        Vec::with_capacity(PRIMARY_HARDWARE.len() + SOFTWARE.len() + VULKAN_HARDWARE.len());
    if prefer_software {
        candidates.extend_from_slice(SOFTWARE);
        candidates.extend_from_slice(PRIMARY_HARDWARE);
        if vulkan_h264_decoder_allowed() {
            candidates.extend_from_slice(VULKAN_HARDWARE);
        }
    } else {
        candidates.extend_from_slice(PRIMARY_HARDWARE);
        if vulkan_h264_decoder_allowed() {
            candidates.extend_from_slice(VULKAN_HARDWARE);
        }
        if auto_software_allowed {
            candidates.extend_from_slice(SOFTWARE);
        }
    }
    candidates
}

/// Return a parse-launch fragment for the selected H.264 decoder.
///
/// Inputs: decoder element name. Output: a pipeline fragment with a stable
/// decoder stage. Why: Vulkan decoders output GPU memory, so they need
/// an explicit download step before the existing CPU-side sinks can consume
/// frames; keeping that in one helper prevents hardware decode from being
/// selected and then immediately failing link negotiation.
#[must_use]
pub fn h264_decoder_launch_fragment(decoder_name: &str) -> String {
    h264_decoder_launch_fragment_named(decoder_name, "decoder")
}

/// Return a parse-launch fragment for the selected H.264 decoder.
///
/// Inputs: decoder element name plus a legacy caller-owned element name.
/// Output: a pipeline fragment. Why: some binary decoder wrappers reject
/// `name=` in launch strings, while duplicated fragments still create
/// independent decoder elements.
#[must_use]
pub fn h264_decoder_launch_fragment_named(decoder_name: &str, _element_name: &str) -> String {
    match decoder_name {
        "vulkanh264dec" => concat!(
            "vulkanh264dec discard-corrupted-frames=true ",
            "automatic-request-sync-points=true ! vulkandownload"
        )
        .to_string(),
        name => name.to_string(),
    }
}

/// Detect whether an Annex-B H.264 access unit carries an IDR frame.
///
/// Inputs: one H.264 access unit. Output: true when it includes NAL type 5.
/// Why: if the downstream renderer drops a predicted frame, resuming on the
/// next non-IDR packet can smear until a keyframe repairs decoder state.
#[must_use]
pub fn contains_idr(h264: &[u8]) -> bool {
    let mut index = 0;
    while index + 4 < h264.len() {
        if h264[index] == 0 && h264[index + 1] == 0 {
            let offset = if h264[index + 2] == 1 {
                3
            } else if h264[index + 2] == 0 && h264[index + 3] == 1 {
                4
            } else {
                index += 1;
                continue;
            };
            let nal_index = index + offset;
            if nal_index < h264.len() && (h264[nal_index] & 0x1F) == 5 {
                return true;
            }
        }
        index += 1;
    }
    false
}

fn buildable_decoder(name: &str) -> bool {
    #[cfg(coverage)]
    if std::env::var("TEST_FAIL_GST_INIT").is_ok() {
        return false;
    }
    if gst::init().is_err() {
        return false;
    }
    #[cfg(coverage)]
    if std::env::var("TEST_DISABLE_H264_DECODER_FACTORY").is_ok() {
        return false;
    }
    gst::ElementFactory::find(name).is_some() && gst::ElementFactory::make(name).build().is_ok()
}
