use std::collections::BTreeMap;
use std::path::Path;

use anyhow::Result;
use serde::{Deserialize, Serialize};
use tokio::fs;
use uuid::Uuid;

use crate::protocol::{HarnessConfig, HarnessInfo, Message};

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct State {
    #[serde(default)]
    pub harnesses: BTreeMap<String, StoredHarness>,
    #[serde(default)]
    pub messages: Vec<Message>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::protocol::HarnessKind;

    fn config(name: &str) -> HarnessConfig {
        HarnessConfig {
            name: name.to_string(),
            kind: HarnessKind::Generic,
            command: "cat".into(),
            args: vec![],
            cwd: "/tmp".into(),
            personality: "p".into(),
            env: vec![],
        }
    }

    #[tokio::test]
    async fn load_missing_returns_default_and_save_round_trips() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("state.json");
        let mut state = State::load(&path).await.unwrap();
        assert!(state.harnesses.is_empty());

        let id = state.upsert(config("one"));
        state.save(&path).await.unwrap();

        let loaded = State::load(&path).await.unwrap();
        assert_eq!(loaded.harnesses["one"].id, id);
        assert_eq!(
            loaded
                .info_for("one", Some(42), true, "/tmp/one.jsonl".into())
                .unwrap()
                .log_path,
            "/tmp/one.jsonl"
        );
    }

    #[tokio::test]
    async fn load_returns_error_for_unreadable_json_source_and_save_allows_bare_file() {
        let dir = tempfile::tempdir().unwrap();
        let err = State::load(dir.path()).await.unwrap_err();
        assert!(!err.to_string().is_empty());

        let file = tempfile::NamedTempFile::new().unwrap();
        let path = file.path().to_path_buf();
        State::default().save(&path).await.unwrap();
        assert!(
            tokio::fs::read_to_string(&path)
                .await
                .unwrap()
                .contains("harnesses")
        );
    }

    #[test]
    fn upsert_existing_keeps_id_and_replaces_config() {
        let mut state = State::default();
        let id = state.upsert(config("one"));
        let mut replacement = config("one");
        replacement.personality = "new".into();
        assert_eq!(state.upsert(replacement), id);
        assert_eq!(state.harnesses["one"].config.personality, "new");
        assert!(state.info_for("missing", None, false, "x".into()).is_none());
    }

    #[test]
    fn info_for_marks_dead_without_pid() {
        let mut state = State::default();
        state.upsert(config("one"));
        let info = state.info_for("one", None, false, "log".into()).unwrap();
        assert!(!info.alive);
        assert_eq!(info.pid, None);
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredHarness {
    pub id: String,
    pub config: HarnessConfig,
}

impl State {
    pub async fn load(path: &Path) -> Result<Self> {
        match fs::read_to_string(path).await {
            Ok(raw) => Ok(serde_json::from_str(&raw)?),
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
            Err(err) => Err(err.into()),
        }
    }

    pub async fn save(&self, path: &Path) -> Result<()> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).await?;
        }
        let raw = serde_json::to_string_pretty(self)?;
        fs::write(path, format!("{raw}\n")).await?;
        Ok(())
    }

    pub fn upsert(&mut self, config: HarnessConfig) -> String {
        if let Some(existing) = self.harnesses.get_mut(&config.name) {
            existing.config = config;
            return existing.id.clone();
        }
        let id = Uuid::new_v4().to_string();
        self.harnesses.insert(
            config.name.clone(),
            StoredHarness {
                id: id.clone(),
                config,
            },
        );
        id
    }

    pub fn info_for(
        &self,
        name: &str,
        pid: Option<u32>,
        alive: bool,
        log_path: String,
    ) -> Option<HarnessInfo> {
        let stored = self.harnesses.get(name)?;
        Some(HarnessInfo {
            id: stored.id.clone(),
            name: stored.config.name.clone(),
            kind: stored.config.kind.clone(),
            cwd: stored.config.cwd.clone(),
            personality: stored.config.personality.clone(),
            pid,
            alive,
            log_path,
        })
    }
}
