//! Key-value store — JSON-backed persistent storage for user settings and preferences.
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;

/// Trait for a simple key-value store that can get, set, delete, and persist entries.
pub trait StoreOps: Send + Sync {
    fn get(&self, key: &str) -> Option<Value>;
    fn set(&self, key: &str, value: Value);
    fn delete(&self, key: &str);
    fn save(&self) -> Result<(), String>;
}

/// A JSON-backed file store that persists key-value data to a single file on disk.
pub struct JsonFileStore {
    path: PathBuf,
    data: Mutex<HashMap<String, Value>>,
}

impl JsonFileStore {
    /// Create a new store backed by `path`, loading existing data if the file is present.
    pub fn new(path: PathBuf) -> Result<Self, String> {
        let data = if path.exists() {
            let content =
                fs::read_to_string(&path).map_err(|e| format!("Failed to read store: {e}"))?;
            if content.trim().is_empty() {
                HashMap::new()
            } else {
                serde_json::from_str::<HashMap<String, Value>>(&content)
                    .map_err(|e| format!("Failed to parse store: {e}"))?
            }
        } else {
            HashMap::new()
        };

        Ok(Self {
            path,
            data: Mutex::new(data),
        })
    }

    /// Convenience constructor accepting any path type; delegates to [`Self::new`].
    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, String> {
        Self::new(path.as_ref().to_path_buf())
    }

    /// Return the path to the backing JSON file.
    #[allow(dead_code)]
    pub fn path(&self) -> &Path {
        &self.path
    }

    fn ensure_parent_dir(&self) -> Result<(), String> {
        if let Some(parent) = self.path.parent() {
            fs::create_dir_all(parent)
                .map_err(|e| format!("Failed to create store directory: {e}"))?;
        }
        Ok(())
    }
}

impl StoreOps for JsonFileStore {
    fn get(&self, key: &str) -> Option<Value> {
        self.data.lock().ok().and_then(|map| map.get(key).cloned())
    }

    fn set(&self, key: &str, value: Value) {
        if let Ok(mut map) = self.data.lock() {
            map.insert(key.to_string(), value);
        }
    }

    fn delete(&self, key: &str) {
        if let Ok(mut map) = self.data.lock() {
            map.remove(key);
        }
    }

    fn save(&self) -> Result<(), String> {
        self.ensure_parent_dir()?;
        let map = self.data.lock().map_err(|e| e.to_string())?;
        let content =
            serde_json::to_string_pretty(&*map).map_err(|e| format!("Serialize failed: {e}"))?;
        fs::write(&self.path, content).map_err(|e| format!("Failed to write store: {e}"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;
    use tempfile::TempDir;

    fn store_in(dir: &TempDir, name: &str) -> JsonFileStore {
        let path = dir.path().join(name);
        JsonFileStore::new(path).expect("store creation should succeed")
    }

    #[test]
    fn new_from_missing_file_creates_empty_store() {
        let dir = TempDir::new().unwrap();
        let store = store_in(&dir, "missing.json");
        assert!(store.get("anything").is_none());
    }

    #[test]
    fn new_from_empty_file_creates_empty_store() {
        let dir = TempDir::new().unwrap();
        let path = dir.path().join("empty.json");
        fs::write(&path, "").unwrap();
        let store = JsonFileStore::new(path).expect("should handle empty file");
        assert!(store.get("anything").is_none());
    }

    #[test]
    fn new_from_corrupt_file_returns_error() {
        let dir = TempDir::new().unwrap();
        let path = dir.path().join("corrupt.json");
        fs::write(&path, "not valid json {{{").unwrap();
        let result = JsonFileStore::new(path);
        match result {
            Err(e) => assert!(e.contains("Failed to parse store"), "unexpected error: {e}"),
            Ok(_) => panic!("expected error for corrupt file"),
        }
    }

    #[test]
    fn get_set_delete_roundtrip() {
        let dir = TempDir::new().unwrap();
        let store = store_in(&dir, "data.json");

        assert!(store.get("key").is_none());

        store.set("key", json!("value"));
        assert_eq!(store.get("key"), Some(json!("value")));

        store.set("key", json!(42));
        assert_eq!(store.get("key"), Some(json!(42)));

        store.delete("key");
        assert!(store.get("key").is_none());
    }

    #[test]
    fn save_persists_and_reload_restores() {
        let dir = TempDir::new().unwrap();
        let path = dir.path().join("persist.json");

        {
            let store = JsonFileStore::new(path.clone()).unwrap();
            store.set("name", json!("huskies"));
            store.set("version", json!(1));
            store.save().expect("save should succeed");
        }

        let store = JsonFileStore::new(path).unwrap();
        assert_eq!(store.get("name"), Some(json!("huskies")));
        assert_eq!(store.get("version"), Some(json!(1)));
    }

    #[test]
    fn save_creates_parent_directories() {
        let dir = TempDir::new().unwrap();
        let path = dir.path().join("nested").join("deep").join("store.json");
        let store = JsonFileStore::new(path.clone()).unwrap();
        store.set("key", json!("value"));
        store.save().expect("save should create parent dirs");
        assert!(path.exists());
    }

    #[test]
    fn delete_nonexistent_key_is_noop() {
        let dir = TempDir::new().unwrap();
        let store = store_in(&dir, "data.json");
        store.delete("nonexistent");
        assert!(store.get("nonexistent").is_none());
    }

    #[test]
    fn from_path_works_like_new() {
        let dir = TempDir::new().unwrap();
        let path = dir.path().join("via_from.json");
        let store = JsonFileStore::from_path(&path).unwrap();
        store.set("test", json!(true));
        assert_eq!(store.get("test"), Some(json!(true)));
    }
}
