//! Blocking `reqwest` client for the IONOS OpenAI-compatible endpoint.
//!
//! Retry policy (parent spec §Error handling):
//! - HTTP 5xx / connection / read-timeout (30s): exp backoff 1s/4s/16s, give up after 3rd retry.
//! - HTTP 429: respect `Retry-After`, else 30s, give up after 5th retry.
//! - HTTP 4xx other than 429: hard fail (typically auth).

use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::time::Duration;

const ENDPOINT: &str = "https://openai.inference.de-txl.ionos.com/v1/chat/completions";

#[derive(Debug, Clone, Serialize)]
pub struct Message {
    pub role: String,
    pub content: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct ChatRequest<'a> {
    pub model: &'a str,
    pub messages: &'a [Message],
    pub temperature: f32,
    pub top_p: f32,
}

#[derive(Debug, Deserialize)]
pub struct ChatResponse {
    pub choices: Vec<Choice>,
    pub usage: Usage,
}

#[derive(Debug, Deserialize)]
pub struct Choice {
    pub message: ChoiceMessage,
}

#[derive(Debug, Deserialize)]
pub struct ChoiceMessage {
    pub content: String,
}

#[derive(Debug, Deserialize, Clone, Copy)]
pub struct Usage {
    pub prompt_tokens: u64,
    pub completion_tokens: u64,
    pub total_tokens: u64,
}

/// Errors the client treats as terminal (caller should mark the run as api_failure).
#[derive(Debug, thiserror::Error)]
pub enum IonosError {
    #[error("authentication failure (HTTP {0}): check IONOS_API_TOKEN")]
    Auth(u16),
    #[error("retry budget exhausted after {tries} attempts: {last_err}")]
    RetriesExhausted { tries: u32, last_err: String },
    #[error("transport error: {0}")]
    Transport(String),
}

pub struct IonosClient {
    http: reqwest::blocking::Client,
    token: String,
}

impl IonosClient {
    /// Build from `IONOS_API_TOKEN` env. Errors if the var is unset or empty.
    pub fn from_env() -> Result<Self> {
        let token = std::env::var("IONOS_API_TOKEN")
            .context("IONOS_API_TOKEN env var is unset")?;
        if token.trim().is_empty() {
            return Err(anyhow!("IONOS_API_TOKEN is empty"));
        }
        let http = reqwest::blocking::Client::builder()
            .timeout(Duration::from_secs(30))
            .build()?;
        Ok(Self { http, token })
    }

    /// Post a chat request with the retry policy above.
    pub fn post(&self, req: &ChatRequest<'_>) -> std::result::Result<ChatResponse, IonosError> {
        let backoffs_5xx = [Duration::from_secs(1), Duration::from_secs(4), Duration::from_secs(16)];
        let mut tries_5xx = 0u32;
        let mut tries_429 = 0u32;
        let mut last_err = String::from("no attempts");
        loop {
            let result = self.http
                .post(ENDPOINT)
                .bearer_auth(&self.token)
                .json(req)
                .send();
            match result {
                Ok(resp) => {
                    let status = resp.status();
                    if status.is_success() {
                        return resp.json::<ChatResponse>().map_err(|e| IonosError::Transport(e.to_string()));
                    }
                    let code = status.as_u16();
                    if code == 429 {
                        let wait = resp
                            .headers()
                            .get("retry-after")
                            .and_then(|v| v.to_str().ok())
                            .and_then(|s| s.parse::<u64>().ok())
                            .map(Duration::from_secs)
                            .unwrap_or(Duration::from_secs(30));
                        if tries_429 >= 5 {
                            return Err(IonosError::RetriesExhausted {
                                tries: tries_429,
                                last_err: format!("429 rate limit; last wait {}s", wait.as_secs()),
                            });
                        }
                        std::thread::sleep(wait);
                        tries_429 += 1;
                        continue;
                    }
                    if (400..500).contains(&code) {
                        return Err(IonosError::Auth(code));
                    }
                    // 5xx
                    if let Some(backoff) = backoffs_5xx.get(tries_5xx as usize) {
                        std::thread::sleep(*backoff);
                        tries_5xx += 1;
                        last_err = format!("HTTP {code}");
                        continue;
                    }
                    return Err(IonosError::RetriesExhausted {
                        tries: tries_5xx,
                        last_err: format!("HTTP {code}"),
                    });
                }
                Err(e) => {
                    if let Some(backoff) = backoffs_5xx.get(tries_5xx as usize) {
                        std::thread::sleep(*backoff);
                        tries_5xx += 1;
                        last_err = e.to_string();
                        continue;
                    }
                    return Err(IonosError::RetriesExhausted {
                        tries: tries_5xx,
                        last_err: e.to_string(),
                    });
                }
            }
        }
    }
}
