use std::{
    ffi::{CString, OsStr},
    fs::{self, File, Metadata},
    io::{self, Write},
    mem::MaybeUninit,
    os::{
        fd::{AsRawFd, FromRawFd, RawFd},
        unix::ffi::OsStrExt,
    },
    path::{Path, PathBuf},
};

use crate::{
    fs::{FileMode, FileModeUpdate, MetadataExt},
    sys::TempFile,
};

pub struct TempFileImpl {
    persist: bool,
    path: PathBuf,
    file: Option<File>,
}

pub struct TempPathImpl {}

impl TempFile for TempFileImpl {
    type TempPath = TempPathImpl;

    fn create(pattern: &str) -> io::Result<Self> {
        let mut template = CString::new(pattern)?.into_bytes_with_nul();
        let fd = unsafe { libc::mkostemp(template.as_mut_ptr().cast(), libc::O_CLOEXEC) };
        if fd < 0 {
            return Err(io::Error::last_os_error());
        }
        let file = unsafe { File::from_raw_fd(fd) };
        let path = PathBuf::from(OsStr::from_bytes(&template[..template.len() - 1]));

        Ok(Self {
            file: Some(file),
            path,
            persist: false,
        })
    }

    fn new<P: AsRef<Path>>(path: P) -> io::Result<Self> {
        let path = path.as_ref().to_owned();
        let file = File::create_new(&path)?;
        Ok(Self {
            path,
            file: Some(file),
            persist: false,
        })
    }

    fn persist(mut self) {
        self.persist = true;
    }

    fn rename<P: AsRef<Path>>(mut self, new_name: P) -> io::Result<()> {
        let _ = self.file.take();
        self.persist = true;
        fs::rename(&self.path, new_name)
    }

    fn into_temp_path(self) -> Self::TempPath {
        todo!()
    }

    fn path(&self) -> &Path {
        &self.path
    }
}

impl Drop for TempFileImpl {
    fn drop(&mut self) {
        let _ = self.file.take();
        if !self.persist {
            fs::remove_file(&self.path).ok();
        }
    }
}

impl AsRawFd for TempFileImpl {
    fn as_raw_fd(&self) -> RawFd {
        self.file.as_ref().unwrap().as_raw_fd()
    }
}

impl Write for TempFileImpl {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.file.as_mut().unwrap().write(buf)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.file.as_mut().unwrap().flush()
    }
}

impl AsRef<Path> for TempPathImpl {
    fn as_ref(&self) -> &Path {
        todo!()
    }
}

impl MetadataExt for Metadata {
    fn mode(&self) -> FileMode {
        use std::os::unix::fs::MetadataExt as ME;
        let raw = <Self as ME>::mode(self);
        FileMode::from(raw)
    }
}

pub fn update_file_mode<P: AsRef<Path>>(
    at: Option<RawFd>,
    path: P,
    mode: FileModeUpdate,
) -> io::Result<()> {
    let at = match at {
        Some(fd) => fd,
        None => libc::AT_FDCWD,
    };
    let path = CString::new(path.as_ref().as_os_str().as_encoded_bytes()).unwrap();
    let mut stat: MaybeUninit<libc::stat> = MaybeUninit::uninit();
    if unsafe {
        libc::fstatat(
            at,
            path.as_ptr(),
            stat.as_mut_ptr(),
            libc::AT_SYMLINK_NOFOLLOW,
        )
    } != 0
    {
        return Err(io::Error::last_os_error());
    }
    let stat = unsafe { stat.assume_init() };

    let new_mode = match mode {
        FileModeUpdate::Set(value) => {
            (stat.st_mode & !0o777) | ((value.bits() as libc::mode_t) & 0o777)
        }
        FileModeUpdate::Modify { set, clear } => {
            let mut bits = stat.st_mode;
            bits &= !((clear.bits() as libc::mode_t) & 0o777);
            bits |= (set.bits() as libc::mode_t) & 0o777;
            bits
        }
    };

    if unsafe { libc::fchmodat(at, path.as_ptr(), new_mode, libc::AT_SYMLINK_NOFOLLOW) } != 0 {
        return Err(io::Error::last_os_error());
    }

    Ok(())
}
