/// ACP Session Manager — session lifecycle management.
///
/// Wraps an [`AcpClient`] to provide strongly-typed session operations:
/// create, prompt, cancel, and receive typed events from the agent.
///
/// Reference:
/// - `reference-code/hermes-vscode-main/src/sessionManager.ts` (primary)
/// - `reference-code/hermes-vscode-main/src/protocol.ts` (dedup logic)
use crate::acp_client::AcpClient;
use crate::acp_types::{
    ContentBlock, ContentBlockWrapper, SessionLoadParams, SessionNewParams, SessionNewResult,
    SessionPromptParams, SessionPromptResult, SessionUpdate, ToolCallStatus,
};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::time::sleep;
use tracing::{debug, info, warn};

// ── Events ───────────────────────────────────────────

/// Strongly-typed event emitted by the session manager when a `session/update` arrives.
#[derive(Debug, Clone)]
pub enum AcpSessionEvent {
    /// Streaming text from the agent's response message.
    TextDelta { text: String },
    /// Streaming reasoning/thinking text.
    ThoughtDelta { text: String },
    /// 工具调用开始。
    ToolCall {
        tool_call_id: String,
        title: String,
        kind: String,
        status: ToolCallStatus,
        raw_input: Option<Value>,
        /// 工具涉及的文件路径。
        locations: Vec<String>,
    },
    /// 工具调用状态更新，可能携带结果内容。
    ToolCallUpdate {
        tool_call_id: String,
        status: ToolCallStatus,
        content: Option<Vec<ContentBlockWrapper>>,
    },
    /// 上下文用量更新。
    UsageUpdate { used: u64, size: u64 },
    /// Agent 发起权限请求。
    PermissionRequest {
        permission_id: String,
        tool_name: String,
        params: Value,
        decision: String,
    },
    /// 会话元数据更新，例如自动标题。
    SessionInfoUpdate { title: String },
    /// 计划条目更新。
    PlanUpdate { entries: Vec<String> },
    /// 连接关闭或异常。
    Disconnected { reason: String },
}

#[derive(Debug, Clone, Default)]
pub struct AcpMnoteToolContext {
    pub mnote_session_id: Option<String>,
    pub run_id: Option<String>,
    pub actor_id: Option<String>,
    pub trace_id: Option<String>,
    pub workspace_id: Option<String>,
    pub document_id: Option<String>,
}

/// Handler for session events.
pub type SessionEventHandler = Arc<dyn Fn(AcpSessionEvent) + Send + Sync + 'static>;

// ── Session state ────────────────────────────────────

#[derive(Debug, Clone, PartialEq)]
pub enum SessionState {
    Idle,
    Running,
    Cancelling,
    Closed,
}

// ── Pending permission ───────────────────────────────

/// 等待前端决策的 `session/request_permission`。
#[derive(Debug, Clone)]
pub struct PendingPermission {
    /// incoming ACP request 的原始 JSON-RPC id，可能是数字或字符串。
    pub jsonrpc_id: Value,
    /// `session/request_permission` 的原始参数。
    pub params: Value,
    /// 请求创建时间，用于后续超时判断。
    pub created_at: std::time::Instant,
}

// ── AcpSessionManager ────────────────────────────────

/// Manages ACP sessions — create, prompt, cancel, and event dispatch.
///
/// Currently supports one active session at a time. The internal [`AcpClient`]
/// handles the JSON-RPC wire protocol; this layer adds session semantics and
/// typed event dispatching.
pub struct AcpSessionManager {
    client: Arc<AcpClient>,
    session_id: Arc<Mutex<Option<String>>>,
    state: Arc<Mutex<SessionState>>,
    event_handler: Arc<Mutex<Option<SessionEventHandler>>>,
    /// Accumulated text for deduplication (per-turn).
    accumulated: Arc<Mutex<String>>,
    /// Whether we're currently inside a prompt (for dedup gating).
    in_prompt: Arc<Mutex<bool>>,
    /// Pending permission requests keyed by permission_id.
    pending_permissions: Arc<Mutex<HashMap<String, PendingPermission>>>,
}

impl AcpSessionManager {
    /// Create a new session manager wrapping an existing [`AcpClient`].
    ///
    /// Registers an internal notification handler that dispatches
    /// `session/update` notifications as typed [`AcpSessionEvent`]s.
    pub fn new(client: Arc<AcpClient>) -> Self {
        let session_id: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
        let state: Arc<Mutex<SessionState>> = Arc::new(Mutex::new(SessionState::Idle));
        let event_handler: Arc<Mutex<Option<SessionEventHandler>>> = Arc::new(Mutex::new(None));
        let accumulated: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
        let in_prompt: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
        let pending_permissions: Arc<Mutex<HashMap<String, PendingPermission>>> =
            Arc::new(Mutex::new(HashMap::new()));

        // ── Incoming request handler ──────────────────
        // 处理 `session/request_permission`：先进入 pending，再由 HTTP
        // resolve-permission 端点决定最终响应。
        let event_handler_for_incoming = event_handler.clone();
        let pending_for_incoming = pending_permissions.clone();
        let client_for_incoming = client.clone();
        client.on_incoming_request(move |id, method, params| {
            if method != "session/request_permission" {
                // 未知方法交回 dispatch_message 回复 method-not-found。
                return false;
            }

            // 优先使用 agent 传来的 permission id；缺失时用 JSON-RPC id 派生稳定 id。
            let permission_id = params
                .get("permissionId")
                .or_else(|| params.get("permission_id"))
                .or_else(|| params.get("requestId"))
                .and_then(Value::as_str)
                .filter(|s| !s.is_empty())
                .map(ToOwned::to_owned)
                .unwrap_or_else(|| format!("acp_perm_{}", id));

            let tool_name = params
                .get("toolName")
                .or_else(|| params.get("tool"))
                .or_else(|| params.get("name"))
                .or_else(|| params.get("method"))
                .and_then(Value::as_str)
                .unwrap_or("session/request_permission")
                .to_string();

            // 记录到 pending map，等待前端 allow / deny。
            {
                let mut pending = pending_for_incoming.lock().unwrap();
                pending.insert(
                    permission_id.clone(),
                    PendingPermission {
                        jsonrpc_id: id.clone(),
                        params: params.clone(),
                        created_at: std::time::Instant::now(),
                    },
                );
                debug!(
                    "ACP permission pending: {} (jsonrpc_id={}, tool={})",
                    permission_id, id, tool_name
                );
            }

            // 向前端发出 `permission.requested`。
            {
                let handler = event_handler_for_incoming.lock().unwrap();
                if let Some(ref h) = *handler {
                    h(AcpSessionEvent::PermissionRequest {
                        permission_id: permission_id.clone(),
                        tool_name,
                        params,
                        decision: "requested".into(),
                    });
                }
            }

            // 60 秒没有决策时自动 deny，避免 agent 永久等待。
            let pending_for_timeout = pending_for_incoming.clone();
            let event_handler_for_timeout = event_handler_for_incoming.clone();
            let client_for_timeout = client_for_incoming.clone();
            let timeout_permission_id = permission_id.clone();
            let timeout_jsonrpc_id = id.clone();
            tokio::spawn(async move {
                sleep(Duration::from_secs(60)).await;
                let should_deny = {
                    let mut pending = pending_for_timeout.lock().unwrap();
                    if pending.remove(&timeout_permission_id).is_some() {
                        true
                    } else {
                        false
                    }
                };
                if should_deny {
                    warn!(
                        "ACP permission timeout: {} (jsonrpc_id={}), auto-denying",
                        timeout_permission_id, timeout_jsonrpc_id
                    );
                    // 向 ACP 回复 permission denied。
                    let _ = client_for_timeout
                        .respond_to_incoming_error(
                            timeout_jsonrpc_id.clone(),
                            -32000,
                            "permission denied by timeout",
                        )
                        .await;
                    // 同步 denied 事件给前端。
                    let handler = event_handler_for_timeout.lock().unwrap();
                    if let Some(ref h) = *handler {
                        h(AcpSessionEvent::PermissionRequest {
                            permission_id: timeout_permission_id,
                            tool_name: "session/request_permission".into(),
                            params: Value::Null,
                            decision: "denied".into(),
                        });
                    }
                }
            });

            true // handler took responsibility for responding
        });

        // ── Notification handler ──────────────────────
        // Only handles `session/update` (streaming events from agent).
        // Incoming request methods like `session/request_permission` are handled
        // above via `on_incoming_request`.
        let session_id_clone = session_id.clone();
        let event_handler_clone = event_handler.clone();
        let accumulated_clone = accumulated.clone();
        let in_prompt_clone = in_prompt.clone();

        client.on_notification(move |method, params| {
            if method != "session/update" {
                return;
            }
            let sid = session_id_clone.lock().unwrap().clone();
            if let Some(ref session_id) = sid {
                if let Some(msg_sid) = params.get("sessionId").and_then(|v| v.as_str()) {
                    if msg_sid != session_id {
                        return; // not our session
                    }
                }
            }

            // Parse the update
            let update: SessionUpdate = match serde_json::from_value(
                params.get("update").cloned().unwrap_or(Value::Null),
            ) {
                Ok(u) => u,
                Err(e) => {
                    warn!("ACP session/update parse error: {e}");
                    return;
                }
            };

            let is_in_prompt = *in_prompt_clone.lock().unwrap();
            let event = Self::session_update_to_event(&update, &accumulated_clone, is_in_prompt);

            if let Some(ev) = event {
                let handler = event_handler_clone.lock().unwrap();
                if let Some(ref h) = *handler {
                    h(ev);
                }
            }
        });

        Self {
            client,
            session_id,
            state,
            event_handler,
            accumulated,
            in_prompt,
            pending_permissions,
        }
    }

    /// Register an event handler for session events.
    /// Only one handler at a time — subsequent calls replace the previous.
    pub fn on_event<F>(&self, handler: F)
    where
        F: Fn(AcpSessionEvent) + Send + Sync + 'static,
    {
        let mut guard = self.event_handler.lock().unwrap();
        *guard = Some(Arc::new(handler));
    }

    /// 按用户决策解析 pending permission。
    ///
    /// 根据 `permission_id` 找到 pending request，向 ACP 子进程回写 result/error，
    /// 发出 `permission.allowed` / `permission.denied` 事件，并从 pending map 移除。
    ///
    /// permission 存在且完成响应时返回 `Ok(())`，否则返回错误原因。
    pub async fn resolve_permission(
        &self,
        permission_id: &str,
        decision: &str,
    ) -> Result<(), String> {
        let pending = {
            let mut map = self.pending_permissions.lock().unwrap();
            map.remove(permission_id)
        };
        let pending = pending.ok_or_else(|| {
            format!("pending permission not found: {permission_id} (already timed out or invalid)")
        })?;

        let tool_name = pending
            .params
            .get("toolName")
            .or_else(|| pending.params.get("tool"))
            .or_else(|| pending.params.get("name"))
            .and_then(Value::as_str)
            .unwrap_or("session/request_permission")
            .to_string();

        let normalized_decision = match decision {
            "allow" | "allowed" => "allow",
            "deny" | "denied" => "deny",
            other => return Err(format!("unknown decision: {other} (expected allow/deny)")),
        };

        let response = permission_response_for_decision(&pending.params, normalized_decision)
            .unwrap_or_else(|| {
                if normalized_decision == "allow" {
                    Err((-32000, "permission allow option not available".into()))
                } else {
                    Err((-32000, "permission denied by user".into()))
                }
            });

        match response {
            Ok(result) => {
                self.client
                    .respond_to_incoming(pending.jsonrpc_id.clone(), result)
                    .await
                    .map_err(|e| format!("ACP respond failed: {e}"))?;
                info!(
                    "ACP permission resolved: {} decision={} (jsonrpc_id={})",
                    permission_id, normalized_decision, pending.jsonrpc_id
                );
            }
            Err((code, message)) => {
                self.client
                    .respond_to_incoming_error(pending.jsonrpc_id.clone(), code, &message)
                    .await
                    .map_err(|e| format!("ACP respond failed: {e}"))?;
                info!(
                    "ACP permission rejected: {} decision={} (jsonrpc_id={})",
                    permission_id, normalized_decision, pending.jsonrpc_id
                );
            }
        }

        // 发出最终 decision 事件。
        {
            let handler = self.event_handler.lock().unwrap();
            if let Some(ref h) = *handler {
                h(AcpSessionEvent::PermissionRequest {
                    permission_id: permission_id.to_string(),
                    tool_name,
                    params: pending.params,
                    decision: if normalized_decision == "allow" {
                        "allowed".into()
                    } else {
                        "denied".into()
                    },
                });
            }
        }

        Ok(())
    }

    /// Create a new ACP session.
    ///
    /// Sends `session/new` to the agent and stores the returned `sessionId`.
    /// The `page_context` is optional metadata about the current document.
    pub async fn create_session(
        &self,
        cwd: Option<&str>,
        _page_context: Option<Value>,
    ) -> Result<String, crate::acp_client::AcpError> {
        let project_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .ancestors()
            .nth(3)
            .unwrap_or(std::path::Path::new("/mnt/Data1T/mnote"))
            .to_string_lossy()
            .to_string();
        let params = SessionNewParams {
            cwd: Some(cwd.map(str::to_string).unwrap_or(project_root)),
            mcp_servers: Some(Vec::new()),
        };

        let result: SessionNewResult = self.client.request("session/new", params).await?;
        let mut sid_guard = self.session_id.lock().unwrap();
        *sid_guard = Some(result.session_id.clone());
        info!("ACP session created: {}", result.session_id);

        Ok(result.session_id)
    }

    /// Load/resume an existing ACP session.
    ///
    /// Sends `session/load { sessionId, cwd, mcpServers: [] }` to the agent.
    /// Returns `true` if the session was loaded and the internal session_id updated,
    /// `false` if the adapter returned null (session not found) or doesn't support the method.
    ///
    /// On success the internal `session_id` is set to the loaded id.
    ///
    /// Reference: `hermes-vscode-main/src/sessionManager.ts` `ensureSession()`
    pub async fn load_session(
        &self,
        session_id: &str,
        cwd: Option<&str>,
    ) -> Result<bool, crate::acp_client::AcpError> {
        let project_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .ancestors()
            .nth(3)
            .unwrap_or(std::path::Path::new("/mnt/Data1T/mnote"))
            .to_string_lossy()
            .to_string();
        let params = SessionLoadParams {
            session_id: session_id.to_string(),
            cwd: Some(cwd.map(str::to_string).unwrap_or(project_root)),
            mcp_servers: Some(Vec::new()),
        };

        match self
            .client
            .request::<_, serde_json::Value>("session/load", params)
            .await
        {
            Ok(result) => {
                // Successful load: adapter returned { sessionId: "..." }
                if let Some(sid) = result.get("sessionId").and_then(|v| v.as_str()) {
                    if !sid.is_empty() {
                        let mut sid_guard = self.session_id.lock().unwrap();
                        *sid_guard = Some(sid.to_string());
                        info!("ACP session loaded: {}", sid);
                        return Ok(true);
                    }
                }
                // Null or missing sessionId → session not found on adapter
                info!(
                    "ACP session/load returned null for session_id={}",
                    session_id
                );
                Ok(false)
            }
            Err(crate::acp_client::AcpError::JsonRpc { code, message }) => {
                // Adapter doesn't support session/load or session expired
                info!(
                    "ACP session/load not supported (code={code}, message={message}), will fallback"
                );
                Ok(false)
            }
            Err(e) => {
                warn!("ACP session/load transport error: {e}");
                Err(e)
            }
        }
    }

    /// Ensure an active ACP session exists.
    ///
    /// 1. If there's already an active session, return it immediately.
    /// 2. If a `stored_session_id` is provided, attempt `session/load` first.
    /// 3. Fall back to `session/new`.
    ///
    /// Reference: `hermes-vscode-main/src/sessionManager.ts` `ensureSession()`
    pub async fn ensure_session(
        &self,
        cwd: Option<&str>,
        stored_session_id: Option<&str>,
    ) -> Result<String, crate::acp_client::AcpError> {
        // 1. Reuse active session if one exists
        {
            let sid = self.session_id.lock().unwrap();
            if let Some(ref sid) = *sid {
                debug!("ACP ensure_session: reusing active session {}", sid);
                return Ok(sid.clone());
            }
        }

        // 2. Try to load a stored session
        if let Some(stored) = stored_session_id {
            if !stored.is_empty() {
                debug!("ACP ensure_session: attempting session/load for {}", stored);
                if self.load_session(stored, cwd).await.unwrap_or(false) {
                    return Ok(stored.to_string());
                }
                info!(
                    "ACP ensure_session: stored session {} not found, creating new",
                    stored
                );
            }
        }

        // 3. Fallback: create new session
        debug!("ACP ensure_session: creating new session");
        self.create_session(cwd, None).await
    }

    /// Send a prompt to the agent and stream events.
    ///
    /// The `prompt` is a list of content blocks (text + optional page context).
    /// Returns once the agent finishes (stopReason received) or on error.
    ///
    /// Sets `in_prompt = true` during the call to enable deduplication,
    /// then resets to `false` and clears accumulated text on completion.
    pub async fn run_prompt(
        &self,
        prompt: Vec<ContentBlock>,
    ) -> Result<SessionPromptResult, crate::acp_client::AcpError> {
        self.run_prompt_with_mnote_context(prompt, None).await
    }

    pub async fn run_prompt_with_mnote_context(
        &self,
        prompt: Vec<ContentBlock>,
        mnote_context: Option<AcpMnoteToolContext>,
    ) -> Result<SessionPromptResult, crate::acp_client::AcpError> {
        {
            let mut in_prompt = self.in_prompt.lock().unwrap();
            *in_prompt = true;
        }
        {
            let mut acc = self.accumulated.lock().unwrap();
            acc.clear();
        }
        {
            let mut state = self.state.lock().unwrap();
            *state = SessionState::Running;
        }

        let sid = self.session_id.lock().unwrap().clone();
        let session_id = sid.ok_or_else(|| {
            self.reset_prompt_state();
            crate::acp_client::AcpError::Internal(
                "no session created yet — call create_session first".into(),
            )
        })?;

        let mnote_context = mnote_context.unwrap_or_default();
        let params = SessionPromptParams {
            session_id: session_id.clone(),
            prompt,
            mnote_session_id: mnote_context.mnote_session_id,
            run_id: mnote_context.run_id,
            actor_id: mnote_context.actor_id,
            trace_id: mnote_context.trace_id,
            workspace_id: mnote_context.workspace_id,
            document_id: mnote_context.document_id,
        };

        debug!("ACP session/prompt (session={})", session_id);
        let result: Result<SessionPromptResult, crate::acp_client::AcpError> =
            self.client.request("session/prompt", params).await;
        if let Ok(ref prompt_result) = result {
            debug!(
                "ACP session/prompt done (session={}, stop_reason={:?})",
                session_id, prompt_result.stop_reason
            );
        }
        self.reset_prompt_state();

        result
    }

    /// Cancel the current prompt.
    ///
    /// Sends `session/cancel` notification to the agent.
    pub async fn cancel(&self) -> Result<(), crate::acp_client::AcpError> {
        let sid = self.session_id.lock().unwrap().clone();
        let session_id =
            sid.ok_or_else(|| crate::acp_client::AcpError::Internal("no active session".into()))?;

        {
            let mut state = self.state.lock().unwrap();
            *state = SessionState::Cancelling;
        }

        self.client
            .notification("session/cancel", json!({ "sessionId": session_id }))
            .await?;

        info!("ACP session cancelled: {}", session_id);
        Ok(())
    }

    /// Close the session and the underlying ACP client.
    pub async fn close(&self) {
        {
            let mut state = self.state.lock().unwrap();
            *state = SessionState::Closed;
        }

        if let Some(handler) = self.event_handler.lock().unwrap().take() {
            handler(AcpSessionEvent::Disconnected {
                reason: "session closed".into(),
            });
        }
    }

    /// Get the current session ID, if any.
    pub async fn session_id(&self) -> Option<String> {
        self.session_id.lock().unwrap().clone()
    }

    /// Get the current session state.
    pub async fn state(&self) -> SessionState {
        self.state.lock().unwrap().clone()
    }

    fn reset_prompt_state(&self) {
        {
            let mut state = self.state.lock().unwrap();
            *state = SessionState::Idle;
        }
        {
            let mut in_prompt = self.in_prompt.lock().unwrap();
            *in_prompt = false;
        }
        {
            let mut acc = self.accumulated.lock().unwrap();
            acc.clear();
        }
    }

    // ── Internal: session/update → event mapping ─────

    /// Convert a parsed [`SessionUpdate`] into an [`AcpSessionEvent`],
    /// applying text deduplication for streaming text.
    ///
    /// Reference: `hermes-vscode-main/src/protocol.ts` `extractTextContent()`,
    /// `deduplicateChunk()`, `parseToolCall()`, `parseToolCallUpdate()`,
    /// `parseUsageUpdate()`, `parseSessionInfoUpdate()`
    fn session_update_to_event(
        update: &SessionUpdate,
        accumulated: &Arc<Mutex<String>>,
        is_in_prompt: bool,
    ) -> Option<AcpSessionEvent> {
        match update {
            SessionUpdate::AgentMessageChunk { content, .. }
            | SessionUpdate::AgentThoughtChunk { content, .. } => {
                let discrim = match update {
                    SessionUpdate::AgentMessageChunk { .. } => "msg",
                    SessionUpdate::AgentThoughtChunk { .. } => "thought",
                    _ => unreachable!(),
                };

                // Deduplication (reference: protocol.ts deduplicateChunk)
                if is_in_prompt {
                    let mut acc = accumulated.lock().unwrap();
                    let text = &content.text;
                    let event = if text == acc.as_str() {
                        // Exact full resend → drop
                        None
                    } else if text.len() > 10 && text.starts_with(acc.as_str()) {
                        // Superset resend → emit only the tail
                        let new_part = text[acc.len()..].to_string();
                        if new_part.is_empty() {
                            None
                        } else {
                            *acc = text.clone();
                            Some(new_part)
                        }
                    } else if text.len() > 10 && acc.ends_with(text) {
                        // Partial resend → drop
                        None
                    } else {
                        // Normal delta
                        let new_acc = format!("{}{}", acc, text);
                        *acc = new_acc;
                        Some(text.clone())
                    };

                    return event.map(|t| match discrim {
                        "msg" => AcpSessionEvent::TextDelta { text: t },
                        "thought" => AcpSessionEvent::ThoughtDelta { text: t },
                        _ => unreachable!(),
                    });
                }

                // Not in prompt — emit directly (historical playback)
                let text = content.text.clone();
                Some(match discrim {
                    "msg" => AcpSessionEvent::TextDelta { text },
                    "thought" => AcpSessionEvent::ThoughtDelta { text },
                    _ => unreachable!(),
                })
            }

            SessionUpdate::ToolCall {
                tool_call_id,
                title,
                kind,
                status,
                raw_input,
                locations,
                ..
            } => {
                let title = title.clone().unwrap_or_else(|| "tool".into());
                let kind_str = match kind {
                    Some(k) => format!("{:?}", k).to_lowercase(),
                    None => "other".into(),
                };
                let status = status.clone().unwrap_or(ToolCallStatus::Pending);
                let locations: Vec<String> = locations.iter().map(|l| l.path.clone()).collect();
                Some(AcpSessionEvent::ToolCall {
                    tool_call_id: tool_call_id.clone(),
                    title,
                    kind: kind_str,
                    status,
                    raw_input: raw_input.clone(),
                    locations,
                })
            }

            SessionUpdate::ToolCallUpdate {
                tool_call_id,
                status,
                content,
                ..
            } => {
                let status = status.clone().unwrap_or(ToolCallStatus::Completed);
                Some(AcpSessionEvent::ToolCallUpdate {
                    tool_call_id: tool_call_id.clone(),
                    status,
                    content: content.clone(),
                })
            }

            SessionUpdate::UsageUpdate { used, size, .. } => Some(AcpSessionEvent::UsageUpdate {
                used: *used,
                size: *size,
            }),

            SessionUpdate::SessionInfoUpdate { title, .. } => {
                Some(AcpSessionEvent::SessionInfoUpdate {
                    title: title.clone(),
                })
            }

            SessionUpdate::Plan { entries, .. } => {
                let summaries: Vec<String> = entries.iter().map(|e| e.content.clone()).collect();
                Some(AcpSessionEvent::PlanUpdate { entries: summaries })
            }

            SessionUpdate::Unknown { .. } => {
                warn!("ACP unknown session/update variant");
                None
            }
        }
    }
}

fn permission_response_for_decision(
    params: &Value,
    decision: &str,
) -> Option<Result<Value, (i64, String)>> {
    let option_id = permission_option_id_by_decision(params, decision);
    match (decision, option_id) {
        ("allow", Some(option_id)) | ("deny", Some(option_id)) => Some(Ok(json!({
            "outcome": "selected",
            "optionId": option_id
        }))),
        ("deny", None) => Some(Err((-32000, "permission denied by user".into()))),
        _ => None,
    }
}

fn permission_option_id_by_decision(params: &Value, decision: &str) -> Option<String> {
    let options = params.get("options").and_then(Value::as_array)?;
    let preferred: &[&str] = if decision == "allow" {
        &["allow_once", "allow", "approve", "yes"]
    } else {
        &["deny_once", "reject_once", "deny", "reject", "no"]
    };

    for keyword in preferred.iter().copied() {
        if let Some(option_id) = options.iter().find_map(|option| {
            let id = permission_option_id(option)?;
            let haystack = format!(
                "{} {}",
                id.to_ascii_lowercase(),
                option
                    .get("kind")
                    .and_then(Value::as_str)
                    .unwrap_or("")
                    .to_ascii_lowercase()
            );
            if haystack.contains(keyword) {
                Some(id)
            } else {
                None
            }
        }) {
            return Some(option_id);
        }
    }

    if decision == "allow" {
        return options.iter().find_map(|option| {
            let id = permission_option_id(option)?;
            let lower = id.to_ascii_lowercase();
            if lower.contains("deny") || lower.contains("reject") || lower == "no" {
                None
            } else {
                Some(id)
            }
        });
    }

    None
}

fn permission_option_id(option: &Value) -> Option<String> {
    option
        .get("optionId")
        .or_else(|| option.get("option_id"))
        .or_else(|| option.get("id"))
        .and_then(Value::as_str)
        .filter(|value| !value.trim().is_empty())
        .map(ToOwned::to_owned)
}

// ── Tests ────────────────────────────────────────────

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

    /// Creates a minimal ACP mock server for testing session operations.
    async fn spawn_mock_acp() -> Arc<AcpClient> {
        let script = r#"
import * as readline from 'node:readline';
import { stdin as input, stdout as output } from 'node:process';
const rl = readline.createInterface({ input, output, terminal: false });
rl.on('line', (line) => {
    const msg = JSON.parse(line);
    if (!msg.id) return;
    if (msg.method === 'session/new') {
        process.stdout.write(JSON.stringify({
            jsonrpc: '2.0', id: msg.id,
            result: { sessionId: 'test_session_1' }
        }) + '\n');
    } else if (msg.method === 'session/prompt') {
        const sessionId = msg.params?.sessionId || 'test';
        process.stdout.write(JSON.stringify({
            jsonrpc: '2.0',
            method: 'session/update',
            params: {
                sessionId,
                update: {
                    sessionUpdate: 'agent_message_chunk',
                    content: { type: 'text', text: 'Hello from mock ACP' }
                }
            }
        }) + '\n');
        process.stdout.write(JSON.stringify({
            jsonrpc: '2.0', id: msg.id,
            result: { stopReason: 'end_turn' }
        }) + '\n');
    } else if (msg.method === 'session/cancel') {
        // No response for notification
    } else if (msg.method === 'initialize') {
        process.stdout.write(JSON.stringify({
            jsonrpc: '2.0', id: msg.id,
            result: { protocolVersion: 1, agentCapabilities: {}, agentInfo: { name: 'mock', version: '1.0' }, authMethods: [] }
        }) + '\n');
    }
});
        "#;
        let dir = std::env::temp_dir();
        let script_path = dir.join("acp_session_test_mock.mjs");
        std::fs::write(&script_path, script).expect("write mock");
        let client = AcpClient::spawn("node", &[script_path.to_str().unwrap()])
            .await
            .expect("spawn");
        Arc::new(client)
    }

    #[tokio::test]
    async fn test_create_session() {
        let client = spawn_mock_acp().await;
        let mgr = AcpSessionManager::new(client);
        let sid = mgr
            .create_session(Some("/test"), None)
            .await
            .expect("create_session");
        assert_eq!(sid, "test_session_1");
        assert_eq!(mgr.session_id().await, Some("test_session_1".into()));
    }

    #[tokio::test]
    async fn test_run_prompt() {
        let client = spawn_mock_acp().await;
        let mgr = AcpSessionManager::new(client);
        mgr.create_session(Some("/test"), None)
            .await
            .expect("create_session");

        let prompt = vec![ContentBlock::Text {
            text: "Hello agent".into(),
        }];
        let result = mgr.run_prompt(prompt).await.expect("run_prompt");
        assert_eq!(format!("{:?}", result.stop_reason), "EndTurn".to_string());
        // After prompt, state should be idle again
        assert_eq!(mgr.state().await, SessionState::Idle);
    }

    #[tokio::test]
    async fn test_cancel() {
        let client = spawn_mock_acp().await;
        let mgr = AcpSessionManager::new(client);
        mgr.create_session(Some("/test"), None)
            .await
            .expect("create_session");
        mgr.cancel().await.expect("cancel");
    }

    #[tokio::test]
    async fn test_event_handler() {
        use std::sync::atomic::{AtomicBool, Ordering};
        let client = spawn_mock_acp().await;
        let mgr = AcpSessionManager::new(client);
        let received = Arc::new(AtomicBool::new(false));
        let r = received.clone();

        mgr.on_event(move |ev| {
            if matches!(ev, AcpSessionEvent::TextDelta { .. }) {
                r.store(true, Ordering::SeqCst);
            }
        });

        mgr.create_session(Some("/test"), None)
            .await
            .expect("create_session");
        let prompt = vec![ContentBlock::Text {
            text: "Hello".into(),
        }];
        mgr.run_prompt(prompt).await.expect("run_prompt");

        // Give the notification handler time to process
        sleep(Duration::from_millis(200)).await;
        assert!(
            received.load(Ordering::SeqCst),
            "should have received TextDelta"
        );
    }

    /// Creates a mock ACP that supports session/load (success case).
    async fn spawn_mock_acp_with_load() -> Arc<AcpClient> {
        let script = r#"
import * as readline from 'node:readline';
import { stdin as input, stdout as output } from 'node:process';
const rl = readline.createInterface({ input, output, terminal: false });
rl.on('line', (line) => {
    const msg = JSON.parse(line);
    if (!msg.id) return;
    if (msg.method === 'session/load') {
        process.stdout.write(JSON.stringify({
            jsonrpc: '2.0', id: msg.id,
            result: { sessionId: msg.params?.sessionId || 'loaded_session_1' }
        }) + '\n');
    } else if (msg.method === 'session/new') {
        process.stdout.write(JSON.stringify({
            jsonrpc: '2.0', id: msg.id,
            result: { sessionId: 'test_session_1' }
        }) + '\n');
    } else if (msg.method === 'session/prompt') {
        const sessionId = msg.params?.sessionId || 'test';
        process.stdout.write(JSON.stringify({
            jsonrpc: '2.0',
            method: 'session/update',
            params: {
                sessionId,
                update: {
                    sessionUpdate: 'agent_message_chunk',
                    content: { type: 'text', text: 'Hello from mock ACP' }
                }
            }
        }) + '\n');
        process.stdout.write(JSON.stringify({
            jsonrpc: '2.0', id: msg.id,
            result: { stopReason: 'end_turn' }
        }) + '\n');
    } else if (msg.method === 'session/cancel') {
        // No response for notification
    } else if (msg.method === 'initialize') {
        process.stdout.write(JSON.stringify({
            jsonrpc: '2.0', id: msg.id,
            result: { protocolVersion: 1, agentCapabilities: {}, agentInfo: { name: 'mock', version: '1.0' }, authMethods: [] }
        }) + '\n');
    }
});
        "#;
        let dir = std::env::temp_dir();
        let script_path = dir.join("acp_session_load_success_mock.mjs");
        std::fs::write(&script_path, script).expect("write mock");
        let client = AcpClient::spawn("node", &[script_path.to_str().unwrap()])
            .await
            .expect("spawn");
        Arc::new(client)
    }

    /// Creates a mock ACP that returns null for session/load (session expired/not found)
    async fn spawn_mock_acp_with_load_null() -> Arc<AcpClient> {
        let script = r#"
import * as readline from 'node:readline';
import { stdin as input, stdout as output } from 'node:process';
const rl = readline.createInterface({ input, output, terminal: false });
rl.on('line', (line) => {
    const msg = JSON.parse(line);
    if (!msg.id) return;
    if (msg.method === 'session/load') {
        // Return null result → session not found on adapter
        process.stdout.write(JSON.stringify({
            jsonrpc: '2.0', id: msg.id,
            result: null
        }) + '\n');
    } else if (msg.method === 'session/new') {
        process.stdout.write(JSON.stringify({
            jsonrpc: '2.0', id: msg.id,
            result: { sessionId: 'fallback_session_1' }
        }) + '\n');
    } else if (msg.method === 'session/prompt') {
        const sessionId = msg.params?.sessionId || 'fallback_session_1';
        process.stdout.write(JSON.stringify({
            jsonrpc: '2.0',
            method: 'session/update',
            params: {
                sessionId,
                update: {
                    sessionUpdate: 'agent_message_chunk',
                    content: { type: 'text', text: 'Hello from fallback session' }
                }
            }
        }) + '\n');
        process.stdout.write(JSON.stringify({
            jsonrpc: '2.0', id: msg.id,
            result: { stopReason: 'end_turn' }
        }) + '\n');
    } else if (msg.method === 'session/cancel') {
        // No response for notification
    } else if (msg.method === 'initialize') {
        process.stdout.write(JSON.stringify({
            jsonrpc: '2.0', id: msg.id,
            result: { protocolVersion: 1, agentCapabilities: {}, agentInfo: { name: 'mock', version: '1.0' }, authMethods: [] }
        }) + '\n');
    }
});
        "#;
        let dir = std::env::temp_dir();
        let script_path = dir.join("acp_session_load_null_mock.mjs");
        std::fs::write(&script_path, script).expect("write mock");
        let client = AcpClient::spawn("node", &[script_path.to_str().unwrap()])
            .await
            .expect("spawn");
        Arc::new(client)
    }

    #[tokio::test]
    async fn test_load_session_success() {
        let client = spawn_mock_acp_with_load().await;
        let mgr = AcpSessionManager::new(client);
        let loaded = mgr
            .load_session("stored_session_1", Some("/test"))
            .await
            .expect("load_session");
        assert!(loaded, "session should be loaded successfully");
        assert_eq!(mgr.session_id().await, Some("stored_session_1".into()));
    }

    #[tokio::test]
    async fn test_load_session_null_fallback() {
        let client = spawn_mock_acp_with_load_null().await;
        let mgr = AcpSessionManager::new(client);

        // load_session returns false for null result
        let loaded = mgr
            .load_session("expired_session", Some("/test"))
            .await
            .expect("load_session");
        assert!(!loaded, "session should not be loaded");

        // No session_id set after failed load
        assert_eq!(mgr.session_id().await, None);
    }

    #[tokio::test]
    async fn test_ensure_session_reuses_active() {
        let client = spawn_mock_acp().await;
        let mgr = AcpSessionManager::new(client);

        // First, create a session
        let created = mgr
            .create_session(Some("/test"), None)
            .await
            .expect("create_session");
        assert_eq!(created, "test_session_1");

        // ensure_session should reuse the active session, not create a new one
        let ensured = mgr
            .ensure_session(Some("/test"), Some("stored_but_ignored"))
            .await
            .expect("ensure_session");
        assert_eq!(ensured, "test_session_1");
        assert_eq!(mgr.session_id().await, Some("test_session_1".into()));
    }

    #[tokio::test]
    async fn test_ensure_session_loads_stored() {
        let client = spawn_mock_acp_with_load().await;
        let mgr = AcpSessionManager::new(client);

        // No active session, stored id provided → should load
        let ensured = mgr
            .ensure_session(Some("/test"), Some("stored_session_1"))
            .await
            .expect("ensure_session");
        assert_eq!(ensured, "stored_session_1");
        assert_eq!(mgr.session_id().await, Some("stored_session_1".into()));
    }

    #[tokio::test]
    async fn test_ensure_session_fallback_to_new() {
        let client = spawn_mock_acp_with_load_null().await;
        let mgr = AcpSessionManager::new(client);

        // No active session, stored id provided but load returns null → fallback to new
        let ensured = mgr
            .ensure_session(Some("/test"), Some("expired_session"))
            .await
            .expect("ensure_session");
        assert_eq!(ensured, "fallback_session_1");
        assert_eq!(mgr.session_id().await, Some("fallback_session_1".into()));
    }

    #[tokio::test]
    async fn test_ensure_session_no_stored_creates_new() {
        let client = spawn_mock_acp().await;
        let mgr = AcpSessionManager::new(client);

        // No active session, no stored id → should create new
        let ensured = mgr
            .ensure_session(Some("/test"), None)
            .await
            .expect("ensure_session");
        assert_eq!(ensured, "test_session_1");
        assert_eq!(mgr.session_id().await, Some("test_session_1".into()));
    }

    #[tokio::test]
    async fn test_ensure_session_fallback_then_prompt_works() {
        let client = spawn_mock_acp_with_load_null().await;
        let mgr = AcpSessionManager::new(client);

        // ensure_session with expired stored id → should create fallback
        mgr.ensure_session(Some("/test"), Some("expired_session"))
            .await
            .expect("ensure_session");

        // Verify prompt still works on the fallback session
        let prompt = vec![ContentBlock::Text {
            text: "Hello after fallback".into(),
        }];
        let result = mgr
            .run_prompt(prompt)
            .await
            .expect("run_prompt after fallback");
        assert_eq!(format!("{:?}", result.stop_reason), "EndTurn".to_string());
    }

    #[test]
    fn test_thought_chunk_maps_to_thought_delta() {
        let accumulated = Arc::new(Mutex::new(String::new()));
        let update = SessionUpdate::AgentThoughtChunk {
            session_update: SessionUpdate::AGENT_THOUGHT_CHUNK.into(),
            content: crate::acp_types::TextContent {
                content_type: "text".into(),
                text: "internal reasoning".into(),
            },
        };

        let event = AcpSessionManager::session_update_to_event(&update, &accumulated, true)
            .expect("thought chunk should emit event");

        match event {
            AcpSessionEvent::ThoughtDelta { text } => assert_eq!(text, "internal reasoning"),
            other => panic!("expected ThoughtDelta, got {other:?}"),
        }
    }

    #[test]
    fn permission_response_selects_allow_and_deny_options() {
        let params = json!({
            "options": [
                {"optionId": "allow_once", "name": "Allow Once", "kind": "allow_once"},
                {"optionId": "reject_once", "name": "Reject Once", "kind": "reject_once"}
            ]
        });

        let allow = permission_response_for_decision(&params, "allow")
            .expect("allow response")
            .expect("allow should select an option");
        assert_eq!(allow["outcome"], "selected");
        assert_eq!(allow["optionId"], "allow_once");

        let deny = permission_response_for_decision(&params, "deny")
            .expect("deny response")
            .expect("deny should select an option");
        assert_eq!(deny["outcome"], "selected");
        assert_eq!(deny["optionId"], "reject_once");
    }
}
