//! FFI bindings to librpifwcrypto.
//!
//! This module provides safe Rust wrappers around the Raspberry Pi firmware
//! cryptography service. Bindings are generated at build time using bindgen.

use std::ffi::CStr;
use std::io;

// Include the bindgen-generated bindings
#[allow(non_upper_case_globals)]
#[allow(non_camel_case_types)]
#[allow(non_snake_case)]
#[allow(dead_code)]
mod ffi {
    include!(concat!(env!("OUT_DIR"), "/rpifwcrypto_bindings.rs"));
}

/// HMAC-SHA256 output size in bytes.
const HMAC_SHA256_SIZE: usize = 32;

/// Maximum message size for HMAC operations.
pub const HMAC_MSG_MAX_SIZE: usize = ffi::RPI_FW_CRYPTO_HMAC_MSG_MAX_SIZE as usize;

/// Error codes from the firmware crypto subsystem.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CryptoStatus(ffi::RPI_FW_CRYPTO_STATUS);

impl CryptoStatus {
    /// Get the human-readable error string from the library.
    pub fn message(&self) -> &'static str {
        // SAFETY: `rpi_fw_crypto_strerror` returns a pointer to a static string
        // literal that is valid for the lifetime of the program. The function
        // is documented to never return NULL and always returns a valid C string.
        unsafe {
            let ptr = ffi::rpi_fw_crypto_strerror(self.0);
            if ptr.is_null() {
                "unknown error"
            } else {
                CStr::from_ptr(ptr).to_str().unwrap_or("unknown error")
            }
        }
    }
}

impl std::fmt::Display for CryptoStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.message())
    }
}

impl std::error::Error for CryptoStatus {}

/// Calculate HMAC-SHA256 using a firmware OTP key.
///
/// # Arguments
///
/// * `key_id` - The ID of the OTP key to use (typically 0 for the device key)
/// * `message` - The message to HMAC (max 2048 bytes)
///
/// # Returns
///
/// A 64-character lowercase hex string of the HMAC-SHA256 digest.
pub fn hmac_sha256(key_id: u32, message: &[u8]) -> Result<String, CryptoError> {
    if message.len() > HMAC_MSG_MAX_SIZE {
        return Err(CryptoError::MessageTooLarge(message.len()));
    }

    let mut hmac = [0u8; HMAC_SHA256_SIZE];

    // SAFETY: We pass valid pointers and lengths:
    // - `message.as_ptr()` is valid for `message.len()` bytes
    // - `hmac.as_mut_ptr()` points to a 32-byte buffer, which is the required
    //   output size for HMAC-SHA256
    // - flags=0 is the documented safe default
    let rc = unsafe {
        ffi::rpi_fw_crypto_hmac_sha256(
            0,
            key_id,
            message.as_ptr(),
            message.len(),
            hmac.as_mut_ptr(),
        )
    };

    if rc != ffi::RPI_FW_CRYPTO_STATUS_RPI_FW_CRYPTO_SUCCESS as i32 {
        // The return code is the negated error status
        let status = (-rc) as ffi::RPI_FW_CRYPTO_STATUS;
        return Err(CryptoError::Firmware(CryptoStatus(status)));
    }

    Ok(hmac.iter().map(|b| format!("{:02x}", b)).collect())
}

/// Error type for crypto operations.
#[derive(Debug)]
pub enum CryptoError {
    /// Message exceeds maximum size.
    MessageTooLarge(usize),
    /// Firmware returned an error.
    Firmware(CryptoStatus),
}

impl std::fmt::Display for CryptoError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CryptoError::MessageTooLarge(len) => {
                write!(
                    f,
                    "message too large ({} bytes, max {})",
                    len, HMAC_MSG_MAX_SIZE
                )
            }
            CryptoError::Firmware(status) => write!(f, "firmware crypto error: {}", status),
        }
    }
}

impl std::error::Error for CryptoError {}

impl From<CryptoError> for io::Error {
    fn from(e: CryptoError) -> Self {
        io::Error::other(e)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use std::process::Command;

    /// Compare our hmac_sha256 wrapper against the rpi-fw-crypto CLI tool.
    ///
    /// This test requires a Raspberry Pi with rpifwcrypto installed and is
    /// disabled by default. Run with: cargo test -- --ignored
    #[test]
    #[ignore]
    fn test_hmac_sha256_matches_cli() {
        let test_message = b"test message for hmac verification";
        let key_id = 1;

        // Get result from our wrapper
        let our_result = hmac_sha256(key_id, test_message).expect("hmac_sha256 failed");

        // Get result from CLI tool
        let mut tmpfile = tempfile::NamedTempFile::new().expect("failed to create temp file");
        tmpfile
            .write_all(test_message)
            .expect("failed to write temp file");

        let output = Command::new("rpi-fw-crypto")
            .args([
                "hmac",
                "--in",
                tmpfile.path().to_str().unwrap(),
                "--key-id",
                &key_id.to_string(),
                "--outform",
                "hex",
            ])
            .output()
            .expect("failed to execute rpi-fw-crypto");

        assert!(output.status.success(), "rpi-fw-crypto failed");

        // Truncate to 64 chars (some versions include trailing newline)
        let cli_result: String = String::from_utf8_lossy(&output.stdout)
            .chars()
            .take(64)
            .collect();

        assert_eq!(
            our_result, cli_result,
            "HMAC mismatch: wrapper={}, cli={}",
            our_result, cli_result
        );
    }

    /// Test with empty message.
    #[test]
    #[ignore]
    fn test_hmac_sha256_empty_message() {
        let test_message = b"";
        let key_id = 1;

        let our_result = hmac_sha256(key_id, test_message).expect("hmac_sha256 failed");

        let tmpfile = tempfile::NamedTempFile::new().expect("failed to create temp file");
        // Empty file - write nothing

        let output = Command::new("rpi-fw-crypto")
            .args([
                "hmac",
                "--in",
                tmpfile.path().to_str().unwrap(),
                "--key-id",
                &key_id.to_string(),
                "--outform",
                "hex",
            ])
            .output()
            .expect("failed to execute rpi-fw-crypto");

        assert!(output.status.success(), "rpi-fw-crypto failed");

        let cli_result: String = String::from_utf8_lossy(&output.stdout)
            .chars()
            .take(64)
            .collect();

        assert_eq!(our_result, cli_result);
    }

    /// Test output format is valid lowercase hex.
    #[test]
    #[ignore]
    fn test_hmac_sha256_output_format() {
        let result = hmac_sha256(1, b"test").expect("hmac_sha256 failed");

        assert_eq!(result.len(), 64, "HMAC hex output should be 64 characters");
        assert!(
            result.chars().all(|c| c.is_ascii_hexdigit()),
            "output should be hex"
        );
        assert!(
            result.chars().all(|c| !c.is_ascii_uppercase()),
            "output should be lowercase"
        );
    }
}
