#![warn(rust_2018_idioms)]

use std::collections::HashMap;
use std::fmt;

use pyo3::prelude::*;
use pyo3::types::PyDict;
use pythonize::pythonize;
use serde_json::Value;

use decider::{init_decider, Context, ContextField, Decider, Decision};

#[pyclass]
pub struct PyDecider {
    decider: Option<Decider>,
    err: Option<PyDeciderError>,
}

#[pyclass]
pub struct PyContext {
    context: Context,
    err: Option<PyDeciderError>,
}

#[pymethods]
impl PyContext {
    pub fn inspect(&self) -> String {
        return format!("err: {:#?} \ncontext: {:#?}", self.err, self.context);
    }

    pub fn err(&self) -> Option<String> {
        self.err.as_ref().map(|e| e.to_string())
    }
}

#[pyclass]
pub struct GetExperimentRes {
    val: Option<Py<PyAny>>,
    pub err: Option<PyDeciderError>,
}

#[pymethods]
impl GetExperimentRes {
    pub fn inspect(&self) -> String {
        format!("err: {:?}, val: {:?}", self.err, self.val)
    }

    pub fn val(&mut self) -> Option<Py<PyAny>> {
        self.val.clone()
    }

    pub fn err(&self) -> Option<String> {
        self.err.as_ref().map(|e| e.to_string())
    }
}

#[pyclass]
pub struct GetBoolRes {
    val: bool,
    err: Option<PyDeciderError>,
}

#[pymethods]
impl GetBoolRes {
    pub fn inspect(&self) -> String {
        format!("err: {:?}, val: {:?}", self.err, self.val)
    }

    pub fn val(&self) -> bool {
        self.val
    }

    pub fn err(&self) -> Option<String> {
        self.err.as_ref().map(|e| e.to_string())
    }
}

#[pyclass]
pub struct GetIntegerRes {
    val: i64,
    err: Option<PyDeciderError>,
}

#[pymethods]
impl GetIntegerRes {
    pub fn inspect(&self) -> String {
        format!("err: {:?}, val: {:?}", self.err, self.val)
    }

    pub fn val(&self) -> i64 {
        self.val
    }

    pub fn err(&self) -> Option<String> {
        self.err.as_ref().map(|e| e.to_string())
    }
}

#[pyclass]
pub struct GetFloatRes {
    val: f64,
    err: Option<PyDeciderError>,
}

#[pymethods]
impl GetFloatRes {
    pub fn inspect(&self) -> String {
        format!("err: {:?}, val: {:?}", self.err, self.val)
    }

    pub fn val(&self) -> f64 {
        self.val
    }

    pub fn err(&self) -> Option<String> {
        self.err.as_ref().map(|e| e.to_string())
    }
}

#[pyclass]
pub struct GetStringRes {
    val: String,
    err: Option<PyDeciderError>,
}

#[pymethods]
impl GetStringRes {
    pub fn inspect(&self) -> String {
        format!("err: {:?}, val: {:?}", self.err, self.val)
    }

    pub fn val(&self) -> String {
        self.val.clone()
    }

    pub fn err(&self) -> Option<String> {
        self.err.as_ref().map(|e| e.to_string())
    }
}

#[pyclass]
pub struct GetMapRes {
    val: Py<PyAny>,
    err: Option<PyDeciderError>,
}

#[pymethods]
impl GetMapRes {
    pub fn inspect(&self) -> String {
        format!("err: {:?}, val: {:?}", self.err, self.val)
    }

    pub fn val(&self) -> Py<PyAny> {
        self.val.clone()
    }

    pub fn err(&self) -> Option<String> {
        self.err.as_ref().map(|e| e.to_string())
    }
}

#[pyclass]
#[derive(Debug, Clone)]
pub struct PyDecision {
    decision: Option<Decision>,
    err: Option<PyDeciderError>,
}

#[pymethods]
impl PyDecision {
    pub fn inspect(&self) -> String {
        format!("err: {:?}, decision: {:?}", self.err, self.decision)
    }

    pub fn decision(&self) -> Option<String> {
        self.decision.as_ref().and_then(|d| d.variant_name.clone())
    }

    pub fn decision_dict(&self) -> HashMap<String, String> {
        match &self.decision {
            None => HashMap::new(),
            Some(d) => match &d.variant_name {
                None => HashMap::new(),
                Some(name) => {
                    let mut out = HashMap::new();

                    out.insert("name".to_string(), name.to_string());
                    out.insert("id".to_string(), d.feature_id.to_string());
                    out.insert("version".to_string(), d.feature_version.to_string());
                    out.insert("experimentName".to_string(), d.feature_name.to_string());

                    out
                }
            },
        }
    }

    pub fn value_dict(&self) -> HashMap<String, Py<PyAny>> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        match &self.decision {
            None => HashMap::new(),
            Some(d) => {
                let mut out: HashMap<String, Py<PyAny>> = HashMap::new();

                out.insert("name".to_string(), d.feature_name.clone().into_py(py));
                out.insert(
                    "type".to_string(),
                    match &d.value_type {
                        Some(vt) => vt.to_string().into_py(py),
                        None => "".into_py(py),
                    },
                );
                out.insert(
                    "value".to_string(),
                    match pythonize(py, &d.value.clone()) {
                        Ok(val) => val,
                        Err(_) => "".into_py(py),
                    },
                );

                out
            }
        }
    }

    pub fn events(&self) -> Vec<String> {
        match &self.decision {
            None => vec![],
            Some(d) => d.event_data.clone(),
        }
    }

    pub fn err(&self) -> Option<String> {
        self.err.as_ref().map(|e| e.to_string())
    }
}

#[pyclass]
pub struct GetAllDecisionsRes {
    decisions: Option<HashMap<String, PyDecision>>,
    err: Option<PyDeciderError>,
}

#[pymethods]
impl GetAllDecisionsRes {
    pub fn inspect(&self) -> String {
        format!("err: {:?}, decisions: {:?}", self.err, self.decisions)
    }

    pub fn decisions(&self) -> Option<HashMap<String, PyDecision>> {
        self.decisions.as_ref().cloned()
    }

    pub fn err(&self) -> Option<String> {
        self.err.as_ref().map(|e| e.to_string())
    }
}

#[pymethods]
impl PyDecider {
    pub fn err(&self) -> Option<String> {
        self.err.as_ref().map(|e| e.to_string())
    }

    pub fn choose(
        &self,
        feature_name: &str,
        ctx: &PyContext,
        identifier_type: Option<&str>,
    ) -> Option<PyDecision> {
        match &self.decider {
            Some(decider) => {
                let bfo = identifier_type.map(ContextField::from);

                let choose_res = decider.choose(feature_name, &ctx.context, bfo);

                match choose_res {
                    Ok(res) => Some(PyDecision {
                        decision: res,
                        err: None,
                    }),
                    Err(e) => Some(PyDecision {
                        decision: None,
                        err: Some(PyDeciderError::GenericError(e.to_string())),
                    }),
                }
            }
            None => Some(PyDecision {
                decision: None,
                err: Some(PyDeciderError::DeciderNotFound),
            }),
        }
    }

    pub fn choose_all(&self, ctx: &PyContext, identifier_type: Option<&str>) -> GetAllDecisionsRes {
        match &self.decider {
            None => GetAllDecisionsRes {
                decisions: None,
                err: Some(PyDeciderError::DeciderNotFound),
            },
            Some(decider) => {
                let bfo = identifier_type.map(ContextField::from);

                let choose_all_res = decider.choose_all(&ctx.context, bfo);

                match choose_all_res {
                    Ok(res) => {
                        let decisions = res
                            .into_iter()
                            .map(|(key, value)| {
                                let decision = match value {
                                    Ok(decision) => PyDecision {
                                        decision,
                                        err: None,
                                    },
                                    Err(err) => PyDecision {
                                        decision: None,
                                        err: Some(PyDeciderError::GenericError(err.to_string())),
                                    },
                                };
                                (key, decision)
                            })
                            .collect();

                        GetAllDecisionsRes {
                            decisions: Some(decisions),
                            err: None,
                        }
                    }
                    Err(e) => GetAllDecisionsRes {
                        decisions: None,
                        err: Some(PyDeciderError::GenericError(e.to_string())),
                    },
                }
            }
        }
    }

    pub fn get_experiment(&self, feature_name: &str) -> GetExperimentRes {
        match &self.decider {
            Some(decider) => match decider.feature_by_name(feature_name) {
                Ok(feature) => {
                    let gil = Python::acquire_gil();
                    let py = gil.python();
                    match pythonize(py, &feature) {
                        Ok(pydict) => GetExperimentRes {
                            val: Some(pydict),
                            err: None,
                        },
                        Err(e) => GetExperimentRes {
                            val: None,
                            err: Some(PyDeciderError::GenericError(e.to_string())),
                        },
                    }
                }
                Err(e) => GetExperimentRes {
                    val: None,
                    err: Some(PyDeciderError::GenericError(e.to_string())),
                },
            },
            None => GetExperimentRes {
                val: None,
                err: Some(PyDeciderError::DeciderNotFound),
            },
        }
    }

    pub fn get_bool(&self, feature_name: &str, ctx: &PyContext) -> GetBoolRes {
        match &self.decider {
            Some(decider) => match decider.get_bool(feature_name, &ctx.context) {
                Ok(b) => GetBoolRes { val: b, err: None },
                Err(e) => GetBoolRes {
                    val: false,
                    err: Some(PyDeciderError::GenericError(e.to_string())),
                },
            },
            None => GetBoolRes {
                val: false,
                err: Some(PyDeciderError::DeciderNotFound),
            },
        }
    }

    pub fn get_int(&self, feature_name: &str, ctx: &PyContext) -> GetIntegerRes {
        match &self.decider {
            Some(decider) => match decider.get_int(feature_name, &ctx.context) {
                Ok(i) => GetIntegerRes { val: i, err: None },
                Err(e) => GetIntegerRes {
                    val: 0,
                    err: Some(PyDeciderError::GenericError(e.to_string())),
                },
            },
            None => GetIntegerRes {
                val: 0,
                err: Some(PyDeciderError::DeciderNotFound),
            },
        }
    }

    pub fn get_float(&self, feature_name: &str, ctx: &PyContext) -> GetFloatRes {
        match &self.decider {
            Some(decider) => match decider.get_float(feature_name, &ctx.context) {
                Ok(f) => GetFloatRes { val: f, err: None },
                Err(e) => GetFloatRes {
                    val: 0.0,
                    err: Some(PyDeciderError::GenericError(e.to_string())),
                },
            },
            None => GetFloatRes {
                val: 0.0,
                err: Some(PyDeciderError::DeciderNotFound),
            },
        }
    }

    pub fn get_string(&self, feature_name: &str, ctx: &PyContext) -> GetStringRes {
        match &self.decider {
            Some(decider) => match decider.get_string(feature_name, &ctx.context) {
                Ok(s) => GetStringRes { val: s, err: None },
                Err(e) => GetStringRes {
                    val: "".to_string(),
                    err: Some(PyDeciderError::GenericError(e.to_string())),
                },
            },
            None => GetStringRes {
                val: "".to_string(),
                err: Some(PyDeciderError::DeciderNotFound),
            },
        }
    }

    pub fn get_map(&self, feature_name: &str, ctx: &PyContext) -> GetMapRes {
        let gil = Python::acquire_gil();
        let py = gil.python();
        match &self.decider {
            Some(decider) => {
                let res = decider.get_map(feature_name, &ctx.context);
                match res {
                    Ok(val) => match pythonize(py, &val) {
                        Ok(pydict) => GetMapRes {
                            val: pydict,
                            err: None,
                        },
                        Err(e) => {
                            let pany: Py<PyAny> = PyDict::new(py).into();
                            GetMapRes {
                                val: pany,
                                err: Some(PyDeciderError::GenericError(e.to_string())),
                            }
                        }
                    },
                    Err(e) => {
                        let pany: Py<PyAny> = PyDict::new(py).into();
                        GetMapRes {
                            val: pany,
                            err: Some(PyDeciderError::GenericError(e.to_string())),
                        }
                    }
                }
            }
            None => {
                let pany: Py<PyAny> = PyDict::new(py).into();
                GetMapRes {
                    val: pany,
                    err: Some(PyDeciderError::DeciderNotFound),
                }
            }
        }
    }

    pub fn get_all_values(&self, ctx: &PyContext) -> GetAllDecisionsRes {
        match &self.decider {
            None => GetAllDecisionsRes {
                decisions: None,
                err: Some(PyDeciderError::DeciderNotFound),
            },
            Some(decider) => {
                let mut out: HashMap<String, PyDecision> = HashMap::new();
                let all_values_res = decider.get_all_values(&ctx.context);
                match all_values_res {
                    Ok(res) => {
                        for (k, v) in res.iter() {
                            let val = PyDecision {
                                decision: Some(v.clone()),
                                err: None,
                            };
                            out.insert(k.clone(), val);
                        }
                        GetAllDecisionsRes {
                            decisions: Some(out),
                            err: None,
                        }
                    }
                    Err(e) => GetAllDecisionsRes {
                        decisions: None,
                        err: Some(PyDeciderError::GenericError(e.to_string())),
                    },
                }
            }
        }
    }
}

#[pyfunction]
pub fn init(decisionmakers: &str, filename: &str) -> PyDecider {
    match init_decider(decisionmakers, filename) {
        Ok(dec) => PyDecider {
            decider: Some(dec),
            err: None,
        },
        Err(e) => PyDecider {
            decider: None,
            err: Some(PyDeciderError::DeciderInitFailed(e.to_string())),
        },
    }
}

#[pyfunction]
pub fn make_ctx(ctx_dict: &PyDict) -> PyContext {
    let mut err_vec: Vec<String> = Vec::new();

    let user_id: Option<String> = match extract_field::<String>(ctx_dict, "user_id", "string") {
        Ok(u_id) => u_id,
        Err(e) => {
            err_vec.push(e);
            None
        }
    };

    let locale: Option<String> = match extract_field::<String>(ctx_dict, "locale", "string") {
        Ok(loc) => loc,
        Err(e) => {
            err_vec.push(e);
            None
        }
    };

    let device_id = match extract_field::<String>(ctx_dict, "device_id", "string") {
        Ok(d_id) => d_id,
        Err(e) => {
            err_vec.push(e);
            None
        }
    };

    let canonical_url = match extract_field::<String>(ctx_dict, "canonical_url", "string") {
        Ok(c_url) => c_url,
        Err(e) => {
            err_vec.push(e);
            None
        }
    };

    let country_code = match extract_field::<String>(ctx_dict, "country_code", "string") {
        Ok(cc) => cc,
        Err(e) => {
            err_vec.push(e);
            None
        }
    };

    let origin_service = match extract_field::<String>(ctx_dict, "origin_service", "string") {
        Ok(os) => os,
        Err(e) => {
            err_vec.push(e);
            None
        }
    };

    let user_is_employee = match extract_field::<bool>(ctx_dict, "user_is_employee", "bool") {
        Ok(uie) => uie,
        Err(e) => {
            err_vec.push(e);
            None
        }
    };

    let logged_in = match extract_field::<bool>(ctx_dict, "logged_in", "bool") {
        Ok(li) => li,
        Err(e) => {
            err_vec.push(e);
            None
        }
    };

    let app_name = match extract_field::<String>(ctx_dict, "app_name", "string") {
        Ok(an) => an,
        Err(e) => {
            err_vec.push(e);
            None
        }
    };

    let build_number = match extract_field::<i32>(ctx_dict, "build_number", "integer") {
        Ok(bn) => bn,
        Err(e) => {
            err_vec.push(e);
            None
        }
    };

    let oauth_client_id = match extract_field::<String>(ctx_dict, "oauth_client_id", "string") {
        Ok(at) => at,
        Err(e) => {
            err_vec.push(e);
            None
        }
    };

    let cookie_created_timestamp =
        match extract_field::<i64>(ctx_dict, "cookie_created_timestamp", "integer") {
            Ok(cct) => cct,
            Err(e) => {
                err_vec.push(e);
                None
            }
        };

    let loid_created_timestamp =
    match extract_field::<i64>(ctx_dict, "loid_created_timestamp", "integer") {
        Ok(lct) => lct,
        Err(e) => {
            err_vec.push(e);
            None
        }
    };

    let other_fields = match extract_field::<HashMap<String, Option<OtherVal>>>(
        ctx_dict,
        "other_fields",
        "hashmap",
    ) {
        Ok(Some(ofm)) => {
            let mut out = HashMap::new();

            for (key, val) in ofm.iter() {
                let v: Value = match val {
                    None => Value::Null,
                    Some(OtherVal::B(b)) => Value::from(*b),
                    Some(OtherVal::I(i)) => Value::from(*i),
                    Some(OtherVal::F(f)) => Value::from(*f),
                    Some(OtherVal::S(s)) => Value::from(s.clone()),
                };
                if v != Value::Null {
                    out.insert(key.clone(), v);
                }
            }
            Some(out)
        }
        Ok(None) => None,
        Err(e) => {
            err_vec.push(e);
            None
        }
    };

    PyContext {
        context: Context {
            user_id,
            locale,
            device_id,
            canonical_url,
            country_code,
            origin_service,
            user_is_employee,
            logged_in,
            app_name,
            build_number,
            oauth_client_id,
            cookie_created_timestamp,
            loid_created_timestamp,
            other_fields,
        },
        err: match err_vec.len() {
            0 => None,
            _ => Some(PyDeciderError::GenericError(err_vec.join("\n"))),
        },
    }
}

fn extract_field<'p, T>(
    ctx_dict: &'p PyDict,
    key: &str,
    field_type: &str,
) -> Result<Option<T>, String>
where
    T: FromPyObject<'p>,
{
    match ctx_dict.get_item(key) {
        Some(val) => {
            if val.is_none() {
                Ok(None)
            } else {
                match val.extract::<T>() {
                    Ok(s) => Ok(Some(s)),
                    _ => Err(format!("{:#?} type mismatch ({:}).", key, field_type)),
                }
            }
        }
        None => Ok(None),
    }
}

#[derive(FromPyObject)]
enum OtherVal {
    B(bool),
    S(String),
    I(i64),
    F(f64),
}

#[derive(Debug, Clone)]
pub enum PyDeciderError {
    DeciderNotFound,
    GenericError(String),
    DeciderInitFailed(String),
}

impl fmt::Display for PyDeciderError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &*self {
            PyDeciderError::DeciderNotFound => write!(f, "Decider not found."),
            PyDeciderError::DeciderInitFailed(e) => {
                write!(f, "Decider initialization failed: {:#}.", e)
            }
            PyDeciderError::GenericError(e) => {
                write!(f, "{}", e)
            }
        }
    }
}

#[pymodule]
fn rust_decider(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(init, m)?)?;
    m.add_function(wrap_pyfunction!(make_ctx, m)?)?;

    Ok(())
}
