//! HTTP helper utilities for providers
//!
//! This module provides low-level, provider-agnostic HTTP utilities.
//! It does NOT know about models, providers, or chat semantics.
//!
//! **Important**: This is a dumb HTTP helper. All provider-specific logic
//! (URL building, authentication, request/response transformation) belongs
//! in the individual provider implementations.

use reqwest::multipart::{Form, Part};
use reqwest::Client;
use serde_json::Value;
use std::time::Duration;

/// HTTP request specification
///
/// This struct encapsulates the parameters needed to make an HTTP POST request.
/// It is provider-agnostic and does not contain any model or provider semantics.
pub struct HttpRequestSpec {
    /// Full URL to POST to
    pub url: String,
    /// HTTP headers as (name, value) pairs
    pub headers: Vec<(String, String)>,
    /// JSON body to send
    pub body: Value,
    /// Request timeout
    pub timeout: Duration,
}

impl Default for HttpRequestSpec {
    fn default() -> Self {
        Self {
            url: String::new(),
            headers: Vec::new(),
            body: Value::Null,
            timeout: Duration::from_secs(30),
        }
    }
}

/// Execute a JSON POST request
///
/// This function performs a POST request with JSON body and returns the
/// response as a JSON value. It is provider-agnostic and does not know
/// about models or chat semantics.
///
/// # Arguments
///
/// * `client` - The reqwest Client to use for the request
/// * `req` - The HTTP request specification
///
/// # Returns
///
/// Returns the response body as a JSON value, or an error if the request fails.
pub async fn post_json(client: &Client, req: HttpRequestSpec) -> anyhow::Result<Value> {
    // Serialize body to string to ensure consistent JSON encoding
    let body_string = serde_json::to_string(&req.body)?;

    let mut request_builder = client
        .post(&req.url)
        .timeout(req.timeout)
        .header("Content-Type", "application/json")
        .body(body_string);

    for (name, value) in req.headers {
        // Skip Content-Type since we set it explicitly above
        if name.to_lowercase() != "content-type" {
            request_builder = request_builder.header(&name, &value);
        }
    }

    let response = request_builder.send().await?;

    if !response.status().is_success() {
        let status = response.status();
        let error_text = response.text().await.unwrap_or_default();
        anyhow::bail!("HTTP error ({}): {}", status, error_text);
    }

    let json: Value = response.json().await?;
    Ok(json)
}

/// HTTP multipart request specification for file uploads
pub struct MultipartRequestSpec {
    /// Full URL to POST to
    pub url: String,
    /// HTTP headers as (name, value) pairs (excluding Content-Type, set by multipart)
    pub headers: Vec<(String, String)>,
    /// File data to upload
    pub file_data: Vec<u8>,
    /// File name for the upload
    pub file_name: String,
    /// MIME type of the file
    pub file_mime_type: String,
    /// Additional form fields as (name, value) pairs
    pub form_fields: Vec<(String, String)>,
    /// Request timeout
    pub timeout: Duration,
}

impl Default for MultipartRequestSpec {
    fn default() -> Self {
        Self {
            url: String::new(),
            headers: Vec::new(),
            file_data: Vec::new(),
            file_name: "audio.wav".to_string(),
            file_mime_type: "audio/wav".to_string(),
            form_fields: Vec::new(),
            timeout: Duration::from_secs(60),
        }
    }
}

/// Execute a multipart POST request (for file uploads like audio transcription)
///
/// # Arguments
///
/// * `client` - The reqwest Client to use for the request
/// * `req` - The multipart request specification
///
/// # Returns
///
/// Returns the response body as a JSON value, or an error if the request fails.
pub async fn post_multipart(client: &Client, req: MultipartRequestSpec) -> anyhow::Result<Value> {
    // Build multipart form
    let file_part = Part::bytes(req.file_data)
        .file_name(req.file_name)
        .mime_str(&req.file_mime_type)?;

    let mut form = Form::new().part("file", file_part);

    // Add additional form fields
    for (name, value) in req.form_fields {
        form = form.text(name, value);
    }

    let mut request_builder = client.post(&req.url).timeout(req.timeout).multipart(form);

    // Add headers (excluding Content-Type which is set by multipart)
    for (name, value) in req.headers {
        if name.to_lowercase() != "content-type" {
            request_builder = request_builder.header(&name, &value);
        }
    }

    let response = request_builder.send().await?;

    if !response.status().is_success() {
        let status = response.status();
        let error_text = response.text().await.unwrap_or_default();
        anyhow::bail!("HTTP error ({}): {}", status, error_text);
    }

    let json: Value = response.json().await?;
    Ok(json)
}

/// HTTP request specification for binary response (e.g., TTS audio)
pub struct BinaryRequestSpec {
    /// Full URL to POST to
    pub url: String,
    /// HTTP headers as (name, value) pairs
    pub headers: Vec<(String, String)>,
    /// JSON body to send
    pub body: Value,
    /// Request timeout
    pub timeout: Duration,
}

impl Default for BinaryRequestSpec {
    fn default() -> Self {
        Self {
            url: String::new(),
            headers: Vec::new(),
            body: Value::Null,
            timeout: Duration::from_secs(60),
        }
    }
}

/// Binary response from a POST request
pub struct BinaryResponse {
    /// Response body bytes
    pub data: Vec<u8>,
    /// Content-Type header from response
    pub content_type: String,
}

/// Execute a POST request that returns binary data (e.g., TTS audio)
///
/// # Arguments
///
/// * `client` - The reqwest Client to use for the request
/// * `req` - The binary request specification
///
/// # Returns
///
/// Returns the response as binary data with content-type, or an error if the request fails.
pub async fn post_binary(
    client: &Client,
    req: BinaryRequestSpec,
) -> anyhow::Result<BinaryResponse> {
    let body_string = serde_json::to_string(&req.body)?;

    let mut request_builder = client
        .post(&req.url)
        .timeout(req.timeout)
        .header("Content-Type", "application/json")
        .body(body_string);

    for (name, value) in req.headers {
        if name.to_lowercase() != "content-type" {
            request_builder = request_builder.header(&name, &value);
        }
    }

    let response = request_builder.send().await?;

    if !response.status().is_success() {
        let status = response.status();
        let error_text = response.text().await.unwrap_or_default();
        anyhow::bail!("HTTP error ({}): {}", status, error_text);
    }

    // Extract content-type header
    let content_type = response
        .headers()
        .get("content-type")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("audio/mpeg")
        .to_string();

    let data = response.bytes().await?.to_vec();

    Ok(BinaryResponse { data, content_type })
}
