//! NDJSON (Newline Delimited JSON) storage utilities.
//!
//! Handles reading and writing NDJSON files for incremental message logs.
//! NDJSON format stores one JSON object per line, making it ideal for
//! append-only logs that can be read incrementally.

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

/// Append a record to an NDJSON file
///
/// This function:
/// 1. Serializes the record to JSON
/// 2. Opens the file in append mode (creates if not exists)
/// 3. Writes the JSON followed by a newline
/// 4. Calls fsync to ensure durability
///
/// # Arguments
/// * `path` - Path to the NDJSON file
/// * `record` - Record to append (must be serializable)
///
/// # Example
/// ```no_run
/// use dirigent_archivist::storage::ndjson::append_ndjson;
/// use serde::{Serialize, Deserialize};
///
/// #[derive(Serialize, Deserialize)]
/// struct LogEntry {
///     message: String,
/// }
///
/// # async fn example() -> std::io::Result<()> {
/// let entry = LogEntry { message: "Hello".to_string() };
/// append_ndjson(std::path::Path::new("log.ndjson"), &entry).await?;
/// # Ok(())
/// # }
/// ```
pub async fn append_ndjson<T: Serialize>(path: &Path, record: &T) -> std::io::Result<()> {
    // Serialize to JSON string
    let json = serde_json::to_string(record)
        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;

    // Open file in append mode (create if not exists)
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)
        .await?;

    // Write JSON + newline
    file.write_all(json.as_bytes()).await?;
    file.write_all(b"\n").await?;

    // Fsync for durability
    file.sync_all().await?;

    Ok(())
}

/// Atomically rewrite an NDJSON file with the given records.
///
/// Uses a temp file + rename for crash safety. If the process crashes during
/// the write, the original file remains untouched. Only after the new content
/// is fully written and fsynced is the old file replaced.
///
/// # Arguments
/// * `path` - Path to the NDJSON file (will be created or overwritten)
/// * `records` - Records to write (one per line)
///
/// # Example
/// ```no_run
/// use dirigent_archivist::storage::ndjson::write_ndjson;
/// use serde::{Serialize, Deserialize};
///
/// #[derive(Serialize, Deserialize)]
/// struct LogEntry {
///     message: String,
/// }
///
/// # async fn example() -> std::io::Result<()> {
/// let entries = vec![
///     LogEntry { message: "First".to_string() },
///     LogEntry { message: "Second".to_string() },
/// ];
/// write_ndjson(std::path::Path::new("log.ndjson"), &entries).await?;
/// # Ok(())
/// # }
/// ```
pub async fn write_ndjson<T: Serialize>(path: &Path, records: &[T]) -> std::io::Result<()> {
    let temp_path = path.with_extension("jsonl.tmp");
    let mut file = OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open(&temp_path)
        .await?;
    for record in records {
        let json = serde_json::to_string(record)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
        file.write_all(json.as_bytes()).await?;
        file.write_all(b"\n").await?;
    }
    file.flush().await?;
    file.sync_all().await?;
    drop(file);
    tokio::fs::rename(&temp_path, path).await?;
    Ok(())
}

/// Read all records from an NDJSON file
///
/// This function:
/// 1. Reads the entire file to a string
/// 2. Splits by newlines
/// 3. Deserializes each non-empty line
///
/// If the file doesn't exist, returns an empty vector.
///
/// # Arguments
/// * `path` - Path to the NDJSON file
///
/// # Returns
/// Vector of deserialized records
///
/// # Example
/// ```no_run
/// use dirigent_archivist::storage::ndjson::read_ndjson;
/// use serde::{Serialize, Deserialize};
///
/// #[derive(Serialize, Deserialize)]
/// struct LogEntry {
///     message: String,
/// }
///
/// # async fn example() -> std::io::Result<()> {
/// let entries: Vec<LogEntry> = read_ndjson(std::path::Path::new("log.ndjson")).await?;
/// # Ok(())
/// # }
/// ```
pub async fn read_ndjson<T: for<'de> Deserialize<'de>>(path: &Path) -> std::io::Result<Vec<T>> {
    // Check if file exists
    if !path.exists() {
        return Ok(Vec::new());
    }

    // Read entire file to string
    let content = tokio::fs::read_to_string(path).await?;

    // Parse line by line
    let mut records = Vec::new();
    for (line_num, line) in content.lines().enumerate() {
        // Skip empty lines
        if line.trim().is_empty() {
            continue;
        }

        // Deserialize the line
        let record: T = serde_json::from_str(line).map_err(|e| {
            std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                format!("Failed to parse line {}: {}", line_num + 1, e),
            )
        })?;

        records.push(record);
    }

    Ok(records)
}

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

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

    #[tokio::test]
    async fn test_append_and_read_roundtrip() {
        // Create a temporary file
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_ndjson_{}.ndjson", Uuid::now_v7()));

        // Append multiple records
        let record1 = TestRecord {
            id: "rec1".to_string(),
            value: 42,
        };
        let record2 = TestRecord {
            id: "rec2".to_string(),
            value: 100,
        };
        let record3 = TestRecord {
            id: "rec3".to_string(),
            value: -5,
        };

        append_ndjson(&file_path, &record1).await.unwrap();
        append_ndjson(&file_path, &record2).await.unwrap();
        append_ndjson(&file_path, &record3).await.unwrap();

        // Read back
        let records: Vec<TestRecord> = read_ndjson(&file_path).await.unwrap();

        // Verify
        assert_eq!(records.len(), 3);
        assert_eq!(records[0], record1);
        assert_eq!(records[1], record2);
        assert_eq!(records[2], record3);

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

    #[tokio::test]
    async fn test_read_empty_file() {
        // Create a temporary empty file
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_empty_{}.ndjson", Uuid::now_v7()));

        tokio::fs::write(&file_path, "").await.unwrap();

        // Read should return empty vector
        let records: Vec<TestRecord> = read_ndjson(&file_path).await.unwrap();
        assert_eq!(records.len(), 0);

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

    #[tokio::test]
    async fn test_read_missing_file() {
        // Read from a non-existent file
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("nonexistent_{}.ndjson", Uuid::now_v7()));

        // Should return empty vector, not error
        let records: Vec<TestRecord> = read_ndjson(&file_path).await.unwrap();
        assert_eq!(records.len(), 0);
    }

    #[tokio::test]
    async fn test_trailing_newlines() {
        // Create a file with trailing newlines
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_trailing_{}.ndjson", Uuid::now_v7()));

        // Write manually with extra newlines
        let content = r#"{"id":"rec1","value":42}

{"id":"rec2","value":100}


"#;
        tokio::fs::write(&file_path, content).await.unwrap();

        // Read should skip empty lines
        let records: Vec<TestRecord> = read_ndjson(&file_path).await.unwrap();
        assert_eq!(records.len(), 2);
        assert_eq!(records[0].id, "rec1");
        assert_eq!(records[1].id, "rec2");

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

    #[tokio::test]
    async fn test_concurrent_appends() {
        // Test appending to different files concurrently
        let temp_dir = std::env::temp_dir();
        let file1 = temp_dir.join(format!("test_concurrent_1_{}.ndjson", Uuid::now_v7()));
        let file2 = temp_dir.join(format!("test_concurrent_2_{}.ndjson", Uuid::now_v7()));

        let record1 = TestRecord {
            id: "file1".to_string(),
            value: 1,
        };
        let record2 = TestRecord {
            id: "file2".to_string(),
            value: 2,
        };

        // Append concurrently
        let (r1, r2) = tokio::join!(
            append_ndjson(&file1, &record1),
            append_ndjson(&file2, &record2)
        );

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

        // Verify both files have correct content
        let records1: Vec<TestRecord> = read_ndjson(&file1).await.unwrap();
        let records2: Vec<TestRecord> = read_ndjson(&file2).await.unwrap();

        assert_eq!(records1.len(), 1);
        assert_eq!(records1[0], record1);
        assert_eq!(records2.len(), 1);
        assert_eq!(records2[0], record2);

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

    #[tokio::test]
    async fn test_invalid_json_error() {
        // Create a file with invalid JSON
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_invalid_{}.ndjson", Uuid::now_v7()));

        let content = r#"{"id":"rec1","value":42}
invalid json here
{"id":"rec2","value":100}"#;
        tokio::fs::write(&file_path, content).await.unwrap();

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

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

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

    #[tokio::test]
    async fn test_fsync_called() {
        // This test verifies that append_ndjson completes without error,
        // which implies fsync was called successfully
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_fsync_{}.ndjson", Uuid::now_v7()));

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

        // Should complete without error (including fsync)
        append_ndjson(&file_path, &record).await.unwrap();

        // Verify file was written
        assert!(file_path.exists());

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