//! Conversation-by-pair domain model (cadrage « conversation par paire », lot C1/C2).
//!
//! A [`Conversation`] is a **thread between two distinct parties** with its own
//! I/O session. Its identity is the **unordered pair** `{left, right}`: the same
//! two parties always denote the same conversation (this is the key to the
//! *lazy materialisation* the [`ConversationRegistry`] performs — `resolve(a, b)`
//! and `resolve(b, a)` yield the same [`ConversationId`]).
//!
//! This module is **pure** (ARCHITECTURE dependency rule): no `tokio`, no
//! `std::fs`, no `std::process`. It owns the value objects and entities plus the
//! [`ConversationRegistry`] port (a trait the application depends on; the infra
//! provides `InMemoryConversationRegistry`). The session handle is held only as a
//! [`SessionRef`] (a reference to a [`crate::terminal::TerminalSession`]); the
//! domain never holds the PTY itself.
//!
//! ## Why this exists
//!
//! The previous coupling «1 live session / agent» was ambiguous once an agent can
//! take part in several threads (`session-registry-agent-ambiguity`). Here the live
//! session is keyed **by conversation**, so a delegation `A↔B` never borrows the
//! `User↔B` thread — the context leak is closed by construction.

use serde::{Deserialize, Serialize};

use crate::ids::{AgentId, SessionId};

/// Identifies one [`Conversation`].
///
/// Newtype around [`uuid::Uuid`], calqué sur [`crate::ids::AgentId`] /
/// [`crate::mailbox::TicketId`]. Minted by the [`ConversationRegistry`] when a pair
/// is first resolved; stable for the lifetime of that pair's thread.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ConversationId(pub uuid::Uuid);

impl ConversationId {
    /// Wraps an existing [`uuid::Uuid`].
    #[must_use]
    pub const fn from_uuid(id: uuid::Uuid) -> Self {
        Self(id)
    }

    /// Mints a fresh random conversation id.
    #[must_use]
    pub fn new_random() -> Self {
        Self(uuid::Uuid::new_v4())
    }

    /// Returns the inner [`uuid::Uuid`].
    #[must_use]
    pub const fn as_uuid(&self) -> uuid::Uuid {
        self.0
    }

    /// **Dérivation déterministe et model-agnostic** de l'id de paire pour le couple
    /// `{a, b}` (ARCHITECTURE §19.7). **Pure** (aucune I/O, aucun registre) et
    /// **insensible à l'ordre** : `for_pair(a, b) == for_pair(b, a)`.
    ///
    /// Sert de **repli stable** quand aucun [`ConversationRegistry`] (stateful) n'est
    /// disponible pour matérialiser le fil — typiquement le lancement **direct** d'une
    /// cellule structurée neuve par l'utilisateur (`LaunchAgent`, lot P8a) : la cellule
    /// doit porter la **même** clé que celle sous laquelle le handoff a été (ou sera)
    /// rangé. Pour la paire canonique `User↔Agent`, le résultat est **exactement**
    /// l'id du repli de `OrchestratorService::resolve_conversation(None, agent)`
    /// (`ConversationId::from_uuid(agent.as_uuid())`), garantissant la cohérence de
    /// clé end-to-end sans coupler `LaunchAgent` à l'orchestrateur ni au registre.
    ///
    /// Pour une paire `Agent↔Agent`, l'id est dérivé des deux UUID agent de façon
    /// commutative (XOR), stable et déterministe.
    #[must_use]
    pub fn for_pair(a: ConversationParty, b: ConversationParty) -> Self {
        match (a.as_agent(), b.as_agent()) {
            // User↔Agent (un seul agent) : aligné sur le repli de `resolve_conversation`.
            (Some(agent), None) | (None, Some(agent)) => Self::from_uuid(agent.as_uuid()),
            // Agent↔Agent : combinaison commutative des deux ids (insensible à l'ordre).
            (Some(x), Some(y)) => {
                let xor = x.as_uuid().as_u128() ^ y.as_uuid().as_u128();
                Self::from_uuid(uuid::Uuid::from_u128(xor))
            }
            // User↔User n'existe pas (invariant `Conversation`) : repli sûr non-panic.
            (None, None) => Self::from_uuid(uuid::Uuid::nil()),
        }
    }
}

impl std::fmt::Display for ConversationId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// One end of a [`Conversation`].
///
/// Invariant (enforced by [`Conversation::try_new`]): a conversation relates **two
/// distinct** parties and carries **at most one** [`ConversationParty::User`]
/// (never `User↔User`, never `Agent(x)↔Agent(x)`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum ConversationParty {
    /// The human operator — a single logical instance on IdeA's side.
    User,
    /// A project agent.
    #[serde(rename_all = "camelCase")]
    Agent {
        /// The agent on this end.
        agent_id: AgentId,
    },
}

impl ConversationParty {
    /// Convenience constructor for an agent party.
    #[must_use]
    pub const fn agent(agent_id: AgentId) -> Self {
        Self::Agent { agent_id }
    }

    /// Returns the [`AgentId`] when this party is an agent.
    #[must_use]
    pub const fn as_agent(&self) -> Option<AgentId> {
        match self {
            Self::Agent { agent_id } => Some(*agent_id),
            Self::User => None,
        }
    }

    /// Whether this party is the human user.
    #[must_use]
    pub const fn is_user(&self) -> bool {
        matches!(self, Self::User)
    }
}

/// Errors raised when constructing a [`Conversation`].
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ConversationError {
    /// The two ends are the **same** party (`left == right`).
    #[error("a conversation must relate two distinct parties")]
    SameParty,
    /// **Both** ends are [`ConversationParty::User`] — at most one is allowed.
    #[error("a conversation may carry at most one User party")]
    TwoUsers,
}

/// The I/O state of a conversation thread.
///
/// The domain holds only a [`SessionRef`]; the live PTY/structured stream is an
/// infrastructure concern (`session-registry`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "state")]
pub enum ConversationSession {
    /// Never launched, or suspended (only `resumable_id` is retained on the
    /// [`Conversation`]).
    Dormant,
    /// A live I/O stream backs this thread.
    #[serde(rename_all = "camelCase")]
    Live {
        /// Reference to the backing [`crate::terminal::TerminalSession`].
        handle_ref: SessionRef,
    },
}

impl ConversationSession {
    /// Whether the thread currently has a live session.
    #[must_use]
    pub const fn is_live(&self) -> bool {
        matches!(self, Self::Live { .. })
    }
}

/// An opaque reference to a live session handle (a [`crate::terminal::TerminalSession`]).
///
/// The domain never owns the PTY; it only names the session by its [`SessionId`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SessionRef(pub SessionId);

impl SessionRef {
    /// Wraps a [`SessionId`].
    #[must_use]
    pub const fn new(id: SessionId) -> Self {
        Self(id)
    }

    /// Returns the referenced [`SessionId`].
    #[must_use]
    pub const fn session_id(&self) -> SessionId {
        self.0
    }
}

/// A thread between two distinct parties, with its own session and resumable id.
///
/// Identity is the **unordered pair** `{left, right}`: see [`Conversation::same_pair`].
/// Pure value/entity — no I/O.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Conversation {
    /// Stable identifier (minted by the registry on first resolve).
    pub id: ConversationId,
    /// One end of the thread.
    pub left: ConversationParty,
    /// The other end of the thread.
    pub right: ConversationParty,
    /// Current I/O state.
    pub session: ConversationSession,
    /// CLI session-id to resume on restart (`suspend` stores it; `Dormant` keeps it).
    pub resumable_id: Option<String>,
}

impl Conversation {
    /// Builds a `Dormant` conversation for the pair `{left, right}`.
    ///
    /// # Errors
    /// - [`ConversationError::SameParty`] if `left == right`.
    /// - [`ConversationError::TwoUsers`] if both ends are [`ConversationParty::User`].
    pub fn try_new(
        id: ConversationId,
        left: ConversationParty,
        right: ConversationParty,
    ) -> Result<Self, ConversationError> {
        if left.is_user() && right.is_user() {
            // More specific than SameParty: "at most one User" invariant.
            return Err(ConversationError::TwoUsers);
        }
        if left == right {
            return Err(ConversationError::SameParty);
        }
        Ok(Self {
            id,
            left,
            right,
            session: ConversationSession::Dormant,
            resumable_id: None,
        })
    }

    /// Whether this conversation relates the **same unordered pair** as `{a, b}`.
    ///
    /// `{left, right}` is order-insensitive: `same_pair(a, b) == same_pair(b, a)`.
    #[must_use]
    pub fn same_pair(&self, a: ConversationParty, b: ConversationParty) -> bool {
        (self.left == a && self.right == b) || (self.left == b && self.right == a)
    }
}

/// A pure wait-for graph for deadlock (cycle) detection on inter-agent delegation.
///
/// Each edge `(A, B)` reads «A is waiting for B». A delegation `from → to` would
/// deadlock iff `to` already (transitively) waits on `from`; [`WaitForGraph::would_cycle`]
/// answers that **without** mutating the graph, so the caller can refuse the ask
/// **before** posting the edge. 100 % pure and testable.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct WaitForGraph {
    edges: Vec<(AgentId, AgentId)>,
}

impl WaitForGraph {
    /// An empty graph.
    #[must_use]
    pub fn new() -> Self {
        Self { edges: Vec::new() }
    }

    /// Records that `from` waits for `to` (no-op if the edge already exists).
    pub fn add_edge(&mut self, from: AgentId, to: AgentId) {
        if !self.edges.contains(&(from, to)) {
            self.edges.push((from, to));
        }
    }

    /// Removes the edge `from → to` (the wait resolved or timed out).
    pub fn remove_edge(&mut self, from: AgentId, to: AgentId) {
        self.edges.retain(|e| *e != (from, to));
    }

    /// Number of recorded edges (test/inspection helper).
    #[must_use]
    pub fn len(&self) -> usize {
        self.edges.len()
    }

    /// Whether the graph holds no edges.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.edges.is_empty()
    }

    /// Whether adding `from → to` would close a cycle in the wait-for graph.
    ///
    /// `true` when `to` already reaches `from` through existing edges (so `from`
    /// waiting on `to` would deadlock), or when `from == to` (self-wait). The graph
    /// is **not** modified.
    #[must_use]
    pub fn would_cycle(&self, from: AgentId, to: AgentId) -> bool {
        if from == to {
            return true;
        }
        // Does `to` already reach `from`? DFS over existing edges from `to`.
        let mut stack = vec![to];
        let mut seen = vec![to];
        while let Some(node) = stack.pop() {
            if node == from {
                return true;
            }
            for (a, b) in &self.edges {
                if *a == node && !seen.contains(b) {
                    seen.push(*b);
                    stack.push(*b);
                }
            }
        }
        false
    }
}

/// Lazy, get-or-create registry of conversations keyed by **unordered pair**.
///
/// Pure port (no I/O); the infra adapter `InMemoryConversationRegistry` owns the
/// interior mutability (`HashMap` + sync mutex, never held across an `.await`).
/// Kept object-safe so the application holds it as `Arc<dyn ConversationRegistry>`.
pub trait ConversationRegistry: Send + Sync {
    /// Get-or-create: returns the thread for the pair `{a, b}`, opening it
    /// (`Dormant`) if it did not exist. Pure registry — opens **no** session.
    /// The same unordered pair always yields the same [`ConversationId`].
    fn resolve(&self, a: ConversationParty, b: ConversationParty) -> Conversation;

    /// Marks the conversation `id` `Live` with the given session reference.
    fn bind_session(&self, id: ConversationId, session: SessionRef);

    /// Suspends `id` (back to `Dormant`), retaining `resumable_id` for restart.
    fn suspend(&self, id: ConversationId, resumable_id: Option<String>);

    /// Returns the current snapshot of `id`, if it exists.
    fn get(&self, id: ConversationId) -> Option<Conversation>;
}

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

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

    fn conv_id(n: u128) -> ConversationId {
        ConversationId::from_uuid(uuid::Uuid::from_u128(n))
    }

    #[test]
    fn conversation_id_roundtrips_through_uuid() {
        let u = uuid::Uuid::from_u128(42);
        let id = ConversationId::from_uuid(u);
        assert_eq!(id.as_uuid(), u);
        assert_eq!(id.to_string(), u.to_string());
    }

    #[test]
    fn rejects_same_party_pair() {
        let a = ConversationParty::agent(agent(1));
        assert_eq!(
            Conversation::try_new(conv_id(1), a, a),
            Err(ConversationError::SameParty)
        );
    }

    #[test]
    fn rejects_two_users() {
        assert_eq!(
            Conversation::try_new(conv_id(1), ConversationParty::User, ConversationParty::User),
            Err(ConversationError::TwoUsers)
        );
    }

    #[test]
    fn accepts_user_agent_and_agent_agent() {
        assert!(Conversation::try_new(
            conv_id(1),
            ConversationParty::User,
            ConversationParty::agent(agent(1))
        )
        .is_ok());
        assert!(Conversation::try_new(
            conv_id(2),
            ConversationParty::agent(agent(1)),
            ConversationParty::agent(agent(2))
        )
        .is_ok());
    }

    #[test]
    fn fresh_conversation_is_dormant_without_resumable() {
        let c = Conversation::try_new(
            conv_id(1),
            ConversationParty::User,
            ConversationParty::agent(agent(1)),
        )
        .unwrap();
        assert_eq!(c.session, ConversationSession::Dormant);
        assert!(!c.session.is_live());
        assert_eq!(c.resumable_id, None);
    }

    #[test]
    fn identity_is_the_unordered_pair() {
        let a = ConversationParty::agent(agent(1));
        let b = ConversationParty::agent(agent(2));
        let c = Conversation::try_new(conv_id(1), a, b).unwrap();
        assert!(c.same_pair(a, b));
        assert!(c.same_pair(b, a), "pair identity is order-insensitive");
        let other = ConversationParty::agent(agent(3));
        assert!(!c.same_pair(a, other));
    }

    #[test]
    fn would_cycle_refuses_direct_back_edge() {
        // A→B exists; B→A would close the cycle.
        let mut g = WaitForGraph::new();
        g.add_edge(agent(1), agent(2)); // A waits B
        assert!(g.would_cycle(agent(2), agent(1)), "B→A closes A→B→A");
    }

    #[test]
    fn would_cycle_allows_linear_chain() {
        // A→B exists; B→C is fine (no path C→…→B).
        let mut g = WaitForGraph::new();
        g.add_edge(agent(1), agent(2)); // A waits B
        assert!(!g.would_cycle(agent(2), agent(3)), "A→B→C is acyclic");
    }

    #[test]
    fn would_cycle_detects_transitive_cycle() {
        // A→B, B→C; C→A would close A→B→C→A.
        let mut g = WaitForGraph::new();
        g.add_edge(agent(1), agent(2));
        g.add_edge(agent(2), agent(3));
        assert!(g.would_cycle(agent(3), agent(1)), "transitive cycle");
    }

    #[test]
    fn would_cycle_refuses_self_wait() {
        let g = WaitForGraph::new();
        assert!(g.would_cycle(agent(1), agent(1)));
    }

    #[test]
    fn add_edge_is_idempotent_and_removable() {
        let mut g = WaitForGraph::new();
        g.add_edge(agent(1), agent(2));
        g.add_edge(agent(1), agent(2));
        assert_eq!(g.len(), 1);
        g.remove_edge(agent(1), agent(2));
        assert!(g.is_empty());
    }
}
