use std::ffi::OsString;
use std::path::{Path, PathBuf};

use crate::error::{Error, Result};
use crate::vfs::VfsWrite;

const TMP_SUFFIX: &str = ".sqpack-rs.tmp";

/// Returns the temporary file path used during an atomic write: `<target>.sqpack-rs.tmp`.
pub(crate) fn tmpfile_path(target: &Path) -> PathBuf {
    let mut buf = OsString::from(target.as_os_str());
    buf.push(TMP_SUFFIX);
    PathBuf::from(buf)
}

/// Writes `bytes` to a `.sqpack-rs.tmp` sibling, syncs, then renames it over `target`.
///
/// The rename is the atomic step: readers that open `target` before the rename see the
/// old file; those that open after see the new one. On failure during write or sync the
/// temporary file is removed and an error is returned; `target` is left untouched.
pub(crate) fn replace_atomically(vfs: &dyn VfsWrite, target: &Path, bytes: &[u8]) -> Result<()> {
    let tmp = tmpfile_path(target);

    let mut handle = vfs
        .open_write(&tmp)
        .map_err(|source| Error::WriteAtomicityFailure {
            context: "index_tmp_open",
            source,
        })?;

    {
        use std::io::Write;
        if let Err(source) = handle.write_all(bytes) {
            drop(handle);
            let _ = vfs.remove_file(&tmp);
            return Err(Error::WriteAtomicityFailure {
                context: "index_tmp_write",
                source,
            });
        }
    }

    if let Err(source) = handle.sync_all() {
        drop(handle);
        let _ = vfs.remove_file(&tmp);
        return Err(Error::WriteAtomicityFailure {
            context: "index_tmp_sync",
            source,
        });
    }

    drop(handle);

    vfs.rename(&tmp, target)
        .map_err(|source| Error::WriteAtomicityFailure {
            context: "index_tmp_rename",
            source,
        })?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::vfs::{InMemoryFs, Vfs};
    use std::io::Read;

    #[test]
    fn tmpfile_path_appends_suffix() {
        let target = PathBuf::from("/a/b.index");
        let tmp = tmpfile_path(&target);
        assert!(tmp.to_string_lossy().ends_with(TMP_SUFFIX));
        assert!(tmp.to_string_lossy().starts_with("/a/b.index"));
    }

    #[test]
    fn replace_atomically_publishes_bytes_under_target() {
        let fs = InMemoryFs::new();
        let target = PathBuf::from("/a/b.index");
        replace_atomically(&fs, &target, b"new-bytes").unwrap();
        let mut r = fs.open_read(&target).unwrap();
        let mut buf = Vec::new();
        r.read_to_end(&mut buf).unwrap();
        assert_eq!(buf, b"new-bytes");
    }

    #[test]
    fn replace_atomically_removes_tmpfile_after_rename() {
        let fs = InMemoryFs::new();
        let target = PathBuf::from("/a/b.index");
        replace_atomically(&fs, &target, b"x").unwrap();
        let tmp = tmpfile_path(&target);
        assert!(!fs.exists(&tmp));
    }

    #[test]
    fn replace_atomically_overwrites_existing_target() {
        let fs = InMemoryFs::new();
        let target = PathBuf::from("/a/b.index");
        fs.insert(&target, b"old".to_vec());
        replace_atomically(&fs, &target, b"new").unwrap();
        let mut r = fs.open_read(&target).unwrap();
        let mut buf = Vec::new();
        r.read_to_end(&mut buf).unwrap();
        assert_eq!(buf, b"new");
    }
}
