//! HTTP client

use crate::consts::*;
use crate::deser::structs::UniqueIds;
use crate::error::{Error, Result};
use base64::Engine as _;
use reqwest::blocking::{Client, ClientBuilder};
use reqwest::header;
use serde::Deserialize;

pub struct ToccoClientConfig<'a> {
    pub cred: ToccoCred<'a>,
    pub business_unit: Option<&'a str>,
    pub base_url: &'a str,
}

pub enum ToccoCred<'a> {
    Some { user: &'a str, api_key: &'a str },
    None,
}

impl<'a> ToccoCred<'_> {
    fn user(&'a self) -> Option<&'a str> {
        match self {
            ToccoCred::Some { user, .. } => Some(user),
            ToccoCred::None => None,
        }
    }
}

pub fn client_for_tocco(config: &ToccoClientConfig) -> Result<Client> {
    let mut headers = header::HeaderMap::new();
    if let ToccoCred::Some { user, api_key } = &config.cred {
        let cred = format!("{user}:{api_key}");
        let encoded_credentials = base64::prelude::BASE64_STANDARD.encode(cred);
        let mut value: header::HeaderValue =
            format!("Basic {encoded_credentials}").parse().unwrap();
        value.set_sensitive(true);
        headers.insert(header::AUTHORIZATION, value);
    }
    headers.insert(
        "X-Business-Unit",
        config
            .business_unit
            .unwrap_or(TOCCO_NULL_BU)
            .parse()
            .map_err(|e| Error::Custom(format!("cannot parse business unit: {e}")))?,
    );
    let client = builder().default_headers(headers).build()?;
    if let Some(bu) = config.business_unit {
        tocco_bu_self_test(config.base_url, bu)?;
    }
    tocco_user_self_test(&client, config.base_url, config.cred.user())?;
    Ok(client)
}

pub fn client() -> Result<Client> {
    Ok(builder().build()?)
}

fn builder() -> ClientBuilder {
    reqwest::blocking::Client::builder()
        .https_only(true)
        .user_agent(concat!("toccofs/", env!("CARGO_PKG_VERSION")))
}

fn tocco_bu_self_test(base_url: &str, business_unit: &str) -> Result<()> {
    // Using a client without X-Business-Unit header set.
    let client = client()?;
    let body: UniqueIds = client
        .get(format!(
            "{base_url}/nice2/rest/entities/2.0/Business_unit?_paths=unique_id&_sort=unique_id&_limit=1000"
        ))
        .send()?
        .error_for_status()?
        .json()?;
    let units = body.unique_ids();
    if units.iter().any(|bu| *bu == business_unit) {
        Ok(())
    } else {
        Err(Error::Custom(format!(
            "invalid business unit {business_unit:?}, available units: {units:?}"
        )))
    }
}

pub fn arbitrary_language(base_url: &str) -> Result<String> {
    // Using a client without X-Business-Unit header set.
    let client = client()?;
    let body: UniqueIds = client
        .get(format!(
            "{base_url}/nice2/rest/entities/2.0/Correspondence_language?_paths=unique_id&_limit=1"
        ))
        .send()?
        .error_for_status()?
        .json()?;
    let langs = body.unique_ids();
    Ok(langs.into_iter().next().expect("no language found"))
}

fn tocco_user_self_test(client: &Client, base_url: &str, user_name: Option<&str>) -> Result<()> {
    #[derive(Deserialize)]
    struct UserName {
        success: bool,

        #[serde(rename = "username")]
        user_name: String,
    }

    let url = format!("{base_url}/nice2/username");
    let body: UserName = client.get(url).send()?.error_for_status()?.json()?;

    let expected_user_name = user_name.unwrap_or("anonymous");
    if body.success && body.user_name == expected_user_name {
        Ok(())
    } else {
        Err(Error::Custom(format!(
            "authentication for {expected_user_name:?} failed: check user name and API key"
        )))
    }
}
