use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd};
use std::path::{Path, PathBuf};

use libc;
use nix::fcntl::{OFlag, open};
use nix::mount::MsFlags;
use nix::sys::stat::{FileStat, Mode, fstat, umask};
use nix::unistd::{Gid, Uid, close};
use oci_spec::runtime::LinuxDevice;

use super::utils::to_sflag;
use crate::syscall::Syscall;
use crate::syscall::syscall::create_syscall;
use crate::utils::PathBufExt;

#[derive(Debug, thiserror::Error)]
pub enum DeviceError {
    #[error("{0:?} is not a valid device path")]
    InvalidDevicePath(std::path::PathBuf),
    #[error("failed syscall to create device")]
    Syscall(#[from] crate::syscall::SyscallError),
    #[error(transparent)]
    Nix(#[from] nix::Error),
    #[error(transparent)]
    Other(Box<dyn std::error::Error + Send + Sync>),
    #[error("{0}")]
    Custom(String),
}

type Result<T> = std::result::Result<T, DeviceError>;

pub(crate) fn open_device_fd(dev_path: &Path) -> nix::Result<(OwnedFd, FileStat)> {
    let fd = open(
        dev_path,
        OFlag::O_PATH | OFlag::O_CLOEXEC,
        Mode::from_bits_truncate(0o000),
    )?;
    let owned = unsafe { OwnedFd::from_raw_fd(fd) };
    let stat = fstat(owned.as_raw_fd())?;
    Ok((owned, stat))
}

pub(crate) fn verify_dev_null(stat: &FileStat) -> Result<()> {
    if stat.st_mode & libc::S_IFMT != libc::S_IFCHR {
        return Err(DeviceError::Custom(
            "device is not a character device".to_string(),
        ));
    }

    let actual_major = libc::major(stat.st_rdev) as i64;
    let actual_minor = libc::minor(stat.st_rdev) as i64;
    if actual_major != 1 || actual_minor != 3 {
        return Err(DeviceError::Custom(format!(
            "device dev null major/minor mismatch: expected 1/3, actual {}/{}",
            actual_major, actual_minor
        )));
    }
    Ok(())
}

pub struct Device {
    syscall: Box<dyn Syscall>,
}

impl Default for Device {
    fn default() -> Self {
        Self::new()
    }
}

impl Device {
    pub fn new() -> Device {
        Device {
            syscall: create_syscall(),
        }
    }

    pub fn new_with_syscall(syscall: Box<dyn Syscall>) -> Device {
        Device { syscall }
    }

    pub fn create_devices<'a, I>(&self, rootfs: &Path, devices: I, bind: bool) -> Result<()>
    where
        I: IntoIterator<Item = &'a LinuxDevice>,
    {
        let old_mode = umask(Mode::from_bits_truncate(0o000));
        devices
            .into_iter()
            .map(|dev| {
                if !dev.path().starts_with("/dev") {
                    tracing::error!(
                        "{:?} is not a valid device path starting with /dev",
                        dev.path()
                    );
                    return Err(DeviceError::InvalidDevicePath(dev.path().to_path_buf()));
                }

                if bind {
                    self.bind_dev(rootfs, dev)
                } else {
                    self.mknod_dev(rootfs, dev)
                }
            })
            .collect::<Result<Vec<_>>>()?;
        umask(old_mode);

        Ok(())
    }

    fn bind_dev(&self, rootfs: &Path, dev: &LinuxDevice) -> Result<()> {
        let full_container_path = create_container_dev_path(rootfs, dev)?;
        tracing::debug!(
            "bind_dev with full container path {:?}",
            full_container_path
        );

        let fd = open(
            &full_container_path,
            OFlag::O_RDWR | OFlag::O_CREAT,
            Mode::from_bits_truncate(0o644),
        )
        .inspect_err(|err| {
            tracing::error!("failed to open bind dev {:?}: {}", full_container_path, err);
        })?;
        close(fd)?;
        self.syscall
            .mount(
                Some(dev.path()),
                &full_container_path,
                Some("bind"),
                MsFlags::MS_BIND,
                None,
            )
            .map_err(|err| {
                tracing::error!(
                    ?err,
                    path = ?full_container_path,
                    "failed to mount bind dev",
                );
                err
            })?;

        Ok(())
    }

    fn mknod_dev(&self, rootfs: &Path, dev: &LinuxDevice) -> Result<()> {
        fn makedev(major: i64, minor: i64) -> u64 {
            ((minor & 0xff)
                | ((major & 0xfff) << 8)
                | ((minor & !0xff) << 12)
                | ((major & !0xfff) << 32)) as u64
        }

        let full_container_path = create_container_dev_path(rootfs, dev)?;

        self.syscall
            .mknod(
                &full_container_path,
                to_sflag(dev.typ()),
                Mode::from_bits_truncate(dev.file_mode().unwrap_or(0o666)),
                makedev(dev.major(), dev.minor()),
            )
            .map_err(|err| {
                tracing::error!(
                    ?err,
                    path = ?full_container_path,
                    major = ?dev.major(),
                    minor = ?dev.minor(),
                    "failed to mknod device"
                );

                err
            })?;
        self.syscall
            .chown(
                &full_container_path,
                dev.uid().map(Uid::from_raw),
                dev.gid().map(Gid::from_raw),
            )
            .map_err(|err| {
                tracing::error!(
                    path = ?full_container_path,
                    ?err,
                    uid = ?dev.uid(),
                    gid = ?dev.gid(),
                    "failed to chown device"
                );

                err
            })?;

        Ok(())
    }
}

fn create_container_dev_path(rootfs: &Path, dev: &LinuxDevice) -> Result<PathBuf> {
    let relative_dev_path = dev.path().as_relative().map_err(|err| {
        tracing::error!(
            "failed to convert {:?} to relative path: {}",
            dev.path(),
            err
        );
        DeviceError::Other(err.into())
    })?;
    let full_container_path = safe_path::scoped_join(rootfs, relative_dev_path).map_err(|err| {
        tracing::error!("failed to join {rootfs:?} with {:?}: {err}", dev.path());
        DeviceError::Other(err.into())
    })?;
    std::fs::create_dir_all(
        full_container_path
            .parent()
            .unwrap_or_else(|| Path::new("")),
    )
    .map_err(|err| {
        tracing::error!(
            "failed to create parent dir of {:?}: {}",
            full_container_path,
            err
        );
        DeviceError::Other(err.into())
    })?;

    Ok(full_container_path)
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use anyhow::Result;
    use nix::sys::stat::SFlag;
    use nix::unistd::{Gid, Uid};
    use oci_spec::runtime::{LinuxDeviceBuilder, LinuxDeviceType};

    use super::*;
    use crate::syscall::test::{ChownArgs, MknodArgs, TestHelperSyscall};

    #[test]
    fn test_bind_dev() -> Result<()> {
        let tmp_dir = tempfile::tempdir()?;
        let device = Device::new_with_syscall(Box::<TestHelperSyscall>::default());
        assert!(
            device
                .bind_dev(
                    tmp_dir.path(),
                    &LinuxDeviceBuilder::default()
                        .path(PathBuf::from("/dev/null"))
                        .build()
                        .unwrap(),
                )
                .is_ok()
        );

        let helper = device
            .syscall
            .as_any()
            .downcast_ref::<TestHelperSyscall>()
            .unwrap();
        let mount_args = helper.get_mount_args();
        assert_eq!(1, mount_args.len());
        let got = &mount_args[0];
        assert_eq!(Some(PathBuf::from("/dev/null")), got.source);
        assert_eq!(tmp_dir.path().join("dev").join("null"), got.target);
        assert_eq!(MsFlags::MS_BIND, got.flags);
        assert!(got.data.is_none());
        Ok(())
    }

    #[test]
    fn test_mknod_dev() -> Result<()> {
        let tmp_dir = tempfile::tempdir()?;
        let device = Device::new_with_syscall(Box::<TestHelperSyscall>::default());
        assert!(
            device
                .mknod_dev(
                    tmp_dir.path(),
                    &LinuxDeviceBuilder::default()
                        .path(PathBuf::from("/null"))
                        .major(1)
                        .minor(3)
                        .typ(LinuxDeviceType::C)
                        .file_mode(0o644u32)
                        .uid(1000u32)
                        .gid(1000u32)
                        .build()
                        .unwrap(),
                )
                .is_ok()
        );

        let want_mknod = MknodArgs {
            path: tmp_dir.path().join("null"),
            kind: SFlag::S_IFCHR,
            perm: Mode::S_IRUSR | Mode::S_IWUSR | Mode::S_IRGRP | Mode::S_IROTH,
            dev: 259,
        };
        let got_mknod = &device
            .syscall
            .as_any()
            .downcast_ref::<TestHelperSyscall>()
            .unwrap()
            .get_mknod_args()[0];
        assert_eq!(want_mknod, *got_mknod);

        let want_chown = ChownArgs {
            path: tmp_dir.path().join("null"),
            owner: Some(Uid::from_raw(1000)),
            group: Some(Gid::from_raw(1000)),
        };
        let got_chown = &device
            .syscall
            .as_any()
            .downcast_ref::<TestHelperSyscall>()
            .unwrap()
            .get_chown_args()[0];
        assert_eq!(want_chown, *got_chown);

        Ok(())
    }

    #[test]
    fn test_create_devices() -> Result<()> {
        let tmp_dir = tempfile::tempdir()?;
        let device = Device::new_with_syscall(Box::<TestHelperSyscall>::default());

        let devices = vec![
            LinuxDeviceBuilder::default()
                .path(PathBuf::from("/dev/null"))
                .major(1)
                .minor(3)
                .typ(LinuxDeviceType::C)
                .file_mode(0o644u32)
                .uid(1000u32)
                .gid(1000u32)
                .build()
                .unwrap(),
        ];

        assert!(
            device
                .create_devices(tmp_dir.path(), &devices, true)
                .is_ok()
        );

        let mount_args = device
            .syscall
            .as_any()
            .downcast_ref::<TestHelperSyscall>()
            .unwrap()
            .get_mount_args();
        assert_eq!(1, mount_args.len());
        let bind = &mount_args[0];
        assert_eq!(Some(PathBuf::from("/dev/null")), bind.source);
        assert_eq!(tmp_dir.path().join("dev/null"), bind.target);
        assert_eq!(MsFlags::MS_BIND, bind.flags);
        assert!(bind.data.is_none());

        assert!(
            device
                .create_devices(tmp_dir.path(), &devices, false)
                .is_ok()
        );

        let want = MknodArgs {
            path: tmp_dir.path().join("dev/null"),
            kind: SFlag::S_IFCHR,
            perm: Mode::S_IRUSR | Mode::S_IWUSR | Mode::S_IRGRP | Mode::S_IROTH,
            dev: 259,
        };
        let got = &device
            .syscall
            .as_any()
            .downcast_ref::<TestHelperSyscall>()
            .unwrap()
            .get_mknod_args()[0];
        assert_eq!(want, *got);

        Ok(())
    }
}
