use super::{Error, ParseError, Profile, Requester, Result};
use async_trait::async_trait;
use log::debug;
use md5::{Digest, Md5};
use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde_json::Value;
use std::collections::HashMap;
use std::fmt::Write;
use std::sync::Arc;
#[cfg(feature = "wasm-bindings")]
use wasm_bindgen::prelude::wasm_bindgen;

#[cfg_attr(feature = "wasm-bindings", wasm_bindgen)]
#[derive(Clone)]
pub struct HafasClient {
    pub(crate) profile: Arc<Box<dyn Profile>>,
    requester: Arc<Box<dyn Requester>>,
}

impl HafasClient {
    pub fn new<P: 'static + Profile, R: 'static + Requester>(profile: P, requester: R) -> Self {
        HafasClient {
            profile: Arc::new(Box::new(profile)),
            requester: Arc::new(Box::new(requester)),
        }
    }
}

#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen]
pub struct BoxedProfile {
    inner: Box<dyn Profile>,
}

#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen]
pub struct BoxedRequester {
    inner: Box<dyn Requester>,
}

#[cfg(feature = "wasm-bindings")]
impl<R: 'static + Requester> From<R> for BoxedRequester {
    fn from(inner: R) -> Self {
        Self {
            inner: Box::new(inner),
        }
    }
}

#[cfg(feature = "wasm-bindings")]
impl<P: 'static + Profile> From<P> for BoxedProfile {
    fn from(inner: P) -> Self {
        Self {
            inner: Box::new(inner),
        }
    }
}

#[cfg(feature = "wasm-bindings")]
#[wasm_bindgen]
impl HafasClient {
    #[wasm_bindgen(constructor)]
    pub fn wasm_new(profile: BoxedProfile, requester: BoxedRequester) -> Self {
        HafasClient {
            profile: Arc::new(profile.inner),
            requester: Arc::new(requester.inner),
        }
    }
}

#[cfg_attr(feature = "rt-multi-thread", async_trait)]
#[cfg_attr(not(feature = "rt-multi-thread"), async_trait(?Send))]
pub trait Client {
    async fn request<T: DeserializeOwned>(&self, req_json: Value) -> Result<T>;
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct HafasResponseInner {
    err: Option<String>,
    err_txt: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct HafasResponseOuter {
    err: Option<String>,
    err_txt: Option<String>,
    svc_res_l: Option<Vec<HafasResponseInner>>,
}

#[derive(Deserialize)]
struct HafasResponseInnerOk<T> {
    res: T,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct HafasResponseOuterOk<T> {
    svc_res_l: Vec<HafasResponseInnerOk<T>>,
}

#[cfg_attr(feature = "rt-multi-thread", async_trait)]
#[cfg_attr(not(feature = "rt-multi-thread"), async_trait(?Send))]
impl Client for HafasClient {
    async fn request<T: DeserializeOwned>(&self, mut req_json: Value) -> Result<T> {
        self.profile.prepare_body(&mut req_json);
        debug!("{}", serde_json::to_string_pretty(&req_json)?);
        let req_str = serde_json::to_string(&req_json)?;
        let mut url = self.profile.url().to_string();

        if let Some(salt) = self.profile.checksum_salt() {
            if self.profile.salt() {
                let mut hasher = Md5::new();
                hasher.update(&req_str);
                hasher.update(salt);
                let checksum = hasher
                    .finalize()
                    .iter()
                    .map(|b| format!("{:02x}", b))
                    .collect::<Vec<_>>()
                    .join("");
                write!(url, "?checksum={}", checksum).expect("Failed to write to string");
            }
            if self.profile.mic_mac() {
                let mut hasher_mic = Md5::new();
                hasher_mic.update(&req_str);
                let mic = hasher_mic
                    .finalize()
                    .iter()
                    .map(|b| format!("{:02x}", b))
                    .collect::<Vec<_>>()
                    .join("");
                let mut hasher_mac = Md5::new();
                hasher_mac.update(&mic);
                if let Ok(s) = hex::decode(salt) {
                    hasher_mac.update(s);
                } else {
                    hasher_mac.update(salt);
                }
                let mac = hasher_mac
                    .finalize()
                    .iter()
                    .map(|b| format!("{:02x}", b))
                    .collect::<Vec<_>>()
                    .join("");
                write!(url, "?mic={}&mac={}", mic, mac).expect("Failed to write to string");
            }
        }

        let mut headers = HashMap::new();
        headers.insert("Content-Type", "application/json");
        headers.insert("Accept", "application/json");
        self.profile.prepare_headers(&mut headers);

        let bytes = self.requester.request(url, req_str, headers).await?;
        debug!(
            "Response: {}",
            serde_json::to_string(&serde_json::from_slice::<serde_json::Value>(&bytes)?)?
        );

        {
            let data = serde_json::from_slice(&bytes)?;
            let HafasResponseOuter {
                err,
                err_txt,
                svc_res_l,
            } = data;
            if let Some(some_err) = err {
                if some_err != "OK" {
                    return Err(Error::Hafas {
                        text: err_txt.unwrap_or_else(|| format!("Code {}", &some_err)),
                        code: some_err,
                    });
                }
            }
            let HafasResponseInner { err, err_txt } = svc_res_l
                .map(|mut x| x.remove(0))
                .ok_or_else(|| ParseError::from("missing svcResL"))?;
            if let Some(some_err) = err {
                if some_err != "OK" {
                    return Err(Error::Hafas {
                        text: err_txt.unwrap_or_else(|| format!("Code {}", &some_err)),
                        code: some_err,
                    });
                }
            }
        }

        {
            let mut data: HafasResponseOuterOk<T> = serde_json::from_slice(&bytes)?;
            let HafasResponseInnerOk { res } = data.svc_res_l.remove(0);
            Ok(res)
        }
    }
}
