// Python/fileutils.c in CPython
#![allow(non_snake_case)]

#[cfg(not(windows))]
pub use rustix::fs::Stat as StatStruct;

#[cfg(windows)]
pub use windows::{StatStruct, fstat};

#[cfg(not(windows))]
pub fn fstat(fd: crate::crt_fd::Borrowed<'_>) -> std::io::Result<StatStruct> {
    rustix::fs::fstat(fd).map_err(Into::into)
}

#[cfg(windows)]
pub mod windows {
    use crate::crt_fd;
    use crate::windows::ToWideString;
    use alloc::ffi::CString;
    use libc::{S_IFCHR, S_IFDIR, S_IFMT};
    use std::ffi::{OsStr, OsString};
    use std::os::windows::io::AsRawHandle;
    use std::sync::OnceLock;
    use windows_sys::Win32::Foundation::{
        ERROR_INVALID_HANDLE, ERROR_NOT_SUPPORTED, FILETIME, FreeLibrary, SetLastError,
    };
    use windows_sys::Win32::Storage::FileSystem::{
        BY_HANDLE_FILE_INFORMATION, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_READONLY,
        FILE_ATTRIBUTE_REPARSE_POINT, FILE_BASIC_INFO, FILE_ID_INFO, FILE_TYPE_CHAR,
        FILE_TYPE_DISK, FILE_TYPE_PIPE, FILE_TYPE_UNKNOWN, FileBasicInfo, FileIdInfo,
        GetFileInformationByHandle, GetFileInformationByHandleEx, GetFileType,
    };
    use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW};
    use windows_sys::Win32::System::SystemServices::IO_REPARSE_TAG_SYMLINK;
    use windows_sys::core::PCWSTR;

    pub const S_IFIFO: libc::c_int = 0o010000;
    pub const S_IFLNK: libc::c_int = 0o120000;

    pub const SECS_BETWEEN_EPOCHS: i64 = 11644473600; // Seconds between 1.1.1601 and 1.1.1970

    #[derive(Clone, Copy, Default)]
    pub struct StatStruct {
        pub st_dev: libc::c_ulong,
        pub st_ino: u64,
        pub st_mode: libc::c_ushort,
        pub st_nlink: i32,
        pub st_uid: i32,
        pub st_gid: i32,
        pub st_rdev: libc::c_ulong,
        pub st_size: u64,
        pub st_atime: libc::time_t,
        pub st_atime_nsec: i32,
        pub st_mtime: libc::time_t,
        pub st_mtime_nsec: i32,
        pub st_ctime: libc::time_t,
        pub st_ctime_nsec: i32,
        pub st_birthtime: libc::time_t,
        pub st_birthtime_nsec: i32,
        pub st_file_attributes: libc::c_ulong,
        pub st_reparse_tag: u32,
        pub st_ino_high: u64,
    }

    impl StatStruct {
        // update_st_mode_from_path in cpython
        pub fn update_st_mode_from_path(&mut self, path: &OsStr, attr: u32) {
            if attr & FILE_ATTRIBUTE_DIRECTORY == 0 {
                let file_extension = path
                    .to_wide()
                    .split(|&c| c == '.' as u16)
                    .next_back()
                    .and_then(|s| String::from_utf16(s).ok());

                if let Some(file_extension) = file_extension
                    && (file_extension.eq_ignore_ascii_case("exe")
                        || file_extension.eq_ignore_ascii_case("bat")
                        || file_extension.eq_ignore_ascii_case("cmd")
                        || file_extension.eq_ignore_ascii_case("com"))
                {
                    self.st_mode |= 0o111;
                }
            }
        }
    }

    // _Py_fstat_noraise in cpython
    pub fn fstat(fd: crt_fd::Borrowed<'_>) -> std::io::Result<StatStruct> {
        let h = match crt_fd::as_handle(fd) {
            Ok(h) => h,
            Err(_) => {
                // An invalid fd is reported as a Win32 handle error so the
                // OSError carries winerror = ERROR_INVALID_HANDLE.
                unsafe { SetLastError(ERROR_INVALID_HANDLE) };
                return Err(std::io::Error::last_os_error());
            }
        };
        let h = h.as_raw_handle();
        // reset stat?

        let file_type = unsafe { GetFileType(h as _) };
        if file_type == FILE_TYPE_UNKNOWN {
            return Err(std::io::Error::last_os_error());
        }
        if file_type != FILE_TYPE_DISK {
            let st_mode = if file_type == FILE_TYPE_CHAR {
                S_IFCHR
            } else if file_type == FILE_TYPE_PIPE {
                S_IFIFO
            } else {
                0
            } as u16;
            return Ok(StatStruct {
                st_mode,
                ..Default::default()
            });
        }

        let mut info = unsafe { core::mem::zeroed() };
        let mut basic_info: FILE_BASIC_INFO = unsafe { core::mem::zeroed() };
        let mut id_info: FILE_ID_INFO = unsafe { core::mem::zeroed() };

        if unsafe { GetFileInformationByHandle(h as _, &mut info) } == 0
            || unsafe {
                GetFileInformationByHandleEx(
                    h as _,
                    FileBasicInfo,
                    &mut basic_info as *mut _ as *mut _,
                    core::mem::size_of_val(&basic_info) as u32,
                )
            } == 0
        {
            return Err(std::io::Error::last_os_error());
        }

        let p_id_info = if unsafe {
            GetFileInformationByHandleEx(
                h as _,
                FileIdInfo,
                &mut id_info as *mut _ as *mut _,
                core::mem::size_of_val(&id_info) as u32,
            )
        } == 0
        {
            None
        } else {
            Some(&id_info)
        };

        Ok(attribute_data_to_stat(
            &info,
            0,
            Some(&basic_info),
            p_id_info,
        ))
    }

    fn large_integer_to_time_t_nsec(input: i64) -> (libc::time_t, libc::c_int) {
        let nsec_out = (input % 10_000_000) * 100; // FILETIME is in units of 100 nsec.
        let time_out = ((input / 10_000_000) - SECS_BETWEEN_EPOCHS) as libc::time_t;
        (time_out, nsec_out as _)
    }

    fn file_time_to_time_t_nsec(in_ptr: FILETIME) -> (libc::time_t, libc::c_int) {
        let in_val: i64 = unsafe { core::mem::transmute_copy(&in_ptr) };
        let nsec_out = (in_val % 10_000_000) * 100; // FILETIME is in units of 100 nsec.
        let time_out = (in_val / 10_000_000) - SECS_BETWEEN_EPOCHS;
        (time_out, nsec_out as _)
    }

    fn attribute_data_to_stat(
        info: &BY_HANDLE_FILE_INFORMATION,
        reparse_tag: u32,
        basic_info: Option<&FILE_BASIC_INFO>,
        id_info: Option<&FILE_ID_INFO>,
    ) -> StatStruct {
        let mut st_mode = attributes_to_mode(info.dwFileAttributes);
        let st_size = ((info.nFileSizeHigh as u64) << 32) + info.nFileSizeLow as u64;
        let st_dev: libc::c_ulong = if let Some(id_info) = id_info {
            id_info.VolumeSerialNumber as _
        } else {
            info.dwVolumeSerialNumber
        };
        let st_rdev = 0;

        let (st_birthtime, st_ctime, st_mtime, st_atime) = if let Some(basic_info) = basic_info {
            (
                large_integer_to_time_t_nsec(basic_info.CreationTime),
                large_integer_to_time_t_nsec(basic_info.ChangeTime),
                large_integer_to_time_t_nsec(basic_info.LastWriteTime),
                large_integer_to_time_t_nsec(basic_info.LastAccessTime),
            )
        } else {
            (
                file_time_to_time_t_nsec(info.ftCreationTime),
                (0, 0),
                file_time_to_time_t_nsec(info.ftLastWriteTime),
                file_time_to_time_t_nsec(info.ftLastAccessTime),
            )
        };
        let st_nlink = info.nNumberOfLinks as i32;

        let st_ino = if let Some(id_info) = id_info {
            let file_id: [u64; 2] = unsafe { core::mem::transmute_copy(&id_info.FileId) };
            file_id
        } else {
            let ino = ((info.nFileIndexHigh as u64) << 32) + info.nFileIndexLow as u64;
            [ino, 0]
        };

        if info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0
            && reparse_tag == IO_REPARSE_TAG_SYMLINK
        {
            st_mode = (st_mode & !(S_IFMT as u16)) | (S_IFLNK as u16);
        }
        let st_file_attributes = info.dwFileAttributes;

        StatStruct {
            st_dev,
            st_ino: st_ino[0],
            st_mode,
            st_nlink,
            st_uid: 0,
            st_gid: 0,
            st_rdev,
            st_size,
            st_atime: st_atime.0,
            st_atime_nsec: st_atime.1,
            st_mtime: st_mtime.0,
            st_mtime_nsec: st_mtime.1,
            st_ctime: st_ctime.0,
            st_ctime_nsec: st_ctime.1,
            st_birthtime: st_birthtime.0,
            st_birthtime_nsec: st_birthtime.1,
            st_file_attributes,
            st_reparse_tag: reparse_tag,
            st_ino_high: st_ino[1],
        }
    }

    const fn attributes_to_mode(attr: u32) -> u16 {
        let mut m = 0;
        if attr & FILE_ATTRIBUTE_DIRECTORY != 0 {
            m |= libc::S_IFDIR | 0o111; // IFEXEC for user,group,other
        } else {
            m |= libc::S_IFREG;
        }
        if attr & FILE_ATTRIBUTE_READONLY != 0 {
            m |= 0o444;
        } else {
            m |= 0o666;
        }
        m as _
    }

    #[derive(Clone, Copy)]
    #[repr(C)]
    pub struct FILE_STAT_BASIC_INFORMATION {
        pub FileId: i64,
        pub CreationTime: i64,
        pub LastAccessTime: i64,
        pub LastWriteTime: i64,
        pub ChangeTime: i64,
        pub AllocationSize: i64,
        pub EndOfFile: i64,
        pub FileAttributes: u32,
        pub ReparseTag: u32,
        pub NumberOfLinks: u32,
        pub DeviceType: u32,
        pub DeviceCharacteristics: u32,
        pub Reserved: u32,
        pub VolumeSerialNumber: i64,
        pub FileId128: [u64; 2],
    }

    #[allow(dead_code)]
    #[derive(Clone, Copy)]
    #[repr(C)]
    pub enum FILE_INFO_BY_NAME_CLASS {
        FileStatByNameInfo,
        FileStatLxByNameInfo,
        FileCaseSensitiveByNameInfo,
        FileStatBasicByNameInfo,
        MaximumFileInfoByNameClass,
    }

    // _Py_GetFileInformationByName in cpython
    pub fn get_file_information_by_name(
        file_name: &OsStr,
        file_information_class: FILE_INFO_BY_NAME_CLASS,
    ) -> std::io::Result<FILE_STAT_BASIC_INFORMATION> {
        static GET_FILE_INFORMATION_BY_NAME: OnceLock<
            Option<
                unsafe extern "system" fn(
                    PCWSTR,
                    FILE_INFO_BY_NAME_CLASS,
                    *mut libc::c_void,
                    u32,
                ) -> i32,
            >,
        > = OnceLock::new();

        let GetFileInformationByName = GET_FILE_INFORMATION_BY_NAME
            .get_or_init(|| {
                let library_name =
                    OsString::from("api-ms-win-core-file-l2-1-4.dll").to_wide_with_nul();
                let module = unsafe { LoadLibraryW(library_name.as_ptr()) };
                if module.is_null() {
                    return None;
                }
                let name = CString::new("GetFileInformationByName").unwrap();
                if let Some(proc) =
                    unsafe { GetProcAddress(module, name.as_bytes_with_nul().as_ptr()) }
                {
                    Some(unsafe {
                        core::mem::transmute::<
                            unsafe extern "system" fn() -> isize,
                            unsafe extern "system" fn(
                                *const u16,
                                FILE_INFO_BY_NAME_CLASS,
                                *mut libc::c_void,
                                u32,
                            ) -> i32,
                        >(proc)
                    })
                } else {
                    unsafe { FreeLibrary(module) };
                    None
                }
            })
            .ok_or_else(|| std::io::Error::from_raw_os_error(ERROR_NOT_SUPPORTED as _))?;

        let file_name = file_name.to_wide_with_nul();
        let file_info_buffer_size = core::mem::size_of::<FILE_STAT_BASIC_INFORMATION>() as u32;
        let mut file_info_buffer = core::mem::MaybeUninit::<FILE_STAT_BASIC_INFORMATION>::uninit();
        unsafe {
            if GetFileInformationByName(
                file_name.as_ptr(),
                file_information_class as _,
                file_info_buffer.as_mut_ptr() as _,
                file_info_buffer_size,
            ) == 0
            {
                Err(std::io::Error::last_os_error())
            } else {
                Ok(file_info_buffer.assume_init())
            }
        }
    }

    #[must_use]
    pub fn stat_basic_info_to_stat(info: &FILE_STAT_BASIC_INFORMATION) -> StatStruct {
        use windows_sys::Win32::Storage::FileSystem;
        use windows_sys::Win32::System::Ioctl;

        const S_IFMT: u16 = self::S_IFMT as _;
        const S_IFDIR: u16 = self::S_IFDIR as _;
        const S_IFCHR: u16 = self::S_IFCHR as _;
        const S_IFIFO: u16 = self::S_IFIFO as _;
        const S_IFLNK: u16 = self::S_IFLNK as _;

        let mut st_mode = attributes_to_mode(info.FileAttributes);
        let st_size = info.EndOfFile as u64;
        let st_birthtime = large_integer_to_time_t_nsec(info.CreationTime);
        let st_ctime = large_integer_to_time_t_nsec(info.ChangeTime);
        let st_mtime = large_integer_to_time_t_nsec(info.LastWriteTime);
        let st_atime = large_integer_to_time_t_nsec(info.LastAccessTime);
        let st_nlink = info.NumberOfLinks as _;
        let st_dev = info.VolumeSerialNumber as u32;
        // File systems with less than 128-bits zero pad into this field
        let st_ino = info.FileId128;
        // bpo-37834: Only actual symlinks set the S_IFLNK flag. But lstat() will
        // open other name surrogate reparse points without traversing them. To
        // detect/handle these, check st_file_attributes and st_reparse_tag.
        let st_reparse_tag = info.ReparseTag;
        if info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0
            && info.ReparseTag == IO_REPARSE_TAG_SYMLINK
        {
            // set the bits that make this a symlink
            st_mode = (st_mode & !S_IFMT) | S_IFLNK;
        }
        let st_file_attributes = info.FileAttributes;
        match info.DeviceType {
            FileSystem::FILE_DEVICE_DISK
            | Ioctl::FILE_DEVICE_VIRTUAL_DISK
            | Ioctl::FILE_DEVICE_DFS
            | FileSystem::FILE_DEVICE_CD_ROM
            | Ioctl::FILE_DEVICE_CONTROLLER
            | Ioctl::FILE_DEVICE_DATALINK => {}
            Ioctl::FILE_DEVICE_DISK_FILE_SYSTEM
            | Ioctl::FILE_DEVICE_CD_ROM_FILE_SYSTEM
            | Ioctl::FILE_DEVICE_NETWORK_FILE_SYSTEM => {
                st_mode = (st_mode & !S_IFMT) | 0x6000; // _S_IFBLK
            }
            Ioctl::FILE_DEVICE_CONSOLE
            | Ioctl::FILE_DEVICE_NULL
            | Ioctl::FILE_DEVICE_KEYBOARD
            | Ioctl::FILE_DEVICE_MODEM
            | Ioctl::FILE_DEVICE_MOUSE
            | Ioctl::FILE_DEVICE_PARALLEL_PORT
            | Ioctl::FILE_DEVICE_PRINTER
            | Ioctl::FILE_DEVICE_SCREEN
            | Ioctl::FILE_DEVICE_SERIAL_PORT
            | Ioctl::FILE_DEVICE_SOUND => {
                st_mode = (st_mode & !S_IFMT) | S_IFCHR;
            }
            Ioctl::FILE_DEVICE_NAMED_PIPE => {
                st_mode = (st_mode & !S_IFMT) | S_IFIFO;
            }
            _ => {
                if info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0 {
                    st_mode = (st_mode & !S_IFMT) | S_IFDIR;
                }
            }
        }

        StatStruct {
            st_dev,
            st_ino: st_ino[0],
            st_mode,
            st_nlink,
            st_uid: 0,
            st_gid: 0,
            st_rdev: 0,
            st_size,
            st_atime: st_atime.0,
            st_atime_nsec: st_atime.1,
            st_mtime: st_mtime.0,
            st_mtime_nsec: st_mtime.1,
            st_ctime: st_ctime.0,
            st_ctime_nsec: st_ctime.1,
            st_birthtime: st_birthtime.0,
            st_birthtime_nsec: st_birthtime.1,
            st_file_attributes,
            st_reparse_tag,
            st_ino_high: st_ino[1],
        }
    }
}

/// C `FILE *` handle as returned by [`fopen`] and consumed by [`fclose`].
pub type CFile = libc::FILE;

/// Close a file opened with [`fopen`].
///
/// # Safety
/// `fp` must be a non-null pointer returned by [`fopen`] and must not have been
/// closed already.
pub unsafe fn fclose(fp: *mut CFile) -> core::ffi::c_int {
    unsafe { libc::fclose(fp) }
}

// _Py_fopen_obj in cpython (Python/fileutils.c:1757-1835)
// Open a file using std::fs::File and convert to FILE*
// Automatically handles path encoding and EINTR retries
#[expect(
    clippy::std_instead_of_core,
    reason = "false positive: core::io::ErrorKind is unstable (core_io)"
)]
pub fn fopen(path: &std::path::Path, mode: &str) -> std::io::Result<*mut CFile> {
    use alloc::ffi::CString;
    use std::fs::File;

    // Currently only supports read mode
    // Can be extended to support "wb", "w+b", etc. if needed
    if mode != "rb" {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("unsupported mode: {mode}"),
        ));
    }

    // Open file using std::fs::File (handles path encoding and EINTR automatically)
    let file = File::open(path)?;

    #[cfg(windows)]
    {
        use std::os::windows::io::IntoRawHandle;

        // Convert File handle to CRT file descriptor
        let handle = file.into_raw_handle();
        let fd = unsafe { libc::open_osfhandle(handle as isize, libc::O_RDONLY) };
        if fd == -1 {
            return Err(std::io::Error::last_os_error());
        }

        // Convert fd to FILE*
        let mode_cstr = CString::new(mode).unwrap();
        let fp = unsafe { libc::fdopen(fd, mode_cstr.as_ptr()) };
        if fp.is_null() {
            unsafe { libc::close(fd) };
            return Err(std::io::Error::last_os_error());
        }

        // Set non-inheritable (Windows needs this explicitly)
        if let Err(e) = set_inheritable(fd, false) {
            unsafe { libc::fclose(fp) };
            return Err(e);
        }

        Ok(fp)
    }

    #[cfg(not(windows))]
    {
        use std::os::fd::IntoRawFd;

        // Convert File to raw fd
        let fd = file.into_raw_fd();

        // Convert fd to FILE*
        let mode_cstr = CString::new(mode).unwrap();
        let fp = unsafe { libc::fdopen(fd, mode_cstr.as_ptr()) };
        if fp.is_null() {
            unsafe { libc::close(fd) };
            return Err(std::io::Error::last_os_error());
        }

        // Unix: O_CLOEXEC is already set by File::open, so non-inheritable is automatic
        Ok(fp)
    }
}

// set_inheritable in cpython (Python/fileutils.c:1443-1570)
// Set the inheritable flag of the specified file descriptor
// Only used on Windows; Unix automatically sets O_CLOEXEC
#[cfg(windows)]
fn set_inheritable(fd: libc::c_int, inheritable: bool) -> std::io::Result<()> {
    use windows_sys::Win32::Foundation::{
        HANDLE, HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE, SetHandleInformation,
    };

    let handle = unsafe { libc::get_osfhandle(fd) };
    if handle == INVALID_HANDLE_VALUE as isize {
        return Err(std::io::Error::last_os_error());
    }

    let flags = if inheritable { HANDLE_FLAG_INHERIT } else { 0 };
    let result = unsafe { SetHandleInformation(handle as HANDLE, HANDLE_FLAG_INHERIT, flags) };
    if result == 0 {
        return Err(std::io::Error::last_os_error());
    }

    Ok(())
}
