//! Configuration types for the LiveSpeech SDK

use serde::Serialize;
use std::time::Duration;

/// Pipeline mode for audio processing
/// 
/// - `Live`: Direct audio-to-audio conversation (default, lower latency)
/// - `Composed`: Uses separate STT + LLM + TTS services (more customizable)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PipelineMode {
    /// Direct audio-to-audio conversation (lower latency)
    #[default]
    Live,
    /// Composed pipeline - separate STT + LLM + TTS services
    Composed,
}

impl PipelineMode {
    /// Get the string representation for the protocol
    pub fn as_str(&self) -> &'static str {
        match self {
            PipelineMode::Live => "live",
            PipelineMode::Composed => "composed",
        }
    }
}

impl std::fmt::Display for PipelineMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

/// Available regions for LiveSpeech service
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Region {
    /// Asia Pacific (Seoul)
    ApNortheast2,
    /// US West (Oregon) - Coming soon
    UsWest2,
}

impl Region {
    /// Get the WebSocket endpoint URL for this region
    pub fn endpoint(&self) -> &'static str {
        match self {
            Region::ApNortheast2 => "wss://talk.drawdream.co.kr",
            Region::UsWest2 => "wss://talk.drawdream.ca", // Coming soon
        }
    }

    /// Get the region identifier string
    pub fn as_str(&self) -> &'static str {
        match self {
            Region::ApNortheast2 => "ap-northeast-2",
            Region::UsWest2 => "us-west-2",
        }
    }
}

impl std::fmt::Display for Region {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

/// Configuration for the LiveSpeech client
#[derive(Debug, Clone)]
pub struct Config {
    /// WebSocket endpoint URL
    pub endpoint: String,
    /// API key for authentication
    pub api_key: String,
    /// User identifier for conversation memory persistence.
    pub user_id: Option<String>,
    /// Connection timeout
    pub connection_timeout: Duration,
    /// Enable automatic reconnection
    pub auto_reconnect: bool,
    /// Maximum reconnection attempts
    pub max_reconnect_attempts: u32,
    /// Base delay between reconnection attempts
    pub reconnect_delay: Duration,
    /// Enable debug logging
    pub debug: bool,
}

impl Config {
    /// Create a new config builder
    pub fn builder() -> ConfigBuilder {
        ConfigBuilder::default()
    }
}

/// Builder for Config
#[derive(Debug, Default)]
pub struct ConfigBuilder {
    region: Option<Region>,
    api_key: Option<String>,
    user_id: Option<String>,
    connection_timeout: Option<Duration>,
    auto_reconnect: Option<bool>,
    max_reconnect_attempts: Option<u32>,
    reconnect_delay: Option<Duration>,
    debug: Option<bool>,
}

impl ConfigBuilder {
    /// Set the region (required)
    /// 
    /// This will automatically resolve the correct endpoint URL for the region.
    /// 
    /// # Example
    /// ```
    /// use livespeech_sdk::{Config, Region};
    /// 
    /// let config = Config::builder()
    ///     .region(Region::ApNortheast2)
    ///     .api_key("your-api-key")
    ///     .build()
    ///     .unwrap();
    /// ```
    pub fn region(mut self, region: Region) -> Self {
        self.region = Some(region);
        self
    }

    /// Set the API key (required)
    pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
        self.api_key = Some(api_key.into());
        self
    }

    /// Set the user ID for conversation memory persistence.
    /// 
    /// # Example
    /// ```
    /// use livespeech_sdk::{Config, Region};
    /// 
    /// let config = Config::builder()
    ///     .region(Region::ApNortheast2)
    ///     .api_key("your-api-key")
    ///     .user_id("user-123")
    ///     .build()
    ///     .unwrap();
    /// ```
    pub fn user_id(mut self, user_id: impl Into<String>) -> Self {
        self.user_id = Some(user_id.into());
        self
    }

    /// Set the connection timeout
    pub fn connection_timeout(mut self, timeout: Duration) -> Self {
        self.connection_timeout = Some(timeout);
        self
    }

    /// Enable or disable auto reconnection
    pub fn auto_reconnect(mut self, enabled: bool) -> Self {
        self.auto_reconnect = Some(enabled);
        self
    }

    /// Set maximum reconnection attempts
    pub fn max_reconnect_attempts(mut self, attempts: u32) -> Self {
        self.max_reconnect_attempts = Some(attempts);
        self
    }

    /// Set base reconnection delay
    pub fn reconnect_delay(mut self, delay: Duration) -> Self {
        self.reconnect_delay = Some(delay);
        self
    }

    /// Enable or disable debug mode
    pub fn debug(mut self, enabled: bool) -> Self {
        self.debug = Some(enabled);
        self
    }

    /// Build the Config
    pub fn build(self) -> Result<Config, ConfigError> {
        let region = self.region.ok_or(ConfigError::MissingRegion)?;
        let api_key = self.api_key.ok_or(ConfigError::MissingApiKey)?;

        Ok(Config {
            endpoint: region.endpoint().to_string(),
            api_key,
            user_id: self.user_id,
            connection_timeout: self.connection_timeout.unwrap_or(Duration::from_secs(30)),
            auto_reconnect: self.auto_reconnect.unwrap_or(true),
            max_reconnect_attempts: self.max_reconnect_attempts.unwrap_or(5),
            reconnect_delay: self.reconnect_delay.unwrap_or(Duration::from_secs(1)),
            debug: self.debug.unwrap_or(false),
        })
    }
}

/// Configuration errors
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
    #[error("region is required")]
    MissingRegion,
    #[error("api_key is required")]
    MissingApiKey,
}

// =============================================================================
// Tool (Function Calling) Types
// =============================================================================

/// Tool definition for function calling
/// 
/// Define functions that the AI can call during conversation.
/// When the AI decides to call a function, you'll receive a `ToolCall` event.
/// 
/// # Example
/// ```
/// use livespeech_sdk::{Tool, FunctionParameters};
/// 
/// let tool = Tool {
///     name: "open_login".to_string(),
///     description: "Opens the login popup when user wants to sign in".to_string(),
///     parameters: Some(FunctionParameters {
///         r#type: "OBJECT".to_string(),
///         properties: serde_json::json!({}),
///         required: vec![],
///     }),
/// };
/// ```
#[derive(Debug, Clone, Serialize)]
pub struct Tool {
    /// Unique function name
    pub name: String,
    /// Description of when the AI should use this function
    pub description: String,
    /// Parameters schema (optional)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parameters: Option<FunctionParameters>,
}

/// Function parameters schema
#[derive(Debug, Clone, Serialize)]
pub struct FunctionParameters {
    /// Type (usually "OBJECT")
    #[serde(rename = "type")]
    pub r#type: String,
    /// Properties schema
    pub properties: serde_json::Value,
    /// Required parameter names
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub required: Vec<String>,
}

impl Tool {
    /// Create a new tool with no parameters
    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            description: description.into(),
            parameters: None,
        }
    }

    /// Create a tool with parameters
    pub fn with_parameters(
        name: impl Into<String>,
        description: impl Into<String>,
        properties: serde_json::Value,
        required: Vec<String>,
    ) -> Self {
        Self {
            name: name.into(),
            description: description.into(),
            parameters: Some(FunctionParameters {
                r#type: "OBJECT".to_string(),
                properties,
                required,
            }),
        }
    }
}

// =============================================================================
// Session Configuration
// =============================================================================

/// Session duration configuration
#[derive(Debug, Clone)]
pub struct SessionDurationConfig {
    /// Max session duration in seconds (required)
    pub max_seconds: u32,
    /// Enable session warning events/messages (default: false)
    pub enable_warning: Option<bool>,
    /// Enable session goodbye events/messages (default: false)
    pub enable_goodbye: Option<bool>,
}

/// Session configuration
#[derive(Debug, Clone)]
pub struct SessionConfig {
    /// System prompt for the AI (optional)
    pub pre_prompt: Option<String>,
    /// Language code (e.g., "en-US", "ko-KR").
    ///
    /// - **Composed mode:** Used for STT speech recognition language AND as fallback
    ///   for TTS voice selection (if `output_language` is not set).
    /// - **Live mode:** Used for TTS voice selection only. Gemini auto-detects
    ///   the input language from the audio stream.
    pub language: Option<String>,
    /// Output language for TTS voice selection (Composed mode only).
    ///
    /// Use this when the AI output language differs from the input language
    /// (e.g., input is "ko-KR" but AI responds in English via pre_prompt translation).
    /// If not set, defaults to `language`.
    ///
    /// **Note:** This field is only used in Composed mode. In Live mode, voice
    /// selection is driven by `language` since Gemini handles the full pipeline natively.
    pub output_language: Option<String>,
    /// Pipeline mode for audio processing (default: Live)
    pub pipeline_mode: PipelineMode,
    /// Enable AI to speak first before user input (live mode only)
    /// When enabled, the AI will initiate the conversation based on the pre_prompt.
    /// Make sure your pre_prompt includes instructions for how the AI should greet the user.
    /// Default: false
    pub ai_speaks_first: bool,
    /// Allow harmful content categories in AI responses
    /// Default: false
    pub allow_harm_category: bool,
    /// Tools (functions) that the AI can call during conversation (live mode only)
    /// Default: None
    pub tools: Option<Vec<Tool>>,
    /// Session duration configuration (enables duration limits when set)
    pub session_duration: Option<SessionDurationConfig>,
}

impl Default for SessionConfig {
    fn default() -> Self {
        Self {
            pre_prompt: None,
            language: None,
            output_language: None,
            pipeline_mode: PipelineMode::Live,
            ai_speaks_first: false,
            allow_harm_category: false,
            tools: None,
            session_duration: None,
        }
    }
}

impl SessionConfig {
    /// Create a new session config with a pre-prompt
    pub fn new(pre_prompt: impl Into<String>) -> Self {
        Self {
            pre_prompt: Some(pre_prompt.into()),
            language: None,
            output_language: None,
            pipeline_mode: PipelineMode::Live,
            ai_speaks_first: false,
            allow_harm_category: false,
            tools: None,
            session_duration: None,
        }
    }

    /// Create an empty session config
    pub fn empty() -> Self {
        Self::default()
    }

    /// Set the language for speech recognition
    pub fn with_language(mut self, language: impl Into<String>) -> Self {
        self.language = Some(language.into());
        self
    }

    /// Set the output language for TTS voice selection.
    /// Use this when the AI output language differs from the input language
    /// (e.g., input is "ko-KR" but AI responds in English via pre_prompt translation).
    /// If not set, defaults to `language`.
    ///
    /// # Example
    /// ```
    /// use livespeech_sdk::SessionConfig;
    ///
    /// let config = SessionConfig::new("Translate Korean to English.")
    ///     .with_language("ko-KR")
    ///     .with_output_language("en-US");
    /// ```
    pub fn with_output_language(mut self, output_language: impl Into<String>) -> Self {
        self.output_language = Some(output_language.into());
        self
    }

    /// Set the pre-prompt
    pub fn with_pre_prompt(mut self, pre_prompt: impl Into<String>) -> Self {
        self.pre_prompt = Some(pre_prompt.into());
        self
    }

    /// Set the pipeline mode
    /// 
    /// - `PipelineMode::Live`: Direct audio-to-audio conversation (default, lower latency)
    /// - `PipelineMode::Composed`: Separate STT + LLM + TTS services (more customizable)
    pub fn with_pipeline_mode(mut self, mode: PipelineMode) -> Self {
        self.pipeline_mode = mode;
        self
    }

    /// Enable AI to speak first before user input (live mode only)
    /// 
    /// When enabled, the AI will initiate the conversation based on the pre_prompt.
    /// Make sure your pre_prompt includes instructions for how the AI should greet the user.
    /// 
    /// # Example
    /// ```
    /// use livespeech_sdk::SessionConfig;
    /// 
    /// let config = SessionConfig::new("You are a helpful assistant. Start by greeting the user.")
    ///     .with_ai_speaks_first(true);
    /// ```
    pub fn with_ai_speaks_first(mut self, enabled: bool) -> Self {
        self.ai_speaks_first = enabled;
        self
    }

    /// Set whether to allow harmful content categories in AI responses
    /// 
    /// # Example
    /// ```
    /// use livespeech_sdk::SessionConfig;
    /// 
    /// // Enable content safety filtering
    /// let config = SessionConfig::empty()
    ///     .with_allow_harm_category(false);
    /// ```
    pub fn with_allow_harm_category(mut self, allow: bool) -> Self {
        self.allow_harm_category = allow;
        self
    }

    /// Set tools (functions) that the AI can call during conversation
    /// 
    /// # Example
    /// ```
    /// use livespeech_sdk::{SessionConfig, Tool};
    /// 
    /// let tools = vec![
    ///     Tool::new("open_login", "Opens the login popup when user wants to sign in"),
    /// ];
    /// 
    /// let config = SessionConfig::new("You are a helpful assistant.")
    ///     .with_tools(tools);
    /// ```
    pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
        self.tools = Some(tools);
        self
    }

    /// Set session duration configuration (enables duration limits)
    pub fn with_session_duration(mut self, config: SessionDurationConfig) -> Self {
        self.session_duration = Some(config);
        self
    }
}

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

    #[test]
    fn test_config_builder_with_region() {
        let config = Config::builder()
            .region(Region::ApNortheast2)
            .api_key("test-key")
            .debug(true)
            .build()
            .unwrap();

        assert_eq!(config.endpoint, "wss://talk.drawdream.co.kr");
        assert_eq!(config.api_key, "test-key");
        assert!(config.debug);
    }

    #[test]
    fn test_config_builder_missing_region() {
        let result = Config::builder().api_key("test-key").build();
        assert!(matches!(result, Err(ConfigError::MissingRegion)));
    }

    #[test]
    fn test_config_builder_missing_api_key() {
        let result = Config::builder().region(Region::ApNortheast2).build();
        assert!(matches!(result, Err(ConfigError::MissingApiKey)));
    }

    #[test]
    fn test_region_endpoint() {
        assert_eq!(Region::ApNortheast2.endpoint(), "wss://talk.drawdream.co.kr");
        assert_eq!(Region::ApNortheast2.as_str(), "ap-northeast-2");
    }

    #[test]
    fn test_session_config() {
        let config = SessionConfig::new("You are a helpful assistant");
        assert_eq!(config.pre_prompt, Some("You are a helpful assistant".to_string()));
    }

    #[test]
    fn test_session_config_empty() {
        let config = SessionConfig::empty();
        assert_eq!(config.pre_prompt, None);
    }

    #[test]
    fn test_pipeline_mode_default() {
        let config = SessionConfig::empty();
        assert_eq!(config.pipeline_mode, PipelineMode::Live);
    }

    #[test]
    fn test_pipeline_mode_composed() {
        let config = SessionConfig::empty().with_pipeline_mode(PipelineMode::Composed);
        assert_eq!(config.pipeline_mode, PipelineMode::Composed);
        assert_eq!(config.pipeline_mode.as_str(), "composed");
    }
}
