use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{Mutex, mpsc};

use crate::bus::{CommittedTurnDelta, ControlMessage, InboundMessage, MessageBus, OutboundMessage};
use crate::gateway::uploads::UploadRegistry;
use crate::protocol::{
    HistoryMessage, MessageAttachment, SlashCommandInfo, WsInbound, WsOutbound, parse_inbound,
};
use crate::session::TurnSnapshot;
use crate::session::{SessionCommand, SessionEvent, UnifiedSessionId};

use super::base::{Channel, ChannelError, LivePolicy, TurnSink, TurnTarget};

// ============================================================================
// Client - Connected CLI client
// ============================================================================

pub(crate) struct Client {
    sender: mpsc::Sender<WsOutbound>,
    chat_id: String,
    current_session_id: Mutex<Option<String>>,
}

impl Client {
    pub(crate) fn chat_id(&self) -> &str {
        &self.chat_id
    }
}

// ============================================================================
// CliChatChannel - Channel implementation for CLI chat
// ============================================================================

pub struct CliChatChannel {
    bus: std::sync::Mutex<Option<Arc<MessageBus>>>,
    clients: Arc<Mutex<HashMap<String, Arc<Client>>>>,
    uploads: UploadRegistry,
}

impl Default for CliChatChannel {
    fn default() -> Self {
        Self::new()
    }
}

impl CliChatChannel {
    pub fn new() -> Self {
        Self::with_upload_registry(UploadRegistry::disabled())
    }

    pub fn with_upload_registry(uploads: UploadRegistry) -> Self {
        Self {
            bus: std::sync::Mutex::new(None),
            clients: Arc::new(Mutex::new(HashMap::new())),
            uploads,
        }
    }

    /// Register a new client connection, returns (session_id, client)
    pub(crate) async fn register_client(
        &self,
        sender: mpsc::Sender<WsOutbound>,
        requested_chat_id: Option<String>,
    ) -> (String, Arc<Client>) {
        // Each WebSocket connection gets a stable chat scope. All user input and
        // dialog controls for this client stay inside that scope unless the
        // protocol explicitly carries a full session id.
        let chat_id = requested_chat_id.unwrap_or_else(crate::util::short_id);

        let client = Arc::new(Client {
            sender,
            chat_id: chat_id.clone(),
            current_session_id: Mutex::new(None),
        });
        self.clients
            .lock()
            .await
            .insert(chat_id.clone(), client.clone());

        // Resume the current/most-recent dialog for a stable TUI identity. Only
        // create a dialog when this client has never connected before.
        let session_id = match self.resume_session_via_control(&chat_id).await {
            Ok(id) => id,
            Err(e) => {
                tracing::error!(error = %e, "Failed to resume initial session");
                UnifiedSessionId::new("cli_chat", &chat_id, crate::util::short_id()).to_string()
            }
        };

        // Set current session id in client
        {
            let mut current = client.current_session_id.lock().await;
            *current = Some(session_id.clone());
        }

        (session_id, client)
    }

    pub(crate) async fn unregister_client(&self, client: &Arc<Client>) {
        let mut clients = self.clients.lock().await;
        if clients
            .get(client.chat_id())
            .is_some_and(|registered| Arc::ptr_eq(registered, client))
        {
            clients.remove(client.chat_id());
        }
    }

    /// Push a structured task-plan update to the WebSocket client that owns
    /// the session. Other channels remain unaffected.
    pub async fn publish_plan_changed(&self, event: crate::work::PlanChanged) {
        let Some(session_id) = UnifiedSessionId::parse(&event.session_id) else {
            return;
        };
        if session_id.channel != "cli_chat" {
            return;
        }
        let client = self.clients.lock().await.get(&session_id.chat_id).cloned();
        if let Some(client) = client {
            let _ = client
                .sender
                .send(WsOutbound::PlanUpdated {
                    session_id: event.session_id,
                    reason: event.reason,
                    changed_item_ids: event.changed_item_ids,
                    plan: event.plan,
                })
                .await;
        }
    }

    /// Push a durable Agent run/event projection update to the owning
    /// WebSocket client.  Clients recalibrate with `GetAgentRuns` when a
    /// broadcast is lagged or lost.
    pub async fn publish_agent_projection(
        &self,
        projection: crate::agent::projection::AgentProjection,
    ) {
        let Some(session_id) = UnifiedSessionId::parse(&projection.session_id) else {
            return;
        };
        if session_id.channel != "cli_chat" {
            return;
        }
        let client = self.clients.lock().await.get(&session_id.chat_id).cloned();
        if let Some(client) = client {
            let message = if let Some(run) = projection.run {
                WsOutbound::AgentRunUpdated {
                    session_id: projection.session_id,
                    revision: projection.revision,
                    run,
                }
            } else if let Some(event) = projection.event {
                WsOutbound::AgentEventUpdated {
                    session_id: projection.session_id,
                    revision: projection.revision,
                    event,
                }
            } else {
                return;
            };
            let _ = client.sender.send(message).await;
        }
    }

    /// Handle an inbound message from a client
    pub(crate) async fn handle_inbound(&self, client: Arc<Client>, raw_msg: &str) {
        match parse_inbound(raw_msg) {
            Ok(inbound) => match self.handle_ws_inbound(client.clone(), inbound).await {
                Ok(()) => {}
                Err(e) => {
                    tracing::warn!(error = %e, "Failed to handle inbound message");
                    let _ = client
                        .sender
                        .send(WsOutbound::Error {
                            code: "INTERNAL_ERROR".to_string(),
                            message: e.to_string(),
                        })
                        .await;
                }
            },
            Err(e) => {
                tracing::warn!(error = %e, "Failed to parse inbound message");
                let _ = client
                    .sender
                    .send(WsOutbound::Error {
                        code: "PARSE_ERROR".to_string(),
                        message: e.to_string(),
                    })
                    .await;
            }
        }
    }

    async fn handle_ws_inbound(
        &self,
        client: Arc<Client>,
        inbound: WsInbound,
    ) -> Result<(), ChannelError> {
        let bus = {
            let guard = self.bus.lock().unwrap();
            guard
                .clone()
                .ok_or_else(|| ChannelError::Other("Channel not started".to_string()))?
        };

        let mut current_session_guard = client.current_session_id.lock().await;

        match inbound {
            WsInbound::UserInput {
                content,
                upload_ids,
                client_message_id,
                chat_id,
                ..
            } => {
                // All messages (including slash commands) go through the normal inbound flow
                // SessionManager handles session creation/reuse internally
                if content.trim().is_empty() && upload_ids.is_empty() {
                    return Err(ChannelError::Other("Message is empty".to_string()));
                }
                // `/queue` is deliberately allowed to carry attachments: it
                // is a message-routing directive whose payload remains a
                // normal user input. Other slash commands still reject
                // attachments because their handlers do not consume media.
                let slash_allows_attachments = crate::channels::parse_slash_command(&content)
                    .is_some_and(|(name, _)| name.eq_ignore_ascii_case("queue"));
                if !upload_ids.is_empty()
                    && crate::channels::parse_slash_command(&content).is_some()
                    && !slash_allows_attachments
                {
                    return Err(ChannelError::Other(
                        "Attachments cannot be sent with slash commands".to_string(),
                    ));
                }
                let target_chat_id = chat_id.unwrap_or_else(|| client.chat_id.clone());
                if target_chat_id != client.chat_id {
                    return Err(ChannelError::Other(
                        "Chat does not belong to this client".to_string(),
                    ));
                }
                let client_message_id =
                    client_message_id.and_then(|raw| match uuid::Uuid::parse_str(&raw) {
                        Ok(id) => Some(id.to_string()),
                        Err(error) => {
                            tracing::warn!(
                                client_message_id = %raw,
                                error = %error,
                                "Ignoring invalid client message id"
                            );
                            None
                        }
                    });
                let uploads = self
                    .uploads
                    .take_many(&client.chat_id, &upload_ids)
                    .await
                    .map_err(|error| ChannelError::Other(error.to_string()))?;
                let media = uploads.iter().map(UploadRegistry::as_media).collect();
                let msg = InboundMessage {
                    channel: self.name().to_string(),
                    sender_id: "cli".to_string(),
                    chat_id: target_chat_id,
                    client_message_id,
                    content,
                    received_at: crate::bus::message::current_timestamp(),
                    media,
                    channel_context: Default::default(),
                };
                if let Err(error) = bus.publish_inbound(msg).await {
                    self.uploads.restore(uploads).await;
                    return Err(error.into());
                }
            }
            WsInbound::ClearHistory {
                chat_id,
                session_id,
            } => {
                let (reply_tx, mut reply_rx) = mpsc::channel(1);
                let session_id = if let Some(session_id) = session_id {
                    Self::parse_client_session(&client, &session_id)?
                } else if let Some(chat_id) = chat_id {
                    if chat_id != client.chat_id {
                        return Err(ChannelError::Other(
                            "Chat does not belong to this client".to_string(),
                        ));
                    }
                    let (current_tx, mut current_rx) = mpsc::channel(1);
                    bus.publish_control(ControlMessage {
                        op: SessionCommand::GetCurrentDialog {
                            channel: "cli_chat".to_string(),
                            chat_id,
                        },
                        reply_tx: current_tx,
                    })
                    .await?;
                    match current_rx.recv().await {
                        Some(Ok(SessionEvent::CurrentDialog {
                            session_id: Some(session_id),
                        })) => session_id,
                        Some(Ok(SessionEvent::CurrentDialog { session_id: None })) => {
                            return Err(ChannelError::Other("No active session".to_string()));
                        }
                        Some(Ok(_)) => {
                            return Err(ChannelError::Other(
                                "Unexpected response type".to_string(),
                            ));
                        }
                        Some(Err(e)) => return Err(e),
                        None => {
                            return Err(ChannelError::Other("Control channel closed".to_string()));
                        }
                    }
                } else {
                    let target = current_session_guard
                        .clone()
                        .ok_or_else(|| ChannelError::Other("No active session".to_string()))?;
                    Self::parse_client_session(&client, &target)?
                };
                let target = session_id.to_string();
                bus.publish_control(ControlMessage {
                    op: SessionCommand::ClearHistory { session_id },
                    reply_tx,
                })
                .await?;

                match reply_rx.recv().await {
                    Some(Ok(SessionEvent::HistoryCleared { .. })) => {
                        let _ = client
                            .sender
                            .send(WsOutbound::HistoryCleared { session_id: target })
                            .await;
                    }
                    Some(Ok(_)) => {
                        // Unexpected response type, ignore
                    }
                    Some(Err(e)) => {
                        return Err(e);
                    }
                    None => {
                        return Err(ChannelError::Other("Control channel closed".to_string()));
                    }
                }
            }
            WsInbound::CreateSession { title } => {
                let (new_id, created_title) = self
                    .create_session_via_control(&client.chat_id, title.as_deref())
                    .await?;
                *current_session_guard = Some(new_id.clone());
                let _ = client
                    .sender
                    .send(WsOutbound::SessionCreated {
                        session_id: new_id,
                        title: created_title,
                    })
                    .await;
            }
            WsInbound::ListSessions { include_archived } => {
                // List dialogs for the current chat
                let chat_id = client.chat_id.clone();
                let (reply_tx, mut reply_rx) = mpsc::channel(1);
                bus.publish_control(ControlMessage {
                    op: SessionCommand::ListDialogs {
                        channel: "cli_chat".to_string(),
                        chat_id,
                        include_archived,
                    },
                    reply_tx,
                })
                .await?;

                match reply_rx.recv().await {
                    Some(Ok(SessionEvent::DialogList {
                        dialogs,
                        current_dialog_id,
                    })) => {
                        // Convert DialogInfo to SessionSummary for backward compatibility
                        let sessions: Vec<crate::protocol::SessionSummary> = dialogs
                            .into_iter()
                            .map(|d| crate::protocol::SessionSummary {
                                session_id: d.session_id.to_string(),
                                title: d.title,
                                channel_name: d.session_id.channel.clone(),
                                chat_id: d.session_id.chat_id,
                                message_count: d.message_count,
                                last_active_at: d.last_active_at,
                                archived_at: d.archived_at,
                            })
                            .collect();
                        let current_session_id = current_dialog_id.map(|did| {
                            UnifiedSessionId::new("cli_chat", &client.chat_id, &did).to_string()
                        });
                        if let Some(ref session_id) = current_session_id {
                            *current_session_guard = Some(session_id.clone());
                        }
                        let _ = client
                            .sender
                            .send(WsOutbound::SessionList {
                                sessions,
                                current_session_id,
                            })
                            .await;
                    }
                    Some(Ok(_)) => {
                        // Unexpected response type
                    }
                    Some(Err(e)) => {
                        return Err(e);
                    }
                    None => {
                        return Err(ChannelError::Other("Control channel closed".to_string()));
                    }
                }
            }
            WsInbound::LoadSession { session_id } => {
                let (reply_tx, mut reply_rx) = mpsc::channel(1);
                let unified_id = UnifiedSessionId::parse(&session_id)
                    .ok_or_else(|| ChannelError::Other("Invalid session ID format".to_string()))?;
                if unified_id.channel != "cli_chat" || unified_id.chat_id != client.chat_id {
                    return Err(ChannelError::Other(
                        "Session does not belong to this client".to_string(),
                    ));
                }
                bus.publish_control(ControlMessage {
                    op: SessionCommand::SwitchDialog {
                        channel: unified_id.channel.clone(),
                        chat_id: unified_id.chat_id.clone(),
                        dialog_id: unified_id.dialog_id.clone(),
                    },
                    reply_tx,
                })
                .await?;

                match reply_rx.recv().await {
                    Some(Ok(SessionEvent::DialogSwitched { session_id })) => {
                        *current_session_guard = Some(session_id.to_string());
                        let _ = client
                            .sender
                            .send(WsOutbound::SessionLoaded {
                                session_id: session_id.to_string(),
                                title: "Session".to_string(),
                                message_count: 0,
                            })
                            .await;
                    }
                    Some(Ok(_)) => {
                        // Unexpected response type
                    }
                    Some(Err(_e)) => {
                        let _ = client
                            .sender
                            .send(WsOutbound::Error {
                                code: "SESSION_NOT_FOUND".to_string(),
                                message: format!("Session not found: {}", session_id),
                            })
                            .await;
                    }
                    None => {
                        return Err(ChannelError::Other("Control channel closed".to_string()));
                    }
                }
            }
            WsInbound::GetSessionHistory { session_id, limit } => {
                let unified_id = Self::parse_client_session(&client, &session_id)?;
                let (reply_tx, mut reply_rx) = mpsc::channel(1);
                bus.publish_control(ControlMessage {
                    op: SessionCommand::GetDialogHistory {
                        session_id: unified_id,
                        limit: limit.unwrap_or(1_000).clamp(1, 2_000),
                    },
                    reply_tx,
                })
                .await?;

                match reply_rx.recv().await {
                    Some(Ok(SessionEvent::DialogHistory {
                        session_id,
                        messages,
                    })) => {
                        let messages = messages
                            .into_iter()
                            .filter(|message| {
                                !message.content.is_empty()
                                    || message.media_refs.is_some()
                                    || message.tool_calls.is_some()
                                    || message.role == "tool"
                            })
                            .map(HistoryMessage::from_message_meta)
                            .collect();
                        let _ = client
                            .sender
                            .send(WsOutbound::SessionHistory {
                                session_id: session_id.to_string(),
                                messages,
                            })
                            .await;
                    }
                    Some(Ok(_)) => {}
                    Some(Err(e)) => return Err(e),
                    None => {
                        return Err(ChannelError::Other("Control channel closed".to_string()));
                    }
                }
            }
            WsInbound::GetSessionPlan { session_id } => {
                let unified_id = Self::parse_client_session(&client, &session_id)?;
                let (reply_tx, mut reply_rx) = mpsc::channel(1);
                bus.publish_control(ControlMessage {
                    op: SessionCommand::GetTaskPlan {
                        session_id: unified_id,
                    },
                    reply_tx,
                })
                .await?;
                match reply_rx.recv().await {
                    Some(Ok(SessionEvent::TaskPlan { session_id, plan })) => {
                        let _ = client
                            .sender
                            .send(WsOutbound::SessionPlan {
                                session_id: session_id.to_string(),
                                plan,
                            })
                            .await;
                    }
                    Some(Ok(_)) => {}
                    Some(Err(error)) => return Err(error),
                    None => return Err(ChannelError::Other("Control channel closed".to_string())),
                }
            }
            WsInbound::GetSessionStats { session_id } => {
                let unified_id = Self::parse_client_session(&client, &session_id)?;
                let (reply_tx, mut reply_rx) = mpsc::channel(1);
                bus.publish_control(ControlMessage {
                    op: SessionCommand::GetSessionStats {
                        session_id: unified_id,
                    },
                    reply_tx,
                })
                .await?;
                match reply_rx.recv().await {
                    Some(Ok(SessionEvent::SessionStats { stats })) => {
                        let _ = client.sender.send(WsOutbound::SessionStats { stats }).await;
                    }
                    Some(Ok(_)) => {}
                    Some(Err(error)) => return Err(error),
                    None => return Err(ChannelError::Other("Control channel closed".to_string())),
                }
            }
            WsInbound::GetAgentRuns {
                session_id,
                cursor,
                limit,
            } => {
                let unified_id = Self::parse_client_session(&client, &session_id)?;
                let (reply_tx, mut reply_rx) = mpsc::channel(1);
                bus.publish_control(ControlMessage {
                    op: SessionCommand::GetAgentRuns {
                        session_id: unified_id,
                        cursor,
                        limit: limit.unwrap_or(100).clamp(1, 200),
                    },
                    reply_tx,
                })
                .await?;
                match reply_rx.recv().await {
                    Some(Ok(SessionEvent::AgentRuns {
                        session_id,
                        revision,
                        runs,
                        next_cursor,
                    })) => {
                        let _ = client
                            .sender
                            .send(WsOutbound::SessionAgentRuns {
                                session_id: session_id.to_string(),
                                revision,
                                runs,
                                next_cursor,
                            })
                            .await;
                    }
                    Some(Ok(_)) => {}
                    Some(Err(error)) => return Err(error),
                    None => return Err(ChannelError::Other("Control channel closed".to_string())),
                }
            }
            WsInbound::GetAgentRun { session_id, run_id } => {
                let unified_id = Self::parse_client_session(&client, &session_id)?;
                let (reply_tx, mut reply_rx) = mpsc::channel(1);
                bus.publish_control(ControlMessage {
                    op: SessionCommand::GetAgentRun {
                        session_id: unified_id,
                        run_id,
                    },
                    reply_tx,
                })
                .await?;
                match reply_rx.recv().await {
                    Some(Ok(SessionEvent::AgentRun {
                        session_id,
                        revision,
                        run,
                    })) => {
                        let _ = client
                            .sender
                            .send(WsOutbound::AgentRunUpdated {
                                session_id: session_id.to_string(),
                                revision,
                                run: run.ok_or_else(|| {
                                    ChannelError::Other("run not found".to_string())
                                })?,
                            })
                            .await;
                    }
                    Some(Ok(_)) => {}
                    Some(Err(error)) => return Err(error),
                    None => return Err(ChannelError::Other("Control channel closed".to_string())),
                }
            }
            WsInbound::RenameSession { session_id, title } => {
                let target = session_id
                    .or(current_session_guard.clone())
                    .ok_or_else(|| ChannelError::Other("No active session".to_string()))?;

                let (reply_tx, mut reply_rx) = mpsc::channel(1);
                let unified_id = Self::parse_client_session(&client, &target)?;
                bus.publish_control(ControlMessage {
                    op: SessionCommand::RenameDialog {
                        session_id: unified_id,
                        title: title.clone(),
                    },
                    reply_tx,
                })
                .await?;

                match reply_rx.recv().await {
                    Some(Ok(SessionEvent::DialogRenamed { session_id, title })) => {
                        let _ = client
                            .sender
                            .send(WsOutbound::SessionRenamed {
                                session_id: session_id.to_string(),
                                title,
                            })
                            .await;
                    }
                    Some(Ok(_)) => {
                        // Unexpected response type
                    }
                    Some(Err(e)) => {
                        return Err(e);
                    }
                    None => {
                        return Err(ChannelError::Other("Control channel closed".to_string()));
                    }
                }
            }
            WsInbound::ArchiveSession { session_id } => {
                let target = session_id
                    .or(current_session_guard.clone())
                    .ok_or_else(|| ChannelError::Other("No active session".to_string()))?;
                let was_current = current_session_guard.as_deref() == Some(&target);

                let (reply_tx, mut reply_rx) = mpsc::channel(1);
                let unified_id = Self::parse_client_session(&client, &target)?;
                bus.publish_control(ControlMessage {
                    op: SessionCommand::ArchiveDialog {
                        session_id: unified_id,
                    },
                    reply_tx,
                })
                .await?;

                match reply_rx.recv().await {
                    Some(Ok(SessionEvent::DialogArchived { session_id })) => {
                        let _ = client
                            .sender
                            .send(WsOutbound::SessionArchived {
                                session_id: session_id.to_string(),
                            })
                            .await;
                        if was_current {
                            let (new_id, title) = self
                                .create_session_via_control(&client.chat_id, None)
                                .await?;
                            *current_session_guard = Some(new_id.clone());
                            let _ = client
                                .sender
                                .send(WsOutbound::SessionCreated {
                                    session_id: new_id,
                                    title,
                                })
                                .await;
                        }
                    }
                    Some(Ok(_)) => {
                        // Unexpected response type
                    }
                    Some(Err(e)) => {
                        return Err(e);
                    }
                    None => {
                        return Err(ChannelError::Other("Control channel closed".to_string()));
                    }
                }
            }
            WsInbound::DeleteSession { session_id } => {
                let target = session_id
                    .or(current_session_guard.clone())
                    .ok_or_else(|| ChannelError::Other("No active session".to_string()))?;

                let (reply_tx, mut reply_rx) = mpsc::channel(1);
                let unified_id = Self::parse_client_session(&client, &target)?;
                bus.publish_control(ControlMessage {
                    op: SessionCommand::DeleteDialog {
                        session_id: unified_id,
                    },
                    reply_tx,
                })
                .await?;

                match reply_rx.recv().await {
                    Some(Ok(SessionEvent::DialogDeleted { session_id })) => {
                        let _ = client
                            .sender
                            .send(WsOutbound::SessionDeleted {
                                session_id: session_id.to_string(),
                            })
                            .await;

                        // If deleting current session, create a new one
                        if current_session_guard.as_deref() == Some(&target) {
                            drop(reply_rx);
                            if let Ok((new_id, title)) =
                                self.create_session_via_control(&client.chat_id, None).await
                            {
                                *current_session_guard = Some(new_id.clone());
                                let _ = client
                                    .sender
                                    .send(WsOutbound::SessionCreated {
                                        session_id: new_id,
                                        title,
                                    })
                                    .await;
                            }
                        }
                    }
                    Some(Ok(_)) => {
                        // Unexpected response type
                    }
                    Some(Err(e)) => {
                        return Err(e);
                    }
                    None => {
                        return Err(ChannelError::Other("Control channel closed".to_string()));
                    }
                }
            }
            WsInbound::GetSlashCommands => {
                // Get commands from session manager via control message
                let (reply_tx, mut reply_rx) = mpsc::channel(1);
                bus.publish_control(ControlMessage {
                    op: SessionCommand::GetSlashCommands {
                        channel: "cli_chat".to_string(),
                        chat_id: client.chat_id.clone(),
                    },
                    reply_tx,
                })
                .await?;

                if let Some(result) = reply_rx.recv().await {
                    match result {
                        Ok(SessionEvent::SlashCommandsList { commands }) => {
                            // Convert to SlashCommand to SlashCommandInfo
                            let command_infos: Vec<SlashCommandInfo> = commands
                                .into_iter()
                                .map(|cmd| SlashCommandInfo {
                                    name: cmd.name.to_string(),
                                    description: cmd.description.to_string(),
                                    aliases: cmd.aliases.iter().map(|&a| a.to_string()).collect(),
                                })
                                .collect();
                            let _ = client
                                .sender
                                .send(WsOutbound::SlashCommandsList {
                                    commands: command_infos,
                                })
                                .await;
                        }
                        Ok(SessionEvent::Error { code, message }) => {
                            let _ = client
                                .sender
                                .send(WsOutbound::Error { code, message })
                                .await;
                        }
                        Err(e) => {
                            let _ = client
                                .sender
                                .send(WsOutbound::Error {
                                    code: "GET_COMMANDS_ERROR".to_string(),
                                    message: e.to_string(),
                                })
                                .await;
                        }
                        _ => {}
                    }
                }
            }
            WsInbound::Ping => {
                let _ = client.sender.send(WsOutbound::Pong).await;
            }
        }
        Ok(())
    }

    /// Create a session via control message and return the session_id
    async fn create_session_via_control(
        &self,
        chat_id: &str,
        title: Option<&str>,
    ) -> Result<(String, String), ChannelError> {
        let bus = {
            let guard = self.bus.lock().unwrap();
            guard
                .clone()
                .ok_or_else(|| ChannelError::Other("Channel not started".to_string()))?
        };

        let (reply_tx, mut reply_rx) = mpsc::channel(1);
        bus.publish_control(ControlMessage {
            op: SessionCommand::CreateDialog {
                channel: "cli_chat".to_string(),
                chat_id: chat_id.to_string(),
                title: title.map(String::from),
            },
            reply_tx,
        })
        .await?;

        match reply_rx.recv().await {
            Some(Ok(SessionEvent::DialogCreated { session_id, title })) => {
                Ok((session_id.to_string(), title))
            }
            Some(Ok(_)) => Err(ChannelError::Other("Unexpected response type".to_string())),
            Some(Err(e)) => Err(e),
            None => Err(ChannelError::Other("Control channel closed".to_string())),
        }
    }

    async fn resume_session_via_control(&self, chat_id: &str) -> Result<String, ChannelError> {
        let bus = {
            let guard = self.bus.lock().unwrap();
            guard
                .clone()
                .ok_or_else(|| ChannelError::Other("Channel not started".to_string()))?
        };

        let (reply_tx, mut reply_rx) = mpsc::channel(1);
        bus.publish_control(ControlMessage {
            op: SessionCommand::GetCurrentDialog {
                channel: "cli_chat".to_string(),
                chat_id: chat_id.to_string(),
            },
            reply_tx,
        })
        .await?;
        if let Some(Ok(SessionEvent::CurrentDialog {
            session_id: Some(session_id),
        })) = reply_rx.recv().await
        {
            return Ok(session_id.to_string());
        }

        let (reply_tx, mut reply_rx) = mpsc::channel(1);
        bus.publish_control(ControlMessage {
            op: SessionCommand::ListDialogs {
                channel: "cli_chat".to_string(),
                chat_id: chat_id.to_string(),
                include_archived: false,
            },
            reply_tx,
        })
        .await?;
        if let Some(Ok(SessionEvent::DialogList { dialogs, .. })) = reply_rx.recv().await
            && let Some(dialog) = dialogs.first()
        {
            let session_id = dialog.session_id.clone();
            let (reply_tx, mut reply_rx) = mpsc::channel(1);
            bus.publish_control(ControlMessage {
                op: SessionCommand::SwitchDialog {
                    channel: session_id.channel.clone(),
                    chat_id: session_id.chat_id.clone(),
                    dialog_id: session_id.dialog_id.clone(),
                },
                reply_tx,
            })
            .await?;
            if let Some(Ok(SessionEvent::DialogSwitched { session_id })) = reply_rx.recv().await {
                return Ok(session_id.to_string());
            }
        }

        self.create_session_via_control(chat_id, None)
            .await
            .map(|(session_id, _)| session_id)
    }

    fn parse_client_session(
        client: &Client,
        session_id: &str,
    ) -> Result<UnifiedSessionId, ChannelError> {
        let unified_id = UnifiedSessionId::parse(session_id)
            .ok_or_else(|| ChannelError::Other("Invalid session ID format".to_string()))?;
        if unified_id.channel != "cli_chat" || unified_id.chat_id != client.chat_id {
            return Err(ChannelError::Other(
                "Session does not belong to this client".to_string(),
            ));
        }
        Ok(unified_id)
    }
}

struct CliChatTurnSink {
    clients: Arc<Mutex<HashMap<String, Arc<Client>>>>,
    chat_id: String,
}

impl CliChatTurnSink {
    async fn publish(&self, snapshot: &TurnSnapshot) {
        let client = self.clients.lock().await.get(&self.chat_id).cloned();
        let Some(client) = client else {
            return;
        };
        if client
            .sender
            .send(WsOutbound::TurnUpdated {
                snapshot: snapshot.clone(),
            })
            .await
            .is_err()
        {
            let mut clients = self.clients.lock().await;
            if clients
                .get(&self.chat_id)
                .is_some_and(|registered| Arc::ptr_eq(registered, &client))
            {
                clients.remove(&self.chat_id);
            }
        }
    }
}

#[async_trait]
impl TurnSink for CliChatTurnSink {
    async fn update(&mut self, snapshot: &TurnSnapshot) -> Result<(), ChannelError> {
        self.publish(snapshot).await;
        Ok(())
    }

    async fn finish(&mut self, snapshot: &TurnSnapshot) -> Result<(), ChannelError> {
        self.publish(snapshot).await;
        Ok(())
    }

    async fn abort(&mut self, snapshot: &TurnSnapshot) -> Result<(), ChannelError> {
        self.publish(snapshot).await;
        Ok(())
    }
}

#[async_trait]
impl Channel for CliChatChannel {
    fn name(&self) -> &str {
        "cli_chat"
    }

    fn is_running(&self) -> bool {
        self.bus.lock().unwrap().is_some()
    }

    async fn start(&self, bus: Arc<MessageBus>) -> Result<(), ChannelError> {
        *self.bus.lock().unwrap() = Some(bus);
        Ok(())
    }

    async fn stop(&self) -> Result<(), ChannelError> {
        *self.bus.lock().unwrap() = None;
        self.clients.lock().await.clear();
        Ok(())
    }

    fn live_policy(&self) -> LivePolicy {
        LivePolicy::Snapshot {
            min_interval: Duration::from_millis(33),
        }
    }

    fn presentation_policy(&self) -> crate::delivery::PresentationPolicy {
        crate::delivery::PresentationPolicy::interactive()
    }

    async fn open_turn(&self, target: TurnTarget) -> Result<Box<dyn TurnSink>, ChannelError> {
        Ok(Box::new(CliChatTurnSink {
            clients: self.clients.clone(),
            chat_id: target.chat_id,
        }))
    }

    async fn commit_turn(
        &self,
        target: &TurnTarget,
        delta: CommittedTurnDelta,
    ) -> Result<(), ChannelError> {
        let client = self.clients.lock().await.get(&target.chat_id).cloned();
        let Some(client) = client else {
            return Ok(());
        };
        let frame = WsOutbound::TurnCommitted {
            session_id: delta.session_id,
            history_revision: delta.history_revision,
            messages: delta
                .messages
                .into_iter()
                .map(HistoryMessage::from)
                .collect(),
        };
        client.sender.send(frame).await.map_err(|_| {
            ChannelError::ConnectionError("CLI client disconnected during turn commit".to_string())
        })
    }

    fn commit_turn_presents_media(&self) -> bool {
        true
    }

    async fn send(&self, msg: OutboundMessage) -> Result<(), ChannelError> {
        let client = self.clients.lock().await.get(&msg.chat_id).cloned();
        let Some(client) = client else {
            tracing::debug!(chat_id = %msg.chat_id, "No active CLI client for outbound message");
            return Ok(());
        };
        let message_type = msg.metadata.get("_type").map(String::as_str);
        let session_id = msg.metadata.get("_session_id").cloned();
        let message_id = msg
            .metadata
            .get("_message_id")
            .cloned()
            .unwrap_or_else(crate::util::short_id);
        let attachments = msg
            .media
            .iter()
            .enumerate()
            .map(|(index, media)| MessageAttachment::from_media_ref(index, &media.to_media_ref()))
            .collect();
        let outbound = if message_type == Some("notification") {
            WsOutbound::SystemNotification {
                content: msg.content,
                session_id,
            }
        } else if message_type == Some("command") {
            WsOutbound::CommandExecuted {
                message: msg.content,
            }
        } else {
            WsOutbound::AssistantResponse {
                id: message_id,
                content: msg.content,
                role: "assistant".to_string(),
                attachments,
                session_id,
            }
        };
        if client.sender.send(outbound).await.is_err() {
            let mut clients = self.clients.lock().await;
            if clients
                .get(&msg.chat_id)
                .is_some_and(|registered| Arc::ptr_eq(registered, &client))
            {
                clients.remove(&msg.chat_id);
            }
        }
        Ok(())
    }
}

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

    #[tokio::test]
    async fn failed_sender_is_pruned_from_client_registry() {
        let channel = CliChatChannel::new();
        let (sender, receiver) = mpsc::channel(1);
        drop(receiver);
        let client = Arc::new(Client {
            sender,
            chat_id: "dead-client".to_string(),
            current_session_id: Mutex::new(None),
        });
        channel
            .clients
            .lock()
            .await
            .insert("dead-client".to_string(), client);

        channel
            .send(OutboundMessage {
                channel: "cli_chat".to_string(),
                chat_id: "dead-client".to_string(),
                content: "message".to_string(),
                reply_to: None,
                media: Vec::new(),
                metadata: Default::default(),
                delivery: None,
            })
            .await
            .unwrap();

        assert!(channel.clients.lock().await.is_empty());
    }

    #[tokio::test]
    async fn stale_connection_cannot_unregister_replacement() {
        let channel = CliChatChannel::new();
        let (old_sender, _old_receiver) = mpsc::channel(1);
        let (new_sender, _new_receiver) = mpsc::channel(1);
        let old = Arc::new(Client {
            sender: old_sender,
            chat_id: "stable-client".to_string(),
            current_session_id: Mutex::new(None),
        });
        let replacement = Arc::new(Client {
            sender: new_sender,
            chat_id: "stable-client".to_string(),
            current_session_id: Mutex::new(None),
        });
        channel
            .clients
            .lock()
            .await
            .insert("stable-client".to_string(), replacement.clone());

        channel.unregister_client(&old).await;

        let registered = channel.clients.lock().await;
        assert!(
            registered
                .get("stable-client")
                .is_some_and(|client| Arc::ptr_eq(client, &replacement))
        );
    }

    #[tokio::test]
    async fn upload_ids_become_inbound_media_paths() {
        let uploads = UploadRegistry::new(crate::config::FileTransferConfig::default());
        uploads
            .register(
                "upload-1".into(),
                "client".into(),
                std::path::PathBuf::from("/tmp/report.pdf"),
                crate::protocol::UploadDescriptor {
                    upload_id: String::new(),
                    name: "report.pdf".into(),
                    media_type: "file".into(),
                    mime_type: "application/pdf".into(),
                    size: 42,
                    expires_at: 0,
                },
            )
            .await
            .unwrap();
        let channel = CliChatChannel::with_upload_registry(uploads);
        let bus = MessageBus::new(4);
        channel.start(bus.clone()).await.unwrap();
        let (sender, _receiver) = mpsc::channel(1);
        let client = Arc::new(Client {
            sender,
            chat_id: "client".into(),
            current_session_id: Mutex::new(None),
        });

        channel
            .handle_ws_inbound(
                client,
                WsInbound::UserInput {
                    content: "/queue 处理附件".into(),
                    upload_ids: vec!["upload-1".into()],
                    client_message_id: Some("550e8400-e29b-41d4-a716-446655440000".into()),
                    channel: None,
                    chat_id: None,
                    sender_id: None,
                },
            )
            .await
            .unwrap();

        let inbound = bus.consume_inbound().await.unwrap();
        assert_eq!(inbound.media.len(), 1);
        assert_eq!(inbound.media[0].path, "/tmp/report.pdf");
        assert_eq!(inbound.media[0].media_type, "file");
        assert_eq!(
            inbound.client_message_id.as_deref(),
            Some("550e8400-e29b-41d4-a716-446655440000")
        );
        assert_eq!(inbound.content, "/queue 处理附件");
    }

    #[tokio::test]
    async fn invalid_client_message_id_is_ignored_at_channel_boundary() {
        let channel = CliChatChannel::new();
        let bus = MessageBus::new(4);
        channel.start(bus.clone()).await.unwrap();
        let (sender, _receiver) = mpsc::channel(1);
        let client = Arc::new(Client {
            sender,
            chat_id: "client".into(),
            current_session_id: Mutex::new(None),
        });

        channel
            .handle_ws_inbound(
                client,
                WsInbound::UserInput {
                    content: "hello".into(),
                    upload_ids: Vec::new(),
                    client_message_id: Some("not-a-uuid".into()),
                    channel: None,
                    chat_id: None,
                    sender_id: None,
                },
            )
            .await
            .unwrap();

        let inbound = bus.consume_inbound().await.unwrap();
        assert_eq!(inbound.client_message_id, None);
    }

    #[tokio::test]
    async fn outbound_media_is_exposed_as_safe_attachment_metadata() {
        let channel = CliChatChannel::new();
        let (sender, mut receiver) = mpsc::channel(1);
        let client = Arc::new(Client {
            sender,
            chat_id: "client".into(),
            current_session_id: Mutex::new(None),
        });
        channel.clients.lock().await.insert("client".into(), client);

        channel
            .send(OutboundMessage {
                channel: "cli_chat".into(),
                chat_id: "client".into(),
                content: "报告".into(),
                reply_to: None,
                media: vec![crate::bus::MediaItem::new("/secret/report.pdf", "file")],
                metadata: HashMap::from([("_message_id".into(), "message-1".into())]),
                delivery: None,
            })
            .await
            .unwrap();

        match receiver.recv().await.unwrap() {
            WsOutbound::AssistantResponse {
                id, attachments, ..
            } => {
                assert_eq!(id, "message-1");
                assert_eq!(attachments[0].name, "report.pdf");
                let json = serde_json::to_string(&attachments).unwrap();
                assert!(!json.contains("/secret"));
            }
            other => panic!("unexpected outbound: {other:?}"),
        }
    }

    #[tokio::test]
    async fn turn_sink_sends_the_same_snapshot_shape_for_running_and_terminal_states() {
        let channel = CliChatChannel::new();
        let (sender, mut receiver) = mpsc::channel(2);
        let client = Arc::new(Client {
            sender,
            chat_id: "client".into(),
            current_session_id: Mutex::new(None),
        });
        channel.clients.lock().await.insert("client".into(), client);
        let mut sink = channel
            .open_turn(TurnTarget {
                channel: "cli_chat".into(),
                chat_id: "client".into(),
                session_id: "cli_chat:client:dialog".into(),
                reply_to: None,
                metadata: HashMap::new(),
            })
            .await
            .unwrap();
        let (controller, emitter, _) =
            crate::session::TurnController::start("cli_chat:client:dialog", "message");
        emitter
            .emit(crate::agent::TurnEvent::TextDelta {
                iteration: 0,
                delta: "stream".into(),
            })
            .unwrap();
        let running = controller.snapshot();
        sink.update(&running).await.unwrap();
        controller.complete(None);
        let completed = controller.snapshot();
        sink.finish(&completed).await.unwrap();

        match receiver.recv().await.unwrap() {
            WsOutbound::TurnUpdated { snapshot } => {
                assert_eq!(snapshot.status, crate::session::TurnStatus::Running);
            }
            other => panic!("unexpected outbound: {other:?}"),
        }
        match receiver.recv().await.unwrap() {
            WsOutbound::TurnUpdated { snapshot } => {
                assert_eq!(snapshot.status, crate::session::TurnStatus::Completed);
                assert!(snapshot.revision > running.revision);
            }
            other => panic!("unexpected outbound: {other:?}"),
        }
    }

    #[tokio::test]
    async fn committed_turn_is_projected_as_incremental_history_frame() {
        let channel = CliChatChannel::new();
        let (sender, mut receiver) = mpsc::channel(1);
        let client = Arc::new(Client {
            sender,
            chat_id: "client".into(),
            current_session_id: Mutex::new(None),
        });
        channel.clients.lock().await.insert("client".into(), client);
        let target = TurnTarget {
            channel: "cli_chat".into(),
            chat_id: "client".into(),
            session_id: "cli_chat:client:dialog".into(),
            reply_to: None,
            metadata: HashMap::new(),
        };

        channel
            .commit_turn(
                &target,
                CommittedTurnDelta {
                    session_id: target.session_id.clone(),
                    history_revision: 4,
                    messages: vec![crate::bus::CommittedMessage {
                        id: "message".into(),
                        seq: 4,
                        role: "assistant".into(),
                        content: "done".into(),
                        reasoning_content: None,
                        completion_status: crate::bus::CompletionStatus::Completed,
                        media_refs: Vec::new(),
                        created_at: 1,
                        tool_call_id: None,
                        tool_name: None,
                        tool_calls: None,
                        turn_origin: crate::bus::TurnOrigin::User,
                    }],
                },
            )
            .await
            .unwrap();

        match receiver.recv().await.unwrap() {
            WsOutbound::TurnCommitted {
                history_revision,
                messages,
                ..
            } => {
                assert_eq!(history_revision, 4);
                assert_eq!(messages[0].id, "message");
            }
            other => panic!("unexpected outbound: {other:?}"),
        }
    }
}
