//! File-based checkpoint store

use async_trait::async_trait;
use serde_json::{from_slice, to_vec_pretty};
use std::path::PathBuf;
use tokio::fs;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

use governor_core::traits::checkpoint_store::{
    Checkpoint, CheckpointInfo, CheckpointStore, StoreError,
};

/// File-based checkpoint store
pub struct FileCheckpointStore {
    /// Directory to store checkpoints
    dir: PathBuf,
}

impl FileCheckpointStore {
    /// Create a new file checkpoint store
    ///
    /// # Errors
    ///
    /// Returns `StoreError` if the directory cannot be created
    pub fn new(dir: PathBuf) -> Result<Self, StoreError> {
        // Create directory if it doesn't exist
        std::fs::create_dir_all(&dir).map_err(|e| {
            StoreError::SaveFailed(format!("Failed to create checkpoint directory: {e}"))
        })?;

        Ok(Self { dir })
    }

    /// Create with default directory (.cargo-governor/checkpoints)
    ///
    /// # Errors
    ///
    /// Returns `StoreError` if the current directory cannot be determined or the directory cannot be created
    pub fn default_dir() -> Result<Self, StoreError> {
        let dir = std::env::current_dir()
            .unwrap_or_else(|_| PathBuf::from("."))
            .join(".cargo-governor")
            .join("checkpoints");

        Self::new(dir)
    }

    /// Get the path for a checkpoint file
    fn checkpoint_path(&self, id: &str) -> PathBuf {
        self.dir.join(format!("{id}.json"))
    }

    /// Get the path for the latest checkpoint symlink
    fn latest_path(&self) -> PathBuf {
        self.dir.join("latest.json")
    }

    /// List all checkpoint files (async helper)
    async fn list_files(&self) -> Result<Vec<PathBuf>, StoreError> {
        let mut files = Vec::new();

        if !self.dir.try_exists().unwrap_or(false) {
            return Ok(files);
        }

        let mut entries = fs::read_dir(&self.dir).await.map_err(|e| {
            StoreError::LoadFailed(format!("Failed to read checkpoint directory: {e}"))
        })?;

        while let Some(entry) = entries
            .next_entry()
            .await
            .map_err(|e| StoreError::LoadFailed(format!("Failed to read directory entry: {e}")))?
        {
            let path = entry.path();

            // Skip the latest symlink
            if path == self.latest_path() {
                continue;
            }

            // Only include .json files
            if path.extension().is_some_and(|e| e == "json") {
                files.push(path);
            }
        }

        Ok(files)
    }
}

#[async_trait]
impl CheckpointStore for FileCheckpointStore {
    async fn save(&self, checkpoint: &Checkpoint) -> Result<(), StoreError> {
        let path = self.checkpoint_path(&checkpoint.id);
        let data =
            to_vec_pretty(checkpoint).map_err(|e| StoreError::SerializationError(e.to_string()))?;

        let mut file = fs::File::create(&path).await.map_err(|e| {
            StoreError::SaveFailed(format!("Failed to create checkpoint file: {e}"))
        })?;

        file.write_all(&data)
            .await
            .map_err(|e| StoreError::SaveFailed(format!("Failed to write checkpoint: {e}")))?;

        // Update latest symlink
        let latest_path = self.latest_path();
        let _ = fs::write(&latest_path, data).await;

        Ok(())
    }

    async fn load(&self) -> Result<Option<Checkpoint>, StoreError> {
        let latest_path = self.latest_path();

        if !latest_path.try_exists().unwrap_or(false) {
            return Ok(None);
        }

        let mut file = fs::File::open(&latest_path)
            .await
            .map_err(|e| StoreError::LoadFailed(format!("Failed to open checkpoint file: {e}")))?;

        let mut data = Vec::new();
        file.read_to_end(&mut data)
            .await
            .map_err(|e| StoreError::LoadFailed(format!("Failed to read checkpoint: {e}")))?;

        let checkpoint = from_slice(&data)
            .map_err(|e| StoreError::LoadFailed(format!("Failed to parse checkpoint: {e}")))?;

        Ok(Some(checkpoint))
    }

    async fn load_by_id(&self, id: &str) -> Result<Option<Checkpoint>, StoreError> {
        let path = self.checkpoint_path(id);

        if !path.try_exists().unwrap_or(false) {
            return Ok(None);
        }

        let mut file = fs::File::open(&path)
            .await
            .map_err(|e| StoreError::LoadFailed(format!("Failed to open checkpoint file: {e}")))?;

        let mut data = Vec::new();
        file.read_to_end(&mut data)
            .await
            .map_err(|e| StoreError::LoadFailed(format!("Failed to read checkpoint: {e}")))?;

        let checkpoint = from_slice(&data)
            .map_err(|e| StoreError::LoadFailed(format!("Failed to parse checkpoint: {e}")))?;

        Ok(Some(checkpoint))
    }

    async fn list(&self) -> Result<Vec<CheckpointInfo>, StoreError> {
        let files = self.list_files().await?;
        let mut checkpoints = Vec::new();

        for path in files {
            let mut file = fs::File::open(&path).await.map_err(|e| {
                StoreError::LoadFailed(format!("Failed to open checkpoint file: {e}"))
            })?;

            let mut data = Vec::new();
            AsyncReadExt::read_to_end(&mut file, &mut data)
                .await
                .map_err(|e| StoreError::LoadFailed(format!("Failed to read checkpoint: {e}")))?;

            if let Ok(checkpoint) = from_slice::<Checkpoint>(&data) {
                checkpoints.push(CheckpointInfo::from(checkpoint));
            }
        }

        Ok(checkpoints)
    }

    async fn delete(&self, id: &str) -> Result<(), StoreError> {
        let path = self.checkpoint_path(id);

        if path.try_exists().unwrap_or(false) {
            fs::remove_file(&path)
                .await
                .map_err(|e| StoreError::SaveFailed(format!("Failed to delete checkpoint: {e}")))?;
        }

        Ok(())
    }

    async fn clear(&self) -> Result<(), StoreError> {
        let files = self.list_files().await?;

        for path in files {
            fs::remove_file(&path)
                .await
                .map_err(|e| StoreError::SaveFailed(format!("Failed to delete checkpoint: {e}")))?;
        }

        // Also remove latest
        let latest_path = self.latest_path();
        if latest_path.try_exists().unwrap_or(false) {
            fs::remove_file(&latest_path)
                .await
                .map_err(|e| StoreError::SaveFailed(format!("Failed to delete checkpoint: {e}")))?;
        }

        Ok(())
    }

    fn name(&self) -> &'static str {
        "file"
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use governor_core::traits::checkpoint_store::Checkpoint;

    #[tokio::test]
    async fn test_file_checkpoint_store() {
        let temp_dir = tempfile::tempdir().unwrap();
        let store = FileCheckpointStore::new(temp_dir.path().to_path_buf()).unwrap();

        let checkpoint = Checkpoint::new(
            "test-workflow".to_string(),
            0,
            vec![],
            serde_json::Value::Object(serde_json::Map::default()),
        );

        store.save(&checkpoint).await.unwrap();

        let loaded = store.load().await.unwrap();
        assert!(loaded.is_some());
        assert_eq!(loaded.unwrap().workflow_id, "test-workflow");
    }

    #[tokio::test]
    async fn test_checkpoint_list() {
        let temp_dir = tempfile::tempdir().unwrap();
        let store = FileCheckpointStore::new(temp_dir.path().to_path_buf()).unwrap();

        let checkpoint1 = Checkpoint::new(
            "workflow-1".to_string(),
            0,
            vec![],
            serde_json::Value::Object(serde_json::Map::default()),
        );
        let checkpoint2 = Checkpoint::new(
            "workflow-2".to_string(),
            1,
            vec!["step1".to_string()],
            serde_json::Value::Object(serde_json::Map::default()),
        );

        store.save(&checkpoint1).await.unwrap();
        store.save(&checkpoint2).await.unwrap();

        let list = store.list().await.unwrap();
        assert_eq!(list.len(), 2);
    }

    #[tokio::test]
    async fn test_checkpoint_delete() {
        let temp_dir = tempfile::tempdir().unwrap();
        let store = FileCheckpointStore::new(temp_dir.path().to_path_buf()).unwrap();

        let checkpoint = Checkpoint::new(
            "test-workflow".to_string(),
            0,
            vec![],
            serde_json::Value::Object(serde_json::Map::default()),
        );

        store.save(&checkpoint).await.unwrap();
        store.delete(&checkpoint.id).await.unwrap();

        let loaded = store.load_by_id(&checkpoint.id).await.unwrap();
        assert!(loaded.is_none());
    }

    #[tokio::test]
    async fn test_checkpoint_clear() {
        let temp_dir = tempfile::tempdir().unwrap();
        let store = FileCheckpointStore::new(temp_dir.path().to_path_buf()).unwrap();

        let checkpoint1 = Checkpoint::new(
            "workflow-1".to_string(),
            0,
            vec![],
            serde_json::Value::Object(serde_json::Map::default()),
        );
        let checkpoint2 = Checkpoint::new(
            "workflow-2".to_string(),
            0,
            vec![],
            serde_json::Value::Object(serde_json::Map::default()),
        );

        store.save(&checkpoint1).await.unwrap();
        store.save(&checkpoint2).await.unwrap();

        store.clear().await.unwrap();

        let list = store.list().await.unwrap();
        assert_eq!(list.len(), 0);
    }
}
