//! Shared chat history persistence used by all transport modules.
//!
//! Each transport (Slack, WhatsApp, Discord, Matrix) stores its conversation
//! history in a transport-specific JSON file but uses the same on-disk format
//! and the same load/save logic provided here.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex as TokioMutex;

use crate::chat::transport::matrix::RoomConversation;
use crate::slog;

/// Shared conversation history type used by all string-keyed chat transports.
#[allow(dead_code)]
pub type ChatConversationHistory = Arc<TokioMutex<HashMap<String, RoomConversation>>>;

/// On-disk format for persisted conversation history.
#[derive(Serialize, Deserialize)]
struct PersistedHistory {
    conversations: HashMap<String, RoomConversation>,
}

/// Load conversation history from `project_root/relative_path`.
///
/// Returns an empty map on any I/O or parse error.
///
/// # Migration fallback
///
/// Older transport-specific files stored the conversation map under a key that
/// varied by transport (`channels` for Slack/Discord, `senders` for WhatsApp,
/// `rooms` for Matrix).  If deserialising the file as a [`PersistedHistory`]
/// (which expects the key `conversations`) fails, this function falls back to
/// parsing the file as a generic [`serde_json::Value`], locates the first
/// top-level object field whose value is itself an object (the conversation
/// map), and returns its contents deserialised as
/// `HashMap<String, RoomConversation>`.  New writes always use the
/// `conversations` key.
pub fn load_chat_history(
    project_root: &std::path::Path,
    relative_path: &str,
    log_tag: &str,
) -> HashMap<String, RoomConversation> {
    let path = project_root.join(relative_path);
    let data = match std::fs::read_to_string(&path) {
        Ok(d) => d,
        Err(_) => return HashMap::new(),
    };
    // Try the new canonical format first.
    if let Ok(p) = serde_json::from_str::<PersistedHistory>(&data) {
        return p.conversations;
    }
    // Migration fallback: find the first map-valued field, whatever its name.
    match serde_json::from_str::<serde_json::Value>(&data) {
        Ok(value) => {
            if let Some(obj) = value.as_object() {
                for (_key, val) in obj {
                    if let Ok(map) =
                        serde_json::from_value::<HashMap<String, RoomConversation>>(val.clone())
                    {
                        return map;
                    }
                }
            }
            slog!(
                "[{}] Failed to parse history file: no valid conversation map found",
                log_tag
            );
            HashMap::new()
        }
        Err(e) => {
            slog!("[{}] Failed to parse history file: {}", log_tag, e);
            HashMap::new()
        }
    }
}

/// Save conversation history to `project_root/relative_path` under the
/// `conversations` key.  Errors are logged but not propagated.
pub fn save_chat_history(
    project_root: &std::path::Path,
    relative_path: &str,
    log_tag: &str,
    history: &HashMap<String, RoomConversation>,
) {
    let persisted = PersistedHistory {
        conversations: history.clone(),
    };
    let path = project_root.join(relative_path);
    match serde_json::to_string_pretty(&persisted) {
        Ok(json) => {
            if let Err(e) = std::fs::write(&path, json) {
                slog!("[{}] Failed to write history file: {}", log_tag, e);
            }
        }
        Err(e) => slog!("[{}] Failed to serialise history: {}", log_tag, e),
    }
}

// ── Tests ───────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::chat::transport::matrix::{ConversationEntry, ConversationRole};

    #[test]
    fn round_trip_save_load_preserves_history() {
        let tmp = tempfile::tempdir().unwrap();
        let huskies_dir = tmp.path().join(".huskies");
        std::fs::create_dir_all(&huskies_dir).unwrap();

        let mut history = HashMap::new();
        history.insert(
            "room-1".to_string(),
            RoomConversation {
                session_id: Some("sess-xyz".to_string()),
                entries: vec![
                    ConversationEntry {
                        role: ConversationRole::User,
                        sender: "alice".to_string(),
                        content: "hello".to_string(),
                    },
                    ConversationEntry {
                        role: ConversationRole::Assistant,
                        sender: String::new(),
                        content: "hi there!".to_string(),
                    },
                ],
                ..Default::default()
            },
        );

        save_chat_history(tmp.path(), ".huskies/test_history.json", "test", &history);
        let loaded = load_chat_history(tmp.path(), ".huskies/test_history.json", "test");

        assert_eq!(loaded.len(), 1);
        let conv = loaded.get("room-1").unwrap();
        assert_eq!(conv.session_id.as_deref(), Some("sess-xyz"));
        assert_eq!(conv.entries.len(), 2);
        assert_eq!(conv.entries[0].role, ConversationRole::User);
        assert_eq!(conv.entries[0].content, "hello");
        assert_eq!(conv.entries[1].role, ConversationRole::Assistant);
        assert_eq!(conv.entries[1].content, "hi there!");
    }

    #[test]
    fn legacy_channels_key_loads_via_migration_fallback() {
        let tmp = tempfile::tempdir().unwrap();
        let huskies_dir = tmp.path().join(".huskies");
        std::fs::create_dir_all(&huskies_dir).unwrap();

        // Simulate a legacy Slack history file with the `channels` top-level key.
        let legacy_json = r#"{
            "channels": {
                "C01ABCDEF": {
                    "entries": [
                        {"role": "user", "sender": "U123", "content": "legacy message"}
                    ]
                }
            }
        }"#;
        std::fs::write(huskies_dir.join("slack_history.json"), legacy_json).unwrap();

        let loaded = load_chat_history(tmp.path(), ".huskies/slack_history.json", "slack");

        assert_eq!(loaded.len(), 1);
        let conv = loaded.get("C01ABCDEF").unwrap();
        assert_eq!(conv.entries.len(), 1);
        assert_eq!(conv.entries[0].content, "legacy message");
        assert_eq!(conv.entries[0].role, ConversationRole::User);
    }

    #[test]
    fn load_returns_empty_when_file_missing() {
        let tmp = tempfile::tempdir().unwrap();
        let loaded = load_chat_history(tmp.path(), ".huskies/missing.json", "test");
        assert!(loaded.is_empty());
    }

    #[test]
    fn load_returns_empty_on_invalid_json() {
        let tmp = tempfile::tempdir().unwrap();
        let huskies_dir = tmp.path().join(".huskies");
        std::fs::create_dir_all(&huskies_dir).unwrap();
        std::fs::write(huskies_dir.join("bad.json"), "not json {{{").unwrap();
        let loaded = load_chat_history(tmp.path(), ".huskies/bad.json", "test");
        assert!(loaded.is_empty());
    }
}
