use super::{
    map_permission_response, spawn_acp_server_in_process, Connection, PermissionDecision, Session,
    SessionData, TestConnectionConfig, TestOutput,
};
use agent_client_protocol::schema::{
    ClientCapabilities, CloseSessionRequest, ContentBlock, CreateTerminalRequest,
    FileSystemCapabilities, ImageContent, InitializeRequest, KillTerminalRequest,
    ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, McpServer, ModelId, ModelInfo,
    NewSessionRequest, PromptRequest, ProtocolVersion, ReadTextFileRequest, ReleaseTerminalRequest,
    RequestPermissionRequest, SessionConfigKind, SessionConfigOptionCategory,
    SessionConfigOptionValue, SessionId, SessionModeId, SessionModelState, SessionNotification,
    SessionUpdate, SetSessionConfigOptionRequest, SetSessionModeRequest, SetSessionModelRequest,
    StopReason, TerminalOutputRequest, TextContent, ToolCallStatus, WaitForTerminalExitRequest,
    WriteTextFileRequest,
};
use agent_client_protocol::{Agent, Client, ConnectionTo};
use async_trait::async_trait;
use goose::config::PermissionManager;
use goose_test_support::{ExpectedSessionId, IgnoreSessionId};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::Notify;

pub struct AcpServerConnection {
    cx: ConnectionTo<Agent>,
    // MCP servers from config, consumed by the first new_session call.
    pending_mcp_servers: Vec<McpServer>,
    cwd: Option<tempfile::TempDir>,
    data_root: std::path::PathBuf,
    updates: Arc<Mutex<Vec<SessionNotification>>>,
    permission: Arc<Mutex<PermissionDecision>>,
    notify: Arc<Notify>,
    permission_manager: Arc<PermissionManager>,
    _openai: super::OpenAiFixture,
    _temp_dir: Option<tempfile::TempDir>,
}

pub struct AcpServerSession {
    cx: ConnectionTo<Agent>,
    session_id: agent_client_protocol::schema::SessionId,
    updates: Arc<Mutex<Vec<SessionNotification>>>,
    permission: Arc<Mutex<PermissionDecision>>,
    notify: Arc<Notify>,
    _work_dir: tempfile::TempDir,
}

impl std::fmt::Debug for AcpServerSession {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AcpServerSession")
            .field("session_id", &self.session_id)
            .finish()
    }
}

impl AcpServerSession {
    pub fn session_updates(&self) -> Vec<SessionUpdate> {
        self.updates
            .lock()
            .unwrap()
            .drain(..)
            .map(|n| n.update)
            .collect()
    }

    async fn send_prompt(
        &mut self,
        content: Vec<ContentBlock>,
        decision: PermissionDecision,
    ) -> anyhow::Result<TestOutput> {
        *self.permission.lock().unwrap() = decision;
        self.updates.lock().unwrap().clear();

        let response = self
            .cx
            .send_request(PromptRequest::new(self.session_id.clone(), content))
            .block_task()
            .await?;

        assert_eq!(response.stop_reason, StopReason::EndTurn);

        let mut updates_len = self.updates.lock().unwrap().len();
        while updates_len == 0 {
            self.notify.notified().await;
            updates_len = self.updates.lock().unwrap().len();
        }

        let text = collect_agent_text(&self.updates);
        let deadline = tokio::time::Instant::now() + Duration::from_millis(500);
        let mut tool_status = extract_tool_status(&self.updates);
        while tool_status.is_none() && tokio::time::Instant::now() < deadline {
            tokio::task::yield_now().await;
            tool_status = extract_tool_status(&self.updates);
        }

        Ok(TestOutput { text, tool_status })
    }
}

impl AcpServerConnection {
    #[allow(dead_code)]
    pub fn cx(&self) -> &ConnectionTo<Agent> {
        &self.cx
    }
}

#[async_trait]
impl Connection for AcpServerConnection {
    type Session = AcpServerSession;

    fn expected_session_id() -> Arc<dyn ExpectedSessionId> {
        // The ACP session ID returned to clients is now a thread ID, which is
        // intentionally different from the internal session ID the agent sends
        // to the LLM provider. Skip strict matching.
        Arc::new(IgnoreSessionId)
    }

    async fn new(config: TestConnectionConfig, openai: super::OpenAiFixture) -> Self {
        let (data_root, temp_dir) = match config.data_root.as_os_str().is_empty() {
            true => {
                let temp_dir = tempfile::tempdir().unwrap();
                (temp_dir.path().to_path_buf(), Some(temp_dir))
            }
            false => (config.data_root.clone(), None),
        };

        let (transport, _handle, permission_manager) = spawn_acp_server_in_process(
            openai.uri(),
            &config.builtins,
            data_root.as_path(),
            config.goose_mode,
            config.provider_factory,
            &config.current_model,
            config.disable_session_naming,
        )
        .await;

        let updates = Arc::new(Mutex::new(Vec::new()));
        let notify = Arc::new(Notify::new());
        let permission = Arc::new(Mutex::new(PermissionDecision::Cancel));

        let mut fs_cap = FileSystemCapabilities::default();
        if config.read_text_file.is_some() {
            fs_cap = fs_cap.read_text_file(true);
        }
        if config.write_text_file.is_some() {
            fs_cap = fs_cap.write_text_file(true);
        }

        let cx = {
            let updates_clone = updates.clone();
            let notify_clone = notify.clone();
            let permission_clone = permission.clone();
            let read_handler = config.read_text_file;
            let write_handler = config.write_text_file;
            let terminal = config.terminal;

            let cx_holder: Arc<Mutex<Option<ConnectionTo<Agent>>>> = Arc::new(Mutex::new(None));
            let cx_holder_clone = cx_holder.clone();

            let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();

            tokio::spawn(async move {
                let result = Client
                    .builder()
                    .on_receive_notification(
                        {
                            let updates = updates_clone.clone();
                            let notify = notify_clone.clone();
                            async move |notification: SessionNotification, _cx| {
                                updates.lock().unwrap().push(notification);
                                notify.notify_waiters();
                                Ok(())
                            }
                        },
                        agent_client_protocol::on_receive_notification!(),
                    )
                    .on_receive_request(
                        {
                            let permission = permission_clone.clone();
                            async move |req: RequestPermissionRequest, responder, _connection_cx| {
                                let decision = *permission.lock().unwrap();
                                responder.respond(map_permission_response(&req, decision))
                            }
                        },
                        agent_client_protocol::on_receive_request!(),
                    )
                    .on_receive_request(
                        async move |req: ReadTextFileRequest, responder, _cx| match read_handler {
                            Some(ref rh) => match rh(&req) {
                                Ok(resp) => responder.respond(resp),
                                Err(msg) => responder.respond_with_internal_error(msg),
                            },
                            None => responder.respond_with_error(
                                agent_client_protocol::Error::method_not_found(),
                            ),
                        },
                        agent_client_protocol::on_receive_request!(),
                    )
                    .on_receive_request(
                        async move |req: WriteTextFileRequest, responder, _cx| match write_handler {
                            Some(ref wh) => match wh(&req) {
                                Ok(resp) => responder.respond(resp),
                                Err(msg) => responder.respond_with_internal_error(msg),
                            },
                            None => responder.respond_with_error(
                                agent_client_protocol::Error::method_not_found(),
                            ),
                        },
                        agent_client_protocol::on_receive_request!(),
                    )
                    .on_receive_request(
                        {
                            let t = terminal.clone();
                            async move |req: CreateTerminalRequest, responder, _cx| match t {
                                Some(ref f) => responder.respond(f.on_create(&req.command)),
                                None => responder.respond_with_error(
                                    agent_client_protocol::Error::method_not_found(),
                                ),
                            }
                        },
                        agent_client_protocol::on_receive_request!(),
                    )
                    .on_receive_request(
                        {
                            let t = terminal.clone();
                            async move |req: WaitForTerminalExitRequest, responder, _cx| match t {
                                Some(ref f) => {
                                    responder.respond(f.on_wait_for_exit(&req.terminal_id))
                                }
                                None => responder.respond_with_error(
                                    agent_client_protocol::Error::method_not_found(),
                                ),
                            }
                        },
                        agent_client_protocol::on_receive_request!(),
                    )
                    .on_receive_request(
                        {
                            let t = terminal.clone();
                            async move |req: TerminalOutputRequest, responder, _cx| match t {
                                Some(ref f) => responder.respond(f.on_output(&req.terminal_id)),
                                None => responder.respond_with_error(
                                    agent_client_protocol::Error::method_not_found(),
                                ),
                            }
                        },
                        agent_client_protocol::on_receive_request!(),
                    )
                    .on_receive_request(
                        {
                            let t = terminal.clone();
                            async move |req: ReleaseTerminalRequest, responder, _cx| match t {
                                Some(ref f) => responder.respond(f.on_release(&req.terminal_id)),
                                None => responder.respond_with_error(
                                    agent_client_protocol::Error::method_not_found(),
                                ),
                            }
                        },
                        agent_client_protocol::on_receive_request!(),
                    )
                    .on_receive_request(
                        {
                            let t = terminal.clone();
                            async move |req: KillTerminalRequest, responder, _cx| match t {
                                Some(ref f) => responder.respond(f.on_kill(&req.terminal_id)),
                                None => responder.respond_with_error(
                                    agent_client_protocol::Error::method_not_found(),
                                ),
                            }
                        },
                        agent_client_protocol::on_receive_request!(),
                    )
                    .connect_with(transport, {
                        let cx_holder = cx_holder_clone;
                        async move |cx: ConnectionTo<Agent>| {
                            let _resp = cx
                                .send_request(
                                    InitializeRequest::new(ProtocolVersion::LATEST)
                                        .client_capabilities(
                                            ClientCapabilities::new()
                                                .fs(fs_cap)
                                                .terminal(terminal.is_some()),
                                        ),
                                )
                                .block_task()
                                .await
                                .unwrap();

                            *cx_holder.lock().unwrap() = Some(cx.clone());
                            let _ = ready_tx.send(());

                            std::future::pending::<Result<(), agent_client_protocol::Error>>().await
                        }
                    })
                    .await;

                if let Err(e) = result {
                    tracing::error!("SACP client error: {e}");
                }
            });

            ready_rx.await.unwrap();
            let cx = cx_holder.lock().unwrap().take().unwrap();
            cx
        };

        Self {
            cx,
            pending_mcp_servers: config.mcp_servers,
            cwd: config.cwd,
            data_root,
            updates,
            permission,
            notify,
            permission_manager,
            _openai: openai,
            _temp_dir: temp_dir,
        }
    }

    async fn new_session(&mut self) -> anyhow::Result<SessionData<AcpServerSession>> {
        let work_dir = self
            .cwd
            .take()
            .unwrap_or_else(|| tempfile::tempdir().unwrap());
        let mcp_servers = std::mem::take(&mut self.pending_mcp_servers);
        let response = self
            .cx
            .send_request(NewSessionRequest::new(work_dir.path()).mcp_servers(mcp_servers))
            .block_task()
            .await?;
        let session = AcpServerSession {
            cx: self.cx.clone(),
            session_id: response.session_id.clone(),
            updates: self.updates.clone(),
            permission: self.permission.clone(),
            notify: self.notify.clone(),
            _work_dir: work_dir,
        };
        let models = response.models.or_else(|| {
            extract_model_state_from_config_options(response.config_options.as_deref())
        });
        self.updates.lock().unwrap().clear();
        Ok(SessionData {
            session,
            models,
            modes: response.modes,
        })
    }

    async fn load_session(
        &mut self,
        session_id: &str,
        mcp_servers: Vec<McpServer>,
    ) -> anyhow::Result<SessionData<AcpServerSession>> {
        self.updates.lock().unwrap().clear();
        let work_dir = tempfile::tempdir().unwrap();
        let session_id = agent_client_protocol::schema::SessionId::new(session_id.to_string());
        let response = self
            .cx
            .send_request(
                LoadSessionRequest::new(session_id.clone(), work_dir.path())
                    .mcp_servers(mcp_servers),
            )
            .block_task()
            .await?;
        let session = AcpServerSession {
            cx: self.cx.clone(),
            session_id,
            updates: self.updates.clone(),
            permission: self.permission.clone(),
            notify: self.notify.clone(),
            _work_dir: work_dir,
        };
        Ok(SessionData {
            session,
            models: response.models,
            modes: response.modes,
        })
    }

    async fn list_sessions(&self) -> anyhow::Result<ListSessionsResponse> {
        self.cx
            .send_request(ListSessionsRequest::new())
            .block_task()
            .await
            .map_err(|e| e.into())
    }

    async fn close_session(&self, session_id: &str) -> anyhow::Result<()> {
        self.cx
            .send_request(CloseSessionRequest::new(SessionId::new(session_id)))
            .block_task()
            .await
            .map(|_| ())
            .map_err(|e| e.into())
    }

    async fn delete_session(&self, session_id: &str) -> anyhow::Result<()> {
        super::send_custom(
            &self.cx,
            "session/delete",
            serde_json::json!({ "sessionId": session_id }),
        )
        .await
        .map(|_| ())
        .map_err(|e| e.into())
    }

    async fn set_mode(&self, session_id: &str, mode_id: &str) -> anyhow::Result<()> {
        self.cx
            .send_request(SetSessionModeRequest::new(
                SessionId::new(session_id),
                SessionModeId::new(mode_id),
            ))
            .block_task()
            .await
            .map(|_| ())
            .map_err(|e| e.into())
    }

    async fn set_model(&self, session_id: &str, model_id: &str) -> anyhow::Result<()> {
        self.cx
            .send_request(SetSessionModelRequest::new(
                SessionId::new(session_id),
                model_id.to_string(),
            ))
            .block_task()
            .await
            .map(|_| ())
            .map_err(|e| e.into())
    }

    async fn set_config_option(
        &self,
        session_id: &str,
        config_id: &str,
        value: &str,
    ) -> anyhow::Result<()> {
        self.cx
            .send_request(SetSessionConfigOptionRequest::new(
                SessionId::new(session_id),
                config_id.to_string(),
                SessionConfigOptionValue::value_id(value.to_string()),
            ))
            .block_task()
            .await
            .map(|_| ())
            .map_err(|e| e.into())
    }

    fn data_root(&self) -> std::path::PathBuf {
        self.data_root.clone()
    }

    fn reset_openai(&self) {
        self._openai.reset();
    }

    fn reset_permissions(&self) {
        // "" matches all extensions, clearing all stored permission decisions
        self.permission_manager.remove_extension("");
    }
}

#[async_trait]
impl Session for AcpServerSession {
    fn session_id(&self) -> &agent_client_protocol::schema::SessionId {
        &self.session_id
    }

    fn work_dir(&self) -> std::path::PathBuf {
        self._work_dir.path().to_path_buf()
    }

    fn session_updates(&self) -> Vec<SessionUpdate> {
        AcpServerSession::session_updates(self)
    }

    fn notifications(&self) -> Vec<super::Notification> {
        super::to_notifications(&self.session_updates())
    }

    async fn prompt(
        &mut self,
        text: &str,
        decision: PermissionDecision,
    ) -> anyhow::Result<TestOutput> {
        self.send_prompt(vec![ContentBlock::Text(TextContent::new(text))], decision)
            .await
    }

    async fn prompt_with_image(
        &mut self,
        text: &str,
        image_b64: &str,
        mime_type: &str,
        decision: PermissionDecision,
    ) -> anyhow::Result<TestOutput> {
        self.send_prompt(
            vec![
                ContentBlock::Image(ImageContent::new(image_b64, mime_type)),
                ContentBlock::Text(TextContent::new(text)),
            ],
            decision,
        )
        .await
    }
}

fn extract_model_state_from_config_options(
    config_options: Option<&[agent_client_protocol::schema::SessionConfigOption]>,
) -> Option<SessionModelState> {
    let option = config_options?
        .iter()
        .find(|option| option.category.as_ref() == Some(&SessionConfigOptionCategory::Model))?;
    let SessionConfigKind::Select(select) = &option.kind else {
        return None;
    };

    let available_models = match &select.options {
        agent_client_protocol::schema::SessionConfigSelectOptions::Ungrouped(options) => options
            .iter()
            .map(|option| {
                ModelInfo::new(
                    ModelId::new(option.value.0.to_string()),
                    option.name.clone(),
                )
            })
            .collect(),
        agent_client_protocol::schema::SessionConfigSelectOptions::Grouped(groups) => groups
            .iter()
            .flat_map(|group| {
                group.options.iter().map(|option| {
                    ModelInfo::new(
                        ModelId::new(option.value.0.to_string()),
                        option.name.clone(),
                    )
                })
            })
            .collect(),
        _ => Vec::new(),
    };

    Some(SessionModelState::new(
        ModelId::new(select.current_value.0.to_string()),
        available_models,
    ))
}

fn collect_agent_text(updates: &Arc<Mutex<Vec<SessionNotification>>>) -> String {
    let guard = updates.lock().unwrap();
    let mut text = String::new();

    for notification in guard.iter() {
        if let SessionUpdate::AgentMessageChunk(chunk) = &notification.update {
            if let ContentBlock::Text(t) = &chunk.content {
                text.push_str(&t.text);
            }
        }
    }

    text
}

fn extract_tool_status(updates: &Arc<Mutex<Vec<SessionNotification>>>) -> Option<ToolCallStatus> {
    let guard = updates.lock().unwrap();
    guard.iter().find_map(|notification| {
        if let SessionUpdate::ToolCallUpdate(update) = &notification.update {
            return update.fields.status;
        }
        None
    })
}
