//! [`InMemoryConversationRegistry`] — the [`ConversationRegistry`] adapter (lot C2).
//!
//! The driven side of conversation-by-pair: a `HashMap<ConversationId, Conversation>`
//! plus a pair→id index, so `resolve` is **lazy get-or-create** and the same
//! unordered pair `{a, b}` always maps to the same [`ConversationId`].
//!
//! ## Concurrency
//!
//! A single **synchronous** [`Mutex`] guards both maps; it is held only for the O(1)
//! lookups/mutations and **never across an `.await`** (this registry is fully sync,
//! cf. the `ask_locks` discipline of `service.rs`).

use std::collections::HashMap;
use std::sync::Mutex;

use domain::conversation::{
    Conversation, ConversationId, ConversationParty, ConversationRegistry, ConversationSession,
    SessionRef,
};

/// Order-insensitive key for a conversation pair `{a, b}`.
///
/// Normalised so that `{a, b}` and `{b, a}` hash/compare equal — this is what makes
/// `resolve(a, b)` and `resolve(b, a)` resolve to the same conversation.
fn pair_key(a: ConversationParty, b: ConversationParty) -> (ConversationParty, ConversationParty) {
    // Stable, total ordering over parties so the smaller end is always `left` in the
    // key. `User` sorts before any agent; agents order by their UUID.
    fn rank(p: ConversationParty) -> (u8, u128) {
        match p {
            ConversationParty::User => (0, 0),
            ConversationParty::Agent { agent_id } => (1, agent_id.as_uuid().as_u128()),
        }
    }
    if rank(a) <= rank(b) {
        (a, b)
    } else {
        (b, a)
    }
}

/// In-memory, pair-keyed conversation registry (the production [`ConversationRegistry`]).
#[derive(Default)]
pub struct InMemoryConversationRegistry {
    inner: Mutex<Inner>,
}

#[derive(Default)]
struct Inner {
    by_id: HashMap<ConversationId, Conversation>,
    by_pair: HashMap<(ConversationParty, ConversationParty), ConversationId>,
}

impl InMemoryConversationRegistry {
    /// Creates an empty registry.
    #[must_use]
    pub fn new() -> Self {
        Self {
            inner: Mutex::new(Inner::default()),
        }
    }

    /// Number of conversations currently held (test/inspection helper).
    #[must_use]
    pub fn len(&self) -> usize {
        self.lock().by_id.len()
    }

    /// Whether the registry is empty.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.lock().by_id.is_empty()
    }

    fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
        self.inner
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }
}

impl ConversationRegistry for InMemoryConversationRegistry {
    fn resolve(&self, a: ConversationParty, b: ConversationParty) -> Conversation {
        let key = pair_key(a, b);
        let mut inner = self.lock();
        if let Some(id) = inner.by_pair.get(&key).copied() {
            // Existing thread for this pair — return its current snapshot.
            return inner
                .by_id
                .get(&id)
                .cloned()
                .expect("by_pair id always present in by_id");
        }
        // Lazy create: mint a Dormant conversation for the pair. The id is **derived
        // deterministically** from the pair (`ConversationId::for_pair`, ARCHITECTURE
        // §19.7) so the **same pair always yields the same id across registry instances**
        // (i.e. it survives an IDE restart with an empty `by_pair`/`by_id`). This keeps
        // the persistence key stable and aligned with `LaunchAgent` (P8a) / the
        // `resolve_conversation` fallback. The pair is valid by construction at call
        // sites; `try_new` still guards the invariants.
        let id = ConversationId::for_pair(key.0, key.1);
        let conv = Conversation::try_new(id, key.0, key.1)
            .expect("pair_key yields a valid distinct/≤1-user pair");
        inner.by_pair.insert(key, id);
        inner.by_id.insert(id, conv.clone());
        conv
    }

    fn bind_session(&self, id: ConversationId, session: SessionRef) {
        let mut inner = self.lock();
        if let Some(conv) = inner.by_id.get_mut(&id) {
            conv.session = ConversationSession::Live {
                handle_ref: session,
            };
        }
    }

    fn suspend(&self, id: ConversationId, resumable_id: Option<String>) {
        let mut inner = self.lock();
        if let Some(conv) = inner.by_id.get_mut(&id) {
            conv.session = ConversationSession::Dormant;
            conv.resumable_id = resumable_id;
        }
    }

    fn get(&self, id: ConversationId) -> Option<Conversation> {
        self.lock().by_id.get(&id).cloned()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use domain::ids::{AgentId, SessionId};

    fn agent(n: u128) -> ConversationParty {
        ConversationParty::agent(AgentId::from_uuid(uuid::Uuid::from_u128(n)))
    }

    #[test]
    fn resolve_is_lazy_get_or_create() {
        let reg = InMemoryConversationRegistry::new();
        assert!(reg.is_empty());
        let c = reg.resolve(ConversationParty::User, agent(1));
        assert_eq!(reg.len(), 1);
        // Same pair ⇒ same id, no new conversation created.
        let c2 = reg.resolve(ConversationParty::User, agent(1));
        assert_eq!(c.id, c2.id);
        assert_eq!(reg.len(), 1);
    }

    #[test]
    fn same_pair_unordered_yields_same_id() {
        let reg = InMemoryConversationRegistry::new();
        let c1 = reg.resolve(agent(1), agent(2));
        let c2 = reg.resolve(agent(2), agent(1)); // swapped order
        assert_eq!(c1.id, c2.id, "unordered pair identity");
        assert_eq!(reg.len(), 1);
    }

    #[test]
    fn distinct_pairs_get_distinct_ids() {
        let reg = InMemoryConversationRegistry::new();
        let user_b = reg.resolve(ConversationParty::User, agent(2));
        let a_b = reg.resolve(agent(1), agent(2));
        assert_ne!(user_b.id, a_b.id, "User↔B and A↔B are different threads");
        assert_eq!(reg.len(), 2);
    }

    #[test]
    fn fresh_resolve_is_dormant() {
        let reg = InMemoryConversationRegistry::new();
        let c = reg.resolve(ConversationParty::User, agent(1));
        assert_eq!(c.session, ConversationSession::Dormant);
    }

    #[test]
    fn bind_session_makes_it_live_then_suspend_restores_dormant() {
        let reg = InMemoryConversationRegistry::new();
        let c = reg.resolve(ConversationParty::User, agent(1));
        let sref = SessionRef::new(SessionId::from_uuid(uuid::Uuid::from_u128(99)));
        reg.bind_session(c.id, sref);
        let live = reg.get(c.id).unwrap();
        assert!(live.session.is_live());
        assert_eq!(live.session, ConversationSession::Live { handle_ref: sref });

        reg.suspend(c.id, Some("sess-abc".to_owned()));
        let dormant = reg.get(c.id).unwrap();
        assert_eq!(dormant.session, ConversationSession::Dormant);
        assert_eq!(dormant.resumable_id.as_deref(), Some("sess-abc"));
    }

    #[test]
    fn get_unknown_is_none() {
        let reg = InMemoryConversationRegistry::new();
        assert!(reg.get(ConversationId::new_random()).is_none());
    }

    // ---------------------------------------------------------------------------
    // Bloc 1 — déterminisme du registre (§19 : la clé de persistance = id de paire,
    // déterministe et **stable au redémarrage**). Scelle l'invariant que Main a
    // rattrapé : deux instances neuves du registre (= deux démarrages de l'IDE)
    // doivent dériver **le même** id pour la même paire, et cet id doit être
    // **exactement** `ConversationId::for_pair`, sinon la clé sous laquelle le
    // handoff/log est rangé dériverait au redémarrage.
    // ---------------------------------------------------------------------------

    #[test]
    fn resolve_is_stable_across_registry_restart() {
        // Deux instances neuves (by_pair/by_id vides) simulent deux démarrages de
        // l'IDE : la même paire User↔Agent doit produire le **même** id.
        let a = agent(7);
        let first = InMemoryConversationRegistry::new()
            .resolve(ConversationParty::User, a)
            .id;
        let second = InMemoryConversationRegistry::new()
            .resolve(ConversationParty::User, a)
            .id;
        assert_eq!(
            first, second,
            "id de paire stable au redémarrage (registre neuf ⇒ même id)"
        );
    }

    #[test]
    fn resolve_is_stable_across_registry_restart_agent_agent() {
        // Même garantie pour une paire Agent↔Agent (dérivation XOR commutative).
        let x = agent(11);
        let y = agent(13);
        let first = InMemoryConversationRegistry::new().resolve(x, y).id;
        let second = InMemoryConversationRegistry::new().resolve(y, x).id;
        assert_eq!(
            first, second,
            "id de paire Agent↔Agent stable au redémarrage, insensible à l'ordre"
        );
    }

    #[test]
    fn resolve_id_equals_for_pair_user_agent() {
        // Alignement de clé : l'id que le registre matérialise == le repli pur
        // `for_pair` == `from_uuid(agent)` (la clé que P8a/`resolve_conversation`
        // dérivent pour la paire canonique User↔Agent).
        let agent_id = AgentId::from_uuid(uuid::Uuid::from_u128(42));
        let party = ConversationParty::agent(agent_id);
        let resolved = InMemoryConversationRegistry::new()
            .resolve(ConversationParty::User, party)
            .id;
        assert_eq!(
            resolved,
            ConversationId::for_pair(ConversationParty::User, party),
            "resolve == for_pair (alignement de clé)"
        );
        assert_eq!(
            resolved,
            ConversationId::from_uuid(agent_id.as_uuid()),
            "User↔Agent ⇒ id == uuid de l'agent (repli resolve_conversation)"
        );
    }

    #[test]
    fn resolve_id_equals_for_pair_agent_agent_commutative() {
        // Commutativité et alignement sur `for_pair` pour Agent↔Agent.
        let x = agent(101);
        let y = agent(202);
        let reg = InMemoryConversationRegistry::new();
        let id_xy = reg.resolve(x, y).id;
        let id_yx = reg.resolve(y, x).id;
        assert_eq!(id_xy, id_yx, "resolve(a,b) == resolve(b,a)");
        assert_eq!(
            id_xy,
            ConversationId::for_pair(x, y),
            "resolve == for_pair (Agent↔Agent)"
        );
        assert_eq!(reg.len(), 1, "une seule conversation pour la paire {{a,b}}");
    }

    #[test]
    fn distinct_pairs_yield_distinct_ids_across_kinds() {
        // Deux paires distinctes ⇒ deux ids distincts (pas de collision de clé).
        let reg = InMemoryConversationRegistry::new();
        let user_a = reg.resolve(ConversationParty::User, agent(1)).id;
        let user_b = reg.resolve(ConversationParty::User, agent(2)).id;
        let a_b = reg.resolve(agent(1), agent(2)).id;
        assert_ne!(user_a, user_b, "User↔A ≠ User↔B");
        assert_ne!(user_a, a_b, "User↔A ≠ A↔B");
        assert_ne!(user_b, a_b, "User↔B ≠ A↔B");
    }
}
