//! Durable FIFA 17 **client-data blob** store (`clientdata` / `userHubData`).
//!
//! FIFA persists opaque per-user client blobs via `PUT/POST …/clientdata/<key>`
//! and reads them back via `GET …/clientdata/<key>`. The blobs are entirely
//! client-defined (UI/hub state) — the server only round-trips them and never
//! interprets their contents. This store keeps them in memory keyed by
//! `<persona>:<key>` and persists the whole map to a JSON file on every write, so
//! the client's saved state survives a host restart.
//!
//! The blobs are non-authoritative client presentation state (NOT economy or
//! ownership), so a missing/unreadable backing file starts empty rather than
//! being a hard failure.

use std::collections::HashMap;
use std::path::PathBuf;

use parking_lot::Mutex;
use serde_json::Value;

/// In-memory client-data blobs, persisted to a JSON file on write.
pub struct ClientDataStore {
    path: PathBuf,
    map: Mutex<HashMap<String, Value>>,
}

impl ClientDataStore {
    /// Open the store, loading any previously-persisted blobs. A missing or
    /// unreadable file starts empty.
    pub fn open(path: impl Into<PathBuf>) -> Self {
        let path = path.into();
        let map = std::fs::read(&path)
            .ok()
            .and_then(|b| serde_json::from_slice::<HashMap<String, Value>>(&b).ok())
            .unwrap_or_default();
        ClientDataStore {
            path,
            map: Mutex::new(map),
        }
    }

    fn compound_key(persona: i64, key: &str) -> String {
        format!("{persona}:{key}")
    }

    /// The stored blob for `<persona>:<key>`, or `None` if never written.
    pub fn get(&self, persona: i64, key: &str) -> Option<Value> {
        self.map
            .lock()
            .get(&Self::compound_key(persona, key))
            .cloned()
    }

    /// Store `value` under `<persona>:<key>` and persist the whole map to disk.
    /// The serialized snapshot is taken under the lock; the file write happens
    /// after the lock is released.
    pub fn put(&self, persona: i64, key: &str, value: Value) {
        let snapshot = {
            let mut map = self.map.lock();
            map.insert(Self::compound_key(persona, key), value);
            serde_json::to_vec(&*map).unwrap_or_default()
        };
        if let Some(parent) = self.path.parent() {
            if !parent.as_os_str().is_empty() {
                let _ = std::fs::create_dir_all(parent);
            }
        }
        let _ = std::fs::write(&self.path, snapshot);
    }
}

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

    fn temp_path(tag: &str) -> PathBuf {
        std::env::temp_dir().join(format!(
            "openfut-clientdata-test-{}-{}.json",
            std::process::id(),
            tag
        ))
    }

    #[test]
    fn round_trips_and_persists_across_reopen() {
        let path = temp_path("roundtrip");
        let _ = std::fs::remove_file(&path);
        let store = ClientDataStore::open(&path);
        assert_eq!(store.get(33_068_179, "userHubData"), None);
        store.put(33_068_179, "userHubData", json!({"tiles": [1, 2, 3]}));
        assert_eq!(
            store.get(33_068_179, "userHubData"),
            Some(json!({"tiles": [1, 2, 3]}))
        );
        // A different persona under the same key is isolated.
        assert_eq!(store.get(1, "userHubData"), None);
        // Reopening reads the persisted blob back.
        let reopened = ClientDataStore::open(&path);
        assert_eq!(
            reopened.get(33_068_179, "userHubData"),
            Some(json!({"tiles": [1, 2, 3]}))
        );
        let _ = std::fs::remove_file(&path);
    }
}
