# Copyright 2020-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.

"""Support for MongoDB Stable API.

.. _versioned-api-ref:

MongoDB Stable API
=====================

Starting in MongoDB 5.0, applications can specify the server API version
to use when creating a :class:`~pymongo.mongo_client.MongoClient`. Doing so
ensures that the driver behaves in a manner compatible with that server API
version, regardless of the server's actual release version.

Declaring an API Version
````````````````````````

.. attention:: Stable API requires MongoDB >=5.0.

To configure MongoDB Stable API, pass the ``server_api`` keyword option to
:class:`~pymongo.mongo_client.MongoClient`::

    >>> from pymongo.mongo_client import MongoClient
    >>> from pymongo.server_api import ServerApi
    >>>
    >>> # Declare API version "1" for MongoClient "client"
    >>> server_api = ServerApi('1')
    >>> client = MongoClient(server_api=server_api)

The declared API version is applied to all commands run through ``client``,
including those sent through the generic
:meth:`~pymongo.database.Database.command` helper.

.. note:: Declaring an API version on the
   :class:`~pymongo.mongo_client.MongoClient` **and** specifying stable
   API options in :meth:`~pymongo.database.Database.command` command document
   is not supported and will lead to undefined behaviour.

To run any command without declaring a server API version or using a different
API version, create a separate :class:`~pymongo.mongo_client.MongoClient`
instance.

Strict Mode
```````````

Configuring ``strict`` mode will cause the MongoDB server to reject all
commands that are not part of the declared :attr:`ServerApi.version`. This
includes command options and aggregation pipeline stages.

For example::

    >>> server_api = ServerApi('1', strict=True)
    >>> client = MongoClient(server_api=server_api)
    >>> client.test.command('count', 'test')
    Traceback (most recent call last):
    ...
    pymongo.errors.OperationFailure: Provided apiStrict:true, but the command count is not in API Version 1, full error: {'ok': 0.0, 'errmsg': 'Provided apiStrict:true, but the command count is not in API Version 1', 'code': 323, 'codeName': 'APIStrictError'

Detecting API Deprecations
``````````````````````````

The ``deprecationErrors`` option can be used to enable command failures
when using functionality that is deprecated from the configured
:attr:`ServerApi.version`. For example::

    >>> server_api = ServerApi('1', deprecation_errors=True)
    >>> client = MongoClient(server_api=server_api)

Note that at the time of this writing, no deprecated APIs exist.

Classes
=======
"""
from __future__ import annotations

from typing import Any, MutableMapping, Optional


class ServerApiVersion:
    """An enum that defines values for :attr:`ServerApi.version`.

    .. versionadded:: 3.12
    """

    V1 = "1"
    """Server API version "1"."""


class ServerApi:
    """MongoDB Stable API."""

    def __init__(
        self, version: str, strict: Optional[bool] = None, deprecation_errors: Optional[bool] = None
    ):
        """Options to configure MongoDB Stable API.

        :param version: The API version string. Must be one of the values in
            :class:`ServerApiVersion`.
        :param strict: Set to ``True`` to enable API strict mode.
            Defaults to ``None`` which means "use the server's default".
        :param deprecation_errors: Set to ``True`` to enable
            deprecation errors. Defaults to ``None`` which means "use the
            server's default".

        .. versionadded:: 3.12
        """
        if version != ServerApiVersion.V1:
            raise ValueError(f"Unknown ServerApi version: {version}")
        if strict is not None and not isinstance(strict, bool):
            raise TypeError(
                "Wrong type for ServerApi strict, value must be an instance "
                f"of bool, not {type(strict)}"
            )
        if deprecation_errors is not None and not isinstance(deprecation_errors, bool):
            raise TypeError(
                "Wrong type for ServerApi deprecation_errors, value must be "
                f"an instance of bool, not {type(deprecation_errors)}"
            )
        self._version = version
        self._strict = strict
        self._deprecation_errors = deprecation_errors

    @property
    def version(self) -> str:
        """The API version setting.

        This value is sent to the server in the "apiVersion" field.
        """
        return self._version

    @property
    def strict(self) -> Optional[bool]:
        """The API strict mode setting.

        When set, this value is sent to the server in the "apiStrict" field.
        """
        return self._strict

    @property
    def deprecation_errors(self) -> Optional[bool]:
        """The API deprecation errors setting.

        When set, this value is sent to the server in the
        "apiDeprecationErrors" field.
        """
        return self._deprecation_errors


def _add_to_command(cmd: MutableMapping[str, Any], server_api: Optional[ServerApi]) -> None:
    """Internal helper which adds API versioning options to a command.

    :param cmd: The command.
    :param server_api: A :class:`ServerApi` or ``None``.
    """
    if not server_api:
        return
    cmd["apiVersion"] = server_api.version
    if server_api.strict is not None:
        cmd["apiStrict"] = server_api.strict
    if server_api.deprecation_errors is not None:
        cmd["apiDeprecationErrors"] = server_api.deprecation_errors