//! JSON storage utilities for session metadata.
//!
//! Handles reading and writing JSON files for session and connector metadata.
//! Uses atomic write operations (write-to-temp + rename) to ensure consistency.

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

/// Write a value to a JSON file atomically
///
/// This function:
/// 1. Serializes the value to pretty-printed JSON
/// 2. Writes to a temporary file (`{path}.tmp`)
/// 3. Renames the temp file to the target path (atomic operation)
///
/// The rename operation is atomic on most filesystems, ensuring that
/// readers will either see the old complete file or the new complete file,
/// never a partially written file.
///
/// # Arguments
/// * `path` - Path to the JSON file
/// * `value` - Value to serialize and write
///
/// # Example
/// ```no_run
/// use dirigent_archivist::storage::json::write_json;
/// use serde::{Serialize, Deserialize};
///
/// #[derive(Serialize, Deserialize)]
/// struct Config {
///     setting: String,
/// }
///
/// # async fn example() -> std::io::Result<()> {
/// let config = Config { setting: "value".to_string() };
/// write_json(std::path::Path::new("config.json"), &config).await?;
/// # Ok(())
/// # }
/// ```
pub async fn write_json<T: Serialize>(path: &Path, value: &T) -> std::io::Result<()> {
    // Serialize to pretty-printed JSON
    let json = serde_json::to_string_pretty(value)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;

    // Create temporary file path (same directory for atomic rename)
    let temp_path = path.with_extension("tmp");

    // Write to temporary file
    let mut file = tokio::fs::File::create(&temp_path).await?;
    file.write_all(json.as_bytes()).await?;
    file.sync_all().await?;
    drop(file); // Close the file before rename

    // Atomically rename temp file to target path
    tokio::fs::rename(&temp_path, path).await?;

    Ok(())
}

/// Read a value from a JSON file
///
/// If the file doesn't exist, returns a NotFound error.
///
/// # Arguments
/// * `path` - Path to the JSON file
///
/// # Returns
/// Deserialized value
///
/// # Example
/// ```no_run
/// use dirigent_archivist::storage::json::read_json;
/// use serde::{Serialize, Deserialize};
///
/// #[derive(Serialize, Deserialize)]
/// struct Config {
///     setting: String,
/// }
///
/// # async fn example() -> std::io::Result<()> {
/// let config: Config = read_json(std::path::Path::new("config.json")).await?;
/// # Ok(())
/// # }
/// ```
pub async fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> std::io::Result<T> {
    // Read file to string
    let content = tokio::fs::read_to_string(path).await?;

    // Deserialize from JSON
    let value: T = serde_json::from_str(&content)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;

    Ok(value)
}

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

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

    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
    struct NestedData {
        flag: bool,
        items: Vec<String>,
    }

    #[tokio::test]
    async fn test_write_and_read_roundtrip() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_json_{}.json", Uuid::now_v7()));

        let data = TestData {
            id: "test-123".to_string(),
            value: 42,
            nested: NestedData {
                flag: true,
                items: vec!["a".to_string(), "b".to_string(), "c".to_string()],
            },
        };

        // Write
        write_json(&file_path, &data).await.unwrap();

        // Read back
        let read_data: TestData = read_json(&file_path).await.unwrap();

        // Verify
        assert_eq!(read_data, data);

        // Clean up
        tokio::fs::remove_file(&file_path).await.ok();
    }

    #[tokio::test]
    async fn test_pretty_printed_output() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_pretty_{}.json", Uuid::now_v7()));

        let data = TestData {
            id: "test".to_string(),
            value: 100,
            nested: NestedData {
                flag: false,
                items: vec!["x".to_string()],
            },
        };

        // Write
        write_json(&file_path, &data).await.unwrap();

        // Read as raw string
        let content = tokio::fs::read_to_string(&file_path).await.unwrap();

        // Verify it's pretty-printed (contains newlines and indentation)
        assert!(content.contains('\n'));
        assert!(content.contains("  ")); // Indentation
        assert!(content.contains(r#""id""#));
        assert!(content.contains(r#""value""#));

        // Clean up
        tokio::fs::remove_file(&file_path).await.ok();
    }

    #[tokio::test]
    async fn test_read_missing_file() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("nonexistent_{}.json", Uuid::now_v7()));

        // Should return NotFound error
        let result: std::io::Result<TestData> = read_json(&file_path).await;
        assert!(result.is_err());

        match result {
            Err(e) => {
                assert_eq!(e.kind(), std::io::ErrorKind::NotFound);
            }
            Ok(_) => panic!("Expected NotFound error"),
        }
    }

    #[tokio::test]
    async fn test_atomic_write() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_atomic_{}.json", Uuid::now_v7()));

        let data1 = TestData {
            id: "first".to_string(),
            value: 1,
            nested: NestedData {
                flag: true,
                items: vec![],
            },
        };

        let data2 = TestData {
            id: "second".to_string(),
            value: 2,
            nested: NestedData {
                flag: false,
                items: vec!["updated".to_string()],
            },
        };

        // Write first version
        write_json(&file_path, &data1).await.unwrap();

        // Verify first version
        let read1: TestData = read_json(&file_path).await.unwrap();
        assert_eq!(read1.id, "first");

        // Overwrite with second version
        write_json(&file_path, &data2).await.unwrap();

        // Verify second version
        let read2: TestData = read_json(&file_path).await.unwrap();
        assert_eq!(read2.id, "second");
        assert_eq!(read2.value, 2);

        // Temp file should not exist after rename
        let temp_path = file_path.with_extension("tmp");
        assert!(!temp_path.exists());

        // Clean up
        tokio::fs::remove_file(&file_path).await.ok();
    }

    #[tokio::test]
    async fn test_invalid_json_error() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_invalid_{}.json", Uuid::now_v7()));

        // Write invalid JSON manually
        tokio::fs::write(&file_path, "{ invalid json }")
            .await
            .unwrap();

        // Reading should fail with InvalidData error
        let result: std::io::Result<TestData> = read_json(&file_path).await;
        assert!(result.is_err());

        match result {
            Err(e) => {
                assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
            }
            Ok(_) => panic!("Expected InvalidData error"),
        }

        // Clean up
        tokio::fs::remove_file(&file_path).await.ok();
    }

    #[tokio::test]
    async fn test_concurrent_writes_different_files() {
        let temp_dir = std::env::temp_dir();
        let file1 = temp_dir.join(format!("test_concurrent_1_{}.json", Uuid::now_v7()));
        let file2 = temp_dir.join(format!("test_concurrent_2_{}.json", Uuid::now_v7()));

        let data1 = TestData {
            id: "file1".to_string(),
            value: 1,
            nested: NestedData {
                flag: true,
                items: vec![],
            },
        };

        let data2 = TestData {
            id: "file2".to_string(),
            value: 2,
            nested: NestedData {
                flag: false,
                items: vec![],
            },
        };

        // Write concurrently
        let (r1, r2) = tokio::join!(write_json(&file1, &data1), write_json(&file2, &data2));

        r1.unwrap();
        r2.unwrap();

        // Verify both files
        let read1: TestData = read_json(&file1).await.unwrap();
        let read2: TestData = read_json(&file2).await.unwrap();

        assert_eq!(read1, data1);
        assert_eq!(read2, data2);

        // Clean up
        tokio::fs::remove_file(&file1).await.ok();
        tokio::fs::remove_file(&file2).await.ok();
    }

    #[tokio::test]
    async fn test_write_creates_parent_directory() {
        let temp_dir = std::env::temp_dir();
        let base_dir = temp_dir.join(format!("test_parent_{}", Uuid::now_v7()));

        // Note: Parent directory does NOT exist yet
        // This test verifies that write_json does NOT auto-create parent dirs
        // (Caller is responsible for creating parent directories)

        let file_path = base_dir.join("subdir").join("test.json");

        let data = TestData {
            id: "test".to_string(),
            value: 42,
            nested: NestedData {
                flag: true,
                items: vec![],
            },
        };

        // This should fail because parent directory doesn't exist
        let result = write_json(&file_path, &data).await;
        assert!(result.is_err());

        // Now create parent directory
        tokio::fs::create_dir_all(file_path.parent().unwrap())
            .await
            .unwrap();

        // Now write should succeed
        write_json(&file_path, &data).await.unwrap();

        // Verify
        let read_data: TestData = read_json(&file_path).await.unwrap();
        assert_eq!(read_data, data);

        // Clean up
        tokio::fs::remove_dir_all(&base_dir).await.ok();
    }
}
