use crate::storage::Error;
use mongodb::bson::{DateTime, oid::ObjectId};
use pbbson::Model;
use pbbson::bson::spec::BinarySubtype;
use pbbson::bson::{self, Bson, binary};
use std::str::FromStr;
use std::time::SystemTime;

pub(crate) fn now_as_datetime() -> DateTime {
    let now = SystemTime::now();
    DateTime::from_system_time(now)
}

pub(crate) fn try_to_object_id(id: &str) -> Result<ObjectId, Error> {
    const WHAT: &str = "IDs";
    require_xid_base32hex(WHAT, id)?;
    let as_xid = match xid::Id::from_str(id) {
        Ok(xid) => xid,
        Err(e) => {
            let msg = format!("{WHAT} must be 12-byte XID's in 20-byte base32hex encoded form ({e:?})");
            return Err(Error::invalid_argument(msg));
        }
    };
    Ok(ObjectId::from_bytes(*as_xid.as_bytes()))
}

pub(crate) fn try_to_uuid(id: &str) -> Result<bson::Binary, Error> {
    let uuid = bson::Uuid::parse_str(id).map_err(|e| Error::internal(e.to_string()))?;
    Ok(binary::Binary::from_uuid(uuid))
}

pub(crate) fn require_xid_base32hex(what: &str, id: &str) -> Result<(), Error> {
    if id.len() != 20 {
        return Err(Error::invalid_argument(format!(
            "{what} must be 12-byte XID's in 20-byte base32hex encoded form"
        )));
    }
    Ok(())
}

pub(crate) fn transform_to_mongo(model: Model) -> Result<Model, Error> {
    let mut local_model = Model::default();
    let local_model_id = match model.get("id") {
        Some(Bson::ObjectId(object_id)) => Some(*object_id),
        Some(Bson::String(id)) => Some(try_to_object_id(id)?),
        _ => None,
    };
    if let Some(local_model_id) = local_model_id {
        local_model.insert("_id", local_model_id);
    }
    for (k, v) in model.iter().filter(|(k, _v)| *k != "id") {
        if k.ends_with("Id") {
            match v {
                Bson::ObjectId(object_id) => {
                    local_model.insert(k, *object_id);
                }
                Bson::String(id) => {
                    if id.is_empty() {
                        local_model.insert(k, Bson::Null);
                    } else if let Ok(object_id) = try_to_object_id(id) {
                        local_model.insert(k, object_id);
                    } else if let Ok(uuid) = try_to_uuid(id) {
                        local_model.insert(k, Bson::Binary(uuid));
                    } else {
                        local_model.insert(k, v);
                    }
                }
                _ => {
                    local_model.insert(k, v);
                }
            };
        } else {
            local_model.insert(k, v);
        }
    }
    Ok(local_model)
}

pub(crate) fn transform_from_mongo(model: Model) -> Result<Model, Error> {
    let mut model = model.clone();
    for (k, v) in model.clone().iter() {
        match v {
            Bson::Null => {
                model.remove(k);
            }
            Bson::Binary(binary) => {
                if binary.subtype == BinarySubtype::Uuid && k == "_id" {
                    let uuid = binary.to_uuid().map_err(|e| Error::internal(e.to_string()))?;
                    model.remove("_id");
                    model.insert("id", uuid.to_string());
                }
            }
            Bson::ObjectId(object_id) => {
                if let Ok(id) = xid::Id::from_bytes(&object_id.bytes()) {
                    if k == "_id" {
                        model.remove("_id");
                        model.insert("id", id.to_string());
                    } else {
                        model.insert(k, id.to_string());
                    }
                }
            }
            _ => {}
        }
    }
    Ok(model)
}

#[cfg(test)]
mod tests {
    use crate::storage::config_store::adapters::mongodb::serde::transform_from_mongo;
    use pbbson::Model;
    use pbbson::bson::oid::ObjectId;
    use pbbson::bson::{Bson, doc};

    #[test]
    fn can_transform_object_ids_to_protobuf() {
        let input = Model::from(doc! {
            "_id": ObjectId::parse_str("68571bb59bcb60707c7c3aac").unwrap(),
            "serviceId": ObjectId::parse_str("68571bb59bcb60707c7c3aad").unwrap(),
            "planId": ObjectId::parse_str("68571bb59bcb60707c7c3aae").unwrap(),
        });
        let output = transform_from_mongo(input).unwrap();
        assert_eq!(
            output.get("id"),
            Some(&Bson::String("d1bhndcrpdg70v3s7am0".to_string()))
        );
        assert_eq!(
            output.get("serviceId"),
            Some(&Bson::String("d1bhndcrpdg70v3s7amg".to_string()))
        );
        assert_eq!(
            output.get("planId"),
            Some(&Bson::String("d1bhndcrpdg70v3s7an0".to_string()))
        );
    }

    #[test]
    fn can_transform_null_string_to_required_defaultable_protobuf_field() {
        let input = Model::from(doc! {
            "providerUid": Bson::Null,
        });
        let output = transform_from_mongo(input).unwrap();
        assert_eq!(output.get("providerUid"), None);
    }
}
