//! `<issuer>/.well-known/openid-configuration` discovery + in-memory cache.
//!
//! Most OIDC providers serve a JSON document at this URL describing the
//! authorization, token, and userinfo endpoints. Doing discovery once per
//! provider and caching the result keeps the per-login overhead to two
//! HTTP calls (token exchange + userinfo).

use hbb_common::log;
use once_cell::sync::Lazy;
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Mutex;

#[derive(Debug, Clone, Deserialize)]
pub struct OidcDiscovery {
    pub authorization_endpoint: String,
    pub token_endpoint: String,
    #[serde(default)]
    pub userinfo_endpoint: Option<String>,
    #[serde(default)]
    pub issuer: Option<String>,
}

static CACHE: Lazy<Mutex<HashMap<String, OidcDiscovery>>> =
    Lazy::new(|| Mutex::new(HashMap::new()));

/// Fetch (or return cached) discovery document for `issuer_url`. Strips a
/// trailing `/` so the cache key is stable across operator typos.
pub async fn discover(issuer_url: &str) -> Result<OidcDiscovery, String> {
    let issuer = issuer_url.trim_end_matches('/').to_string();
    if let Some(d) = CACHE.lock().unwrap().get(&issuer).cloned() {
        return Ok(d);
    }
    let url = format!("{}/.well-known/openid-configuration", issuer);
    log::info!("oidc: discovering {}", url);
    let body = http_get(&url).await?;
    let parsed: OidcDiscovery = serde_json::from_str(&body)
        .map_err(|e| format!("discovery parse {}: {}", url, e))?;
    CACHE.lock().unwrap().insert(issuer, parsed.clone());
    Ok(parsed)
}

/// Blocking HTTP GET wrapped in `spawn_blocking`. We use the existing
/// `reqwest::blocking::Client` rather than adding an async client, because
/// (a) discovery happens at most once per provider and (b) the rustdesk
/// reqwest fork is configured for blocking-only use throughout the server.
pub async fn http_get(url: &str) -> Result<String, String> {
    let url = url.to_owned();
    hbb_common::tokio::task::spawn_blocking(move || {
        let client = reqwest::blocking::Client::builder()
            .timeout(std::time::Duration::from_secs(15))
            .build()
            .map_err(|e| format!("http client build: {}", e))?;
        let resp = client
            .get(&url)
            .send()
            .map_err(|e| format!("http get {}: {}", url, e))?;
        let status = resp.status();
        let body = resp.text().map_err(|e| format!("read body: {}", e))?;
        if !status.is_success() {
            return Err(format!("http {} -> {}: {}", url, status, body));
        }
        Ok(body)
    })
    .await
    .map_err(|e| format!("spawn_blocking: {}", e))?
}

pub async fn http_post_form(
    url: &str,
    form: &[(&str, &str)],
) -> Result<String, String> {
    let url = url.to_owned();
    let owned: Vec<(String, String)> = form
        .iter()
        .map(|(k, v)| (k.to_string(), v.to_string()))
        .collect();
    hbb_common::tokio::task::spawn_blocking(move || {
        let client = reqwest::blocking::Client::builder()
            .timeout(std::time::Duration::from_secs(15))
            .build()
            .map_err(|e| format!("http client build: {}", e))?;
        let pairs: Vec<(&str, &str)> = owned
            .iter()
            .map(|(k, v)| (k.as_str(), v.as_str()))
            .collect();
        let resp = client
            .post(&url)
            .form(&pairs)
            .send()
            .map_err(|e| format!("http post {}: {}", url, e))?;
        let status = resp.status();
        let body = resp.text().map_err(|e| format!("read body: {}", e))?;
        if !status.is_success() {
            return Err(format!("http {} -> {}: {}", url, status, body));
        }
        Ok(body)
    })
    .await
    .map_err(|e| format!("spawn_blocking: {}", e))?
}

pub async fn http_get_with_bearer(
    url: &str,
    bearer: &str,
) -> Result<String, String> {
    let url = url.to_owned();
    let bearer = bearer.to_owned();
    hbb_common::tokio::task::spawn_blocking(move || {
        let client = reqwest::blocking::Client::builder()
            .timeout(std::time::Duration::from_secs(15))
            .build()
            .map_err(|e| format!("http client build: {}", e))?;
        let resp = client
            .get(&url)
            .header("Authorization", format!("Bearer {}", bearer))
            .send()
            .map_err(|e| format!("http get {}: {}", url, e))?;
        let status = resp.status();
        let body = resp.text().map_err(|e| format!("read body: {}", e))?;
        if !status.is_success() {
            return Err(format!("http {} -> {}: {}", url, status, body));
        }
        Ok(body)
    })
    .await
    .map_err(|e| format!("spawn_blocking: {}", e))?
}
