//! Local filesystem storage backend

use super::{StorageBackend, StorageError, StorageResult};
use async_trait::async_trait;
use bytes::Bytes;
use lightningstor_types::ObjectId;
use std::path::{Path, PathBuf};
use tokio::fs;
use tokio::io::AsyncWriteExt;

/// Local filesystem storage backend
///
/// Stores objects as files in a directory structure:
/// - Objects: `{data_dir}/objects/{object_id}`
/// - Parts: `{data_dir}/parts/{upload_id}/{part_number}`
pub struct LocalFsBackend {
    /// Objects directory
    objects_dir: PathBuf,
    /// Parts directory
    parts_dir: PathBuf,
    /// Whether writes should be flushed before success is returned.
    sync_on_write: bool,
}

impl LocalFsBackend {
    /// Create a new local filesystem backend
    ///
    /// # Arguments
    /// * `data_dir` - Base directory for object storage
    ///
    /// # Returns
    /// * `Ok(Self)` if directories could be created
    /// * `Err(StorageError)` if directory creation failed
    pub async fn new(data_dir: impl AsRef<Path>, sync_on_write: bool) -> StorageResult<Self> {
        let data_dir = data_dir.as_ref().to_path_buf();
        let objects_dir = data_dir.join("objects");
        let parts_dir = data_dir.join("parts");
        
        // Create directories if they don't exist
        fs::create_dir_all(&objects_dir).await?;
        fs::create_dir_all(&parts_dir).await?;
        
        Ok(Self {
            objects_dir,
            parts_dir,
            sync_on_write,
        })
    }
    
    /// Get object file path
    fn object_file_name(&self, object_id: &ObjectId) -> String {
        object_id.to_string()
    }

    fn shard_components(name: &str) -> (&str, &str) {
        let first = name.get(0..2).unwrap_or("xx");
        let second = name.get(2..4).unwrap_or("yy");
        (first, second)
    }

    fn object_path(&self, object_id: &ObjectId) -> PathBuf {
        let file_name = self.object_file_name(object_id);
        let (first, second) = Self::shard_components(&file_name);
        self.objects_dir.join(first).join(second).join(file_name)
    }

    fn legacy_object_path(&self, object_id: &ObjectId) -> PathBuf {
        self.objects_dir.join(self.object_file_name(object_id))
    }
    
    /// Get part file path
    fn part_path(&self, upload_id: &str, part_number: u32) -> StorageResult<PathBuf> {
        self.validate_upload_id(upload_id)?;
        Ok(self.parts_dir.join(upload_id).join(part_number.to_string()))
    }
    
    /// Get upload directory path
    fn upload_dir(&self, upload_id: &str) -> StorageResult<PathBuf> {
        self.validate_upload_id(upload_id)?;
        Ok(self.parts_dir.join(upload_id))
    }

    fn validate_upload_id(&self, upload_id: &str) -> StorageResult<()> {
        if upload_id.is_empty()
            || upload_id.contains('/')
            || upload_id.contains('\\')
            || upload_id.contains("..")
        {
            return Err(StorageError::Backend(format!(
                "invalid upload_id: {}",
                upload_id
            )));
        }
        Ok(())
    }

    async fn resolve_existing_object_path(&self, object_id: &ObjectId) -> StorageResult<PathBuf> {
        let path = self.object_path(object_id);
        if fs::try_exists(&path).await? {
            return Ok(path);
        }

        let legacy_path = self.legacy_object_path(object_id);
        if fs::try_exists(&legacy_path).await? {
            return Ok(legacy_path);
        }

        Err(StorageError::NotFound(*object_id))
    }
}

#[async_trait]
impl StorageBackend for LocalFsBackend {
    async fn put_object(&self, object_id: &ObjectId, data: Bytes) -> StorageResult<()> {
        let path = self.object_path(object_id);
        
        // Create parent directory if needed (shouldn't be needed, but be safe)
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).await?;
        }
        
        // Write data atomically using temporary file + rename
        let temp_path = path.with_extension(".tmp");
        let mut file = fs::File::create(&temp_path).await?;
        file.write_all(&data).await?;
        if self.sync_on_write {
            file.sync_data().await?;
        }
        drop(file);
        
        // Atomic rename
        fs::rename(&temp_path, &path).await?;
        
        tracing::debug!(
            object_id = %object_id,
            size = data.len(),
            path = %path.display(),
            "Stored object to local filesystem"
        );
        
        Ok(())
    }
    
    async fn get_object(&self, object_id: &ObjectId) -> StorageResult<Bytes> {
        let path = self.resolve_existing_object_path(object_id).await?;
        let data = fs::read(&path).await?;
        
        tracing::debug!(
            object_id = %object_id,
            size = data.len(),
            "Read object from local filesystem"
        );
        
        Ok(Bytes::from(data))
    }
    
    async fn delete_object(&self, object_id: &ObjectId) -> StorageResult<()> {
        let path = match self.resolve_existing_object_path(object_id).await {
            Ok(path) => path,
            Err(StorageError::NotFound(_)) => return Ok(()),
            Err(err) => return Err(err),
        };

        if fs::try_exists(&path).await? {
            fs::remove_file(&path).await?;
            tracing::debug!(object_id = %object_id, "Deleted object from local filesystem");
        }
        
        Ok(())
    }
    
    async fn object_exists(&self, object_id: &ObjectId) -> StorageResult<bool> {
        Ok(self.resolve_existing_object_path(object_id).await.is_ok())
    }
    
    async fn object_size(&self, object_id: &ObjectId) -> StorageResult<u64> {
        let path = self.resolve_existing_object_path(object_id).await?;
        let metadata = fs::metadata(&path).await?;
        Ok(metadata.len())
    }
    
    async fn put_part(
        &self,
        upload_id: &str,
        part_number: u32,
        data: Bytes,
    ) -> StorageResult<()> {
        let path = self.part_path(upload_id, part_number)?;
        
        // Create upload directory if needed
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).await?;
        }
        
        // Write part data atomically
        let temp_path = path.with_extension(".tmp");
        let mut file = fs::File::create(&temp_path).await?;
        file.write_all(&data).await?;
        if self.sync_on_write {
            file.sync_data().await?;
        }
        drop(file);
        
        fs::rename(&temp_path, &path).await?;
        
        tracing::debug!(
            upload_id = upload_id,
            part_number = part_number,
            size = data.len(),
            "Stored part to local filesystem"
        );
        
        Ok(())
    }
    
    async fn get_part(
        &self,
        upload_id: &str,
        part_number: u32,
    ) -> StorageResult<Bytes> {
        let path = self.part_path(upload_id, part_number)?;
        
        if !fs::try_exists(&path).await? {
            return Err(StorageError::Backend(format!(
                "Part {} of upload {} not found",
                part_number, upload_id
            )));
        }
        
        let data = fs::read(&path).await?;
        
        Ok(Bytes::from(data))
    }
    
    async fn delete_part(&self, upload_id: &str, part_number: u32) -> StorageResult<()> {
        let path = self.part_path(upload_id, part_number)?;
        
        if fs::try_exists(&path).await? {
            fs::remove_file(&path).await?;
        }
        
        Ok(())
    }
    
    async fn delete_upload_parts(&self, upload_id: &str) -> StorageResult<()> {
        let upload_dir = self.upload_dir(upload_id)?;
        
        if fs::try_exists(&upload_dir).await? {
            fs::remove_dir_all(&upload_dir).await?;
            tracing::debug!(upload_id = upload_id, "Deleted all parts for upload");
        }
        
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;
    
    #[tokio::test]
    async fn test_put_get_object() {
        let temp_dir = TempDir::new().unwrap();
        let backend = LocalFsBackend::new(temp_dir.path(), false).await.unwrap();
        
        let object_id = ObjectId::new();
        let data = Bytes::from("test data");
        
        // Put object
        backend.put_object(&object_id, data.clone()).await.unwrap();
        
        // Get object
        let retrieved = backend.get_object(&object_id).await.unwrap();
        assert_eq!(retrieved, data);
    }
    
    #[tokio::test]
    async fn test_object_exists() {
        let temp_dir = TempDir::new().unwrap();
        let backend = LocalFsBackend::new(temp_dir.path(), false).await.unwrap();
        
        let object_id = ObjectId::new();
        
        // Object doesn't exist
        assert!(!backend.object_exists(&object_id).await.unwrap());
        
        // Put object
        backend.put_object(&object_id, Bytes::from("data")).await.unwrap();
        
        // Object exists
        assert!(backend.object_exists(&object_id).await.unwrap());
    }
    
    #[tokio::test]
    async fn test_delete_object() {
        let temp_dir = TempDir::new().unwrap();
        let backend = LocalFsBackend::new(temp_dir.path(), false).await.unwrap();
        
        let object_id = ObjectId::new();
        
        // Put object
        backend.put_object(&object_id, Bytes::from("data")).await.unwrap();
        assert!(backend.object_exists(&object_id).await.unwrap());
        
        // Delete object
        backend.delete_object(&object_id).await.unwrap();
        assert!(!backend.object_exists(&object_id).await.unwrap());
    }
    
    #[tokio::test]
    async fn test_object_size() {
        let temp_dir = TempDir::new().unwrap();
        let backend = LocalFsBackend::new(temp_dir.path(), false).await.unwrap();
        
        let object_id = ObjectId::new();
        let data = Bytes::from("test data");
        
        // Put object
        backend.put_object(&object_id, data.clone()).await.unwrap();
        
        // Check size
        let size = backend.object_size(&object_id).await.unwrap();
        assert_eq!(size, data.len() as u64);
    }
    
    #[tokio::test]
    async fn test_multipart_parts() {
        let temp_dir = TempDir::new().unwrap();
        let backend = LocalFsBackend::new(temp_dir.path(), false).await.unwrap();
        
        let upload_id = "test-upload-123";
        let part1_data = Bytes::from("part 1");
        let part2_data = Bytes::from("part 2");
        
        // Put parts
        backend.put_part(upload_id, 1, part1_data.clone()).await.unwrap();
        backend.put_part(upload_id, 2, part2_data.clone()).await.unwrap();
        
        // Get parts
        let retrieved1 = backend.get_part(upload_id, 1).await.unwrap();
        let retrieved2 = backend.get_part(upload_id, 2).await.unwrap();
        assert_eq!(retrieved1, part1_data);
        assert_eq!(retrieved2, part2_data);
        
        // Delete parts
        backend.delete_part(upload_id, 1).await.unwrap();
        assert!(backend.get_part(upload_id, 1).await.is_err());
        
        // Delete all parts
        backend.delete_upload_parts(upload_id).await.unwrap();
        assert!(backend.get_part(upload_id, 2).await.is_err());
    }
}
