use super::path::{sync_directory, target_directory};
use crate::cache_file::CacheFileError;
use std::{
    fs,
    io::Write,
    path::{Path, PathBuf},
    sync::atomic::{AtomicU64, Ordering},
    time::{SystemTime, UNIX_EPOCH},
};

static ATOMIC_WRITE_COUNTER: AtomicU64 = AtomicU64::new(0);

pub fn write_text_atomically(target_path: &Path, contents: &str) -> Result<(), CacheFileError> {
    let target_dir = target_directory(target_path)?;
    let target_file = target_path
        .file_name()
        .and_then(|file| file.to_str())
        .unwrap_or("cache");
    let temp_path = atomic_temp_path(target_dir, target_file);
    let write_result = (|| {
        let mut temp = fs::OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(&temp_path)
            .map_err(|source| CacheFileError::WriteTemp {
                path: temp_path.clone(),
                source,
            })?;
        temp.write_all(contents.as_bytes())
            .map_err(|source| CacheFileError::WriteTemp {
                path: temp_path.clone(),
                source,
            })?;
        temp.sync_all().map_err(|source| CacheFileError::SyncTemp {
            path: temp_path.clone(),
            source,
        })?;
        Ok(())
    })();
    if let Err(err) = write_result {
        remove_temp_file(&temp_path);
        return Err(err);
    }
    if let Err(source) = fs::rename(&temp_path, target_path) {
        remove_temp_file(&temp_path);
        return Err(CacheFileError::Replace {
            temp_path,
            target_path: target_path.to_path_buf(),
            source,
        });
    }
    sync_directory(target_dir)
}

#[must_use]
fn atomic_temp_path(target_dir: &Path, target_file: &str) -> PathBuf {
    let now_nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_or(0, |duration| duration.as_nanos());
    let counter = ATOMIC_WRITE_COUNTER.fetch_add(1, Ordering::Relaxed);
    target_dir.join(format!(
        "{target_file}.tmp.{}.{}.{}",
        std::process::id(),
        now_nanos,
        counter
    ))
}

fn remove_temp_file(path: &Path) {
    let _ = fs::remove_file(path);
}
