//! Session lifecycle methods for ACP.
//!
//! This module implements session management methods including:
//! - `session/new` - Create new sessions with working directory and MCP servers
//! - `session/load` - Load existing sessions with history replay
//! - `session/set_mode` - Change session mode (e.g., "code", "chat")
//! - `session/set_model` - Change model (optional, unstable in spec)
//! - `session/cancel` - Cancel ongoing prompt turns

use crate::acp::connector_state::{
    AgentCapabilities, McpServer,
};
use crate::acp::transport::{JsonRpcRequest, JsonRpcResult, Transport, TransportError};
use serde::{Deserialize, Serialize};
use std::path::Path;

/// Request to create a new session.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionNewRequest {
    /// Working directory for the session (absolute path).
    pub cwd: String,
    /// MCP servers to use for this session.
    #[serde(default)]
    pub mcp_servers: Vec<McpServer>,
}

impl SessionNewRequest {
    /// Create a new session/new request.
    ///
    /// # Arguments
    ///
    /// * `cwd` - Working directory (absolute path)
    /// * `mcp_servers` - MCP servers to use
    ///
    /// # Panics
    ///
    /// Panics if cwd is not an absolute path.
    pub fn new(cwd: impl Into<String>, mcp_servers: Vec<McpServer>) -> Self {
        let cwd = cwd.into();
        assert!(
            Path::new(&cwd).is_absolute(),
            "Working directory must be an absolute path: {}",
            cwd
        );
        Self { cwd, mcp_servers }
    }

    /// Convert to JSON-RPC request.
    pub fn to_jsonrpc(&self, id: u64) -> JsonRpcRequest {
        JsonRpcRequest::new(
            id,
            "session/new",
            Some(serde_json::to_value(self).expect("Failed to serialize SessionNewRequest")),
        )
    }
}

/// Response from session/new.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionNewResponse {
    /// Unique identifier for the created session.
    pub session_id: String,
}

impl SessionNewResponse {
    /// Parse from JSON-RPC result.
    pub fn from_jsonrpc(result: &serde_json::Value) -> Result<Self, serde_json::Error> {
        serde_json::from_value(result.clone())
    }
}

/// Request to load an existing session.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionLoadRequest {
    /// Session ID to load.
    pub session_id: String,
    /// Working directory for the session (absolute path).
    pub cwd: String,
    /// MCP servers to use for this session.
    #[serde(default)]
    pub mcp_servers: Vec<McpServer>,
}

impl SessionLoadRequest {
    /// Create a new session/load request.
    ///
    /// # Arguments
    ///
    /// * `session_id` - ID of the session to load
    /// * `cwd` - Working directory (absolute path)
    /// * `mcp_servers` - MCP servers to use
    ///
    /// # Panics
    ///
    /// Panics if cwd is not an absolute path.
    pub fn new(
        session_id: impl Into<String>,
        cwd: impl Into<String>,
        mcp_servers: Vec<McpServer>,
    ) -> Self {
        let cwd = cwd.into();
        assert!(
            Path::new(&cwd).is_absolute(),
            "Working directory must be an absolute path: {}",
            cwd
        );
        Self {
            session_id: session_id.into(),
            cwd,
            mcp_servers,
        }
    }

    /// Convert to JSON-RPC request.
    pub fn to_jsonrpc(&self, id: u64) -> JsonRpcRequest {
        JsonRpcRequest::new(
            id,
            "session/load",
            Some(serde_json::to_value(self).expect("Failed to serialize SessionLoadRequest")),
        )
    }
}

/// Response from session/load.
///
/// The response itself is empty (null in JSON), but the agent will send
/// session/update notifications to replay the conversation history.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionLoadResponse;

impl SessionLoadResponse {
    /// Parse from JSON-RPC result.
    pub fn from_jsonrpc(_result: &serde_json::Value) -> Result<Self, serde_json::Error> {
        Ok(SessionLoadResponse)
    }
}

/// Request to change session mode.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionSetModeRequest {
    /// Session ID to update.
    pub session_id: String,
    /// Mode to switch to (e.g., "code", "chat").
    pub mode: String,
}

impl SessionSetModeRequest {
    /// Create a new session/set_mode request.
    pub fn new(session_id: impl Into<String>, mode: impl Into<String>) -> Self {
        Self {
            session_id: session_id.into(),
            mode: mode.into(),
        }
    }

    /// Convert to JSON-RPC request.
    pub fn to_jsonrpc(&self, id: u64) -> JsonRpcRequest {
        JsonRpcRequest::new(
            id,
            "session/set_mode",
            Some(serde_json::to_value(self).expect("Failed to serialize SessionSetModeRequest")),
        )
    }
}

/// Response from session/set_mode (empty).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionSetModeResponse;

impl SessionSetModeResponse {
    /// Parse from JSON-RPC result.
    pub fn from_jsonrpc(_result: &serde_json::Value) -> Result<Self, serde_json::Error> {
        Ok(SessionSetModeResponse)
    }
}

/// Request to change session model (UNSTABLE in spec).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionSetModelRequest {
    /// Session ID to update.
    pub session_id: String,
    /// Model identifier to switch to.
    pub model: String,
}

impl SessionSetModelRequest {
    /// Create a new session/set_model request.
    pub fn new(session_id: impl Into<String>, model: impl Into<String>) -> Self {
        Self {
            session_id: session_id.into(),
            model: model.into(),
        }
    }

    /// Convert to JSON-RPC request.
    pub fn to_jsonrpc(&self, id: u64) -> JsonRpcRequest {
        JsonRpcRequest::new(
            id,
            "session/set_model",
            Some(serde_json::to_value(self).expect("Failed to serialize SessionSetModelRequest")),
        )
    }
}

/// Response from session/set_model (empty).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionSetModelResponse;

impl SessionSetModelResponse {
    /// Parse from JSON-RPC result.
    pub fn from_jsonrpc(_result: &serde_json::Value) -> Result<Self, serde_json::Error> {
        Ok(SessionSetModelResponse)
    }
}

/// Notification to cancel an ongoing prompt turn.
///
/// This is a notification (no response expected). The agent should stop
/// LLM requests and tool executions, then respond to the original
/// session/prompt with stopReason: cancelled.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionCancelNotification {
    /// Session ID to cancel.
    pub session_id: String,
}

impl SessionCancelNotification {
    /// Create a new session/cancel notification.
    pub fn new(session_id: impl Into<String>) -> Self {
        Self {
            session_id: session_id.into(),
        }
    }

    /// Convert to JSON-RPC notification (no id field).
    pub fn to_jsonrpc(&self) -> JsonRpcRequest {
        JsonRpcRequest::notification(
            "session/cancel",
            Some(serde_json::to_value(self).expect("Failed to serialize SessionCancelNotification")),
        )
    }
}

/// Validate MCP server configurations against agent capabilities.
///
/// Returns an error if any MCP server uses a transport that the agent
/// doesn't support.
pub fn validate_mcp_servers(
    mcp_servers: &[McpServer],
    agent_capabilities: &AgentCapabilities,
) -> Result<(), String> {
    let mcp_caps = agent_capabilities.mcp.as_ref();

    for server in mcp_servers {
        match server {
            McpServer::Stdio { .. } => {
                // Stdio is always supported
            }
            McpServer::Http { .. } => {
                let supported = mcp_caps
                    .and_then(|caps| caps.http)
                    .unwrap_or(false);
                if !supported {
                    return Err(format!(
                        "Agent does not support HTTP MCP servers (server: {})",
                        match server {
                            McpServer::Http { name, .. } => name,
                            _ => unreachable!(),
                        }
                    ));
                }
            }
            McpServer::Sse { .. } => {
                let supported = mcp_caps
                    .and_then(|caps| caps.sse)
                    .unwrap_or(false);
                if !supported {
                    return Err(format!(
                        "Agent does not support SSE MCP servers (server: {})",
                        match server {
                            McpServer::Sse { name, .. } => name,
                            _ => unreachable!(),
                        }
                    ));
                }
            }
        }
    }

    Ok(())
}

/// Create a new session.
///
/// Sends a session/new request to the agent and returns the session ID.
///
/// # Arguments
///
/// * `transport` - The transport to use for communication
/// * `cwd` - Working directory (absolute path)
/// * `mcp_servers` - MCP servers to configure
/// * `agent_capabilities` - Agent capabilities (for validation)
///
/// # Returns
///
/// The session ID of the created session.
pub async fn session_new(
    transport: &mut dyn Transport,
    cwd: impl Into<String>,
    mcp_servers: Vec<McpServer>,
    agent_capabilities: &AgentCapabilities,
) -> Result<String, TransportError> {
    // Validate MCP servers
    validate_mcp_servers(&mcp_servers, agent_capabilities)
        .map_err(|e| TransportError::Other(e))?;

    // Create request
    let request = SessionNewRequest::new(cwd, mcp_servers);
    let jsonrpc_request = request.to_jsonrpc(2); // Use ID 2 (after initialize which uses 1)

    // Send request
    let result = transport.send_request(jsonrpc_request).await?;

    // Handle response
    match result {
        JsonRpcResult::Success(response) => {
            let session_response = SessionNewResponse::from_jsonrpc(&response.result)
                .map_err(|e| TransportError::SerializationError(e))?;
            Ok(session_response.session_id)
        }
        JsonRpcResult::Error(error_response) => {
            Err(TransportError::JsonRpcError(error_response.error))
        }
    }
}

/// Load an existing session.
///
/// Sends a session/load request to the agent. The agent will respond with
/// an empty response, then send session/update notifications to replay
/// the conversation history.
///
/// # Arguments
///
/// * `transport` - The transport to use for communication
/// * `session_id` - ID of the session to load
/// * `cwd` - Working directory (absolute path)
/// * `mcp_servers` - MCP servers to configure
/// * `agent_capabilities` - Agent capabilities (for validation)
///
/// # Returns
///
/// Ok(()) if the session load was initiated successfully. History will be
/// replayed via session/update notifications.
pub async fn session_load(
    transport: &mut dyn Transport,
    session_id: impl Into<String>,
    cwd: impl Into<String>,
    mcp_servers: Vec<McpServer>,
    agent_capabilities: &AgentCapabilities,
) -> Result<(), TransportError> {
    // Check if agent supports load_session
    if !agent_capabilities.supports_load_session() {
        return Err(TransportError::Other(
            "Agent does not support session loading".to_string(),
        ));
    }

    // Validate MCP servers
    validate_mcp_servers(&mcp_servers, agent_capabilities)
        .map_err(|e| TransportError::Other(e))?;

    // Create request
    let request = SessionLoadRequest::new(session_id, cwd, mcp_servers);
    let jsonrpc_request = request.to_jsonrpc(3); // Use next available ID

    // Send request
    let result = transport.send_request(jsonrpc_request).await?;

    // Handle response
    match result {
        JsonRpcResult::Success(_) => Ok(()),
        JsonRpcResult::Error(error_response) => {
            Err(TransportError::JsonRpcError(error_response.error))
        }
    }
}

/// Change the session mode.
///
/// # Arguments
///
/// * `transport` - The transport to use for communication
/// * `session_id` - ID of the session to update
/// * `mode` - Mode to switch to (e.g., "code", "chat")
pub async fn session_set_mode(
    transport: &mut dyn Transport,
    session_id: impl Into<String>,
    mode: impl Into<String>,
) -> Result<(), TransportError> {
    let request = SessionSetModeRequest::new(session_id, mode);
    let jsonrpc_request = request.to_jsonrpc(4); // Use next available ID

    let result = transport.send_request(jsonrpc_request).await?;

    match result {
        JsonRpcResult::Success(_) => Ok(()),
        JsonRpcResult::Error(error_response) => {
            Err(TransportError::JsonRpcError(error_response.error))
        }
    }
}

/// Change the session model (UNSTABLE).
///
/// # Arguments
///
/// * `transport` - The transport to use for communication
/// * `session_id` - ID of the session to update
/// * `model` - Model identifier to switch to
pub async fn session_set_model(
    transport: &mut dyn Transport,
    session_id: impl Into<String>,
    model: impl Into<String>,
) -> Result<(), TransportError> {
    let request = SessionSetModelRequest::new(session_id, model);
    let jsonrpc_request = request.to_jsonrpc(5); // Use next available ID

    let result = transport.send_request(jsonrpc_request).await?;

    match result {
        JsonRpcResult::Success(_) => Ok(()),
        JsonRpcResult::Error(error_response) => {
            Err(TransportError::JsonRpcError(error_response.error))
        }
    }
}

/// Cancel an ongoing prompt turn.
///
/// Sends a session/cancel notification (no response expected). The agent
/// should stop LLM requests and tool executions, then respond to the
/// original session/prompt with stopReason: cancelled.
///
/// # Arguments
///
/// * `transport` - The transport to use for communication
/// * `session_id` - ID of the session to cancel
pub async fn session_cancel(
    transport: &mut dyn Transport,
    session_id: impl Into<String>,
) -> Result<(), TransportError> {
    let notification = SessionCancelNotification::new(session_id);
    let jsonrpc_request = notification.to_jsonrpc();

    // Convert the notification-style request to a JsonRpcNotification
    let jsonrpc_notification = crate::acp::transport::JsonRpcNotification {
        jsonrpc: "2.0".to_string(),
        method: jsonrpc_request.method,
        params: jsonrpc_request.params,
    };

    // Send notification (no response expected)
    transport.send_notification(jsonrpc_notification).await
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::acp::connector_state::{
        EnvVariable, McpCapabilities, SessionState,
    };

    #[test]
    fn test_mcp_server_serialization() {
        let server = McpServer::Stdio {
            name: "test-server".to_string(),
            command: "test-cmd".to_string(),
            args: vec!["arg1".to_string()],
            env: vec![EnvVariable {
                name: "VAR".to_string(),
                value: "value".to_string(),
            }],
        };

        let json = serde_json::to_string(&server).unwrap();
        let deserialized: McpServer = serde_json::from_str(&json).unwrap();

        assert_eq!(server, deserialized);
    }

    #[test]
    fn test_session_new_request() {
        let request = SessionNewRequest::new("/absolute/path", vec![]);

        assert_eq!(request.cwd, "/absolute/path");
        assert_eq!(request.mcp_servers.len(), 0);
    }

    #[test]
    #[should_panic(expected = "Working directory must be an absolute path")]
    fn test_session_new_request_relative_path() {
        SessionNewRequest::new("relative/path", vec![]);
    }

    #[test]
    fn test_session_new_request_serialization() {
        let request = SessionNewRequest::new("/test/path", vec![]);

        let json = serde_json::to_string(&request).unwrap();
        let deserialized: SessionNewRequest = serde_json::from_str(&json).unwrap();

        assert_eq!(request, deserialized);
    }

    #[test]
    fn test_session_load_request() {
        let request = SessionLoadRequest::new("session-123", "/absolute/path", vec![]);

        assert_eq!(request.session_id, "session-123");
        assert_eq!(request.cwd, "/absolute/path");
    }

    #[test]
    fn test_session_set_mode_request() {
        let request = SessionSetModeRequest::new("session-123", "code");

        assert_eq!(request.session_id, "session-123");
        assert_eq!(request.mode, "code");
    }

    #[test]
    fn test_session_cancel_notification() {
        let notification = SessionCancelNotification::new("session-123");

        assert_eq!(notification.session_id, "session-123");
    }

    #[test]
    fn test_session_state() {
        let mut state = SessionState::new("session-123".to_string(), "/path".to_string(), vec![]);

        assert!(state.is_ready());
        assert!(!state.prompt_in_progress);

        state.start_prompt();
        assert!(!state.is_ready());
        assert!(state.prompt_in_progress);

        state.complete_prompt();
        assert!(state.is_ready());
        assert!(!state.prompt_in_progress);
    }

    #[test]
    fn test_session_state_cancellation() {
        let mut state = SessionState::new("session-123".to_string(), "/path".to_string(), vec![]);

        state.start_prompt();
        state.start_cancellation();

        assert!(!state.is_ready());
        assert!(state.cancelling);

        state.complete_prompt();
        assert!(state.is_ready());
        assert!(!state.cancelling);
    }

    #[test]
    fn test_session_state_loading() {
        let mut state = SessionState::new("session-123".to_string(), "/path".to_string(), vec![]);

        state.start_loading();
        assert!(!state.is_ready());
        assert!(state.loading);

        state.complete_loading();
        assert!(state.is_ready());
        assert!(!state.loading);
    }

    #[test]
    fn test_validate_mcp_servers_stdio() {
        let servers = vec![McpServer::Stdio {
            name: "test".to_string(),
            command: "cmd".to_string(),
            args: vec![],
            env: vec![],
        }];

        let caps = AgentCapabilities {
            load_session: None,
            prompt_capabilities: None,
            mcp: None,
            _meta: None,
        };

        // Stdio is always supported
        assert!(validate_mcp_servers(&servers, &caps).is_ok());
    }

    #[test]
    fn test_validate_mcp_servers_http_unsupported() {
        let servers = vec![McpServer::Http {
            name: "test".to_string(),
            url: "http://localhost".to_string(),
            headers: vec![],
        }];

        let caps = AgentCapabilities {
            load_session: None,
            prompt_capabilities: None,
            mcp: None,
            _meta: None,
        };

        // HTTP not supported
        assert!(validate_mcp_servers(&servers, &caps).is_err());
    }

    #[test]
    fn test_validate_mcp_servers_http_supported() {
        let servers = vec![McpServer::Http {
            name: "test".to_string(),
            url: "http://localhost".to_string(),
            headers: vec![],
        }];

        let caps = AgentCapabilities {
            load_session: None,
            prompt_capabilities: None,
            mcp: Some(McpCapabilities {
                http: Some(true),
                sse: None,
            }),
            _meta: None,
        };

        // HTTP supported
        assert!(validate_mcp_servers(&servers, &caps).is_ok());
    }

    #[test]
    fn test_validate_mcp_servers_sse_unsupported() {
        let servers = vec![McpServer::Sse {
            name: "test".to_string(),
            url: "http://localhost/events".to_string(),
            headers: vec![],
        }];

        let caps = AgentCapabilities {
            load_session: None,
            prompt_capabilities: None,
            mcp: Some(McpCapabilities {
                http: Some(true),
                sse: None,
            }),
            _meta: None,
        };

        // SSE not supported
        assert!(validate_mcp_servers(&servers, &caps).is_err());
    }
}
