//! LLM provider abstraction and implementations
//!
//! This module provides both low-level provider traits and a high-level client facade.
//!
//! # Recommended Usage
//!
//! For most use cases, use [`LlmClient`] which provides a simplified API:
//!
//! ```no_run
//! use typedialog_ag_core::llm::LlmClient;
//!
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
//! let client = LlmClient::from_model("claude-3-5-sonnet-20241022")?;
//! let response = client.ask("What is Rust?").await?;
//! println!("{}", response);
//! # Ok(())
//! # }
//! ```
//!
//! For advanced use cases (implementing custom providers), use [`LlmProvider`] trait.

pub mod claude;
pub mod gemini;
pub mod ollama;
pub mod openai;
pub mod provider;

pub use claude::ClaudeProvider;
pub use gemini::GeminiProvider;
pub use ollama::OllamaProvider;
pub use openai::OpenAIProvider;
pub use provider::{
    LlmMessage, LlmProvider, LlmRequest, LlmResponse, LlmStream, MessageRole, StreamChunk,
    StreamMetadata, TokenUsage,
};

use crate::error::{Error, Result};
use std::sync::Arc;

/// Create an LLM provider based on model name
pub fn create_provider(model: &str) -> Result<Box<dyn LlmProvider>> {
    // Determine provider from model name
    if model.starts_with("claude") || model.starts_with("anthropic") {
        Ok(Box::new(ClaudeProvider::new()?))
    } else if model.starts_with("gpt")
        || model.starts_with("o1")
        || model.starts_with("o3")
        || model.starts_with("o4")
    {
        Ok(Box::new(OpenAIProvider::new()?))
    } else if model.starts_with("gemini") {
        Ok(Box::new(GeminiProvider::new()?))
    } else if is_ollama_model(model) {
        Ok(Box::new(OllamaProvider::new()?))
    } else {
        Err(Error::execution(
            "Unknown model provider",
            format!("Model: {}. Supported: claude-*, gpt-*, o1-*, o3-*, o4-*, gemini-*, llama*, mistral*, phi*, codellama*, etc.", model),
        ))
    }
}

/// Check if a model name is a known Ollama model
fn is_ollama_model(model: &str) -> bool {
    // Common Ollama model prefixes
    let ollama_prefixes = [
        "llama",
        "mistral",
        "phi",
        "codellama",
        "mixtral",
        "vicuna",
        "orca",
        "wizardlm",
        "falcon",
        "starcoder",
        "deepseek",
        "qwen",
        "yi",
        "solar",
        "dolphin",
        "openhermes",
        "neural",
        "zephyr",
    ];

    ollama_prefixes
        .iter()
        .any(|prefix| model.starts_with(prefix))
}

// ============================================================================
// LLM CLIENT FACADE (Recommended High-Level API)
// ============================================================================

/// High-level LLM client facade providing a simplified API
///
/// `LlmClient` wraps the lower-level [`LlmProvider`] trait and provides
/// convenience methods for common use cases. This is the recommended way
/// to interact with LLMs for most applications.
///
/// # Examples
///
/// ## Simple question-answer
///
/// ```no_run
/// use typedialog_ag_core::llm::LlmClient;
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let client = LlmClient::from_model("claude-3-5-sonnet-20241022")?;
/// let answer = client.ask("Explain Rust ownership").await?;
/// println!("{}", answer);
/// # Ok(())
/// # }
/// ```
///
/// ## With custom parameters
///
/// ```no_run
/// use typedialog_ag_core::llm::LlmClient;
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let client = LlmClient::from_model("gpt-4")?
///     .with_temperature(0.7)
///     .with_max_tokens(2000);
///
/// let response = client.ask("Write a haiku about coding").await?;
/// println!("{}", response);
/// # Ok(())
/// # }
/// ```
///
/// ## Streaming responses
///
/// ```no_run
/// use typedialog_ag_core::llm::LlmClient;
/// use futures::StreamExt;
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let client = LlmClient::from_model("claude-3-5-sonnet-20241022")?;
/// let mut stream = client.ask_stream("Tell me a story").await?;
///
/// while let Some(chunk) = stream.next().await {
///     match chunk? {
///         typedialog_ag_core::llm::StreamChunk::Content(text) => print!("{}", text),
///         typedialog_ag_core::llm::StreamChunk::Done(_) => println!("\n[Done]"),
///         typedialog_ag_core::llm::StreamChunk::Error(e) => eprintln!("Error: {}", e),
///     }
/// }
/// # Ok(())
/// # }
/// ```
pub struct LlmClient {
    provider: Arc<dyn LlmProvider>,
    model: String,
    temperature: Option<f64>,
    max_tokens: Option<usize>,
    system: Option<String>,
}

impl LlmClient {
    /// Create a new client from a specific provider
    ///
    /// For most use cases, prefer [`from_model`](Self::from_model) which auto-detects the provider.
    pub fn new(provider: Arc<dyn LlmProvider>, model: impl Into<String>) -> Self {
        Self {
            provider,
            model: model.into(),
            temperature: None,
            max_tokens: None,
            system: None,
        }
    }

    /// Create a new client by auto-detecting the provider from the model name
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use typedialog_ag_core::llm::LlmClient;
    ///
    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// let claude = LlmClient::from_model("claude-3-5-sonnet-20241022")?;
    /// let gpt = LlmClient::from_model("gpt-4")?;
    /// let gemini = LlmClient::from_model("gemini-pro")?;
    /// let ollama = LlmClient::from_model("llama2")?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn from_model(model: impl Into<String>) -> Result<Self> {
        let model_str = model.into();
        let provider = create_provider(&model_str)?;
        Ok(Self::new(Arc::from(provider), model_str))
    }

    /// Set the temperature parameter (0.0 to 1.0)
    ///
    /// Higher values make output more random, lower values more deterministic.
    pub fn with_temperature(mut self, temperature: f64) -> Self {
        self.temperature = Some(temperature);
        self
    }

    /// Set the maximum number of tokens to generate
    pub fn with_max_tokens(mut self, max_tokens: usize) -> Self {
        self.max_tokens = Some(max_tokens);
        self
    }

    /// Set a system message to guide the model's behavior
    pub fn with_system(mut self, system: impl Into<String>) -> Self {
        self.system = Some(system.into());
        self
    }

    /// Ask a simple question and get a text response
    ///
    /// This is the simplest way to interact with an LLM.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use typedialog_ag_core::llm::LlmClient;
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// let client = LlmClient::from_model("claude-3-5-sonnet-20241022")?;
    /// let response = client.ask("What is 2+2?").await?;
    /// assert!(response.contains("4"));
    /// # Ok(())
    /// # }
    /// ```
    pub async fn ask(&self, prompt: impl Into<String>) -> Result<String> {
        let request = LlmRequest {
            model: self.model.clone(),
            messages: vec![LlmMessage {
                role: MessageRole::User,
                content: prompt.into(),
            }],
            max_tokens: self.max_tokens,
            temperature: self.temperature,
            system: self.system.clone(),
        };

        let response = self.provider.complete(request).await?;
        Ok(response.content)
    }

    /// Ask a question and receive a streaming response
    ///
    /// Use this for long responses or when you want to display progress.
    pub async fn ask_stream(&self, prompt: impl Into<String>) -> Result<LlmStream> {
        let request = LlmRequest {
            model: self.model.clone(),
            messages: vec![LlmMessage {
                role: MessageRole::User,
                content: prompt.into(),
            }],
            max_tokens: self.max_tokens,
            temperature: self.temperature,
            system: self.system.clone(),
        };

        self.provider.stream(request).await
    }

    /// Execute a full completion request with complete control
    ///
    /// Use this for multi-turn conversations or advanced scenarios.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use typedialog_ag_core::llm::{LlmClient, LlmRequest, LlmMessage, MessageRole};
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// let client = LlmClient::from_model("claude-3-5-sonnet-20241022")?;
    ///
    /// let request = LlmRequest {
    ///     model: "claude-3-5-sonnet-20241022".to_string(),
    ///     messages: vec![
    ///         LlmMessage {
    ///             role: MessageRole::User,
    ///             content: "Hello!".to_string(),
    ///         },
    ///         LlmMessage {
    ///             role: MessageRole::Assistant,
    ///             content: "Hi! How can I help?".to_string(),
    ///         },
    ///         LlmMessage {
    ///             role: MessageRole::User,
    ///             content: "Tell me about Rust".to_string(),
    ///         },
    ///     ],
    ///     max_tokens: Some(1000),
    ///     temperature: Some(0.7),
    ///     system: None,
    /// };
    ///
    /// let response = client.complete(request).await?;
    /// println!("{}", response.content);
    /// # Ok(())
    /// # }
    /// ```
    pub async fn complete(&self, request: LlmRequest) -> Result<LlmResponse> {
        self.provider.complete(request).await
    }

    /// Stream a full completion request
    pub async fn stream_complete(&self, request: LlmRequest) -> Result<LlmStream> {
        self.provider.stream(request).await
    }

    /// Get the underlying provider name
    pub fn provider_name(&self) -> &str {
        self.provider.name()
    }

    /// Get the model name
    pub fn model(&self) -> &str {
        &self.model
    }
}

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

    #[test]
    fn test_client_builder_pattern() {
        // This just tests the builder pattern compiles
        let model = "claude-3-5-sonnet-20241022";
        let _would_create = || -> Result<LlmClient> {
            Ok(LlmClient::from_model(model)?
                .with_temperature(0.7)
                .with_max_tokens(2000)
                .with_system("You are a helpful assistant"))
        };
    }

    #[test]
    fn test_is_ollama_model() {
        assert!(is_ollama_model("llama2"));
        assert!(is_ollama_model("mistral"));
        assert!(is_ollama_model("phi"));
        assert!(!is_ollama_model("claude-3"));
        assert!(!is_ollama_model("gpt-4"));
    }
}
