use std::sync::Arc;

use dashmap::DashMap;

use crate::Result;

#[cfg(feature = "ai")]
use crate::ai::session::SessionManager;

use super::message::ChannelMessage;

/// Maps channel thread keys to session IDs, creating sessions on first contact.
pub struct ChannelSessionMap {
    map: DashMap<String, String>,
    #[cfg(feature = "ai")]
    session_manager: Arc<SessionManager>,
}

impl ChannelSessionMap {
    #[cfg(feature = "ai")]
    pub fn new(session_manager: Arc<SessionManager>) -> Self {
        Self {
            map: DashMap::new(),
            session_manager,
        }
    }

    /// Build a unique key from channel message metadata.
    ///
    /// Key format: `{channel}:{identifier}` where identifier depends on the channel:
    /// - telegram: `chat_id`
    /// - slack: `channel_id:thread_ts` (or just `channel_id` if no thread)
    /// - discord: `channel_id`
    /// - fallback: `sender` or "unknown"
    pub fn channel_key(message: &ChannelMessage) -> String {
        let channel = &message.channel;
        match channel.as_str() {
            "telegram" => {
                let chat_id = message
                    .metadata
                    .get("chat_id")
                    .map(|s| s.as_str())
                    .unwrap_or("unknown");
                format!("telegram:{chat_id}")
            }
            "slack" => {
                let channel_id = message
                    .metadata
                    .get("channel_id")
                    .map(|s| s.as_str())
                    .unwrap_or("unknown");
                if let Some(thread_ts) = message.metadata.get("thread_ts") {
                    format!("slack:{channel_id}:{thread_ts}")
                } else {
                    format!("slack:{channel_id}")
                }
            }
            "discord" => {
                let channel_id = message
                    .metadata
                    .get("channel_id")
                    .map(|s| s.as_str())
                    .unwrap_or("unknown");
                format!("discord:{channel_id}")
            }
            other => {
                let sender = message.sender.as_deref().unwrap_or("unknown");
                format!("{other}:{sender}")
            }
        }
    }

    /// Resolve an existing session or create a new one for the given channel key.
    /// Lookup order: (1) DashMap cache → (2) DB by channel_key → (3) create new.
    /// On UNIQUE constraint violation (TOCTOU race), retries by re-querying the DB.
    #[cfg(feature = "ai")]
    pub async fn resolve_session(&self, channel_key: &str, channel_name: &str) -> Result<String> {
        // 1. Check in-memory cache
        if let Some(session_id) = self.map.get(channel_key) {
            return Ok(session_id.clone());
        }

        // 2. Check DB for existing session with this channel_key
        if let Some(session) = self
            .session_manager
            .find_session_by_channel_key(channel_key)
            .await?
        {
            let session_id = session.id.clone();
            self.map.insert(channel_key.to_string(), session_id.clone());
            return Ok(session_id);
        }

        // 3. Create new session with channel_key and descriptive title
        let identifier = channel_key.split(':').nth(1).unwrap_or("unknown");
        let title = format!("{} #{}", capitalize_first(channel_name), identifier);
        match self
            .session_manager
            .create_session_with_channel_key(&title, channel_name, channel_key)
            .await
        {
            Ok(session) => {
                let session_id = session.id.clone();
                self.map.insert(channel_key.to_string(), session_id.clone());
                Ok(session_id)
            }
            Err(_) => {
                // TOCTOU retry: another task created the session concurrently.
                // Re-query the DB for the session that was just created.
                if let Some(session) = self
                    .session_manager
                    .find_session_by_channel_key(channel_key)
                    .await?
                {
                    let session_id = session.id.clone();
                    self.map.insert(channel_key.to_string(), session_id.clone());
                    Ok(session_id)
                } else {
                    Err(crate::ZeniiError::Channel(format!(
                        "failed to resolve session for channel_key: {channel_key}"
                    )))
                }
            }
        }
    }

    /// List all active channel-to-session mappings.
    pub fn list_channel_sessions(&self) -> Vec<(String, String)> {
        self.map
            .iter()
            .map(|entry| (entry.key().clone(), entry.value().clone()))
            .collect()
    }
}

fn capitalize_first(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(c) => c.to_uppercase().to_string() + chars.as_str(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;

    // CR.1 — channel_key builds correct key from telegram message metadata
    #[test]
    fn channel_key_telegram() {
        let mut meta = HashMap::new();
        meta.insert("chat_id".into(), "12345".into());
        let msg = ChannelMessage::new("telegram", "hello").with_metadata(meta);
        assert_eq!(ChannelSessionMap::channel_key(&msg), "telegram:12345");
    }

    // CR.2 — channel_key builds correct key from slack message with thread_ts
    #[test]
    fn channel_key_slack_thread() {
        let mut meta = HashMap::new();
        meta.insert("channel_id".into(), "C123".into());
        meta.insert("thread_ts".into(), "1234567890.123456".into());
        let msg = ChannelMessage::new("slack", "hello").with_metadata(meta);
        assert_eq!(
            ChannelSessionMap::channel_key(&msg),
            "slack:C123:1234567890.123456"
        );
    }

    // CR.3 — channel_key builds correct key from discord message
    #[test]
    fn channel_key_discord() {
        let mut meta = HashMap::new();
        meta.insert("channel_id".into(), "987654".into());
        let msg = ChannelMessage::new("discord", "hello").with_metadata(meta);
        assert_eq!(ChannelSessionMap::channel_key(&msg), "discord:987654");
    }

    // CR.4 — resolve_session creates new session on first message
    #[cfg(feature = "ai")]
    #[tokio::test]
    async fn resolve_creates_session() {
        let dir = tempfile::TempDir::new().unwrap();
        let path = dir.path().join("test.db");
        let pool = crate::db::init_pool(&path).unwrap();
        crate::db::with_db(&pool, |conn| crate::db::run_migrations(conn))
            .await
            .unwrap();
        let mgr = Arc::new(crate::ai::session::SessionManager::new(pool));
        let map = ChannelSessionMap::new(mgr.clone());

        let session_id = map
            .resolve_session("telegram:12345", "telegram")
            .await
            .unwrap();
        assert!(!session_id.is_empty());

        // Verify session was created in DB
        let session = mgr.get_session(&session_id).await.unwrap();
        assert_eq!(session.title, "Telegram #12345");
        assert_eq!(session.source, "telegram");
        assert_eq!(session.channel_key.as_deref(), Some("telegram:12345"));
    }

    // CR.5 — resolve_session returns same session_id for same channel_key
    #[cfg(feature = "ai")]
    #[tokio::test]
    async fn resolve_returns_existing() {
        let dir = tempfile::TempDir::new().unwrap();
        let path = dir.path().join("test.db");
        let pool = crate::db::init_pool(&path).unwrap();
        crate::db::with_db(&pool, |conn| crate::db::run_migrations(conn))
            .await
            .unwrap();
        let mgr = Arc::new(crate::ai::session::SessionManager::new(pool));
        let map = ChannelSessionMap::new(mgr);

        let id1 = map
            .resolve_session("telegram:12345", "telegram")
            .await
            .unwrap();
        let id2 = map
            .resolve_session("telegram:12345", "telegram")
            .await
            .unwrap();
        assert_eq!(id1, id2);
    }

    // CR.6 — new session has correct source field
    #[cfg(feature = "ai")]
    #[tokio::test]
    async fn session_source_matches_channel() {
        let dir = tempfile::TempDir::new().unwrap();
        let path = dir.path().join("test.db");
        let pool = crate::db::init_pool(&path).unwrap();
        crate::db::with_db(&pool, |conn| crate::db::run_migrations(conn))
            .await
            .unwrap();
        let mgr = Arc::new(crate::ai::session::SessionManager::new(pool));
        let map = ChannelSessionMap::new(mgr.clone());

        let session_id = map.resolve_session("slack:C123", "slack").await.unwrap();
        let session = mgr.get_session(&session_id).await.unwrap();
        assert_eq!(session.source, "slack");
    }

    // CR.7 — list_channel_sessions returns all active mappings
    #[cfg(feature = "ai")]
    #[tokio::test]
    async fn list_all_sessions() {
        let dir = tempfile::TempDir::new().unwrap();
        let path = dir.path().join("test.db");
        let pool = crate::db::init_pool(&path).unwrap();
        crate::db::with_db(&pool, |conn| crate::db::run_migrations(conn))
            .await
            .unwrap();
        let mgr = Arc::new(crate::ai::session::SessionManager::new(pool));
        let map = ChannelSessionMap::new(mgr);

        map.resolve_session("telegram:111", "telegram")
            .await
            .unwrap();
        map.resolve_session("slack:C222", "slack").await.unwrap();

        let sessions = map.list_channel_sessions();
        assert_eq!(sessions.len(), 2);
    }
}
