use crate::storage::Error;
use mongodb::bson::Bson;
use mongodb::bson::oid::ObjectId;
use pbbson::Model;
use pbbson::bson;
use std::str::FromStr;

pub(crate) fn from_object_id(id: ObjectId) -> Result<String, Error> {
    let id = xid::Id::from_bytes(&id.bytes()).map_err(|e| Error::internal(e.to_string()))?;
    Ok(id.to_string())
}

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(bson::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_from_mongo(model: Model) -> Result<Model, Error> {
    let mut model = model.clone();
    if let Some(Bson::ObjectId(object_id)) = model.get("_id") {
        let id = xid::Id::from_bytes(&object_id.bytes()).map_err(|e| Error::internal(e.to_string()))?;
        model.insert("id".to_string(), id.to_string());
        model.remove("_id");
    }
    Ok(model)
}

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)
}
