/*
 * Copyright 2022-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 <bson/bson.h>

#include <limits.h> // SIZE_MAX

#include "mc-fle2-encryption-placeholder-private.h"
#include "mongocrypt-buffer-private.h"
#include "mongocrypt.h"

#define CLIENT_ERR_PREFIXED_HELPER(Prefix, ErrorString, ...) CLIENT_ERR(Prefix ": " ErrorString, ##__VA_ARGS__)
#define CLIENT_ERR_PREFIXED(ErrorString, ...) CLIENT_ERR_PREFIXED_HELPER(ERROR_PREFIX, ErrorString, ##__VA_ARGS__)

// Common logic for testing field name, tracking duplication, and presence.
#define IF_FIELD(Name)                                                                                                 \
    if (0 == strcmp(field, #Name)) {                                                                                   \
        if (has_##Name) {                                                                                              \
            CLIENT_ERR_PREFIXED("Duplicate field '" #Name "' in placeholder bson");                                    \
            goto fail;                                                                                                 \
        }                                                                                                              \
        has_##Name = true;

#define END_IF_FIELD                                                                                                   \
    continue;                                                                                                          \
    }

#define CHECK_HAS(Name)                                                                                                \
    if (!has_##Name) {                                                                                                 \
        CLIENT_ERR_PREFIXED("Missing field '" #Name "' in placeholder");                                               \
        goto fail;                                                                                                     \
    }

void mc_FLE2EncryptionPlaceholder_init(mc_FLE2EncryptionPlaceholder_t *placeholder) {
    memset(placeholder, 0, sizeof(mc_FLE2EncryptionPlaceholder_t));
}

#define ERROR_PREFIX "Error parsing FLE2EncryptionPlaceholder"

bool mc_FLE2EncryptionPlaceholder_parse(mc_FLE2EncryptionPlaceholder_t *out,
                                        const bson_t *in,
                                        mongocrypt_status_t *status) {
    bson_iter_t iter = {0};
    bool has_t = false, has_a = false, has_v = false, has_cm = false;
    bool has_ki = false, has_ku = false;
    bool has_s = false;

    BSON_ASSERT_PARAM(out);
    BSON_ASSERT_PARAM(in);

    mc_FLE2EncryptionPlaceholder_init(out);
    if (!bson_validate(in, BSON_VALIDATE_NONE, NULL) || !bson_iter_init(&iter, in)) {
        CLIENT_ERR_PREFIXED("invalid BSON");
        return false;
    }

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

        IF_FIELD(t) {
            int32_t type;
            if (!BSON_ITER_HOLDS_INT32(&iter)) {
                CLIENT_ERR_PREFIXED("invalid marking, 't' must be an int32");
                goto fail;
            }
            type = bson_iter_int32(&iter);
            if ((type != MONGOCRYPT_FLE2_PLACEHOLDER_TYPE_INSERT) && (type != MONGOCRYPT_FLE2_PLACEHOLDER_TYPE_FIND)) {
                CLIENT_ERR_PREFIXED("invalid placeholder type value: %d", type);
                goto fail;
            }
            out->type = (mongocrypt_fle2_placeholder_type_t)type;
        }
        END_IF_FIELD

        IF_FIELD(a) {
            int32_t algorithm;
            if (!BSON_ITER_HOLDS_INT32(&iter)) {
                CLIENT_ERR_PREFIXED("invalid marking, 'a' must be an int32");
                goto fail;
            }
            algorithm = bson_iter_int32(&iter);
            if (algorithm != MONGOCRYPT_FLE2_ALGORITHM_UNINDEXED && algorithm != MONGOCRYPT_FLE2_ALGORITHM_EQUALITY
                && algorithm != MONGOCRYPT_FLE2_ALGORITHM_RANGE) {
                CLIENT_ERR_PREFIXED("invalid algorithm value: %d", algorithm);
                goto fail;
            }
            out->algorithm = (mongocrypt_fle2_encryption_algorithm_t)algorithm;
        }
        END_IF_FIELD

        IF_FIELD(ki) {
            if (!_mongocrypt_buffer_from_uuid_iter(&out->index_key_id, &iter)) {
                CLIENT_ERR_PREFIXED("index key id must be a UUID");
                goto fail;
            }
        }
        END_IF_FIELD

        IF_FIELD(ku) {
            if (!_mongocrypt_buffer_from_uuid_iter(&out->user_key_id, &iter)) {
                CLIENT_ERR_PREFIXED("user key id must be a UUID");
                goto fail;
            }
        }
        END_IF_FIELD

        IF_FIELD(v) {
            memcpy(&out->v_iter, &iter, sizeof(bson_iter_t));
        }
        END_IF_FIELD

        IF_FIELD(cm) {
            if (!BSON_ITER_HOLDS_INT64(&iter)) {
                CLIENT_ERR_PREFIXED("invalid marking, 'cm' must be an int64");
                goto fail;
            }
            out->maxContentionFactor = bson_iter_int64(&iter);
            if (!mc_validate_contention(out->maxContentionFactor, status)) {
                goto fail;
            }
        }
        END_IF_FIELD

        IF_FIELD(s) {
            if (!BSON_ITER_HOLDS_INT64(&iter)) {
                CLIENT_ERR_PREFIXED("invalid marking, 's' must be an int64");
                goto fail;
            }
            out->sparsity = bson_iter_int64(&iter);
            if (!mc_validate_sparsity(out->sparsity, status)) {
                goto fail;
            }
        }
        END_IF_FIELD
    }

    CHECK_HAS(t)
    CHECK_HAS(a)
    CHECK_HAS(ki)
    CHECK_HAS(ku)
    CHECK_HAS(v)
    CHECK_HAS(cm)
    // Do not error if sparsity (s) is not present.
    // 's' was added in version 6.1 of query analysis (mongocryptd or mongo_crypt
    // shared library). 's' is not present in query analysis 6.0.

    return true;

fail:
    mc_FLE2EncryptionPlaceholder_cleanup(out);
    return false;
}

void mc_FLE2EncryptionPlaceholder_cleanup(mc_FLE2EncryptionPlaceholder_t *placeholder) {
    BSON_ASSERT_PARAM(placeholder);

    _mongocrypt_buffer_cleanup(&placeholder->index_key_id);
    _mongocrypt_buffer_cleanup(&placeholder->user_key_id);
    mc_FLE2EncryptionPlaceholder_init(placeholder);
}

#undef ERROR_PREFIX
#define ERROR_PREFIX "Error validating contention"

bool mc_validate_contention(int64_t contention, mongocrypt_status_t *status) {
    if (contention < 0) {
        CLIENT_ERR_PREFIXED("contention must be non-negative, got: %" PRId64, contention);
        return false;
    }
    if (contention == INT64_MAX) {
        CLIENT_ERR_PREFIXED("contention must be < INT64_MAX, got: %" PRId64, contention);
        return false;
    }
    return true;
}

#undef ERROR_PREFIX
#define ERROR_PREFIX "Error validating sparsity"

bool mc_validate_sparsity(int64_t sparsity, mongocrypt_status_t *status) {
    if (sparsity < 0) {
        CLIENT_ERR_PREFIXED("sparsity must be non-negative, got: %" PRId64, sparsity);
        return false;
    }
    // mc_getEdgesInt expects a size_t sparsity.
    if ((uint64_t)sparsity >= SIZE_MAX) {
        CLIENT_ERR_PREFIXED("sparsity must be < %zu, got: %" PRId64, SIZE_MAX, sparsity);
        return false;
    }
    return true;
}

#undef ERROR_PREFIX
#define ERROR_PREFIX "Error parsing FLE2RangeFindSpecEdgesInfo"

static bool mc_FLE2RangeFindSpecEdgesInfo_parse(mc_FLE2RangeFindSpecEdgesInfo_t *out,
                                                const bson_iter_t *in,
                                                bool use_range_v2,
                                                mongocrypt_status_t *status) {
    bson_iter_t iter;
    bool has_lowerBound = false, has_lbIncluded = false, has_upperBound = false, has_ubIncluded = false,
         has_indexMin = false, has_indexMax = false, has_precision = false, has_trimFactor = false;

    BSON_ASSERT_PARAM(out);
    BSON_ASSERT_PARAM(in);

    iter = *in;

    if (!BSON_ITER_HOLDS_DOCUMENT(&iter)) {
        CLIENT_ERR_PREFIXED("must be an iterator to a document");
        return false;
    }
    bson_iter_recurse(&iter, &iter);

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

        IF_FIELD(lowerBound) {
            out->lowerBound = iter;
        }
        END_IF_FIELD

        IF_FIELD(lbIncluded) {
            if (!BSON_ITER_HOLDS_BOOL(&iter)) {
                CLIENT_ERR_PREFIXED("'lbIncluded' must be a bool");
                goto fail;
            }
            out->lbIncluded = bson_iter_bool(&iter);
        }
        END_IF_FIELD

        IF_FIELD(upperBound) {
            out->upperBound = iter;
        }
        END_IF_FIELD

        IF_FIELD(ubIncluded) {
            if (!BSON_ITER_HOLDS_BOOL(&iter)) {
                CLIENT_ERR_PREFIXED("'ubIncluded' must be a bool");
                goto fail;
            }
            out->ubIncluded = bson_iter_bool(&iter);
        }
        END_IF_FIELD

        IF_FIELD(indexMin) {
            out->indexMin = iter;
        }
        END_IF_FIELD

        IF_FIELD(indexMax) {
            out->indexMax = iter;
        }
        END_IF_FIELD

        IF_FIELD(precision) {
            if (!BSON_ITER_HOLDS_INT32(&iter)) {
                CLIENT_ERR_PREFIXED("'precision' must be an int32");
                goto fail;
            }
            int32_t val = bson_iter_int32(&iter);
            if (val < 0) {
                CLIENT_ERR_PREFIXED("'precision' must be non-negative");
                goto fail;
            }

            out->precision = OPT_I32(val);
        }
        END_IF_FIELD

        IF_FIELD(trimFactor) {
            if (!BSON_ITER_HOLDS_INT32(&iter)) {
                CLIENT_ERR_PREFIXED("'trimFactor' must be an int32");
                goto fail;
            }
            int32_t val = bson_iter_int32(&iter);
            if (val < 0) {
                CLIENT_ERR_PREFIXED("'trimFactor' must be non-negative");
                goto fail;
            }

            out->trimFactor = OPT_I32(val);
        }
        END_IF_FIELD
    }

    CHECK_HAS(lowerBound)
    CHECK_HAS(lbIncluded)
    CHECK_HAS(upperBound)
    CHECK_HAS(ubIncluded)
    CHECK_HAS(indexMin)
    CHECK_HAS(indexMax)
    // Do not error if precision is not present. Precision optional and only
    // applies to double/decimal128.

    if (!use_range_v2 && out->trimFactor.set) {
        CLIENT_ERR_PREFIXED("'trimFactor' is not supported for QE range v1");
        return false;
    }

    return true;

fail:
    return false;
}

#undef ERROR_PREFIX
#define ERROR_PREFIX "Error parsing FLE2RangeFindSpec"

bool mc_FLE2RangeFindSpec_parse(mc_FLE2RangeFindSpec_t *out,
                                const bson_iter_t *in,
                                bool use_range_v2,
                                mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(out);
    BSON_ASSERT_PARAM(in);

    bson_iter_t iter = *in;
    bool has_edgesInfo = false, has_payloadId = false, has_firstOperator = false, has_secondOperator = false;

    *out = (mc_FLE2RangeFindSpec_t){{{{0}}}};

    if (!BSON_ITER_HOLDS_DOCUMENT(&iter)) {
        CLIENT_ERR_PREFIXED("must be an iterator to a document");
        return false;
    }
    bson_iter_recurse(&iter, &iter);

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

        IF_FIELD(edgesInfo) {
            if (!mc_FLE2RangeFindSpecEdgesInfo_parse(&out->edgesInfo.value, &iter, use_range_v2, status)) {
                goto fail;
            }
            out->edgesInfo.set = true;
        }
        END_IF_FIELD

        IF_FIELD(payloadId) {
            if (!BSON_ITER_HOLDS_INT32(&iter)) {
                CLIENT_ERR_PREFIXED("'payloadId' must be an int32");
                goto fail;
            }
            out->payloadId = bson_iter_int32(&iter);
        }
        END_IF_FIELD

        IF_FIELD(firstOperator) {
            if (!BSON_ITER_HOLDS_INT32(&iter)) {
                CLIENT_ERR_PREFIXED("'firstOperator' must be an int32");
                goto fail;
            }
            const int32_t first_op = bson_iter_int32(&iter);
            if (first_op < FLE2RangeOperator_min_val || first_op > FLE2RangeOperator_max_val) {
                CLIENT_ERR_PREFIXED("'firstOperator' must be between %d and %d",
                                    FLE2RangeOperator_min_val,
                                    FLE2RangeOperator_max_val);
                goto fail;
            }
            out->firstOperator = (mc_FLE2RangeOperator_t)first_op;
        }
        END_IF_FIELD

        IF_FIELD(secondOperator) {
            if (!BSON_ITER_HOLDS_INT32(&iter)) {
                CLIENT_ERR_PREFIXED("'secondOperator' must be an int32");
                goto fail;
            }
            const int32_t second_op = bson_iter_int32(&iter);
            if (second_op < FLE2RangeOperator_min_val || second_op > FLE2RangeOperator_max_val) {
                CLIENT_ERR_PREFIXED("'secondOperator' must be between %d and %d",
                                    FLE2RangeOperator_min_val,
                                    FLE2RangeOperator_max_val);
                goto fail;
            }
            out->secondOperator = (mc_FLE2RangeOperator_t)second_op;
        }
        END_IF_FIELD
    }

    // edgesInfo is optional. Do not require it.
    CHECK_HAS(payloadId)
    CHECK_HAS(firstOperator)
    // secondOperator is optional. Do not require it.
    return true;

fail:
    return false;
}

#undef ERROR_PREFIX
#define ERROR_PREFIX "Error parsing FLE2RangeInsertSpec"

bool mc_FLE2RangeInsertSpec_parse(mc_FLE2RangeInsertSpec_t *out,
                                  const bson_iter_t *in,
                                  bool use_range_v2,
                                  mongocrypt_status_t *status) {
    BSON_ASSERT_PARAM(out);
    BSON_ASSERT_PARAM(in);

    *out = (mc_FLE2RangeInsertSpec_t){{0}};

    bson_iter_t iter = *in;
    bool has_v = false, has_min = false, has_max = false, has_precision = false, has_trimFactor = false;

    if (!BSON_ITER_HOLDS_DOCUMENT(&iter)) {
        CLIENT_ERR_PREFIXED("must be an iterator to a document");
        return false;
    }
    bson_iter_recurse(&iter, &iter);

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

        IF_FIELD(v) {
            out->v = iter;
        }
        END_IF_FIELD

        IF_FIELD(min) {
            out->min = iter;
        }
        END_IF_FIELD

        IF_FIELD(max) {
            out->max = iter;
        }
        END_IF_FIELD

        IF_FIELD(precision) {
            if (!BSON_ITER_HOLDS_INT32(&iter)) {
                CLIENT_ERR_PREFIXED("'precision' must be an int32");
                goto fail;
            }
            int32_t val = bson_iter_int32(&iter);
            if (val < 0) {
                CLIENT_ERR_PREFIXED("'precision' must be non-negative");
                goto fail;
            }
            out->precision = OPT_I32(val);
        }
        END_IF_FIELD

        IF_FIELD(trimFactor) {
            if (!BSON_ITER_HOLDS_INT32(&iter)) {
                CLIENT_ERR_PREFIXED("'trimFactor' must be an int32");
                goto fail;
            }
            int32_t val = bson_iter_int32(&iter);
            if (val < 0) {
                CLIENT_ERR_PREFIXED("'trimFactor' must be non-negative");
                goto fail;
            }
            out->trimFactor = OPT_I32(val);
        }
        END_IF_FIELD
    }

    CHECK_HAS(v)
    CHECK_HAS(min)
    CHECK_HAS(max)
    // Do not error if precision is not present. Precision optional and only
    // applies to double/decimal128.

    if (!use_range_v2 && out->trimFactor.set) {
        CLIENT_ERR_PREFIXED("'trimFactor' is not supported for QE range v1");
        return false;
    }

    return true;

fail:
    return false;
}

#undef ERROR_PREFIX
