use std::path::PathBuf;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use spacesh_proto::workspace::{Group, Workspace};

/// The full persisted snapshot of structure (no live processes).
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct PersistState {
    pub version: u32,
    #[serde(default)]
    pub groups: Vec<Group>,
    #[serde(default)]
    pub workspaces: Vec<Workspace>,
}

pub trait StateStore: Send + Sync {
    fn load(&self) -> Result<PersistState>;
    fn save(&self, state: &PersistState) -> Result<()>;
}

/// JSON file store with atomic write (temp + rename) and corrupt-file backup.
pub struct JsonStateStore {
    path: PathBuf,
}

impl JsonStateStore {
    pub fn new(path: PathBuf) -> Self {
        Self { path }
    }

    fn backup_corrupt(&self, ts: u128) {
        let bak = self.path.with_extension(format!("corrupt-{ts}"));
        let _ = std::fs::rename(&self.path, bak);
    }
}

impl StateStore for JsonStateStore {
    fn load(&self) -> Result<PersistState> {
        if !self.path.exists() {
            return Ok(PersistState { version: 1, ..Default::default() });
        }
        let bytes = std::fs::read(&self.path)?;
        match serde_json::from_slice::<PersistState>(&bytes) {
            Ok(state) => Ok(state),
            Err(_) => {
                // Corrupt file: back it up and start fresh.
                let ts = std::time::SystemTime::now()
                    .duration_since(std::time::UNIX_EPOCH)
                    .map(|d| d.as_nanos())
                    .unwrap_or(0);
                self.backup_corrupt(ts);
                Ok(PersistState { version: 1, ..Default::default() })
            }
        }
    }

    fn save(&self, state: &PersistState) -> Result<()> {
        if let Some(parent) = self.path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        let tmp = self.path.with_extension("json.tmp");
        let bytes = serde_json::to_vec_pretty(state)?;
        std::fs::write(&tmp, &bytes)?;
        // fsync the temp file before rename for durability.
        let f = std::fs::File::open(&tmp)?;
        f.sync_all()?;
        std::fs::rename(&tmp, &self.path)?;
        Ok(())
    }
}

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

    fn tmp_file(name: &str) -> PathBuf {
        let mut p = std::env::temp_dir();
        let n = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
        p.push(format!("spacesh-state-{name}-{n}.json"));
        p
    }

    fn sample() -> PersistState {
        PersistState {
            version: 1,
            groups: vec![],
            workspaces: vec![Workspace {
                id: WorkspaceId("w_1".into()),
                path: "/tmp/p".into(),
                name: "p".into(),
                group_id: None,
                order: 0,
                unread: false,
                pinned: false,
                layout: None,
                zoomed: None,
                surfaces: std::collections::HashMap::new(),
            }],
        }
    }

    #[test]
    fn save_then_load_round_trips() {
        let path = tmp_file("roundtrip");
        let store = JsonStateStore::new(path.clone());
        store.save(&sample()).unwrap();
        let back = store.load().unwrap();
        assert_eq!(back, sample());
        let _ = std::fs::remove_file(path);
    }

    #[test]
    fn missing_file_loads_default() {
        let store = JsonStateStore::new(tmp_file("missing"));
        let s = store.load().unwrap();
        assert_eq!(s.version, 1);
        assert!(s.workspaces.is_empty());
    }

    #[test]
    fn corrupt_file_is_backed_up_and_load_returns_default() {
        let path = tmp_file("corrupt");
        std::fs::write(&path, b"{ this is not valid json").unwrap();
        let store = JsonStateStore::new(path.clone());
        let s = store.load().unwrap();
        assert!(s.workspaces.is_empty());
        // original path no longer holds the corrupt bytes (renamed away)
        assert!(!path.exists());
        let _ = std::fs::remove_file(path);
    }
}
