extern crate crypto;
use self::crypto::digest::Digest;
use self::crypto::sha1::Sha1;
use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use serde_json::{Error, Result as JsonResult, Value};
use std::collections::HashMap;
use std::convert::TryInto;
use std::fmt;
use std::fs::File;
use std::io::{prelude::*, BufReader};

/*
Open questions:

#1 what should the return type of decide() be?  It has to be able to return nothing, because
the feature might be disabled/whatever.  It _might_ want to be able to return an error,
because maybe there is bad config or something.  (we _could_, if we wanted to, validate
on boot that a given feature was valid), but it might be hard to convince the compiler
that there is no possibility of error.  So the return seems like it has to be either:
Result<Option<Decision>> or Option<Decision> or CustomType, which would handle errors
we want to return, if any, and also the "nothing here" case, and also potential ish with
bad Contexts, etc..

#2 events: have the caller handle them?

#3 how to expose this to python/js/go
*/
pub fn init_decider(_decisionmakers: String, filename: String) -> Result<Decider, DeciderFailType> {
    // let f = Feature {
    //     id: 7,
    //     name: "foo".to_string(),
    //     enabled: true,
    //     start_ts: 0,
    //     stop_ts: 1,
    //     version: 1,
    //     shuffle_version: 0,
    //     variants: vec![],
    //     targeting: None,
    //     overrides: None,
    // };

    //let cfg: String = read_config_file(filename)?;
    let file = File::open(filename)?;
    let reader = BufReader::new(file);
    let ec: ExperimentConfig = serde_json::from_reader(reader)?;
    println!("ec: {:#?}", ec);
    let fl: Vec<Feature> = ec
        .into_values()
        .map(|exp| experiment_to_feature(&exp))
        .collect();

    println!("fl: {:#?}", fl);

    Ok(Decider {
        features: fl,
        decisionmakers: vec![darkmode, fractional_availability],
    })
}

type Decisionmaker = fn(f: &Feature, ctx: &Context) -> Result<DMRes, DeciderFailType>;

fn read_config_file(filename: String) -> std::io::Result<String> {
    let mut file = File::open(filename)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    return Ok(contents);
}

fn string_to_decisionmakers(cfg: String) -> Result<Decisionmaker, DeciderFailType> {
    //cfg.split_whitespace()
    unimplemented!();
}

fn name_to_decisionmaker(
    name: &str,
) -> Option<fn(f: &Feature, ctx: &Context) -> Result<DMRes, DeciderFailType>> {
    return match name {
        "darkmode" => Some(darkmode),
        "locale" => Some(locale),
        "fractional_availability" => Some(fractional_availability),
        _ => None,
    };
}

fn decide(
    fns: &[fn(f: &Feature, ctx: &Context) -> Result<DMRes, DeciderFailType>],
    f: &Feature,
    ctx: &Context,
) -> Result<Option<Decision>, DeciderFailType> {
    // TODO: decide whether decide should return a Result<Option<Decision>>, to
    // account for ill-defined Features or missing data on the Context. Probably
    // yes, but currently Decision also has an option on name/bucketing.  Maybe
    // decide should return a Result<Decision>?
    println!(
        "calling decide on feature name: {:#?} with context:{:#?}",
        f.name, ctx
    );

    for fun in fns {
        let res = fun(&f, &ctx)?; // Should I let errors bubble up?  probably not.
        match res {
            DMRes::None => return Ok(None),
            DMRes::Pass(s) => {
                println!("decisionmaker passed: {}", s);
                ()
            }
            DMRes::Decided(d) => {
                println!("got a decision:{:#?}", d);
                return Ok(Some(d));
            }
        }
    }
    return Ok(None); // default open (just return nothing)
}

fn darkmode(f: &Feature, _ctx: &Context) -> Result<DMRes, DeciderFailType> {
    if f.enabled {
        return Ok(DMRes::Pass("darkmode:enabled".to_string()));
    } else {
        return Ok(DMRes::None);
    }
}

fn locale(f: &Feature, _ctx: &Context) -> Result<DMRes, DeciderFailType> {
    if !f.enabled {
        // TODO: add locales to Feature, compare ctx.locale to that
        return Ok(DMRes::Pass("locale:fixme".to_string()));
    } else {
        return Ok(DMRes::Pass("locale:fixme".to_string()));
    }
}

fn fractional_availability(f: &Feature, ctx: &Context) -> Result<DMRes, DeciderFailType> {
    let b_str = f.bucketing_string(ctx);
    let i = bucket(b_str);
    let flt = (i as f32) / 1000.0;
    let v = f.variants.iter().find(|x| x.lo <= flt && x.hi > flt);
    return match v {
        None => Ok(DMRes::Pass("frac_avail:not in variant".to_string())),
        Some(variant) => Ok(DMRes::Decided(Decision {
            name: variant.name.clone(),
            bucket: Some(i),
            emit_event: false,
        })),
    };
}

#[derive(Debug)]
pub enum DeciderFailType {
    FeatureNotFound,
    InvalidFeature,
    IoError(std::io::Error),
    SerdeError(serde_json::Error),
}

impl From<std::io::Error> for DeciderFailType {
    fn from(e: std::io::Error) -> DeciderFailType {
        DeciderFailType::IoError(e)
    }
}

impl From<serde_json::Error> for DeciderFailType {
    fn from(e: serde_json::Error) -> DeciderFailType {
        DeciderFailType::SerdeError(e)
    }
}

pub struct Decider {
    features: Vec<Feature>,
    decisionmakers: Vec<fn(f: &Feature, ctx: &Context) -> Result<DMRes, DeciderFailType>>,
}

impl fmt::Debug for Decider {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Decider: {:#?}", self.features)
    }
}

impl Decider {
    pub fn choose(
        &self,
        feature_name: String,
        ctx: &Context,
    ) -> Result<Option<Decision>, DeciderFailType> {
        // Ok(Some(Decision {
        //     name: feature_name,
        //     emit_event: true,
        //     bucket: None,
        // }))
        let f = self.feature_by_name(feature_name)?;
        return decide(&self.decisionmakers, &f, ctx);
    }

    fn choose_string(
        &self,
        feature_name: String,
        ctx: &Context,
    ) -> Result<Option<String>, DeciderFailType> {
        Ok(Some(feature_name))
        //  let f = self.feature_by_name(feature_name)?;
        //  return decide(&self.decisionmakers, &f, ctx);
    }

    fn feature_by_name(&self, feature_name: String) -> Result<Feature, DeciderFailType> {
        let fo = self.features.iter().find(|f| f.name == feature_name);
        return match fo {
            None => Err(DeciderFailType::FeatureNotFound),
            Some(feature) => Ok(feature.clone()),
        };
    }
}

#[derive(Serialize, Deserialize, Debug)]
enum DMRes {
    // TODO: find a better nome
    Pass(String),      // I didn't make a decision because...
    None,              // response is nothing
    Decided(Decision), // actual decision.
}

#[derive(Serialize, Deserialize, Debug, Clone)]
struct Feature {
    // TODO: rationalize this and Experiments.
    id: u32,
    name: String,
    enabled: bool,
    start_ts: u32, // TODO: consider whether this should just be created_at
    stop_ts: u32,  // TODO: should we get rid of a version by creating a new one?
    version: u32,
    shuffle_version: u32,
    variants: Vec<Variant>,
    //platform_bitmask: u128, //TODO: should we make platform choice very fast?
    targeting: Option<TargetingTree>,
    overrides: Option<HashMap<String, TargetingTree>>,
}

impl Feature {
    fn bucketing_string(&self, ctx: &Context) -> String {
        return format!("{}.{}.{}", self.name, self.shuffle_version, ctx.user_id);
    }

    fn decision_at(&self, i: i32) -> Option<Decision> {
        let f = (i as f32) / 1000.0;
        let v = self.variants.iter().find(|x| x.lo <= f && x.hi > f);
        return match v {
            None => None,
            Some(variant) => Some(Decision {
                name: variant.name.clone(),
                bucket: Some(i),
                emit_event: true,
            }),
        };
    }
}

fn experiment_to_feature(exp: &Experiment) -> Feature {
    return Feature {
        // FIXME: surely there must be a better way
        id: exp.id,
        name: exp.name.clone(),
        enabled: exp.enabled,
        start_ts: exp.start_ts,
        stop_ts: exp.stop_ts,
        version: exp.experiment.experiment_version,
        shuffle_version: exp.experiment.shuffle_version,
        variants: exp.experiment.variants.clone(),
        targeting: exp.experiment.targeting.clone(),
        overrides: exp.experiment.overrides.clone(),
    };
}

type FeatureConfig = HashMap<String, Feature>;

type ExperimentConfig = HashMap<String, Experiment>;

#[derive(Serialize, Deserialize, Debug)]
struct Experiment {
    id: u32,
    name: String,
    enabled: bool,
    version: String,
    r#type: ExperimentType,
    start_ts: u32,
    stop_ts: u32,
    experiment: InnerExperiment,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all(deserialize = "snake_case"))]
enum ExperimentType {
    RangeVariant,
    FeatureRollout, // FIXME: get rid of this after the great RangeVariant takeover.
}

#[derive(Serialize, Deserialize, Debug)]
struct InnerExperiment {
    // FIXME: better name, plzkkthxbai?
    variants: Vec<Variant>, // TODO: figure out how to make a variable-length array in a struct, maybe?
    experiment_version: u32,
    shuffle_version: u32,
    bucket_val: String,
    overrides: Option<HashMap<String, TargetingTree>>,
    targeting: Option<TargetingTree>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
struct Variant {
    name: String,
    #[serde(rename = "range_start")]
    lo: f32,
    #[serde(rename = "range_end")]
    hi: f32,
}

#[derive(PartialEq, Eq, Serialize, Deserialize, Debug)]
pub struct Decision {
    name: String,
    emit_event: bool,
    bucket: Option<i32>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Context {
    // FIXME: add other fields.
    user_id: i64, // FIXME: make this u64 after figuring out how to make range literals unsigned.
    locale: Option<String>,
    geolocation: Option<String>,
    device_id: Option<String>,
    origin_service: Option<String>,
}

impl Context {
    fn get_field(&self, field: &String) -> Option<String> {
        if field == "user_id" {
            Some(self.user_id.to_string())
        } else {
            return None;
        }
    }

    fn cmp(&self, field: &String, value: &String) -> bool {
        let fo = self.get_field(field);
        match fo {
            None => false,
            Some(s) => &s == value, // TODO: make sure this isn't doing pointer compares.
        }
    }
}

enum Comp {
    EQ,
    GT,
    LT,
    GE,
    LE,
    NE,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
enum TargetingTree {
    NULL,
    ALL(Vec<TargetingTree>),
    ANY(Vec<TargetingTree>),
    NOT(Box<TargetingTree>),
    EQ { field: String, values: Vec<String> },
    GT { field: String, value: String },
    LT { field: String, value: String },
    GE { field: String, value: String },
    LE { field: String, value: String },
    NE { field: String, value: String },
}

impl TargetingTree {
    fn eval(&self, ctx: &Context) -> bool {
        match self {
            TargetingTree::NULL => true,
            TargetingTree::ALL(xs) => xs.iter().all(|x| x.eval(ctx)),
            TargetingTree::ANY(xs) => xs.iter().any(|x| x.eval(ctx)),
            TargetingTree::NOT(x) => !x.eval(ctx),
            TargetingTree::EQ { field, values } => values.iter().any(|x| x == field),
            TargetingTree::GT { field, value } => ctx.cmp(&field, value),
            TargetingTree::LT { field, value } => field == value, // FIXME: fetch from ctx
            TargetingTree::GE { field, value } => field == value,
            TargetingTree::LE { field, value } => field == value,
            TargetingTree::NE { field, value } => field == value,
        }
    }
}

impl Experiment {
    fn variant(&self, ctx: &Context) -> Option<Decision> {
        let b_str = self.bucketing_string(ctx);
        let i = bucket(b_str);
        return self.decision_at(i);
    }

    fn bucketing_string(&self, ctx: &Context) -> String {
        return format!(
            "{}.{}.{}",
            self.name, self.experiment.shuffle_version, ctx.user_id
        );
    }

    fn decision_at(&self, i: i32) -> Option<Decision> {
        let f = (i as f32) / 1000.0;
        let v = self
            .experiment
            .variants
            .iter()
            .find(|x| x.lo <= f && x.hi > f);
        return match v {
            None => None,
            Some(variant) => Some(Decision {
                name: variant.name.clone(),
                bucket: Some(i),
                emit_event: true,
            }),
        };
    }

    fn eval_targeting(&self, ctx: &Context) -> bool {
        // allowed returns whether this user _might_ be in the experiment.
        // it therefore defaults true, and returns false when something about
        // ctx indicates that this can't be an impression.
        match &self.experiment.targeting {
            Some(targeting_tree) => eval_bool(&targeting_tree, ctx),
            None => false,
        }
    }

    fn eval_overrides(&self, _v: &Value, _ctx: &Context) -> Option<Decision> {
        // overrides put a context into a given variant.
        //let v = self.experiment.variants.iter().find(|x| );
        return None;
    }
}

fn bucket(bucketing_str: String) -> i32 {
    // FIXME: take in number of buckets as a param.
    let mut hasher = Sha1::new();
    hasher.input_str(&bucketing_str);
    let bigint = BigUint::parse_bytes(hasher.result_str().as_bytes(), 16);
    let res = match bigint {
        Some(v) => v % 1000u32,
        None => BigUint::from(9999u32), // FIXME: don'T use sentinel out-of-range values to indicate error.
    };
    let n_as_i32: i32 = res.try_into().unwrap(); // FIXME: get rid of the unwrap
    return n_as_i32;
}

fn eval_bool(tt: &TargetingTree, ctx: &Context) -> bool {
    // evaluates a gob of JSON rep'd by a TargetingTree into a bool
    println!("eval'ing tt={:#?} ctx={:#?}", tt, ctx);
    return tt.eval(ctx);
}

pub fn build_ctx() -> Context {
    return Context {
        user_id: 795244,
        locale: Some("US".to_string()),
        device_id: None,
        geolocation: None,
        origin_service: None,
    };
}
#[cfg(test)]
mod tests {
    use super::*;

    fn build_decider() -> Decider {
        let exp1 = build_exp();
        let exp2 = build_exp2();
        let f1 = experiment_to_feature(&exp1);
        let f2 = experiment_to_feature(&exp2);
        let disabled = Feature {
            name: "disabled".to_string(),
            enabled: false,
            ..f2.clone()
        };
        return Decider {
            features: vec![f1, f2, disabled],
            decisionmakers: vec![darkmode, locale, fractional_availability],
        };
    }

    fn build_exp() -> Experiment {
        let data = r#"
 {
    "enabled": true,
    "version": "2",
    "type": "range_variant",
    "start_ts": 0,
    "stop_ts": 1999999999
    "experiment": {
        "variants": [
            {
                "name": "create_cta",
                "range_start": 0,
                "range_end": 0.1
            },
            {
                "name": "control_1",
                "range_start": 0.9,
                "range_end": 1.0
            }
        ],
        "experiment_version": 2,
        "shuffle_version": 0,
        "bucket_val": "user_id",
        "log_bucketing": false,
        "overrides": [
            {
                "create_cta": {
                    "EQ": {
                        "field": "user_id",
                        "values": [
                            "1"
                        ]
                    }
                }
            },
            {
                "control_1": {
                    "EQ": {
                        "field": "user_id",
                        "values": [
                            "2",
                            "3"
                        ]
                    }
                }
            }
        ],
        "targeting": {
            "EQ": {
                "field": "app_name",
                "value": "android"
            }
        },
        "subscribers": [
            {
                "email": "matt.knox@reddit.com"
            }
        ]
    },
    "id": 2714,
    "owner": "matt.knox@reddit.com",
    "name": "android_community_creation_post_composer_cta"
 }
"#;
        let e: Experiment = serde_json::from_str(data).unwrap();
        println!("{:#?}", e);
        return e;
    }

    fn build_exp2() -> Experiment {
        let data = r#"
{
    "id": 3,
    "enabled": true,
    "name": "my_first",
    "version": "2",
    "type": "range_variant",
    "start_ts": 0,
    "stop_ts": 1999999999,
    "experiment": {
        "variants": [
            {
                "name": "t1",
                "range_start": 0.0,
                "range_end": 0.3
            },
            {
                "name": "c1",
                "range_start": 0.7,
                "range_end": 1.0
            }
        ],
        "experiment_version": 2,
        "shuffle_version": 0,
        "bucket_val": "user_id",
        "log_bucketing": false,
        "targeting": {"NE": {"field": "user_id", "value": "795244"}},
        "overrides": {"t1": {"EQ": {"field": "user_id", "values": ["795244"]}}}
    }
}
"#;
        let e: Experiment = serde_json::from_str(data).unwrap();
        return e;
    }

    #[test]
    fn call_func() {
        assert_eq!(208, bucket("my_first.0.795244".to_string()));
    }

    #[test]
    fn call_variant_method() {
        let exp = build_exp2();
        let ctx = build_ctx();
        let dr_exp = Decision {
            name: "t1".to_string(),
            bucket: Some(208),
            emit_event: true,
        };
        assert_eq!(dr_exp, exp.variant(&ctx).unwrap());
    }

    #[test]
    fn disable_stops_exp() {
        let ctx = build_ctx();
        let exp = build_exp2();
        let disabled_exp = Experiment {
            enabled: false,
            ..exp
        };

        let disabled_f = experiment_to_feature(&disabled_exp);

        assert_eq!(disabled_f.enabled, false);
        assert_eq!(None, decide(&[darkmode], &disabled_f, &ctx).unwrap());
    }
}
