//! Kernel and initramfs extraction from RVF files.
//!
//! Opens the RVF store read-only, locates the KERNEL_SEG, parses the
//! KernelHeader, and writes the kernel image and optional initramfs to
//! temporary files that persist until the returned handles are dropped.

use std::io::Write;
use std::path::{Path, PathBuf};

use rvf_runtime::RvfStore;
use rvf_types::kernel::KernelHeader;

use crate::error::LaunchError;

/// Extracted kernel artifacts ready for QEMU consumption.
pub struct ExtractedKernel {
    /// Path to the extracted kernel image (bzImage or equivalent).
    pub kernel_path: PathBuf,
    /// Path to the extracted initramfs, if present in the KERNEL_SEG.
    pub initramfs_path: Option<PathBuf>,
    /// The parsed KernelHeader.
    pub header: KernelHeader,
    /// The kernel command line string.
    pub cmdline: String,
    /// Temp directory holding the extracted files (kept alive via ownership).
    _tempdir: tempfile::TempDir,
}

impl std::fmt::Debug for ExtractedKernel {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ExtractedKernel")
            .field("kernel_path", &self.kernel_path)
            .field("initramfs_path", &self.initramfs_path)
            .field("cmdline", &self.cmdline)
            .finish_non_exhaustive()
    }
}

/// Extract kernel and initramfs from an RVF file to temporary files.
///
/// The returned `ExtractedKernel` owns a `TempDir`; the files are cleaned
/// up when it is dropped.
pub fn extract_kernel(rvf_path: &Path) -> Result<ExtractedKernel, LaunchError> {
    let store = RvfStore::open_readonly(rvf_path)
        .map_err(|e| LaunchError::KernelExtraction(format!("failed to open store: {e:?}")))?;

    let (header_bytes, remainder) = store
        .extract_kernel()
        .map_err(|e| LaunchError::KernelExtraction(format!("segment read error: {e:?}")))?
        .ok_or_else(|| LaunchError::NoKernelSegment {
            path: rvf_path.to_path_buf(),
        })?;

    if header_bytes.len() < 128 {
        return Err(LaunchError::KernelExtraction(
            "KernelHeader too short".into(),
        ));
    }

    let mut hdr_array = [0u8; 128];
    hdr_array.copy_from_slice(&header_bytes[..128]);
    let header = KernelHeader::from_bytes(&hdr_array)
        .map_err(|e| LaunchError::KernelExtraction(format!("bad KernelHeader: {e:?}")))?;

    // The wire format after the 128-byte KernelHeader (which is already
    // split off into `header_bytes`) is:
    //
    //   For simple embed_kernel (no binding):
    //     kernel_image || cmdline
    //
    //   For embed_kernel_with_binding:
    //     KernelBinding(128) || cmdline || kernel_image
    //
    // We determine the layout from header.image_size which tells us the
    // kernel image length. The cmdline is header.cmdline_length bytes.

    let image_size = header.image_size as usize;
    let cmdline_length = header.cmdline_length as usize;

    // Simple format: image comes first in the remainder, then cmdline
    let (kernel_image, cmdline) = if image_size > 0 && image_size <= remainder.len() {
        let img = &remainder[..image_size];
        let cmd = if cmdline_length > 0 && image_size + cmdline_length <= remainder.len() {
            String::from_utf8_lossy(&remainder[image_size..image_size + cmdline_length])
                .into_owned()
        } else {
            String::new()
        };
        (img, cmd)
    } else {
        // Fallback: treat entire remainder as kernel image
        (&remainder[..], String::new())
    };

    // Write to temp files
    let tempdir = tempfile::tempdir().map_err(LaunchError::TempFile)?;

    let kernel_file_path = tempdir.path().join("vmlinuz");
    {
        let mut f =
            std::fs::File::create(&kernel_file_path).map_err(LaunchError::TempFile)?;
        f.write_all(kernel_image).map_err(LaunchError::TempFile)?;
        f.sync_all().map_err(LaunchError::TempFile)?;
    }

    // For now we do not split out a separate initramfs from the kernel
    // image. A future version could detect an appended initramfs using
    // the standard Linux trailer magic (0x6d65736800000000).
    let initramfs_path = None;

    Ok(ExtractedKernel {
        kernel_path: kernel_file_path,
        initramfs_path,
        header,
        cmdline,
        _tempdir: tempdir,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use rvf_runtime::options::RvfOptions;
    use rvf_types::kernel::KernelArch;

    #[test]
    fn extract_from_store_with_kernel() {
        let dir = tempfile::tempdir().unwrap();
        let rvf_path = dir.path().join("test.rvf");

        let opts = RvfOptions {
            dimension: 4,
            ..Default::default()
        };
        let mut store = RvfStore::create(&rvf_path, opts).unwrap();

        let image = b"MZ\x00fake-kernel-image-for-testing";
        store
            .embed_kernel(
                KernelArch::X86_64 as u8,
                0x01,
                0,
                image,
                8080,
                Some("console=ttyS0"),
            )
            .unwrap();
        store.close().unwrap();

        let extracted = extract_kernel(&rvf_path).unwrap();
        assert!(extracted.kernel_path.exists());

        let on_disk = std::fs::read(&extracted.kernel_path).unwrap();
        assert_eq!(on_disk, image);
        assert_eq!(extracted.header.api_port, 8080);
        assert_eq!(extracted.cmdline, "console=ttyS0");
    }

    #[test]
    fn extract_kernel_no_cmdline() {
        let dir = tempfile::tempdir().unwrap();
        let rvf_path = dir.path().join("no_cmd.rvf");

        let opts = RvfOptions {
            dimension: 4,
            ..Default::default()
        };
        let mut store = RvfStore::create(&rvf_path, opts).unwrap();

        let image = b"fake-kernel";
        store
            .embed_kernel(
                KernelArch::X86_64 as u8,
                0x01,
                0,
                image,
                9090,
                None,
            )
            .unwrap();
        store.close().unwrap();

        let extracted = extract_kernel(&rvf_path).unwrap();
        let on_disk = std::fs::read(&extracted.kernel_path).unwrap();
        assert_eq!(on_disk, image);
        assert!(extracted.cmdline.is_empty());
    }

    #[test]
    fn extract_returns_error_when_no_kernel() {
        let dir = tempfile::tempdir().unwrap();
        let rvf_path = dir.path().join("no_kernel.rvf");

        let opts = RvfOptions {
            dimension: 4,
            ..Default::default()
        };
        let store = RvfStore::create(&rvf_path, opts).unwrap();
        store.close().unwrap();

        let result = extract_kernel(&rvf_path);
        assert!(result.is_err());
        match result.unwrap_err() {
            LaunchError::NoKernelSegment { .. } => {}
            other => panic!("expected NoKernelSegment, got: {other}"),
        }
    }
}
