/*
 * Copyright 2019-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "mongocrypt-private.h"
#include "mongocrypt-key-private.h"


/* Check if two single entries are equal (i.e. ignore the 'next' pointer). */
static bool
_one_key_alt_name_equal (_mongocrypt_key_alt_name_t *ptr_a,
                         _mongocrypt_key_alt_name_t *ptr_b)
{
   BSON_ASSERT (ptr_a->value.value_type == BSON_TYPE_UTF8);
   BSON_ASSERT (ptr_b->value.value_type == BSON_TYPE_UTF8);
   return 0 == strcmp (_mongocrypt_key_alt_name_get_string (ptr_a),
                       _mongocrypt_key_alt_name_get_string (ptr_b));
}

static bool
_find (_mongocrypt_key_alt_name_t *list, _mongocrypt_key_alt_name_t *entry)
{
   for (; NULL != list; list = list->next) {
      if (_one_key_alt_name_equal (list, entry)) {
         return true;
      }
   }
   return false;
}

static uint32_t
_list_len (_mongocrypt_key_alt_name_t *list)
{
   uint32_t count = 0;

   while (NULL != list) {
      count++;
      list = list->next;
   }
   return count;
}

static bool
_check_unique (_mongocrypt_key_alt_name_t *list)
{
   for (; NULL != list; list = list->next) {
      /* Check if we can find the current entry in the remaining. */
      if (_find (list->next, list)) {
         return false;
      }
   }
   return true;
}

static bool
_parse_masterkey (bson_iter_t *iter,
                  _mongocrypt_key_doc_t *out,
                  mongocrypt_status_t *status)
{
   bson_iter_t subiter;
   bool has_cmk = false, has_region = false, has_provider = false;

   if (!BSON_ITER_HOLDS_DOCUMENT (iter)) {
      CLIENT_ERR ("invalid 'masterKey', expected document");
      return false;
   }

   if (!bson_iter_recurse (iter, &subiter)) {
      CLIENT_ERR ("invalid 'masterKey', malformed BSON");
      return false;
   }

   while (bson_iter_next (&subiter)) {
      const char *field;

      field = bson_iter_key (&subiter);
      BSON_ASSERT (field);
      if (0 == strcmp ("provider", field)) {
         const char *provider;

         has_provider = true;
         if (!BSON_ITER_HOLDS_UTF8 (&subiter)) {
            CLIENT_ERR ("invalid 'masterKey.provider', expected string");
            return false;
         }
         provider = bson_iter_utf8 (&subiter, NULL);
         BSON_ASSERT (provider);
         if (0 == strcmp (provider, "aws")) {
            out->masterkey_provider = MONGOCRYPT_KMS_PROVIDER_AWS;
         } else if (0 == strcmp (provider, "local")) {
            out->masterkey_provider = MONGOCRYPT_KMS_PROVIDER_LOCAL;
         } else {
            CLIENT_ERR (
               "invalid 'masterKey.provider', expected 'aws' or 'local'");
            return false;
         }
         continue;
      }

      if (0 == strcmp ("region", field)) {
         has_region = true;
         if (!BSON_ITER_HOLDS_UTF8 (&subiter)) {
            CLIENT_ERR ("invalid 'masterKey.region', expected string");
            return false;
         }
         out->masterkey_region = bson_strdup (bson_iter_utf8 (&subiter, NULL));
         continue;
      }

      if (0 == strcmp ("key", field)) {
         /* Don't need the CMK. Check that it's present and ignore it. */
         has_cmk = true;
         continue;
      }

      if (0 == strcmp ("endpoint", field)) {
         if (!BSON_ITER_HOLDS_UTF8 (&subiter)) {
            CLIENT_ERR ("invalid 'masterKey.endpoint', expected string");
            return false;
         }
         out->endpoint = bson_strdup (bson_iter_utf8 (&subiter, NULL));
         continue;
      }

      CLIENT_ERR ("unrecognized provider field '%s'", field);
      return false;
   }

   /* Check that required fields were set. */
   if (!has_provider) {
      CLIENT_ERR ("invalid 'masterKey', no 'provider'");
      return false;
   }

   if (out->masterkey_provider == MONGOCRYPT_KMS_PROVIDER_AWS) {
      if (!has_region) {
         CLIENT_ERR ("invalid 'masterKey', no 'region'");
         return false;
      }

      if (!has_cmk) {
         CLIENT_ERR ("invalid 'masterKey', no 'key'");
         return false;
      }
   }
   return true;
}


bool
_mongocrypt_key_alt_name_from_iter (const bson_iter_t *iter_in,
                                    _mongocrypt_key_alt_name_t **out,
                                    mongocrypt_status_t *status)
{
   _mongocrypt_key_alt_name_t *key_alt_names = NULL, *tmp;
   bson_iter_t iter;


   memcpy (&iter, iter_in, sizeof (iter));
   *out = NULL;

   /* A key parsed with no keyAltNames will have a zero'ed out bson value. Not
    * an error. */
   if (!BSON_ITER_HOLDS_ARRAY (&iter)) {
      CLIENT_ERR ("malformed keyAltNames, expected array");
      return false;
   }

   if (!bson_iter_recurse (&iter, &iter)) {
      CLIENT_ERR ("malformed keyAltNames, could not recurse into array");
      return false;
   }

   while (bson_iter_next (&iter)) {
      if (!BSON_ITER_HOLDS_UTF8 (&iter)) {
         _mongocrypt_key_alt_name_destroy_all (key_alt_names);
         CLIENT_ERR ("unexpected non-UTF8 keyAltName");
         return false;
      }

      tmp = _mongocrypt_key_alt_name_new (bson_iter_value (&iter));
      tmp->next = key_alt_names;
      key_alt_names = tmp;
   }

   if (!_check_unique (key_alt_names)) {
      _mongocrypt_key_alt_name_destroy_all (key_alt_names);
      CLIENT_ERR ("unexpected duplicate keyAltNames");
      return false;
   }

   *out = key_alt_names;
   return true;
}

/* Takes ownership of all fields. */
bool
_mongocrypt_key_parse_owned (const bson_t *bson,
                             _mongocrypt_key_doc_t *out,
                             mongocrypt_status_t *status)
{
   bson_iter_t iter;
   bool has_id = false, has_key_material = false, has_status = false,
        has_creation_date = false, has_update_date = false,
        has_master_key = false;

   if (!bson_validate (bson, BSON_VALIDATE_NONE, NULL) ||
       !bson_iter_init (&iter, bson)) {
      CLIENT_ERR ("invalid BSON");
      return false;
   }

   bson_destroy (&out->bson);
   bson_copy_to (bson, &out->bson);

   while (bson_iter_next (&iter)) {
      const char *field;

      field = bson_iter_key (&iter);
      if (!field) {
         CLIENT_ERR ("invalid BSON, could not retrieve field name");
         return false;
      }
      if (0 == strcmp ("_id", field)) {
         has_id = true;
         if (!_mongocrypt_buffer_copy_from_uuid_iter (&out->id, &iter)) {
            CLIENT_ERR ("invalid key, '_id' is not a UUID");
            return false;
         }
         continue;
      }

      /* keyAltNames (optional) */
      if (0 == strcmp ("keyAltNames", field)) {
         if (!_mongocrypt_key_alt_name_from_iter (
                &iter, &out->key_alt_names, status)) {
            return false;
         }
         continue;
      }

      if (0 == strcmp ("keyMaterial", field)) {
         has_key_material = true;
         if (!_mongocrypt_buffer_copy_from_binary_iter (&out->key_material,
                                                        &iter)) {
            CLIENT_ERR ("invalid 'keyMaterial', expected binary");
            return false;
         }
         if (out->key_material.subtype != BSON_SUBTYPE_BINARY) {
            CLIENT_ERR ("invalid 'keyMaterial', expected subtype 0");
            return false;
         }
         continue;
      }

      if (0 == strcmp ("masterKey", field)) {
         has_master_key = true;
         if (!_parse_masterkey (&iter, out, status)) {
            return false;
         }
         continue;
      }

      if (0 == strcmp ("version", field)) {
         if (!BSON_ITER_HOLDS_INT (&iter)) {
            CLIENT_ERR ("invalid 'version', expect int");
            return false;
         }
         if (bson_iter_as_int64 (&iter) != 0) {
            CLIENT_ERR (
               "unsupported key document version, only supports version=0");
            return false;
         }
         continue;
      }

      if (0 == strcmp ("status", field)) {
         /* Don't need status. Check that it's present and ignore it. */
         has_status = true;
         continue;
      }

      if (0 == strcmp ("creationDate", field)) {
         has_creation_date = true;

         if (!BSON_ITER_HOLDS_DATE_TIME (&iter)) {
            CLIENT_ERR ("invalid 'creationDate', expect datetime");
            return false;
         }

         out->creation_date = bson_iter_date_time (&iter);
         continue;
      }

      if (0 == strcmp ("updateDate", field)) {
         has_update_date = true;

         if (!BSON_ITER_HOLDS_DATE_TIME (&iter)) {
            CLIENT_ERR ("invalid 'updateDate', expect datetime");
            return false;
         }

         out->update_date = bson_iter_date_time (&iter);
         continue;
      }

      CLIENT_ERR ("unrecognized field '%s'", field);
      return false;
   }

   /* Check that required fields were set. */
   if (!has_id) {
      CLIENT_ERR ("invalid key, no '_id'");
      return false;
   }

   if (!has_master_key) {
      CLIENT_ERR ("invalid key, no 'masterKey'");
      return false;
   }

   if (!has_key_material) {
      CLIENT_ERR ("invalid key, no 'keyMaterial'");
      return false;
   }

   if (!has_status) {
      CLIENT_ERR ("invalid key, no 'status'");
      return false;
   }

   if (!has_creation_date) {
      CLIENT_ERR ("invalid key, no 'creationDate'");
      return false;
   }

   if (!has_update_date) {
      CLIENT_ERR ("invalid key, no 'updateDate'");
      return false;
   }

   return true;
}


_mongocrypt_key_doc_t *
_mongocrypt_key_new ()
{
   _mongocrypt_key_doc_t *key_doc;

   key_doc = (_mongocrypt_key_doc_t *) bson_malloc0 (sizeof *key_doc);
   bson_init (&key_doc->bson);

   return key_doc;
}


bool
_mongocrypt_key_equal (const _mongocrypt_key_doc_t *a,
                       const _mongocrypt_key_doc_t *b)
{
   return bson_equal (&a->bson, &b->bson);
}


void
_mongocrypt_key_destroy (_mongocrypt_key_doc_t *key)
{
   if (!key) {
      return;
   }

   _mongocrypt_buffer_cleanup (&key->id);
   _mongocrypt_key_alt_name_destroy_all (key->key_alt_names);
   _mongocrypt_buffer_cleanup (&key->key_material);
   bson_free (key->masterkey_region);
   bson_free (key->masterkey_cmk);
   bson_free (key->endpoint);
   bson_destroy (&key->bson);
   bson_free (key);
}


void
_mongocrypt_key_doc_copy_to (_mongocrypt_key_doc_t *src,
                             _mongocrypt_key_doc_t *dst)
{
   BSON_ASSERT (src);
   BSON_ASSERT (dst);

   _mongocrypt_buffer_copy_to (&src->id, &dst->id);
   _mongocrypt_buffer_copy_to (&src->key_material, &dst->key_material);
   dst->key_alt_names = _mongocrypt_key_alt_name_copy_all (src->key_alt_names);
   bson_destroy (&dst->bson);
   bson_copy_to (&src->bson, &dst->bson);
   dst->masterkey_provider = src->masterkey_provider;
   dst->masterkey_region = bson_strdup (src->masterkey_region);
   dst->masterkey_cmk = bson_strdup (src->masterkey_cmk);
}

_mongocrypt_key_alt_name_t *
_mongocrypt_key_alt_name_copy_all (_mongocrypt_key_alt_name_t *ptr)
{
   _mongocrypt_key_alt_name_t *ptr_copy = NULL, *head = NULL;

   while (ptr) {
      _mongocrypt_key_alt_name_t *copied;
      copied = bson_malloc0 (sizeof (*copied));
      BSON_ASSERT (copied);

      bson_value_copy (&ptr->value, &copied->value);

      if (!ptr_copy) {
         ptr_copy = copied;
         head = ptr_copy;
      } else {
         ptr_copy->next = copied;
         ptr_copy = ptr_copy->next;
      }
      ptr = ptr->next;
   }
   return head;
}

void
_mongocrypt_key_alt_name_destroy_all (_mongocrypt_key_alt_name_t *ptr)
{
   _mongocrypt_key_alt_name_t *next;
   while (ptr) {
      next = ptr->next;
      bson_value_destroy (&ptr->value);
      bson_free (ptr);
      ptr = next;
   }
}

bool
_mongocrypt_key_alt_name_intersects (_mongocrypt_key_alt_name_t *ptr_a,
                                     _mongocrypt_key_alt_name_t *ptr_b)
{
   _mongocrypt_key_alt_name_t *orig_ptr_b = ptr_b;
   for (; ptr_a; ptr_a = ptr_a->next) {
      for (ptr_b = orig_ptr_b; ptr_b; ptr_b = ptr_b->next) {
         if (_one_key_alt_name_equal (ptr_a, ptr_b)) {
            return true;
         }
      }
   }
   return false;
}


_mongocrypt_key_alt_name_t *
_mongocrypt_key_alt_name_create (const char *name, ...)
{
   va_list args;
   const char *arg_ptr;
   _mongocrypt_key_alt_name_t *head, *prev;

   head = NULL;
   prev = NULL;
   va_start (args, name);
   arg_ptr = name;
   while (arg_ptr) {
      _mongocrypt_key_alt_name_t *curr;

      curr = bson_malloc0 (sizeof (*curr));
      BSON_ASSERT (curr);

      curr->value.value_type = BSON_TYPE_UTF8;
      curr->value.value.v_utf8.str = bson_strdup (arg_ptr);
      curr->value.value.v_utf8.len = (uint32_t) strlen (arg_ptr);
      if (!prev) {
         head = curr;
      } else {
         prev->next = curr;
      }

      arg_ptr = va_arg (args, const char *);
      prev = curr;
   }
   va_end (args);

   return head;
}

_mongocrypt_key_alt_name_t *
_mongocrypt_key_alt_name_new (const bson_value_t *value)
{
   _mongocrypt_key_alt_name_t *name = bson_malloc0 (sizeof (*name));
   BSON_ASSERT (name);

   bson_value_copy (value, &name->value);
   return name;
}

bool
_mongocrypt_key_alt_name_unique_list_equal (_mongocrypt_key_alt_name_t *list_a,
                                            _mongocrypt_key_alt_name_t *list_b)
{
   _mongocrypt_key_alt_name_t *ptr;

   BSON_ASSERT (_check_unique (list_a));
   BSON_ASSERT (_check_unique (list_b));
   if (_list_len (list_a) != _list_len (list_b)) {
      return false;
   }
   for (ptr = list_a; NULL != ptr; ptr = ptr->next) {
      if (!_find (list_b, ptr)) {
         return false;
      }
   }
   return true;
}

const char *
_mongocrypt_key_alt_name_get_string (_mongocrypt_key_alt_name_t *key_alt_name)
{
   return key_alt_name->value.value.v_utf8.str;
}