use std::{fs, io, path::Path};

use tempfile::{NamedTempFile, TempPath};
use tracing::{debug, warn};

/// Atomically install a file.
///
/// This is a blocking function, must be called in `block_in_place` mode.
pub fn atomic_install(src: &Path, dst: &Path) -> io::Result<()> {
    debug!(
        "Attempting to atomically rename from '{}' to '{}'",
        src.display(),
        dst.display()
    );

    if let Err(err) = fs::rename(src, dst) {
        warn!("Attempting at atomic rename failed: {err:#?}, fallback to other methods.");

        #[cfg(target_os = "windows")]
        {
            match win::replace_file(src, dst) {
                Ok(()) => {
                    debug!("ReplaceFileW succeeded.",);
                    return Ok(());
                }
                Err(err) => {
                    warn!("ReplaceFileW failed: {err}, fallback to using tempfile plus rename",);
                }
            }
        }

        // src and dst is not on the same filesystem/mountpoint.
        // Fallback to creating NamedTempFile on the parent dir of
        // dst.

        let mut src_file = fs::File::open(src)?;

        let parent = dst.parent().unwrap();
        debug!("Creating named tempfile at '{}'", parent.display());
        let mut tempfile = NamedTempFile::new_in(parent)?;

        debug!(
            "Copying from '{}' to '{}'",
            src.display(),
            tempfile.path().display()
        );
        io::copy(&mut src_file, tempfile.as_file_mut())?;

        debug!("Retrieving permissions of '{}'", src.display());
        let permissions = src_file.metadata()?.permissions();

        debug!(
            "Setting permissions of '{}' to '{permissions:#?}'",
            tempfile.path().display()
        );
        tempfile.as_file().set_permissions(permissions)?;

        persist(tempfile.into_temp_path(), dst)?;
    } else {
        debug!("Attempting at atomically succeeded.");
    }

    Ok(())
}

fn symlink_file(original: &Path, link: &Path) -> io::Result<()> {
    #[cfg(target_family = "unix")]
    std::os::unix::fs::symlink(original, link)?;

    // Symlinks on Windows are disabled in some editions, so creating one is unreliable.
    #[cfg(target_family = "windows")]
    std::os::windows::fs::symlink_file(original, link)
        .or_else(|_| std::fs::copy(original, link).map(drop))?;
    Ok(())
}

/// Atomically install symlink "link" to a file "dst".
///
/// This is a blocking function, must be called in `block_in_place` mode.
pub fn atomic_symlink_file(dest: &Path, link: &Path) -> io::Result<()> {
    let parent = link.parent().unwrap();

    debug!("Creating tempPath at '{}'", parent.display());
    let temp_path = NamedTempFile::new_in(parent)?.into_temp_path();
    fs::remove_file(&temp_path)?;

    debug!(
        "Creating symlink '{}' to file '{}'",
        temp_path.display(),
        dest.display()
    );
    symlink_file(dest, &temp_path)?;

    persist(temp_path, link)
}

fn persist(temp_path: TempPath, to: &Path) -> io::Result<()> {
    debug!("Persisting '{}' to '{}'", temp_path.display(), to.display());
    match temp_path.persist(to) {
        Ok(()) => Ok(()),
        #[cfg(target_os = "windows")]
        Err(tempfile::PathPersistError {
            error,
            path: temp_path,
        }) => {
            warn!(
                "Failed to persist symlink '{}' to '{}': {error}, fallback to ReplaceFileW",
                temp_path.display(),
                to.display(),
            );
            win::replace_file(&temp_path, to).map_err(io::Error::from)
        }
        #[cfg(not(target_os = "windows"))]
        Err(err) => Err(err.into()),
    }
}

#[cfg(target_os = "windows")]
mod win {
    use std::{os::windows::ffi::OsStrExt, path::Path};

    use windows::{
        core::{Error, PCWSTR},
        Win32::Storage::FileSystem::{ReplaceFileW, REPLACE_FILE_FLAGS},
    };

    pub(super) fn replace_file(src: &Path, dst: &Path) -> Result<(), Error> {
        let mut src: Vec<_> = src.as_os_str().encode_wide().collect();
        let mut dst: Vec<_> = dst.as_os_str().encode_wide().collect();

        // Ensure it is terminated with 0
        src.push(0);
        dst.push(0);

        // SAFETY: We use it according its doc
        // https://learn.microsoft.com/en-nz/windows/win32/api/winbase/nf-winbase-replacefilew
        //
        // NOTE that this function is available since windows XP, so we don't need to
        // lazily load this function.
        unsafe {
            ReplaceFileW(
                PCWSTR::from_raw(dst.as_ptr()), // lpreplacedfilename
                PCWSTR::from_raw(src.as_ptr()), // lpreplacementfilename
                PCWSTR::null(),                 // lpbackupfilename, null for no backup file
                REPLACE_FILE_FLAGS(0),          // dwreplaceflags
                None,                           // lpexclude, unused
                None,                           // lpreserved, unused
            )
        }
        .ok()
    }
}