//! Content-addressable file storage.
//!
//! Handles storage and retrieval of binary files (images, documents, etc.)
//! using content-addressable naming based on SHA-256 hashes.
//!
//! Files are stored with deduplication:
//! - Same content = same file_id = stored once
//! - Multiple sessions can reference the same file
//! - File index tracks all referencing sessions

use crate::storage::{ndjson, paths::ArchivePaths};
use crate::types::FileRecord;
use sha2::{Digest, Sha256};
use uuid::Uuid;

/// Store a file in the archive
///
/// This function:
/// 1. Computes SHA-256 hash of content
/// 2. Generates file_id: `sha256:{hex_digest}`
/// 3. Stores blob in sharded directory (if not already exists - deduplication)
/// 4. Updates file index to track the session referencing this file
/// 5. Returns the file_id
///
/// # Arguments
/// * `paths` - Archive paths helper
/// * `content` - File content bytes
/// * `original_name` - Original filename
/// * `mime` - Optional MIME type
/// * `session` - Session UUID that references this file
///
/// # Returns
/// The file_id (e.g., "sha256:abc123...")
pub async fn store_file(
    paths: &ArchivePaths,
    content: &[u8],
    original_name: String,
    mime: Option<String>,
    session: Uuid,
) -> std::io::Result<String> {
    // Compute SHA-256 hash
    let hash = Sha256::digest(content);
    let hex_digest = hex::encode(hash);
    let file_id = format!("sha256:{}", hex_digest);

    // Get blob path
    let blob_path = paths.file_blob_path(&file_id);

    // Create parent directories for blob
    if let Some(parent) = blob_path.parent() {
        tokio::fs::create_dir_all(parent).await?;
    }

    // Write blob if it doesn't exist (deduplication)
    if !blob_path.exists() {
        tokio::fs::write(&blob_path, content).await?;
    }

    // Update file index
    let index_path = paths.root().join(".files").join("file_index.jsonl");

    // Create .files directory if it doesn't exist
    if let Some(parent) = index_path.parent() {
        tokio::fs::create_dir_all(parent).await?;
    }

    // Serialize the read-modify-rewrite below. Concurrent callers against
    // the same archive would otherwise lose records (both read the same
    // snapshot) and race on `rename(.tmp → .ndjson)` (second call hits
    // ENOENT because the first already consumed the shared temp path).
    let index_lock = paths.file_index_lock();
    let _index_guard = index_lock.lock().await;

    // Read existing index
    let mut records: Vec<FileRecord> = ndjson::read_ndjson(&index_path).await?;

    // Find or create FileRecord
    if let Some(existing) = records.iter_mut().find(|r| r.file_id == file_id) {
        // File already exists - add session if not already present
        if !existing.sessions.contains(&session) {
            existing.sessions.push(session);
        }
    } else {
        // New file - create record
        let relative_path = blob_path
            .strip_prefix(paths.root())
            .unwrap_or(&blob_path)
            .to_string_lossy()
            .to_string();

        let new_record = FileRecord {
            version: 1,
            file_id: file_id.clone(),
            path: relative_path,
            size: content.len() as u64,
            mime: mime.clone(),
            original_name: original_name.clone(),
            sessions: vec![session],
            metadata: serde_json::json!({}),
        };

        records.push(new_record);
    }

    // Rewrite entire index atomically
    // Use temp file + rename pattern
    let temp_index_path = index_path.with_extension("tmp");

    // Clear temp file and write all records
    if temp_index_path.exists() {
        tokio::fs::remove_file(&temp_index_path).await?;
    }

    for rec in &records {
        ndjson::append_ndjson(&temp_index_path, rec).await?;
    }

    // Rename to final location
    tokio::fs::rename(&temp_index_path, &index_path).await?;

    Ok(file_id)
}

/// Retrieve a file from the archive
///
/// # Arguments
/// * `paths` - Archive paths helper
/// * `file_id` - File identifier (e.g., "sha256:abc123...")
///
/// # Returns
/// File content bytes
pub async fn get_file(paths: &ArchivePaths, file_id: &str) -> std::io::Result<Vec<u8>> {
    let blob_path = paths.file_blob_path(file_id);
    tokio::fs::read(&blob_path).await
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_content_deduplication() {
        let temp_dir =
            std::env::temp_dir().join(format!("archivist_files_test_{}", Uuid::now_v7()));
        let paths = ArchivePaths::new(temp_dir.clone());

        let content = b"Hello, world! This is test content.";
        let session1 = Uuid::now_v7();
        let session2 = Uuid::now_v7();

        // Store same content from two different sessions
        let file_id1 = store_file(
            &paths,
            content,
            "test1.txt".to_string(),
            Some("text/plain".to_string()),
            session1,
        )
        .await
        .unwrap();

        let file_id2 = store_file(
            &paths,
            content,
            "test2.txt".to_string(), // Different name
            Some("text/plain".to_string()),
            session2,
        )
        .await
        .unwrap();

        // Same content should produce same file_id
        assert_eq!(file_id1, file_id2);

        // Verify blob was only written once
        let blob_path = paths.file_blob_path(&file_id1);
        assert!(blob_path.exists());

        // Verify index tracks both sessions
        let index_path = paths.root().join(".files").join("file_index.jsonl");
        let records: Vec<FileRecord> = ndjson::read_ndjson(&index_path).await.unwrap();

        let record = records.iter().find(|r| r.file_id == file_id1).unwrap();
        assert_eq!(record.sessions.len(), 2);
        assert!(record.sessions.contains(&session1));
        assert!(record.sessions.contains(&session2));

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

    #[tokio::test]
    async fn test_sharding_distributes_files() {
        let temp_dir =
            std::env::temp_dir().join(format!("archivist_files_test_{}", Uuid::now_v7()));
        let paths = ArchivePaths::new(temp_dir.clone());

        let session = Uuid::now_v7();

        // Store files with different content
        let content1 = b"Content A";
        let content2 = b"Content B";
        let content3 = b"Content C";

        let file_id1 = store_file(&paths, content1, "file1.txt".to_string(), None, session)
            .await
            .unwrap();

        let file_id2 = store_file(&paths, content2, "file2.txt".to_string(), None, session)
            .await
            .unwrap();

        let file_id3 = store_file(&paths, content3, "file3.txt".to_string(), None, session)
            .await
            .unwrap();

        // Verify different content produces different file_ids
        assert_ne!(file_id1, file_id2);
        assert_ne!(file_id2, file_id3);

        // Verify files are distributed across sharded directories
        let blob_path1 = paths.file_blob_path(&file_id1);
        let blob_path2 = paths.file_blob_path(&file_id2);
        let blob_path3 = paths.file_blob_path(&file_id3);

        assert!(blob_path1.exists());
        assert!(blob_path2.exists());
        assert!(blob_path3.exists());

        // Verify sharding creates subdirectories
        let files_dir = paths.root().join(".files");
        let mut shard_dirs = Vec::new();
        for entry in std::fs::read_dir(&files_dir).unwrap() {
            let entry = entry.unwrap();
            if entry.file_type().unwrap().is_dir() {
                shard_dirs.push(entry.path());
            }
        }

        // Should have at least one shard directory
        assert!(!shard_dirs.is_empty());

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

    #[tokio::test]
    async fn test_index_tracks_sessions() {
        let temp_dir =
            std::env::temp_dir().join(format!("archivist_files_test_{}", Uuid::now_v7()));
        let paths = ArchivePaths::new(temp_dir.clone());

        let content = b"Shared content";
        let session1 = Uuid::now_v7();
        let session2 = Uuid::now_v7();
        let session3 = Uuid::now_v7();

        // Store from session1
        let file_id = store_file(
            &paths,
            content,
            "file.txt".to_string(),
            Some("text/plain".to_string()),
            session1,
        )
        .await
        .unwrap();

        // Verify index has 1 session
        let index_path = paths.root().join(".files").join("file_index.jsonl");
        let records: Vec<FileRecord> = ndjson::read_ndjson(&index_path).await.unwrap();
        let record = records.iter().find(|r| r.file_id == file_id).unwrap();
        assert_eq!(record.sessions.len(), 1);
        assert_eq!(record.sessions[0], session1);

        // Store same content from session2
        store_file(
            &paths,
            content,
            "file2.txt".to_string(),
            Some("text/plain".to_string()),
            session2,
        )
        .await
        .unwrap();

        // Verify index now has 2 sessions
        let records: Vec<FileRecord> = ndjson::read_ndjson(&index_path).await.unwrap();
        let record = records.iter().find(|r| r.file_id == file_id).unwrap();
        assert_eq!(record.sessions.len(), 2);
        assert!(record.sessions.contains(&session1));
        assert!(record.sessions.contains(&session2));

        // Store same content from session3
        store_file(&paths, content, "file3.txt".to_string(), None, session3)
            .await
            .unwrap();

        // Verify index now has 3 sessions
        let records: Vec<FileRecord> = ndjson::read_ndjson(&index_path).await.unwrap();
        let record = records.iter().find(|r| r.file_id == file_id).unwrap();
        assert_eq!(record.sessions.len(), 3);
        assert!(record.sessions.contains(&session1));
        assert!(record.sessions.contains(&session2));
        assert!(record.sessions.contains(&session3));

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

    #[tokio::test]
    async fn test_concurrent_writes_different_files() {
        let temp_dir =
            std::env::temp_dir().join(format!("archivist_files_test_{}", Uuid::now_v7()));
        let paths = ArchivePaths::new(temp_dir.clone());

        let content1 = b"Content 1";
        let content2 = b"Content 2";
        let session = Uuid::now_v7();

        // Store concurrently
        let (file_id1, file_id2) = tokio::join!(
            store_file(&paths, content1, "file1.txt".to_string(), None, session,),
            store_file(&paths, content2, "file2.txt".to_string(), None, session,)
        );

        let file_id1 = file_id1.unwrap();
        let file_id2 = file_id2.unwrap();

        // Verify both files exist
        assert_ne!(file_id1, file_id2);

        let retrieved1 = get_file(&paths, &file_id1).await.unwrap();
        let retrieved2 = get_file(&paths, &file_id2).await.unwrap();

        assert_eq!(retrieved1, content1);
        assert_eq!(retrieved2, content2);

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

    #[tokio::test]
    async fn test_get_file_missing() {
        let temp_dir =
            std::env::temp_dir().join(format!("archivist_files_test_{}", Uuid::now_v7()));
        let paths = ArchivePaths::new(temp_dir.clone());

        // Try to get non-existent file
        let result = get_file(&paths, "sha256:nonexistent").await;
        assert!(result.is_err());

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

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

    #[tokio::test]
    async fn test_roundtrip_binary_content() {
        let temp_dir =
            std::env::temp_dir().join(format!("archivist_files_test_{}", Uuid::now_v7()));
        let paths = ArchivePaths::new(temp_dir.clone());

        // Binary content (not UTF-8)
        let content: Vec<u8> = (0..256).map(|i| i as u8).collect();
        let session = Uuid::now_v7();

        // Store
        let file_id = store_file(
            &paths,
            &content,
            "binary.dat".to_string(),
            Some("application/octet-stream".to_string()),
            session,
        )
        .await
        .unwrap();

        // Retrieve
        let retrieved = get_file(&paths, &file_id).await.unwrap();

        // Verify exact match
        assert_eq!(retrieved, content);

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

    #[tokio::test]
    async fn test_file_metadata_preserved() {
        let temp_dir =
            std::env::temp_dir().join(format!("archivist_files_test_{}", Uuid::now_v7()));
        let paths = ArchivePaths::new(temp_dir.clone());

        let content = b"Test content";
        let session = Uuid::now_v7();
        let original_name = "document.pdf".to_string();
        let mime = Some("application/pdf".to_string());

        // Store
        let file_id = store_file(
            &paths,
            content,
            original_name.clone(),
            mime.clone(),
            session,
        )
        .await
        .unwrap();

        // Read index
        let index_path = paths.root().join(".files").join("file_index.jsonl");
        let records: Vec<FileRecord> = ndjson::read_ndjson(&index_path).await.unwrap();

        let record = records.iter().find(|r| r.file_id == file_id).unwrap();

        // Verify metadata
        assert_eq!(record.original_name, original_name);
        assert_eq!(record.mime, mime);
        assert_eq!(record.size, content.len() as u64);
        assert!(record.path.contains(".files"));

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

    #[tokio::test]
    async fn test_deduplicate_same_session() {
        let temp_dir =
            std::env::temp_dir().join(format!("archivist_files_test_{}", Uuid::now_v7()));
        let paths = ArchivePaths::new(temp_dir.clone());

        let content = b"Duplicate content";
        let session = Uuid::now_v7();

        // Store same content twice from same session
        let file_id1 = store_file(&paths, content, "file1.txt".to_string(), None, session)
            .await
            .unwrap();

        let file_id2 = store_file(&paths, content, "file2.txt".to_string(), None, session)
            .await
            .unwrap();

        // Same file_id
        assert_eq!(file_id1, file_id2);

        // Session should only appear once in the index
        let index_path = paths.root().join(".files").join("file_index.jsonl");
        let records: Vec<FileRecord> = ndjson::read_ndjson(&index_path).await.unwrap();
        let record = records.iter().find(|r| r.file_id == file_id1).unwrap();

        assert_eq!(record.sessions.len(), 1);
        assert_eq!(record.sessions[0], session);

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