//! Anthropic backend — `POST /v1/messages`.
//!
//! Uses the Anthropic Messages API with:
//! - Header `x-api-key: <key>`
//! - Header `anthropic-version: 2023-06-01`
//! - Prompt caching via `cache_control` on the system message
//!
//! ## Prompt caching
//!
//! The system message is annotated with `"cache_control": {"type": "ephemeral"}`
//! to enable Anthropic prompt caching. Reduces latency and cost for repeated
//! calls sharing the same system prompt.
//!
//! ## Request body format
//!
//! ```json
//! {
//!   "model": "claude-haiku-4-5",
//!   "max_tokens": 512,
//!   "system": [{"type": "text", "text": "...", "cache_control": {"type": "ephemeral"}}],
//!   "messages": [{"role": "user", "content": "..."}]
//! }
//! ```
//!
//! ## Response format
//!
//! `content[0].text` contains the response text.

use std::time::Duration;

use async_trait::async_trait;
use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Serialize};

use crate::backend::{CuratorDecision, LlmBackend, LlmError};
use crate::openai_compat::{map_reqwest_err, parse_curator_decision};

const DEFAULT_TIMEOUT_SECS: u64 = 30;

/// Maximum number of tokens generated by the LLM when no value is configured.
///
/// 1024 matches the legacy gatekeeper default (`max_tokens: Some(1024)`)
/// and is well above the ceiling for a complete curator JSON response
/// (section + 5 tags + 5 wikilinks + duplicate_hint ≈ 150–300 tokens).
const DEFAULT_MAX_TOKENS: u32 = 1024;

const ANTHROPIC_API_BASE: &str = "https://api.anthropic.com";
const ANTHROPIC_VERSION: &str = "2023-06-01";

/// Anthropic Messages API backend with prompt caching.
///
/// # Example
///
/// ```rust,no_run
/// use gradatum_chat::anthropic_compat::AnthropicCompatBackend;
/// use secrecy::SecretString;
///
/// let backend = AnthropicCompatBackend::new(
///     SecretString::new("sk-ant-...".to_string().into()),
///     "claude-haiku-4-5".to_string(),
/// );
/// ```
pub struct AnthropicCompatBackend {
    client: reqwest::Client,
    base_url: String,
    model: String,
    api_key: SecretString,
    /// Maximum number of tokens in the LLM response.
    /// Default: [`DEFAULT_MAX_TOKENS`] (1024). Configurable via [`with_max_tokens`].
    ///
    /// [`with_max_tokens`]: AnthropicCompatBackend::with_max_tokens
    max_tokens: u32,
}

impl AnthropicCompatBackend {
    /// Creates an Anthropic backend targeting the public API with a 30-second timeout.
    ///
    /// `max_tokens` is initialised to `DEFAULT_MAX_TOKENS` (1024).
    /// Use [`with_max_tokens`] to override from TOML configuration.
    ///
    /// [`with_max_tokens`]: AnthropicCompatBackend::with_max_tokens
    pub fn new(api_key: SecretString, model: String) -> Self {
        let client = reqwest::Client::builder()
            .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
            .build()
            .expect("construction reqwest Client avec timeout valide");
        Self {
            client,
            base_url: ANTHROPIC_API_BASE.to_string(),
            model,
            api_key,
            max_tokens: DEFAULT_MAX_TOKENS,
        }
    }

    /// Replaces the base URL (for tests or proxies).
    #[must_use]
    pub fn with_base_url(self, base_url: String) -> Self {
        Self { base_url, ..self }
    }

    /// Replaces the timeout (rebuilds the internal client).
    #[must_use]
    pub fn with_timeout(self, timeout: Duration) -> Self {
        let client = reqwest::Client::builder()
            .timeout(timeout)
            .build()
            .expect("construction reqwest Client avec timeout valide");
        Self { client, ..self }
    }

    /// Replaces the maximum number of tokens generated by the LLM.
    ///
    /// Wired from `CuratorPipelineConfig.llm_review_max_tokens` via `from_config()`.
    /// The TOML value `[curator] llm_review_max_tokens = N` propagates here.
    #[must_use]
    pub fn with_max_tokens(self, n: u32) -> Self {
        Self {
            max_tokens: n,
            ..self
        }
    }
}

// --- DTOs Anthropic ---

#[derive(Serialize)]
struct AnthropicRequest<'a> {
    model: &'a str,
    max_tokens: u32,
    /// System prompt with `cache_control` for prompt caching.
    system: Vec<AnthropicSystemBlock<'a>>,
    messages: Vec<AnthropicMessage<'a>>,
}

#[derive(Serialize)]
struct AnthropicSystemBlock<'a> {
    #[serde(rename = "type")]
    block_type: &'a str,
    text: &'a str,
    #[serde(skip_serializing_if = "Option::is_none")]
    cache_control: Option<AnthropicCacheControl<'a>>,
}

#[derive(Serialize)]
struct AnthropicCacheControl<'a> {
    #[serde(rename = "type")]
    cache_type: &'a str,
}

#[derive(Serialize)]
struct AnthropicMessage<'a> {
    role: &'a str,
    content: &'a str,
}

#[derive(Deserialize)]
struct AnthropicResponse {
    content: Vec<AnthropicContentBlock>,
}

#[derive(Deserialize)]
struct AnthropicContentBlock {
    text: String,
}

#[async_trait]
impl LlmBackend for AnthropicCompatBackend {
    fn name(&self) -> &'static str {
        "anthropic"
    }

    fn is_local(&self) -> bool {
        false
    }

    async fn classify(&self, system: &str, user: &str) -> Result<CuratorDecision, LlmError> {
        let url = format!("{}/v1/messages", self.base_url.trim_end_matches('/'));

        let req = AnthropicRequest {
            model: &self.model,
            max_tokens: self.max_tokens,
            // Système avec cache_control ephemeral pour le prompt caching
            system: vec![AnthropicSystemBlock {
                block_type: "text",
                text: system,
                cache_control: Some(AnthropicCacheControl {
                    cache_type: "ephemeral",
                }),
            }],
            messages: vec![AnthropicMessage {
                role: "user",
                content: user,
            }],
        };

        let resp = self
            .client
            .post(&url)
            .header("x-api-key", self.api_key.expose_secret())
            .header("anthropic-version", ANTHROPIC_VERSION)
            .json(&req)
            .send()
            .await
            .map_err(map_reqwest_err)?;

        let status = resp.status();
        if status == 401 || status == 403 {
            return Err(LlmError::AuthError);
        }
        if status == 429 {
            return Err(LlmError::RateLimit);
        }
        if status == 400 {
            let body = resp.text().await.unwrap_or_default();
            return Err(LlmError::BadRequest(body));
        }
        if status.is_server_error() {
            let body = resp.text().await.unwrap_or_default();
            return Err(LlmError::ServerError {
                status: status.as_u16(),
                body,
            });
        }

        let parsed: AnthropicResponse = resp
            .json()
            .await
            .map_err(|e| LlmError::Parse(e.to_string()))?;

        let text = parsed
            .content
            .into_iter()
            .next()
            .ok_or_else(|| LlmError::Parse("empty content array in Anthropic response".into()))?
            .text;

        parse_curator_decision(&text)
    }
}
