//! JSON read/write helpers with atomic writes.
//!
//! Follows the archivist pattern: write to .tmp, then rename.

use serde::{Deserialize, Serialize};
use std::path::Path;
use tokio::io::AsyncWriteExt;

/// Write a value to a JSON file atomically.
///
/// 1. Serializes to pretty-printed JSON
/// 2. Writes to `{path}.tmp`
/// 3. Renames temp file to target (atomic on most filesystems)
pub async fn write_json<T: Serialize>(path: &Path, value: &T) -> std::io::Result<()> {
    let json = serde_json::to_string_pretty(value)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;

    let temp_path = path.with_extension("tmp");

    let mut file = tokio::fs::File::create(&temp_path).await?;
    file.write_all(json.as_bytes()).await?;
    file.sync_all().await?;
    drop(file);

    tokio::fs::rename(&temp_path, path).await?;

    Ok(())
}

/// Read a value from a JSON file.
///
/// Returns `NotFound` if the file doesn't exist.
pub async fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> std::io::Result<T> {
    let content = tokio::fs::read_to_string(path).await?;
    let value: T = serde_json::from_str(&content)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
    Ok(value)
}

/// Read a value from a JSON file, returning a default if the file doesn't exist.
pub async fn read_json_or_default<T: for<'de> Deserialize<'de> + Default>(
    path: &Path,
) -> std::io::Result<T> {
    match read_json(path).await {
        Ok(value) => Ok(value),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(T::default()),
        Err(e) => Err(e),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde::{Deserialize, Serialize};

    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
    struct TestData {
        id: String,
        value: i32,
    }

    #[tokio::test]
    async fn test_write_and_read_roundtrip() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("test.json");

        let data = TestData {
            id: "test".to_string(),
            value: 42,
        };

        write_json(&path, &data).await.unwrap();
        let read: TestData = read_json(&path).await.unwrap();
        assert_eq!(read, data);
    }

    #[tokio::test]
    async fn test_read_missing_file() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("missing.json");

        let result: std::io::Result<TestData> = read_json(&path).await;
        assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::NotFound);
    }

    #[tokio::test]
    async fn test_read_json_or_default() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("missing.json");

        let result: Vec<String> = read_json_or_default(&path).await.unwrap();
        assert!(result.is_empty());
    }

    #[tokio::test]
    async fn test_atomic_overwrite() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("test.json");

        let data1 = TestData {
            id: "first".to_string(),
            value: 1,
        };
        let data2 = TestData {
            id: "second".to_string(),
            value: 2,
        };

        write_json(&path, &data1).await.unwrap();
        write_json(&path, &data2).await.unwrap();

        let read: TestData = read_json(&path).await.unwrap();
        assert_eq!(read, data2);

        // Temp file should not remain
        assert!(!path.with_extension("tmp").exists());
    }
}
