//! ACP Connector configuration
//!
//! This module provides configuration types for ACP connectors, including
//! transport selection (Stdio vs HTTP), feature support, and validation logic.

use dirigent_protocol::SessionOwnership;
use dirigent_tools::EmbeddingConfig;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

/// Well-known agent types for ACP connectors.
///
/// This identifies the specific agent implementation behind an ACP connector,
/// enabling automatic mode/model mapping during session transfers.
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Copy)]
#[serde(rename_all = "lowercase")]
pub enum ConnectorAgentType {
    /// Custom or unknown agent (no automatic mode/model mapping)
    #[default]
    Custom,
    /// Anthropic Claude Code / Claude API
    Claude,
    /// OpenAI Codex / ChatGPT Code Interpreter
    Codex,
    /// Google Gemini Code Assist
    Gemini,
}

impl ConnectorAgentType {
    /// Parse from string magic word (case-insensitive).
    ///
    /// Supports aliases like "openai" for Codex and "google" for Gemini.
    pub fn from_magic_word(word: &str) -> Option<Self> {
        match word.to_lowercase().as_str() {
            "claude" => Some(Self::Claude),
            "codex" | "openai" => Some(Self::Codex),
            "gemini" | "google" => Some(Self::Gemini),
            _ => None,
        }
    }
}

/// Well-known ACP feature identifiers
///
/// These constants define standard features that ACP agents may support.
/// Use these when configuring `supported_features` in `AcpConfig`.
pub mod features {
    /// Agent supports `session/load` to resume existing sessions
    ///
    /// Without this feature, archived sessions cannot be loaded back into the agent.
    /// The UI should disable "load from archive" functionality.
    pub const SESSION_RESUME: &str = "session_resume";

    /// Agent supports `session/list` to enumerate available sessions
    ///
    /// Without this feature, the connector cannot list sessions from the agent.
    pub const SESSION_LIST: &str = "session_list";

    /// Agent provides message history when loading sessions
    ///
    /// Without this feature, loaded sessions start empty (no replay).
    pub const MESSAGE_HISTORY: &str = "message_history";

    /// Agent supports cancellation via `session/cancel`
    pub const CANCELLATION: &str = "cancellation";

    /// Agent supports model selection
    pub const MODEL_SELECTION: &str = "model_selection";

    /// Agent supports mode selection (e.g., plan mode, bypass permissions)
    pub const MODE_SELECTION: &str = "mode_selection";
}

/// Transport mechanism for ACP communication
///
/// ACP supports multiple transport mechanisms for different deployment scenarios:
/// - Stdio: For locally spawned agent processes
/// - Http: For remote agents accessible via HTTP/SSE
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum TransportKind {
    /// Stdio transport (spawn process, communicate via stdin/stdout)
    ///
    /// # Fields
    ///
    /// * `command` - Executable path or name (resolved via PATH)
    /// * `args` - Command-line arguments to pass to the process
    /// * `cwd` - Optional working directory for the process
    /// * `env` - Optional environment variables
    Stdio {
        /// Command to execute (e.g., "dirigate", "/path/to/agent")
        command: String,
        /// Command-line arguments
        #[serde(default)]
        args: Vec<String>,
        /// Working directory (defaults to current directory)
        #[serde(skip_serializing_if = "Option::is_none")]
        cwd: Option<PathBuf>,
        /// Environment variables
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        env: Vec<(String, String)>,
    },

    /// HTTP transport (connect to remote agent via HTTP/SSE)
    ///
    /// # Fields
    ///
    /// * `base_url` - Base URL of the ACP agent HTTP endpoint
    /// * `timeout_ms` - Optional request timeout in milliseconds
    Http {
        /// Base URL (e.g., "http://localhost:3000", "https://agent.example.com")
        base_url: String,
        /// Request timeout in milliseconds (default: 30000)
        #[serde(skip_serializing_if = "Option::is_none")]
        timeout_ms: Option<u64>,
    },
}

/// Configuration for ACP connector
///
/// Contains all the information needed to create and connect to an ACP agent,
/// including transport configuration and operational parameters.
///
/// Note: Title and supported_features are now orchestration-level fields
/// stored in ConnectorConfig, not here.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AcpConfig {
    /// Transport configuration
    ///
    /// Determines how to connect to the ACP agent (stdio process or HTTP endpoint).
    pub transport: TransportKind,

    /// Protocol version to use (default: 1)
    ///
    /// Specifies which version of the ACP protocol to negotiate during initialization.
    #[serde(default = "default_protocol_version")]
    pub protocol_version: u32,

    /// Current working directory for sessions (default: ".")
    ///
    /// Passed to the agent when creating new sessions. Determines the file system
    /// context for tools and operations.
    #[serde(default = "default_cwd")]
    pub cwd: String,

    /// Connection retry configuration
    ///
    /// Controls how the connector behaves when connection attempts fail.
    #[serde(default)]
    pub retry: RetryConfig,

    /// File embedding configuration
    ///
    /// Controls how files are embedded in prompts (size limits, redaction, etc.)
    #[serde(default)]
    pub embedding: EmbeddingConfig,

    /// Default ownership for capability negotiation (used during initialize)
    ///
    /// This ownership model determines what capabilities are advertised to the agent
    /// during the initialize handshake. For UI-created connectors, this defaults to
    /// internal ownership (empty capabilities). For connectors created by ACP Acceptor
    /// (incoming external clients), this should be set to external_forwarded with the
    /// client's capabilities.
    #[serde(default)]
    pub default_ownership: SessionOwnership,

    /// Override directory for ACP protocol logging (optional)
    ///
    /// All JSON-RPC messages (stdin/stdout) are logged to JSONL files.
    /// Defaults to `data_dir/logs/acp/` when not set. Set this to override
    /// the default location.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub acp_log_dir: Option<PathBuf>,

    /// Agent type for automatic mode/model mapping.
    ///
    /// When set to a specific agent type (Claude, Codex, Gemini), the system
    /// will automatically translate Gateway mode/model identifiers to
    /// agent-specific identifiers during session transfers.
    #[serde(default)]
    pub agent_type: ConnectorAgentType,
}

/// Retry configuration for connection failures
///
/// Controls reconnection behavior when the transport fails or disconnects.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RetryConfig {
    /// Maximum number of retry attempts (default: 5)
    ///
    /// After this many failed attempts, the connector enters Error state
    /// and waits for manual Reconnect command.
    #[serde(default = "default_max_retries")]
    pub max_retries: usize,

    /// Retry delays in milliseconds (default: [60000, 60000, 60000, 60000, 60000])
    ///
    /// Delays between retry attempts. Defaults to 60 seconds to avoid reconnect
    /// spam when SSE connections drop. If retries exceed the length of this
    /// array, the last delay is reused.
    #[serde(default = "default_retry_delays")]
    pub retry_delays_ms: Vec<u64>,
}

impl Default for RetryConfig {
    fn default() -> Self {
        Self {
            max_retries: default_max_retries(),
            retry_delays_ms: default_retry_delays(),
        }
    }
}

// Default value functions for serde

fn default_protocol_version() -> u32 {
    1
}

fn default_cwd() -> String {
    ".".to_string()
}

fn default_max_retries() -> usize {
    5
}

fn default_retry_delays() -> Vec<u64> {
    // First retry after 60 seconds to avoid spam when SSE disconnects
    // Subsequent retries use same 60-second delay
    vec![60000, 60000, 60000, 60000, 60000]
}

impl AcpConfig {
    /// Set the maximum number of retry attempts
    ///
    /// # Arguments
    ///
    /// * `max` - Maximum retry attempts before entering Error state
    ///
    /// # Returns
    ///
    /// Self for method chaining
    pub fn with_retry_max_attempts(mut self, max: usize) -> Self {
        self.retry.max_retries = max;
        self
    }

    /// Set the initial retry delay
    ///
    /// # Arguments
    ///
    /// * `delay` - Initial delay before first retry
    ///
    /// # Returns
    ///
    /// Self for method chaining
    pub fn with_retry_initial_delay(mut self, delay: std::time::Duration) -> Self {
        let delay_ms = delay.as_millis() as u64;
        if !self.retry.retry_delays_ms.is_empty() {
            self.retry.retry_delays_ms[0] = delay_ms;
        }
        self
    }

    /// Set the request timeout for HTTP transport
    ///
    /// # Arguments
    ///
    /// * `timeout` - Request timeout duration
    ///
    /// # Returns
    ///
    /// Self for method chaining
    ///
    /// # Note
    ///
    /// This only applies to HTTP transport. For stdio transport, this is a no-op.
    pub fn with_request_timeout(mut self, timeout: std::time::Duration) -> Self {
        if let TransportKind::Http { timeout_ms, .. } = &mut self.transport {
            *timeout_ms = Some(timeout.as_millis() as u64);
        }
        self
    }

    /// Validate configuration and return errors if invalid
    ///
    /// Checks for common configuration mistakes like:
    /// - Empty command for stdio transport
    /// - Invalid URL for HTTP transport
    /// - Negative or zero timeouts
    ///
    /// # Returns
    ///
    /// `Ok(())` if configuration is valid, `Err(String)` with error message otherwise.
    pub fn validate(&self) -> Result<(), String> {
        // Validate transport
        match &self.transport {
            TransportKind::Stdio { command, .. } => {
                if command.trim().is_empty() {
                    return Err("Stdio transport requires non-empty command".to_string());
                }
            }
            TransportKind::Http { base_url, timeout_ms } => {
                if base_url.trim().is_empty() {
                    return Err("HTTP transport requires non-empty base_url".to_string());
                }
                // Basic URL validation
                if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
                    return Err("HTTP base_url must start with http:// or https://".to_string());
                }
                if let Some(timeout) = timeout_ms {
                    if *timeout == 0 {
                        return Err("HTTP timeout must be greater than 0".to_string());
                    }
                }
            }
        }

        // Validate protocol version
        if self.protocol_version == 0 {
            return Err("Protocol version must be >= 1".to_string());
        }

        // Validate retry config
        if self.retry.max_retries == 0 {
            return Err("max_retries must be >= 1".to_string());
        }
        if self.retry.retry_delays_ms.is_empty() {
            return Err("retry_delays_ms cannot be empty".to_string());
        }

        Ok(())
    }

    // REMOVED: supports_feature() and with_features() methods
    // supported_features is now in ConnectorConfig, not AcpConfig

    /// Create a default stdio configuration for testing
    ///
    /// # Arguments
    ///
    /// * `command` - Command to execute
    /// * `args` - Command-line arguments
    ///
    /// # Returns
    ///
    /// AcpConfig with stdio transport and default values
    pub fn stdio(command: impl Into<String>, args: Vec<String>) -> Self {
        Self {
            transport: TransportKind::Stdio {
                command: command.into(),
                args,
                cwd: None,
                env: vec![],
            },
            protocol_version: 1,
            cwd: ".".to_string(),
            retry: RetryConfig::default(),
            embedding: EmbeddingConfig::default(),
            default_ownership: SessionOwnership::default(),
            acp_log_dir: None,
            agent_type: ConnectorAgentType::default(),
        }
    }

    /// Create a default HTTP configuration
    ///
    /// # Arguments
    ///
    /// * `base_url` - Base URL of the ACP agent
    ///
    /// # Returns
    ///
    /// AcpConfig with HTTP transport and default values
    pub fn http(base_url: impl Into<String>) -> Self {
        Self {
            transport: TransportKind::Http {
                base_url: base_url.into(),
                timeout_ms: Some(30_000),
            },
            protocol_version: 1,
            cwd: ".".to_string(),
            retry: RetryConfig::default(),
            embedding: EmbeddingConfig::default(),
            default_ownership: SessionOwnership::default(),
            acp_log_dir: None,
            agent_type: ConnectorAgentType::default(),
        }
    }
}

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

    #[test]
    fn test_transport_kind_stdio_serialization() {
        let transport = TransportKind::Stdio {
            command: "test-agent".to_string(),
            args: vec!["--stdio".to_string()],
            cwd: None,
            env: vec![],
        };

        let json = serde_json::to_string(&transport).unwrap();
        let deserialized: TransportKind = serde_json::from_str(&json).unwrap();

        assert_eq!(deserialized, transport);
    }

    #[test]
    fn test_transport_kind_http_serialization() {
        let transport = TransportKind::Http {
            base_url: "http://localhost:3000".to_string(),
            timeout_ms: Some(60_000),
        };

        let json = serde_json::to_string(&transport).unwrap();
        let deserialized: TransportKind = serde_json::from_str(&json).unwrap();

        assert_eq!(deserialized, transport);
    }

    #[test]
    fn test_acp_config_defaults() {
        let config = AcpConfig::stdio("agent", vec![]);

        assert_eq!(config.protocol_version, 1);
        assert_eq!(config.cwd, ".");
        assert_eq!(config.retry.max_retries, 5);
        assert_eq!(config.retry.retry_delays_ms.len(), 5);
    }

    #[test]
    fn test_acp_config_validation_stdio_empty_command() {
        let config = AcpConfig::stdio("", vec![]);

        let result = config.validate();
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("non-empty command"));
    }

    #[test]
    fn test_acp_config_validation_http_empty_url() {
        let config = AcpConfig::http("");

        let result = config.validate();
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("non-empty base_url"));
    }

    #[test]
    fn test_acp_config_validation_http_invalid_url() {
        let config = AcpConfig::http("not-a-url");

        let result = config.validate();
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("http://"));
    }

    // REMOVED: test_acp_config_validation_empty_title
    // Title is now in ConnectorConfig, not AcpConfig

    #[test]
    fn test_acp_config_validation_zero_protocol_version() {
        let mut config = AcpConfig::stdio("agent", vec![]);
        config.protocol_version = 0;

        let result = config.validate();
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("Protocol version"));
    }

    #[test]
    fn test_acp_config_validation_valid() {
        let config = AcpConfig::stdio("agent", vec!["--stdio".to_string()]);

        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_acp_config_http_valid() {
        let config = AcpConfig::http("https://localhost:3000");

        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_retry_config_default() {
        let retry = RetryConfig::default();

        assert_eq!(retry.max_retries, 5);
        // Default delays: 60 seconds each to avoid spam when SSE disconnects
        assert_eq!(retry.retry_delays_ms, vec![60000, 60000, 60000, 60000, 60000]);
    }

    #[test]
    fn test_acp_config_serialization() {
        let config = AcpConfig::stdio("test-agent", vec!["--flag".to_string()]);

        let json = serde_json::to_string(&config).unwrap();
        let deserialized: AcpConfig = serde_json::from_str(&json).unwrap();

        assert_eq!(deserialized.protocol_version, config.protocol_version);
        assert_eq!(deserialized.cwd, config.cwd);
    }
}
