use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

macro_rules! js_value_conversions {
    ($t:ty) => {
        impl std::convert::From<wasm_bindgen::JsValue> for $t {
            fn from(t: JsValue) -> $t {
                t.into_serde::<$t>().unwrap()
            }
        }

        impl std::convert::From<$t> for wasm_bindgen::JsValue {
            fn from(t: $t) -> wasm_bindgen::JsValue {
                wasm_bindgen::JsValue::from_serde(&t).unwrap()
            }
        }
    };
}

#[wasm_bindgen]
extern "C" {
    type KV;

    #[wasm_bindgen(catch, js_name = "get", static_method_of = KV)]
    async fn get(key: &JsValue, r#type: &JsValue) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(catch, js_name = "getWithMetadata", static_method_of = KV)]
    async fn get_with_metadata(key: &JsValue, r#type: &JsValue) -> Result<JsValue, JsValue>;

    #[wasm_bindgen(catch, js_name = "put", static_method_of = KV)]
    async fn put(key: &JsValue, value: &JsValue, options: &JsValue) -> Result<(), JsValue>;

    #[wasm_bindgen(catch, js_name = "delete", static_method_of = KV)]
    async fn delete(key: &JsValue) -> Result<(), JsValue>;

    #[wasm_bindgen(catch, js_name = "list", static_method_of = KV)]
    async fn list(options: &JsValue) -> Result<JsValue, JsValue>;
}

pub struct Implementation;

impl Implementation {
    pub async fn get(key: String, r#type: Option<Type>) -> Result<Option<Value>, JsValue> {
        // Convert the key into a JS value:
        let key: JsValue = key.into();
        // Convert the type into a JS value:
        let r#type: JsValue = match r#type {
            Some(r#type) => r#type.into(),
            None => JsValue::UNDEFINED,
        };
        // Call the imported getWithMetadata JS method:
        let result = match KV::get(&key, &r#type).await {
            Ok(result) => result,
            Err(_) => return Err("oops".into()),
        };
        // Handle null/undefined values:
        if result.is_null() {
            return Ok(None);
        }
        // Return result:
        Ok(Some(result.into()))
    }

    pub async fn get_with_metadata(
        key: String,
        r#type: Option<Type>,
    ) -> Result<ValueWithMetadata, JsValue> {
        // Convert the key into a JS value:
        let key: JsValue = key.into();
        // Convert the type into a JS value:
        let r#type: JsValue = match r#type {
            Some(r#type) => r#type.into(),
            None => JsValue::UNDEFINED,
        };
        // Call the imported getWithMetadata JS method:
        match KV::get_with_metadata(&key, &r#type).await {
            Ok(result) => Ok(result.into()),
            Err(_) => Err("oops".into()),
        }
    }

    pub async fn put(
        key: String,
        value: Value,
        options: Option<PutOptions>,
    ) -> Result<(), JsValue> {
        // Convert the key into a JS value:
        let key: JsValue = key.into();
        // Convert the value into a JS value:
        let value: JsValue = value.into();
        // Convert the options into a JS value:
        let options: JsValue = match options {
            Some(options) => options.into(),
            None => JsValue::UNDEFINED,
        };
        // Call the imported put JS method:
        match KV::put(&key, &value, &options).await {
            Ok(result) => Ok(result),
            Err(_) => Err("oops".into()),
        }
    }

    pub async fn delete(key: String) -> Result<(), JsValue> {
        // Convert the key into a JS value:
        let key: JsValue = key.into();
        // Call the imported delete JS method:
        match KV::delete(&key).await {
            Ok(result) => Ok(result),
            Err(_) => Err("oops".into()),
        }
    }

    pub async fn list(options: Option<ListOptions>) -> Result<ListValue, JsValue> {
        // Convert the options into a JS value:
        let options: JsValue = match options {
            Some(options) => options.into(),
            None => JsValue::UNDEFINED,
        };
        // Call the imported list JS method:
        match KV::list(&options).await {
            Ok(result) => Ok(result.into()),
            Err(_) => Err("oops".into()),
        }
    }
}

pub type JSON = HashMap<String, serde_json::Value>;

pub enum Value {
    Text(String),
    JSON(JSON),
    ArrayBuffer(js_sys::ArrayBuffer),
    Stream(web_sys::ReadableStream),
}
impl std::convert::From<wasm_bindgen::JsValue> for Value {
    fn from(t: JsValue) -> Value {
        if let Ok(value) = t.clone().dyn_into::<js_sys::ArrayBuffer>() {
            return Value::ArrayBuffer(value);
        }
        if let Ok(value) = t.clone().dyn_into::<web_sys::ReadableStream>() {
            return Value::Stream(value);
        }
        if let Ok(value) = t.clone().into_serde() {
            return Value::JSON(value);
        }
        Value::Text(String::from(js_sys::JsString::from(t)))
    }
}
impl std::convert::From<Value> for wasm_bindgen::JsValue {
    fn from(t: Value) -> wasm_bindgen::JsValue {
        match t {
            Value::Text(value) => JsValue::from(value),
            Value::JSON(value) => JsValue::from_serde(&value).unwrap(),
            Value::ArrayBuffer(value) => JsValue::from(value),
            Value::Stream(value) => JsValue::from(value),
        }
    }
}

pub struct ValueWithMetadata {
    pub value: Option<Value>,
    pub metadata: Option<JSON>,
}
impl std::convert::From<wasm_bindgen::JsValue> for ValueWithMetadata {
    fn from(t: JsValue) -> ValueWithMetadata {
        t.into()
    }
}
impl std::convert::From<ValueWithMetadata> for wasm_bindgen::JsValue {
    fn from(t: ValueWithMetadata) -> wasm_bindgen::JsValue {
        t.into()
    }
}

#[derive(Deserialize, Serialize)]
pub enum Type {
    #[serde(rename = "text")]
    Text,
    #[serde(rename = "json")]
    JSON,
    #[serde(rename = "arrayBuffer")]
    ArrayBuffer,
    #[serde(rename = "stream")]
    Stream,
}
js_value_conversions!(Type);

#[derive(Deserialize, Serialize)]
pub struct PutOptions {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expiration: Option<u64>,
    #[serde(rename = "expirationTtl", skip_serializing_if = "Option::is_none")]
    pub expiration_ttl: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<JSON>,
}
js_value_conversions!(PutOptions);

#[derive(Deserialize, Serialize)]
pub struct ListOptions {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub prefix: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub limit: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cursor: Option<String>,
}
js_value_conversions!(ListOptions);

#[derive(Deserialize, Serialize)]
pub struct ListValue {
    pub keys: Vec<ListValueKey>,
    pub list_complete: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cursor: Option<String>,
}
js_value_conversions!(ListValue);

#[derive(Deserialize, Serialize)]
pub struct ListValueKey {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub expiration: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<JSON>,
}
js_value_conversions!(ListValueKey);
