//! mkfs functionality for creating ext4 filesystems.

use crate::block_device::BlockDevice;
use crate::{Error, Result};
use ext4_mkfs_sys::{self as ffi, F_SET_EXT2, F_SET_EXT3, F_SET_EXT4};
use std::ffi::CString;
use std::os::raw::{c_int, c_void};
use std::ptr;

/// Filesystem type to create.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FsType {
    /// ext2 filesystem
    Ext2,
    /// ext3 filesystem
    Ext3,
    /// ext4 filesystem (default)
    #[default]
    Ext4,
}

impl FsType {
    fn to_c_int(self) -> c_int {
        match self {
            FsType::Ext2 => F_SET_EXT2,
            FsType::Ext3 => F_SET_EXT3,
            FsType::Ext4 => F_SET_EXT4,
        }
    }
}

/// Configuration for creating an ext4 filesystem.
#[derive(Debug, Clone)]
pub struct MkfsConfig {
    /// Filesystem type (ext2, ext3, or ext4)
    pub fs_type: FsType,
    /// Block size in bytes (1024, 2048, or 4096)
    pub block_size: u32,
    /// Volume label (max 16 characters)
    pub label: Option<String>,
    /// UUID for the filesystem
    pub uuid: Option<[u8; 16]>,
    /// Enable journaling (only for ext3/ext4)
    pub journal: bool,
    /// Inode size (128 or 256)
    pub inode_size: u32,
}

impl Default for MkfsConfig {
    fn default() -> Self {
        Self {
            fs_type: FsType::Ext4,
            block_size: 4096,
            label: None,
            uuid: None,
            journal: false, // Disabled by default for simplicity
            inode_size: 256,
        }
    }
}

impl MkfsConfig {
    /// Create a new configuration with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the filesystem type.
    pub fn fs_type(mut self, fs_type: FsType) -> Self {
        self.fs_type = fs_type;
        self
    }

    /// Set the block size.
    pub fn block_size(mut self, size: u32) -> Self {
        self.block_size = size;
        self
    }

    /// Set the volume label.
    pub fn label(mut self, label: impl Into<String>) -> Self {
        self.label = Some(label.into());
        self
    }

    /// Set the UUID.
    pub fn uuid(mut self, uuid: [u8; 16]) -> Self {
        self.uuid = Some(uuid);
        self
    }

    /// Enable or disable journaling.
    pub fn journal(mut self, enable: bool) -> Self {
        self.journal = enable;
        self
    }

    /// Set the inode size.
    pub fn inode_size(mut self, size: u32) -> Self {
        self.inode_size = size;
        self
    }
}

/// Context passed to FFI callbacks (type-erased for C compatibility)
struct BlockDevContext {
    device: Box<dyn BlockDevice>,
    buffer: Vec<u8>,
}

/// Open callback for lwext4
unsafe extern "C" fn bdev_open(bdev: *mut ffi::ext4_blockdev) -> c_int {
    let ctx = (*(*bdev).bdif).p_user as *mut BlockDevContext;
    if ctx.is_null() {
        return -1;
    }

    match (*ctx).device.open() {
        Ok(_) => 0,
        Err(_) => -1,
    }
}

/// Close callback for lwext4
unsafe extern "C" fn bdev_close(bdev: *mut ffi::ext4_blockdev) -> c_int {
    let ctx = (*(*bdev).bdif).p_user as *mut BlockDevContext;
    if ctx.is_null() {
        return -1;
    }

    match (*ctx).device.close() {
        Ok(_) => 0,
        Err(_) => -1,
    }
}

/// Read callback for lwext4
unsafe extern "C" fn bdev_bread(
    bdev: *mut ffi::ext4_blockdev,
    buf: *mut c_void,
    blk_id: u64,
    blk_cnt: u32,
) -> c_int {
    let ctx = (*(*bdev).bdif).p_user as *mut BlockDevContext;
    if ctx.is_null() || buf.is_null() {
        return -1;
    }

    let block_size = (*(*bdev).bdif).ph_bsize as usize;
    let size = blk_cnt as usize * block_size;
    let slice = std::slice::from_raw_parts_mut(buf as *mut u8, size);

    match (*ctx).device.read_blocks(slice, blk_id, blk_cnt) {
        Ok(_) => 0,
        Err(_) => -1,
    }
}

/// Write callback for lwext4
unsafe extern "C" fn bdev_bwrite(
    bdev: *mut ffi::ext4_blockdev,
    buf: *const c_void,
    blk_id: u64,
    blk_cnt: u32,
) -> c_int {
    let ctx = (*(*bdev).bdif).p_user as *mut BlockDevContext;
    if ctx.is_null() || buf.is_null() {
        return -1;
    }

    let block_size = (*(*bdev).bdif).ph_bsize as usize;
    let size = blk_cnt as usize * block_size;
    let slice = std::slice::from_raw_parts(buf as *const u8, size);

    match (*ctx).device.write_blocks(slice, blk_id, blk_cnt) {
        Ok(_) => 0,
        Err(_) => -1,
    }
}

/// Minimum filesystem size (1 MB)
const MIN_FS_SIZE: u64 = 1024 * 1024;

/// Format a block device as an ext4 filesystem.
///
/// # Arguments
/// * `device` - The block device to format
/// * `config` - Configuration for the filesystem
///
/// # Example
/// ```no_run
/// use ext4_mkfs::{mkfs, MkfsConfig, IoBlockDevice};
/// use std::fs::OpenOptions;
///
/// let file = OpenOptions::new()
///     .read(true)
///     .write(true)
///     .open("disk.img")
///     .unwrap();
///
/// let device = IoBlockDevice::new(file, 512, 100 * 1024 * 1024);
/// mkfs(device, MkfsConfig::default()).unwrap();
/// ```
pub fn mkfs<D: BlockDevice + 'static>(device: D, config: MkfsConfig) -> Result<()> {
    let block_size = device.block_size();
    let block_count = device.block_count();
    let total_size = block_size as u64 * block_count;

    // Validate minimum size
    if total_size < MIN_FS_SIZE {
        return Err(Error::DeviceTooSmall {
            min: MIN_FS_SIZE,
            actual: total_size,
        });
    }

    // Validate block size
    if ![1024, 2048, 4096].contains(&config.block_size) {
        return Err(Error::InvalidConfig(format!(
            "block size must be 1024, 2048, or 4096, got {}",
            config.block_size
        )));
    }

    // Create the buffer for physical block operations
    let buffer = vec![0u8; block_size as usize];

    // Create context for callbacks (boxed for type erasure)
    let mut ctx = Box::new(BlockDevContext {
        device: Box::new(device),
        buffer,
    });

    // Create and initialize the block device interface
    let mut bdif = ffi::ext4_blockdev_iface {
        open: Some(bdev_open),
        bread: Some(bdev_bread),
        bwrite: Some(bdev_bwrite),
        close: Some(bdev_close),
        lock: None,
        unlock: None,
        ph_bsize: block_size,
        ph_bcnt: block_count,
        ph_bbuf: ctx.buffer.as_mut_ptr(),
        ph_refctr: 0,
        bread_ctr: 0,
        bwrite_ctr: 0,
        p_user: ctx.as_mut() as *mut BlockDevContext as *mut c_void,
    };

    // Create the block device structure
    let mut bdev = ffi::ext4_blockdev {
        bdif: &mut bdif,
        part_offset: 0,
        part_size: total_size,
        bc: ptr::null_mut(),
        lg_bsize: 0,
        lg_bcnt: 0,
        cache_write_back: 0,
        fs: ptr::null_mut(),
        journal: ptr::null_mut(),
    };

    // Prepare mkfs info
    let label_cstring = config.label.as_ref().map(|s| {
        let truncated: String = s.chars().take(16).collect();
        CString::new(truncated).unwrap_or_else(|_| CString::new("").unwrap())
    });

    let mut info = ffi::ext4_mkfs_info {
        len: total_size,
        block_size: config.block_size,
        blocks_per_group: 0, // Let lwext4 calculate
        inodes_per_group: 0, // Let lwext4 calculate
        inode_size: config.inode_size,
        inodes: 0,           // Let lwext4 calculate
        journal_blocks: 0,   // Let lwext4 calculate
        feat_ro_compat: 0,
        feat_compat: 0,
        feat_incompat: 0,
        bg_desc_reserve_blocks: 0,
        dsc_size: 0,
        uuid: config.uuid.unwrap_or([0u8; 16]),
        journal: config.journal,
        label: label_cstring
            .as_ref()
            .map(|s| s.as_ptr())
            .unwrap_or(ptr::null()),
    };

    // Allocate filesystem structure on heap (it's large)
    // We use a zeroed allocation since ext4_fs is opaque
    let fs_size = 4096; // Generous size for ext4_fs structure
    let fs_layout = std::alloc::Layout::from_size_align(fs_size, 8).unwrap();
    let fs_ptr = unsafe { std::alloc::alloc_zeroed(fs_layout) as *mut ffi::ext4_fs };

    if fs_ptr.is_null() {
        return Err(Error::InvalidConfig("failed to allocate filesystem structure".into()));
    }

    // Initialize block device
    let ret = unsafe { ffi::ext4_block_init(&mut bdev) };
    if ret != 0 {
        unsafe { std::alloc::dealloc(fs_ptr as *mut u8, fs_layout) };
        return Err(Error::from_lwext4(ret));
    }

    // Create the filesystem
    let ret = unsafe { ffi::ext4_mkfs(fs_ptr, &mut bdev, &mut info, config.fs_type.to_c_int()) };

    // Cleanup
    unsafe {
        ffi::ext4_block_fini(&mut bdev);
        std::alloc::dealloc(fs_ptr as *mut u8, fs_layout);
    }

    if ret != 0 {
        return Err(Error::from_lwext4(ret));
    }

    Ok(())
}

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

    #[test]
    fn test_config_builder() {
        let config = MkfsConfig::new()
            .fs_type(FsType::Ext4)
            .block_size(4096)
            .label("test")
            .journal(true);

        assert_eq!(config.fs_type, FsType::Ext4);
        assert_eq!(config.block_size, 4096);
        assert_eq!(config.label, Some("test".to_string()));
        assert!(config.journal);
    }

    #[test]
    fn test_fs_type_to_c_int() {
        assert_eq!(FsType::Ext2.to_c_int(), F_SET_EXT2);
        assert_eq!(FsType::Ext3.to_c_int(), F_SET_EXT3);
        assert_eq!(FsType::Ext4.to_c_int(), F_SET_EXT4);
    }
}
