use crate::storage::associations::{BelongsTo, HasMany};
use crate::storage::{session_store::SessionStore, Error};
use async_trait::async_trait;
use bson::{Bson, Document};
use fred::prelude::{ClientLike, FredResult, HashesInterface, KeysInterface, TransactionInterface, Value};
use fred::types::InfoKind;
use inflection_rs::inflection;
use log::debug;
use pbbson::Model;
use std::collections::HashMap;
use std::fmt::Debug;
use std::time::Duration;
use url::Url;

#[allow(unused)]
pub struct RedisSessionStore<B: Clone + Send + Sync + ToString> {
    redis_client: fred::clients::Client,

    #[allow(unused)]
    belongs_tos_by_bucket: HashMap<String, Vec<BelongsTo<B>>>,

    #[allow(unused)]
    has_manys_by_bucket: HashMap<String, Vec<HasMany<B>>>,
}

impl<B: Clone + Send + Sync + ToString> RedisSessionStore<B> {
    #[allow(unused)]
    pub async fn new(
        url: &Url,
        belongs_tos_by_bucket: HashMap<String, Vec<BelongsTo<B>>>,
        has_manys_by_bucket: HashMap<String, Vec<HasMany<B>>>,
    ) -> Result<Self, Error> {
        let redis_client = {
            let config = fred::prelude::Config::from_url(url.as_str()).map_err(|e| Error::internal(e.to_string()))?;
            debug!(config:?; "Using Redis config");
            let client = fred::prelude::Builder::from_config(config)
                .build()
                .map_err(|e| Error::internal(e.to_string()))?;
            client.init().await.map_err(|e| Error::internal(e.to_string()))?;
            client
        };

        // Validate the connection
        redis_client
            .info::<Value>(Some(InfoKind::Server))
            .await
            .map_err(|e| Error::internal(e.to_string()))?;

        Ok(Self {
            redis_client,
            belongs_tos_by_bucket,
            has_manys_by_bucket,
        })
    }
}

#[async_trait]
impl<B: Clone + Debug + Send + Sync + ToString> SessionStore<B> for RedisSessionStore<B> {
    async fn create(&self, bucket: B, model: Model, maybe_expires: Option<Duration>) -> Result<Model, Error> {
        let mut model = model.clone();
        let id = match model.id() {
            Ok(id) => id,
            _ => {
                let id = xid::new().to_string();
                model.insert("id", id.clone());
                id
            }
        };
        let doc = Document::from(transform_to_redis(model.clone())?);
        let json = serde_json::to_string(&doc).map_err(|e| Error::internal(e.to_string()))?;
        let mut map: HashMap<String, serde_json::Value> =
            serde_json::from_str(&json).map_err(|e| Error::internal(e.to_string()))?;
        map.remove("id");
        let mut values = Vec::new();
        for (k, v) in map.iter() {
            let v = match v {
                serde_json::Value::String(s) => s.to_string(),
                _ => v.to_string(),
            };
            values.push((k.clone(), v.to_string()));
        }
        let key = to_key(bucket, &id);
        let tx = self.redis_client.multi();
        if values.is_empty() {
            let _: Value = tx
                .set(key.clone(), "1", None, None, false)
                .await
                .map_err(|e| Error::internal(e.to_string()))?;
        } else {
            let _: Value = tx
                .hset(key.clone(), values)
                .await
                .map_err(|e| Error::internal(e.to_string()))?;
        }
        if let Some(expires) = maybe_expires {
            let _: Value = tx
                .expire(key, expires.as_secs() as i64, None)
                .await
                .map_err(|e| Error::internal(e.to_string()))?;
        }
        let _: Value = tx.exec(true).await.map_err(|e| Error::internal(e.to_string()))?;
        Ok(model)
    }

    async fn find(&self, bucket: B, id: &str) -> Result<Model, Error> {
        let key = to_key(bucket, id);
        let exists: bool = self
            .redis_client
            .exists(&key)
            .await
            .map_err(|e| Error::internal(e.to_string()))?;
        if !exists {
            return Err(Error::not_found("No such record"));
        }

        let maybe_fields: FredResult<HashMap<String, String>> = self.redis_client.hgetall(&key).await;
        let mut model = match maybe_fields {
            Ok(fields) => {
                let doc = bson::to_document(&fields).map_err(|e| Error::internal(e.to_string()))?;
                Model::from(doc)
            }
            Err(e) => match e.kind() {
                fred::error::ErrorKind::InvalidArgument => Model::default(),
                _ => return Err(Error::internal(e.to_string())),
            },
        };
        model.insert("id", id.to_string());
        Ok(model)
    }
}

fn to_key<B: Debug>(bucket: B, id: &str) -> String {
    let prefix = inflection::singularize(change_case::lower_case_first(&format!("{:?}", bucket)));
    format!("{prefix}:{id}")
}

fn transform_to_redis(model: Model) -> Result<Model, Error> {
    let mut local_model = Model::default();
    for (k, v) in model.iter() {
        local_model.insert(
            k,
            match v {
                Bson::DateTime(dt) => {
                    dt.try_to_rfc3339_string().map_err(|e| Error::internal(e.to_string()))?
                }
                Bson::String(s) => s.clone(),
                _ => v.to_string(),
            });
    }
    Ok(local_model)
}
