# Copyright (C) 2019 Majormode.  All rights reserved.
#
# This software is the confidential and proprietary information of
# Majormode or one of its subsidiaries.  You shall not disclose this
# confidential information and shall use it only in accordance with the
# terms of the license agreement or other applicable agreement you
# entered into with Majormode.
#
# MAJORMODE MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY
# OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
# TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE, OR NON-INFRINGEMENT.  MAJORMODE SHALL NOT BE LIABLE FOR ANY
# LOSSES OR DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING
# OR DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.

from __future__ import annotations

from typing import Any
import base64
import datetime
import hashlib
import hmac
import uuid

from majormode.perseus.constant.obj import ObjectStatus
from majormode.perseus.constant.team import MemberRole
from majormode.perseus.constant.stage import EnvironmentStage
from majormode.perseus.model.app import ClientApplication
from majormode.perseus.model.geolocation import GeoPoint
from majormode.perseus.model.obj import Object
from majormode.perseus.model.version import Version
from majormode.perseus.service.account.account_service import AccountService
from majormode.perseus.service.application.application_service import ApplicationService
from majormode.perseus.service.base_rdbms_service import BaseRdbmsService
from majormode.perseus.service.base_rdbms_service import RdbmsConnection
from majormode.perseus.service.team.team_service import TeamService
from majormode.perseus.utils import cast
from majormode.perseus.utils.date_util import ISO8601DateTime
from majormode.xeberus.model.device import BatteryStateChangeEvent
from majormode.xeberus.model.device import LocationUpdate

import settings


class DeviceService(BaseRdbmsService):
    # Cryptographic function to hash sensitive information used to generate
    # device activation code.
    DEFAULT_CRYPTOGRAPHIC_HASH_FUNCTION = hashlib.sha1

    # Default minimum remaining time-to-live in seconds of an activation
    # code that could be reused for consecutive activation code generation
    # request.
    #
    # The function {@link DeviceService.generate_activation_code} returns
    # the last generated activation code if the remaining time-to-live of
    # this activation code is above the minimum time threshold, otherwise
    # the function generates a new activation code (even if the existing
    # activation code is still valid).
    #
    # An activation code needs to have enough remaining time-to-live in
    # order:
    #
    # (1) The device Web service to send this activation code over the
    #     Internet (network latency)
    #
    # (2) The administration tool that requests to generate an activation
    #     code to generate the corresponding QR code
    #
    # (3) The client application running on the mobile device to scan and
    #     convert the QR code to the activation code value
    #
    # (4) The client application to send the activation code to the device
    #     Web service (network latency)
    #
    # (5) The device Web service to validate the activation code and to
    #     activate the mobile device
    DEFAULT_DEVICE_ACTIVATION_CODE_MINIMUM_REMAINING_TTL_FOR_REUSE = 2

    # Default time-to-live in seconds of a device activation code.
    #
    # A single activation code can be used to activate several ID-R devices
    # of a same organization in a row.  The longest the time-to-live, the
    # less secure the activation code (spoofing attack), but the more
    # user-friendly.
    #
    # @note: This value MUST be above {@link
    #     DeviceService#DEFAULT_DEVICE_ACTIVATION_CODE_MINIMUM_REMAINING_TTL_FOR_REUSE}
    DEFAULT_DEVICE_ACTIVATION_CODE_TTL = 30

    # Default time approximation in seconds of a mobile device.
    DEFAULT_DEVICE_TIME_APPROXIMATION = 3

    # Default number of characters that compose the nonce of the token
    # generated by the device application.
    DEFAULT_TOKEN_NONCE_LENGTH = 8

    def __activate_device_app(
            self,
            device_app: any,
            activation_code: str,
            connection: RdbmsConnection = None) -> None:
        """
        Activate a client application running o mobile device.

        If the mobile device has been already activated, the function simply
        returns the information about the individual user or the organization
        that has activated this device.


        @note: The function doesn't disable the activation code after the
            function has activated the device.  This activation code can be
            still reused to activate other device of the individual user or
            the organization, until this code expires.


        @param device_app: The client application to activate.  The object
            must contain the following attributes:

            - `account_id: uuid.UUID` (optional): The identification of the account
              of the individual user who owns this mobile device, if any.  This
              information is required when the mobile device has been temporarily
              deactivated by its owner ({@link ObjectStatus.disabled}).

            - `app_id: uuid.UUID` (required): The identification of the client
              application to be activated on the mobile device.

            - `device_id: str` (required): The identification of the device which
              the client application needs to be activated.

            - `team_id: uuid.UUID` (optional): The identification of the
              organization that owns this mobile device, if any.  This information
              is required when the mobile device has been temporarily deactivated
              by the organization that manages it ({@link ObjectStatus.disabled}).

        @param activation_code: A code that has been generated by an
            individual user owning the mobile device or the organization
            managing the mobile device, and scanned by the mobile device.

        @param connection: An existing connection to the device database, with
            the option `auto_commit` enabled.


        @raise DeletedObjectException: If the activation code has expired.

        @raise IllegalAccessException: If the individual user or the
            organization that owns this mobile device is not the same as the
            one that has generated the activation code.

        @raise UndefinedObjectException: If the activation code doesn't exist.
        """
        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            # Retrieve the activation request corresponding to the specified
            # code.  An exception is raised if the activation code has expired.
            activation_request = self.__get_device_app_activation_request(
                activation_code,
                check_status=True,
                connection=connection)

            # If the device has been already activated, or it has been temporarily
            # deactivated, check that the activation code has been requested by the
            # individual user or the organization that owns this device.
            #
            # @note: We still need to check that the activation code has been
            #     initiated by the same individual user or organization that owns
            #     the device because the confidential information about this device
            #     will be returned.
            if device_app.object_status in (ObjectStatus.disabled, ObjectStatus.enabled):
                if device_app.team_id or activation_request.team_id:
                    if device_app.team_id != activation_request.team_id:
                        raise self.IllegalAccessException(
                            f"The mobile device {device_app.device_id} has been registered by another organization")
                elif device_app.account_id != activation_request.account_id:
                    raise self.IllegalAccessException(
                        f"The mobile device {device_app.device_id} has been registered by another individual user")

            if device_app.object_status == ObjectStatus.enabled:  # Device already activated
                return

            # Activate the device, and register the individual user or the
            # organization that owns this device (in case it wasn't already done).
            connection.execute(
                """
                UPDATE
                    device
                  SET
                    account_id = %(account_id)s,
                    object_status = %(OBJECT_STATUS_ENABLED)s,
                    team_id = %(team_id)s,
                    update_time = current_timestamp
                  WHERE
                    device_id = %(device_id)s
                """,
                {
                    'OBJECT_STATUS_ENABLED': ObjectStatus.enabled,
                    'account_id': activation_request.account_id if activation_request.team_id is None else None,
                    'device_id': device_app.device_id,
                    'team_id': activation_request.team_id,
                }
            )

            # Generate a security key to be shared with the client application.
            security_key = self.__generate_security_key(device_app)

            # Activate the client application running on this device and store its
            # security key.
            connection.execute(
                """
                UPDATE 
                    device_app
                  SET
                    activation_time = current_timestamp,
                    object_status = %(OBJECT_STATUS_ENABLED)s,
                    security_key = %(security_key)s,
                    update_time = current_timestamp
                  WHERE
                    app_id = %(app_id)s
                    AND device_id = %(device_id)s
                """,
                {
                    'OBJECT_STATUS_ENABLED': ObjectStatus.enabled,
                    'app_id': device_app.app_id,
                    'device_id': device_app.device_id,
                    'security_key': security_key,
                }
            )

    def __check_device_access(
            self,
            device,
            account_id=None,
            connection=None,
            team_id=None) -> None:
        """
        Check whether the specified user account is granted access to a
        device.

        The function verifies that either the user is the owner of the device,
        either a member of the organization managing this device.


        @param device: An instance representing a device.

        @param account_id: The identification of the account of a user who
            requests the access to the device.

        @param connection: An existing connection to the device database for
            read-only access.

        @param team_id: The identification of the organization that the user
            belongs to.  This organization must be responsible for the device.


        @raise IllegalAccessException: If the specified account doesn't belong
            to the individual user who owns this device, or if the specified
            organization is not managing for this device.
        """
        # If the device is registered on behalf of an organization, verify that
        # the specified user is a member of this organization.
        if device.team_id:
            if team_id != device.team_id:
                raise self.IllegalAccessException(
                    f"The organization {team_id} is not managing for the device {device.device_id}")

            TeamService().assert_member(account_id, team_id, check_status=True, connection=connection)

        # If the device is registered on behalf of an individual user, verify
        # that the specified user is the owner of this device.
        elif account_id != device.account_id:
            AccountService.get_account(account_id, check_status=True, connection=connection)

    def __check_device_status(self, device: Any, strict_status=False) -> None:
        """
        Check that a device is enabled.


        @param device: An instance representing a device.

        @param strict_status: Indicate whether the device MUST be enabled (i.e.,
            activated), or whether its activation can be still pending.


        @raise DeleteObjectException: If the device has been banned by an
            administrator of the cloud service.

        @raise DisabledObjectException: If the device has been suspended by
            the individual user who owns this device or an administrator of
            the organization that manages this device.

        @raise InvalidOperationException: If the activation of this device is
            pending.
        """
        if device.object_status == ObjectStatus.disabled:
            raise self.DisabledObjectException(f"The device {device.device_id} has been deactivated")
        elif device.object_status == ObjectStatus.deleted:
            raise self.DeletedObjectException(f"The device {device.device_id} has been suspended")
        elif device.object_status == ObjectStatus.pending and strict_status:
            raise self.InvalidOperationException(f"The device {device.device_id} has not been activated")

    def __filter_out_duplicated_battery_events(
            self,
            device_app: Any,
            events: list[BatteryStateChangeEvent],
            connection: RdbmsConnection = None):
        """
        Filter out battery state update events that would have been already
        stored.

        A client application may send battery state update events multiple
        times. This happens when network outage occurs after the client
        application reported successfully a batch of events, but the network
        connection with the cloud service timed out before the client
        application had the chance to receive the acknowledgement from the
        cloud service.  Therefore, the client application reattempts to report
        one more time these events.


        @param device_app: An object representing a client application
            installed on a mobile device.  This object must contain the
            following attributes

            - `account_id: uuid.UUID` (optional): The identification of the account
              of the individual user that owns this device.

            - `team_id: uuid.UUID` (optional): The identification of the
              organization that manages this device.

        @param events: A list of battery state update events.

        @param connection: An existing connection to the device database.


        @return: The list of battery state update events that have not been
            stored yet.
        """
        if events:
            with self.acquire_rdbms_connection(auto_commit=False, connection=connection) as connection:
                cursor = connection.execute(
                    """
                    SELECT
                        event_ref
                      FROM
                        device_battery_event
                      WHERE
                        app_id = %(app_id)s
                        AND device_id = %(device_id)s
                        AND event_ref IN (%[event_ref]s)
                    """,
                    {
                        'app_id': device_app.app_id,
                        'device_id': device_app.device_id,
                        'event_ref': [
                            event.event_ref
                            for event in events
                        ],
                    }
                )

                duplicated_event_refs = [
                    row.get_value('event_ref')
                    for row in cursor.fetch_all()
                ]

                if duplicated_event_refs:
                    events = [
                        event
                        for event in events
                        if event.event_ref not in duplicated_event_refs
                    ]

        return events

    def __filter_out_duplicated_location_events(
            self,
            device_app: Any,
            events: list[LocationUpdate],
            connection: RdbmsConnection = None):
        """
        Filter out battery state update events that would have been already
        stored.

        A client application may send battery state update events multiple
        times. This happens when network outage occurs after the client
        application reported successfully a batch of events, but the network
        connection with the cloud service timed out before the client
        application had the chance to receive the acknowledgement from the
        cloud service.  Therefore, the client application reattempts to report
        one more time these events.


        @param device_app: An object representing a client application
            installed on a mobile device.  This object must contain the
            following attributes

            - `account_id: uuid.UUID` (optional): The identification of the account
              of the individual user that owns this device.

            - `team_id: uuid.UUID` (optional): The identification of the
              organization that manages this device.

        @param events: A list of location update events.

        @param connection: An existing connection to the device database.


        @return: The list of battery state update events that have not been
            stored yet.
        """
        if events:
            with self.acquire_rdbms_connection(auto_commit=False, connection=connection) as connection:
                cursor = connection.execute(
                    """
                    SELECT
                        event_ref
                      FROM
                        device_location_event
                      WHERE
                        app_id = %(app_id)s
                        AND device_id = %(device_id)s
                        AND event_ref IN (%[event_ref]s)
                    """,
                    {
                        'app_id': device_app.app_id,
                        'device_id': device_app.device_id,
                        'event_ref': [
                            event.event_ref
                            for event in events
                        ]
                    }
                )

                duplicated_event_refs = [
                    row.get_value('event_ref')
                    for row in cursor.fetch_all()
                ]

                if duplicated_event_refs:
                    events = [
                        event
                        for event in events
                        if event.event_ref not in duplicated_event_refs
                    ]

        return events

    def __generate_new_activation_code(
            self,
            app_id: uuid.UUID,
            account_id: uuid.UUID,
            activation_code_ttl: int,
            connection: RdbmsConnection = None,
            team_id: uuid.UUID = None) -> Any:
        """
        Generate a new code to activate client applications running on one or
        more mobile devices.

        This activation code needs to be used before it expires.  A mobile
        device needs to scan this activation code (for instance, as a
        QR code) and to send it to the cloud service in order this device to
        be activated on behalf the individual user who owns this device or the
        organization that manages this device.


        @param app_id: The identification of the administration application
            that requests to generate a code for activating a client
            application running on a mobile device.

        @param account_id: The identification of the account of the user who
            requests to generate an activation code.  This user is either the
            individual who owns this mobile device or an administrator of the
            organization that manages mobile devices.

        @param activation_code_ttl: The time-to-live (TTL) in seconds of the
            activation code.

        @param connection: An existing connection to the device database, with
            the option `auto_commit` enabled.

        @param team_id: Identification of the organization that is responsible
            for managing this device.


        @return: An object containing the following attributes:

            - `activation_code: str` (required): An activation code to be scanned
              by one or more mobile devices.

            - `expiration_time: ISO8601DateTime` (required): The time when this
              activation code will expire.
        """
        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            # Generate a new activation identification.
            activation_id = uuid.uuid4()

            # Encrypt the activation identification with a key composed of the
            # identifications of the user account and the organization passed to
            # this function.
            encryption_key = f"{account_id}{team_id or ''}".encode()

            activation_code = hmac \
                .new(
                    encryption_key,
                    msg=activation_id.hex.encode(),
                    digestmod=self.DEFAULT_CRYPTOGRAPHIC_HASH_FUNCTION
                ) \
                .hexdigest()

            # Define the time when this activation code will expire.
            expiration_time = ISO8601DateTime.now() + datetime.timedelta(
                seconds=activation_code_ttl or self.DEFAULT_DEVICE_ACTIVATION_CODE_TTL)

            # Store the activation code's properties into database.
            connection.execute(
                """
                INSERT INTO device_app_activation_request (
                    account_id,
                    activation_code,
                    app_id,
                    expiration_time,
                    object_status,
                    team_id
                  )
                  VALUES (
                    %(account_id)s,
                    %(activation_code)s,
                    %(app_id)s,
                    %(expiration_time)s,
                    %(OBJECT_STATUS_ENABLED)s,
                    %(team_id)s
                  )
                """,
                {
                    'OBJECT_STATUS_ENABLED': ObjectStatus.enabled,
                    'account_id': account_id,
                    'activation_code': activation_code,
                    'app_id': app_id,
                    'expiration_time': expiration_time,
                    'team_id': team_id,
                })

            activation_request = Object(
                activation_code=activation_code,
                expiration_time=expiration_time,
            )

            return activation_request

    def __generate_security_key(self, device_app: any) -> str:
        """
        Generate a security key to be shared with the client application to
        secure the communication with the cloud service.


        @param device_app: An object representing a client application
            installed on a mobile device.  This object must contain the
            following attributes

            - `account_id: uuid.UUID` (optional): The identification of the account
              of the individual user that owns this device.

            - `team_id: uuid.UUID` (optional): The identification of the
              organization that manages this device.


        @return: A security key.
        """
        # Generated a random number.
        nonce = uuid.uuid4()

        # Encrypt the nonce with a key composed of the identification of either
        # the account of the individual user or either the organization that
        # owns this mobile device.
        encryption_key = f'{device_app.account_id or device_app.team_id}'.encode()

        security_key = hmac \
            .new(
                encryption_key,
                msg=nonce.hex.encode(),
                digestmod=self.DEFAULT_CRYPTOGRAPHIC_HASH_FUNCTION
            ) \
            .hexdigest()

        return security_key

    def __get_device_app(
            self,
            device_id: str,
            app_id: uuid.UUID,
            check_status: bool = False,
            connection: RdbmsConnection = None,
            include_security_info: bool = False) -> Any:
        """
        Return information about a client application installed on a mobile
        device.


        @param device_id: The identification of a mobile device.

        @param app_id: The identification of a client application installed on
            the mobile device.

        @param check_status: Indicate whether to check the status of the
            registration of the client application installed on the mobile
            device.

        @param connection: An existing connection to the device database.

        @param include_security_info: Indicate whether to include the security
            key generated by the cloud service and shared to the client
            application running on the mobile device (during the activation).


        @return: An object containing the following attributes:

            - `activation_time: ISO8601DateTime` (optional): The time when the
               client application has been activated on the mobile device.

            - `account_id: uuid.UUID` (optional): The identification of the account
              of the individual user, that owns this mobile device, if any.

            - `app_id: uuid.UUID` (required): The identification of the
              client application.

            - `app_version: Version` (required): The version of the client
              application installed on the mobile device.

            - `device_id: str` (required): The identification of the device which
              which the client application is installed on.

            - `mac_address: uuid.UUID` (optional): The Media Access Control (MAC)
              address of the mobile device.  This attribute is returned only if the
              argument `include_security_info` equals `True`.


            - `object_status: ObjectStatus` (required): The current status of the
              client application.

            - `security_key: str` (optional): The security key generated by the
              cloud service and shared with the client application installed on
              the device to secure the communication.  This information is only
              available when the client application has been activated on the device.
              This attribute is returned only if the argument `include_security_info`
              equals `True`.

            - `team_id: uuid.UUID` (optional): The identification of the
              organization that owns this mobile device, if any.

            - `update_time: ISO8601DateTime` (required): The time of the most
              recent modification of one or more attribute of the client
              application.


        @raise DeletedObjectException: If the client application has been
            deactivated on the mobile device by the cloud service, for
            instance, when the ownership of the device has been transferred to
            another individual user or organization.

        @raise DisabledObjectException: If the client application has been
            deactivated by the individual user or an administrator of the
            organization that owns this device.

        @raise UndefinedObjectException: If the registration of the client
            application installed on the mobile device doesn't exist.  The
            most common reason is that the client application didn't send a
            handshake to the cloud service.
        """
        with self.acquire_rdbms_connection(auto_commit=False, connection=connection) as connection:
            cursor = connection.execute(
                """
                SELECT
                    device_app.activation_time,
                    device.account_id,
                    device_app.app_id,
                    device_app.app_version,
                    device_app.device_id,
                    device.mac_address,
                    device_app.object_status,
                    device_app.security_key,
                    device.team_id,
                    device_app.update_time
                  FROM
                    device_app
                  INNER JOIN device
                    USING (device_id)
                  WHERE
                    device_app.app_id = %(app_id)s
                    AND device_app.device_id = %(device_id)s
                """,
                {
                    'app_id': app_id,
                    'device_id': device_id,
                }
            )

            row = cursor.fetch_one()
            if row is None:
                raise self.UndefinedObjectException(
                    f"The registration of the app {app_id} and the device {device_id} does not exist")

            device_app = row.get_object({
                'account_id': cast.string_to_uuid,
                'activation_time': cast.string_to_timestamp,
                'app_id': cast.string_to_uuid,
                'app_version': Version,
                'object_status': ObjectStatus,
                'team_id': cast.string_to_uuid,
                'update_time': cast.string_to_timestamp,
            })

            if check_status:
                if device_app.object_status == ObjectStatus.deleted:
                    raise self.DeletedObjectException(
                        f"The app {app_id} has been deactivated on the device {device_id} by the cloud service")
                elif device_app.object_status == ObjectStatus.disabled:
                    raise self.DisabledObjectException(
                        f"The app {app_id} has been deactivated on the device {device_id} by its owner")

            if not include_security_info:
                del device_app.mac_address
                del device_app.security_key

            return device_app

    def __get_device_apps(self, device_id, connection=None) -> list[Any]:
        """
        Return the list of client applications installed in a mobile device.


        @param device_id: The identification of a mobile device.

        @param connection: An existing connection to the device database.


        @return: A list of objects containing the following attributes:

            - `app_id: uuid.UUID`: The identification of the application.

            - `app_version: Version`: The version of the application installed in
              the mobile device.

            - `creation_time: ISO8601DateTime`: The time when the application has
              been registered as installed on the mobile device.

            - `object_status: ObjectStatus`: The current status of the application
              installed in this mobile device.

            - `picture_id: uuid.UUID`: The identification of the application's logo.

            - `picture_url: str`: The Uniform Resource Locator (URL) of the
              application's logo.

            - `update_time: ISO8601DateTime`: The time of the most recent
              modification of an attribute of the application installed in the
              mobile device (version, status, and/or logo).
        """
        with self.acquire_rdbms_connection(auto_commit=False, connection=connection) as connection:

            cursor = connection.execute(
                """
                SELECT
                    application.app_id,
                    application.app_name,
                    device_application.app_version,
                    device_application.creation_time,
                    device_application.object_status,
                    device_application.picture_id,
                    device_application.update_time
                  FROM
                    device_application
                  INNER JOIN application
                    USING (app_id)
                  WHERE
                    device_application.device_id = %(device_id)s
                """,
                {
                    'device_id': device_id,
                }
            )

            rows = cursor.fetch_all()
            apps = [
                row.get_object({
                    'app_id': cast.string_to_uuid,
                    'app_version': Version,
                    'creation_time': cast.string_to_timestamp,
                    'object_status': ObjectStatus,
                    'picture_id': cast.string_to_uuid,
                    'update_time': cast.string_to_timestamp,
                })
                for row in rows
            ]

            for app in apps:
                app.picture_url = ApplicationService().build_picture_url(app.app_id)

            return apps

    def __get_device_app_activation_request(
            self,
            activation_code: str,
            check_status: bool = False,
            connection: RdbmsConnection = None) -> Any:
        """
        Return extended information about an activation request of a client
        application running on a mobile device


        @param activation_code: A code that have been generated by an
            individual user or an administrator of an organization to activate
            a mobile device.

        @param check_status: Indicate whether to check the current status of
            this activation request.

        @param connection: An existing connection to the device database.


        @return: An object containing the following attributes:

            - `account_id: uuid.UUID` (required): The identification of the
              account of the administrator of the organization who requested the
              generation of this activation code.

            - `app_id: uuid.UUID` (required): The identification of the
              administration application that requests to generate this code for
              activating a client application running on a mobile device.

            - `creation_time: ISO8601DateTime` (required): The time when this
              activation code has been generated.

            - `expiration_time: ISO8601DateTime` (required): The time when the
              activation code expires.

            - `object_status: ObjectStatus` (required): The current status of this
              activation code.

            - `team_id: uuid.UUID` (required): The identification of the
              organization on behalf of which this activation code has been
              generated.

            - `update_time: ISO8601DateTime` (required): The time of the most
              recent modification of one or more attributes of this activation
              request.


        @raise DeletedObjectException: If the activation code has expired,
            while the argument `check_status` is `True`.
.
        @raise UndefinedObjectException: If the specified code doesn't refer
            to any device activation request registered to the
            platform.
        """
        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            # Retrieve the information of the activation code.
            cursor = connection.execute(
                """
                SELECT
                    account_id,
                    app_id,
                    creation_time,
                    expiration_time,
                    object_status,
                    team_id,
                    update_time
                  FROM
                    device_app_activation_request
                  WHERE
                    activation_code = %(activation_code)s
                """,
                {
                    'activation_code': activation_code,
                }
            )

            row = cursor.fetch_one()
            if row is None:
                raise self.UndefinedObjectException(f"The activation code {activation_code} doesn't exist")

            activation_request = row.get_object({
                'account_id': cast.string_to_uuid,
                'app_id': cast.string_to_uuid,
                'creation_time': cast.string_to_timestamp,
                'expiration_time': cast.string_to_timestamp,
                'object_status': ObjectStatus,
                'team_id': cast.string_to_uuid,
                'update_time': cast.string_to_timestamp,
            })

            # Automatically change the status of this activation code to `deleted`,
            # if this activation code has expired.
            if activation_request.object_status == ObjectStatus.enabled and activation_request.expiration_time < ISO8601DateTime.now():
                activation_request.object_status = ObjectStatus.deleted

            # When required, check that the activation code is still enabled.
            if check_status and activation_request.object_status == ObjectStatus.deleted:
                raise self.DeletedObjectException(f"The activation code {activation_code } has expired")

            return activation_request

    def __get_device_by_id(
            self,
            device_id: str,
            account_id: uuid.UUID = None,
            check_access: bool = False,
            check_status: bool = False,
            connection: RdbmsConnection = None,
            include_app_list: bool = False,
            include_system_info: bool = False,
            strict_status: bool = True,
            team_id: uuid.UUID = None) -> Any:
        """
        Return the information about a mobile device.


        @param device_id: The identification of a mobile device.

        @param account_id: The identification of the user on behalf of whom
            the function is called.  This user must either be the individual
            user who owns this mobile device, either a member of the
            organization that manages this device.

        @param check_access: Indicate whether to check the status of the
            mobile device.  The function raises an exception if the user and/
            or the organization on behalf of whom/which the function is called
            is not granted access to the mobile device.

        @param check_status: Indicate whether to check the status of the
            mobile device.  The function raises an exception if the device is
            not enabled.

        @param connection: An existing connection to the device database.

        @param include_app_list: Indicate whether to return information about
            the client applications installed in the mobile device.

        @param include_system_info: Indicate whether to return system
            information about the device, such as its serial number, its MAC
            address, its operating system.

        @param strict_status: Indicate whether the device MUST be enabled (i.e.,
            activated), or whether its activation can be still pending.

        @param team_id: The identification of the organization that the user
            (on behalf of whom the function is called) belongs to.


        @return: An object containing the following attributes:

            - `account_id: uuid.UUID` (optional): The identification of the
              individual user who owns this device, or the identification of the
              member of the organization on behalf of which the device has been
              activated.

            - `activation_time: ISO8601DateTime (required)': The time when the
              device has been activated.

            - `apps: list` (optional): The list of the client applications installed
              on this device.

            - `creation_time: ISO8601DateTime` (required): The time when the device
              has been registered to the cloud service.

            - `device_model: str` (optional): The end-user visible name of mobile
              device model.

            - `mac_address: str` (optional): The Media Access Control (MAC) address
              of the mobile device.

            - `object_status: ObjectStatus` (required): The current status of the
              device:

              - `ObjectStatus.deleted`: The device has been banned by an
                administrator of the cloud service.

              - `ObjectStatus.disabled`: The device has been suspended by the
                individual user who owns this device or an administrator of the
                organization that manages this device.

              - `ObjectStatus.enabled`: The device is activated and is expected to
                operate properly.

              - `ObjectStatus.pending`: The device has not been activated yet.

            - `os_name: str` (optional): The Name of the operating system of the
              mobile device.  This information is immutable.

            - `os_version: Version` (optional): The version of the operating system
              of the mobile device.  This information may be updated from time to
              time when the operating system of the device is upgraded.

            - `picture_id: uuid.UUID` (optional): The identification of the mobile
              device's picture.

            - `picture_url: str` (optional): The Uniform Resource Locator (URL) of
              the mobile device's picture.

            - `serial_number: str`: The hardware serial number of the mobile device.
              It corresponds to a unique number assigned by the manufacturer to help
              identify an individual device.

            - `team_id: uuid.UUID`: The identification of the organization that
             manages this device.

            - `update_time: ISO8601DateTime`: The time of the most recent
              modification of one or more attributes of this device.
        """
        with self.acquire_rdbms_connection(auto_commit=False, connection=connection) as connection:
            cursor = connection.execute(
                """
                SELECT
                    account_id,
                    creation_time,
                    device_model,
                    mac_address,
                    object_status,
                    os_name,
                    os_version,
                    picture_id,
                    serial_number,
                    team_id,
                    update_time
                  FROM
                    device
                  WHERE
                    device_id = %(device_id)s
                """,
                {
                    'device_id': device_id
                }
            )

            row = cursor.fetch_one()
            if row is None:
                raise self.UndefinedObjectException(f"No device registered with the identification {device_id}")

            device = row.get_object({
                'account_id': cast.string_to_uuid,
                'creation_time': cast.string_to_timestamp,
                'object_status': ObjectStatus,
                'os_version': Version,
                'picture_id': cast.string_to_uuid,
                'team_id': cast.string_to_uuid,
                'update_time': cast.string_to_timestamp,
            })

            # Check whether the user is granted access to the device.
            if check_access:
                self.__check_device_access(device, account_id=account_id, connection=connection, team_id=team_id)

            # Check whether the device is enabled.
            if check_status:
                self.__check_device_status(device, strict_status=strict_status)

            if include_app_list:
                device.apps = self.__get_device_apps(device_id)

            # Remove extended information if not requested.
            if not include_system_info:
                del device.device_model
                del device.mac_address
                del device.os_name
                del device.os_version
                del device.serial_number

            return device

    def __get_device_by_mac_address(
            self,
            mac_address: str,
            connection: RdbmsConnection = None) -> Any:
        """
        Return the information of a mobile device specified with its MAC
        address.


        @param mac_address: The Media Access Control (MAC) address of the
            mobile device.

        @param connection: An existing connection to the device database for
            read-only access.


        @return: An object containing the following attributes:

            - `account_id: uuid.UUID` (optional): The identification of the account
              of the user who has activated the device.  This is either an
              individual user (the owner of the device), either an administrator of
              the organization that manages this device.

            - `device_id: str` (required): The identification of the device.

            - `object_status: ObjectStatus` (required): The current status of the
              device:

              - {@link ObjectStatus.deleted}: The device has been banned by an
                administrator of the cloud service.

              - {@link ObjectStatus.disabled}: The device has been suspended by the
                individual user who has activated this device or an administrator of
                the organization that manages this device.

              - {@link ObjectStatus.enabled}: The device is activated and is expected
                to operate properly.

              - {@link ObjectStatus.pending}: The device has not been activated yet.

            - `team_id: uuid.UUID` (optional): The organization on behalf of which
              an administrator has activated the device.

            - `update_time: ISO8601DateTime` (required): The time of the most
              recent modification of one or more attributes of the device.


        @raise DeletedObjectException: If the team that is managing the device
            has been deleted, or if the account of the individual user who has
            activated this device has been deleted.

        @raise DisabledObjectException: If the team that is managing the device
            has been disabled, or if the account of the individual user who has
            activated this device has been disabled.

        @raise UndefinedObjectException: If no device is registered with the
            specified MAC address.
        """
        with self.acquire_rdbms_connection(auto_commit=False, connection=connection) as connection:
            cursor = connection.execute(
                """
                SELECT
                    account_id,
                    device_id,
                    object_status,
                    team_id,
                    update_time
                  FROM
                    device
                  WHERE
                    mac_address = %(mac_address)s
                """,
                {
                    'mac_address': mac_address,
                }
            )

            row = cursor.fetch_one()
            if not row:
                raise self.UndefinedObjectException(f"No device registered with the MAC address {mac_address}")

            device = row.get_object({
                'account_id': cast.string_to_uuid,
                'object_status': ObjectStatus,
                'team_id': cast.string_to_uuid,
                'update_time': cast.string_to_timestamp,
            })

            # If the device is managed by an organization, retrieve information
            # about this organization.
            if device.team_id:
                device.team = TeamService().get_team(
                    device.team_id,
                    check_status=True,
                    connection=connection,
                    include_extended_info=False,
                    include_contacts=False)
                del device.team_id

            # If the device has been activated by an individual user, check that
            # the account of this user is enabled.
            elif device.account_id:
                AccountService().get_account(
                    device.account_id,
                    check_status=True,
                    connection=False,
                    include_contacts=False)

            return device

    def __get_last_keepalive_event(
            self,
            device_id: str,
            app_id: uuid.UUID,
            connection: RdbmsConnection = None) -> Any | None:
        """
        Return the last keep-alive event sent by a client application
        installed on a mobile device.


        @todo Retrieve the last keep-alive event of the client application
            installed on the mobile device from a memory cache database (Redis).


        @param device_id: The identification of a mobile device.

        @param app_id: The identification of a client application to return
            the last keep-alive event.

        @param connection: An existing connection to the device database.


        @return: An object containing the following attributes:

            - `event_id`: The identification of the last keep-alive event sent by
              the client application installed on the mobile device.

            - `event_time`: The time when the client application has sent this
              keep-alive event.
        """
        with self.acquire_rdbms_connection(auto_commit=False, connection=connection) as connection:
            cursor = connection.execute(
                """
                SELECT
                    event_id,
                    event_time
                  FROM
                    device_keepalive_event
                  WHERE
                    app_id = %(app_id)s
                    AND device_id = %(device_id)s
                  ORDER BY 
                    event_time DESC
                  LIMIT 1
                """,
                {
                    'app_id': app_id,
                    'device_id': device_id,
                }
            )

            row = cursor.fetch_one()
            last_keepalive_event = row and row.get_object({
                'event_id': cast.string_to_uuid,
                'event_time': cast.string_to_timestamp,
            })

            return last_keepalive_event

    def __insert_battery_events(
            self,
            device_app: Any,
            events: list[BatteryStateChangeEvent],
            account_id: uuid.UUID,
            connection: RdbmsConnection = None) -> None:
        """
        Store a list of battery state change events reported by a client
        application running on a mobile device.


        @param device_app: An object representing the client application that
            sent these events:

            - `app_id: uuid.UUID` (required): The identification of the client
              application.

            - `device_id: str` (required): The identification of the device.

        @param events: A list of battery state changes.

        @param account_id: The identification of the account of the user who
            is currently using the client application on this mobile device.

        @param connection: An existing connection to the device database, with
            the option `auto_commit` enabled.
        """
        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            connection.execute(
                """
                INSERT INTO device_battery_event (
                    account_id,
                    accuracy,
                    app_id,
                    battery_level,
                    bearing,
                    device_id,
                    event_ref,
                    event_time,
                    event_type,
                    fix_time,
                    location,
                    provider,
                    speed,
                    team_id
                  )
                  VALUES
                    %[values]s
                """,
                {
                    'values': [
                        (
                            account_id,
                            event.location and event.location.accuracy,
                            device_app.app_id,
                            event.battery_level,
                            event.location and event.location.bearing,
                            device_app.device_id,
                            event.event_ref,
                            event.event_time,
                            event.event_type,
                            event.location and event.location.fix_time,
                            event.location and (f'ST_SetSRID(ST_MakePoint({event.location.longitude}, {event.location.latitude}, {event.location.altitude}), 4326)',),
                            event.location and event.location.provider,
                            event.location and event.location.speed,
                            device_app.team_id,
                        )
                        for event in events
                    ]
                }
            )

    def __insert_device(
            self,
            device_id: str,
            agent_application: ClientApplication,
            mac_address: str,
            serial_number: str,
            connection: RdbmsConnection = None) -> Any:
        """
        Insert the information of a mobile device into the database.


        @param device_id: The identification of the device.

        @param agent_application: The information about the client application
            and the device that initiates this call.

        @param mac_address: The Media Access Control (MAC) address of the
            mobile device.  A MAC address is a unique identifier assigned to a
            network interface for communications at the data link layer of a
            network segment.

        @param serial_number:  The hardware serial number of the mobile device.
            It corresponds to a unique number assigned by the manufacturer to
            help identify an individual device.

            Because the operating system of the mobile device may restrict
            access to persistent device identifiers, the serial number of the
            mobile device may not be provided.

            Serial number is case-sensitive.

        @param connection: An existing connection to the device database, with
            auto-commit option enabled.


        @return: An object containing the following attributes:

            - `device_id: str` (required): The identification of the device.

            - `object_status: ObjectStatus` (required): The current status of the
              mobile device ({@link ObjectStatus.pending}).

            - `update_time: ISO8601DateTime` (required): The time of the most
              recent modification of one or more attributes of the device.
        """
        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            cursor = connection.execute(
                """
                INSERT INTO device (
                    device_id,
                    device_model,
                    mac_address,
                    object_status,
                    os_name,
                    os_version,
                    serial_number
                  )
                  VALUES (
                    %(device_id)s,
                    %(device_model)s,
                    %(mac_address)s,
                    %(OBJECT_STATUS_PENDING)s,
                    %(os_name)s,
                    %(os_version)s,
                    %(serial_number)s
                  )
                  RETURNING
                    device_id,
                    object_status,
                    update_time
                """,
                {
                    'OBJECT_STATUS_PENDING': ObjectStatus.pending,
                    'device_id': device_id,
                    'device_model': agent_application.device_model,
                    'mac_address': mac_address,
                    'os_name': agent_application.os_name,
                    'os_version': str(agent_application.os_version),
                    'serial_number': serial_number,
                }
            )

            row = cursor.fetch_one()
            device = row.get_object({
                'object_status': ObjectStatus,
                'update_time': cast.string_to_timestamp,
            })

            return device

    def __insert_device_app(
            self,
            device_id: str,
            app_id: uuid.UUID,
            agent_application: ClientApplication,
            connection: RdbmsConnection = None):
        """
        Insert into the device database the information about a client
        application installed on a mobile device.


        @param device_id: The identification of the mobile device which the
            client application is installed on.

        @param app_id: The identification of the client application.

        @param agent_application: The information about the client application
            and the device that initiates this call.

        @param connection: An existing connection to the device database, with
            auto-commit option enabled.
        """
        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            connection.execute(
                """
                INSERT INTO device_app (
                    app_id,
                    app_version,
                    device_id,
                    object_status
                  )
                  VALUES (
                    %(app_id)s,
                    %(app_version)s,
                    %(device_id)s,
                    %(OBJECT_STATUS_PENDING)s
                  )
                """,
                {
                    'OBJECT_STATUS_PENDING': ObjectStatus.pending,
                    'app_id': app_id,
                    'app_version': str(agent_application.product_version),
                    'device_id': device_id,
                }
            )

    def __insert_keepalive_event(
            self,
            device_app: Any,
            event_time: ISO8601DateTime,
            account_id: uuid.UUID = None,
            connection: RdbmsConnection = None,
            location: GeoPoint = None,
            network_type: str = None) -> Any:
        """
        Store a keep-alive event sent by a mobile device.


        @param device_app: An object representing the client application that
            sent this keep-alive event:

            - `app_id: uuid.UUID` (required): The identification of the client
              application.

            - `device_id: str` (required): The identification of the device.

        @param event_time: The time when the client application sent this
            keep-alive event.

        @param account_id: The identification of the account of the user who
            is currently using the client application on this mobile device.

        @param connection: An existing connection to the device database, with
            the option `auto_commit` enabled.

        @param location: The last known location of the mobile device when the
            client application sent this keep-alive event.

        @param network_type: A string representation of a tuple of the form
            operator:type[:subtype]` where:

            - `operator: string` (required): The identifier of the mobile phone
              operator, composed of the Mobile Country Code (MCC) and the Mobile
              Network Code (MNC) of this carrier.

            - `type: string` (required): A human-readable name that describes the
              type of the network that the device is connected to, such as `wifi`,
              `mobile`, `unknown`.

            - `subtype: string` (optional): A human-readable name that describes
              the subtype of this network when applicable, such as the radio
              technology currently in use on the device for data transmission.
              Network of type `wifi` has no subtype.  Network of type `mobile` can
              have a subtype such as `egde`, `gprs`, `hsdpa`, `hspa`, `hspa+`,
              `umts`, etc.


        @return: An object containing the following attributes:

            - `event_id: uuid.UUID` (required): The identification of the keep-
              alive event.

            - `creation_time: ISO8601DateTime (required): The time when this keep-
              alive event has been stored.
        """
        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            cursor = connection.execute(
                """
                INSERT INTO device_keepalive_event (
                    account_id,
                    accuracy,
                    app_id,
                    bearing,
                    device_id,
                    event_time,
                    fix_time,
                    location,
                    network_type,
                    provider,
                    speed,
                    team_id
                  )
                  VALUES (
                    %(account_id)s,
                    %(accuracy)s,
                    %(app_id)s,
                    %(bearing)s,
                    %(device_id)s,
                    %(event_time)s,
                    %(fix_time)s,
                    ST_SetSRID(ST_MakePoint(%(longitude)s, %(latitude)s, %(altitude)s), 4326),
                    %(network_type)s,
                    %(provider)s,
                    %(speed)s,
                    %(team_id)s
                  )
                  RETURNING
                    creation_time,
                    event_id
                """,
                {
                    'account_id': account_id,
                    'accuracy': location and location.accuracy,
                    'altitude': location and location.altitude,
                    'app_id': device_app.app_id,
                    'bearing': location and location.bearing,
                    'device_id': device_app.device_id,
                    'event_time': event_time,
                    'fix_time': location and location.fix_time,
                    'latitude': location and location.latitude,
                    'longitude': location and location.longitude,
                    'network_type': network_type,
                    'provider': location and location.provider,
                    'speed': location and location.speed,
                    'team_id': device_app.team_id,
                }
            )

            row = cursor.fetch_one()
            keepalive_event = row.get_object({
                'creation_time': cast.string_to_timestamp,
                'event_id': cast.string_to_uuid,
            })

            return keepalive_event

    def __insert_location_events(
            self,
            device_app: Any,
            events: list[LocationUpdate],
            account_id: uuid.UUID,
            connection: RdbmsConnection = None) -> None:
        """
        Store a list of location update events reported by a client
        application running on a mobile device.


        @param device_app: An object representing the client application that
            sent these events:

            - `app_id: uuid.UUID` (required): The identification of the client
              application.

            - `device_id: str` (required): The identification of the device.

        @param events: A list of location updates.

        @param account_id: The identification of the account of the user who
            is currently using the client application on this mobile device.

        @param connection: An existing connection to the device database, with
            the option `auto_commit` enabled.
        """
        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            connection.execute(
                """
                INSERT INTO device_location_event (
                    account_id,
                    accuracy,
                    app_id,
                    bearing,
                    device_id,
                    event_ref,
                    event_time,
                    fix_time,
                    location,
                    network_type,
                    provider,
                    satellites,
                    speed,
                    team_id
                  )
                  VALUES
                    %[values]s
                """,
                {
                    'values': [
                        (
                            account_id,
                            event.location and event.location.accuracy,
                            device_app.app_id,
                            event.location and event.location.bearing,
                            device_app.device_id,
                            event.event_ref,
                            event.event_time,
                            event.location and event.location.fix_time,
                            event.location and (f'ST_SetSRID(ST_MakePoint({event.location.longitude}, {event.location.latitude}, {event.location.altitude}), 4326)',),
                            event.location and event.network_type,
                            event.location and event.location.provider,
                            event.location and event.satellites,
                            event.location and event.location.speed,
                            device_app.team_id,
                        )
                        for event in events
                    ]
                }
            )

    def __register_device(
            self,
            device_id: str,
            app_id: uuid.UUID,
            agent_application: ClientApplication,
            ip_address: str,
            connection: RdbmsConnection = None,
            location: GeoPoint = None,
            mac_address: str = None,
            serial_number: str = None):
        """
        Register a device that shakes hands for the first time.

        An individual user or an administrator of the organization officially
        responsible for managing this device will need to activate this device.


        @param device_id: The identification of the device.

        @param app_id: The identification of the client application that
            initiates this call.

        @param agent_application: The information about the client application
            and the device that initiates this call.

        @param ip_address: A dotted-decimal notation of an IPv4 address,
            consisting of four decimal numbers, each ranging from ``0`` to
            ``255``, separated by dots.

        @param location: The geographical location of the device when it
            sent a handshake request.

        @param mac_address: The Media Access Control (MAC) address of the
            mobile device.  A MAC address is a unique identifier assigned to a
            network interface for communications at the data link layer of a
            network segment.

        @param serial_number: The hardware serial number of the mobile device.
            It corresponds to a unique number assigned by the manufacturer to
            help identify an individual device.  Serial number is case-
            sensitive.

        @param connection: An existing connection to the device database, with
            auto-commit option enabled.


        @return: An object containing the following attributes:

            - `device_id: str` (required): The identification of the device.

            - `object_status: ObjectStatus` (required): The current status of the
              mobile device ({@link ObjectStatus.pending}).

            - `update_time: ISO8601DateTime` (required): The time of the most
              recent modification of one or more attributes of the device.
        """
        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            device = self.__insert_device(
                device_id,
                agent_application,
                mac_address,
                serial_number,
                connection=connection)

            self.__insert_device_app(
                device.device_id,
                app_id,
                agent_application,
                connection=connection)

            return device

    def __reuse_previous_activation_code(
            self,
            app_id: uuid.UUID,
            account_id: uuid.UUID,
            minimum_remaining_ttl_for_reuse: int,
            connection: RdbmsConnection = None,
            team_id: uuid.UUID = None) -> Any:
        """
        Reuse a previous activation code that is not yet expired.

        A previously generated activation code can be reused for the same
        individual user or the same organization that has requested the
        generation of this activation code, as long as this activation code
        has not expired yet.


        @note: For security reasons, the function does not reuse an
            activation code that has been previously generated on behalf of
            another administration application.


        @param app_id: The identification of the administration application
            that requests to generate a code for activating a client
            application running on a mobile device.

        @param account_id: The identification of the account of an individual
            user or an administrator of an organization that requests to
            activate a client application running on a mobile device.

        @param minimum_remaining_ttl_for_reuse: The minimal remaining time in
            seconds of an activation code to reuse it.  If a previous generated
            activation code is about to expire, the function won't reuse it.

        @param connection: An existing connection to the device database, with
            the option `auto_commit` enabled.

        @param team_id: Identification of the organization that is responsible
            for managing this device.


        @return: An existing activation code that can be reused, or `None`.

            An activation code is represented with an object containing the
            following attributes:

            - `activation_code: str` (required): An activation code to be scanned
              by one or more mobile devices.

            - `expiration_time: ISO8601DateTime` (required): The time when this
              activation code will expire.
        """
        # Calculate the expiration time of an eligible activation code for reuse.
        # The expiration time of an activation code must include the specified
        # minimal remaining delay before expiration.
        expiration_time = ISO8601DateTime.now() \
            + datetime.timedelta(seconds=minimum_remaining_ttl_for_reuse)

        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            cursor = connection.execute(
                """
                SELECT
                    activation_code,
                    expiration_time
                  FROM
                    device_app_activation_request
                  WHERE
                    app_id = %(app_id)s
                    AND (%(team_id)s IS NOT NULL OR account_id = %(account_id)s)  -- Individual user ...
                    AND (%(team_id)s IS NULL OR team_id = %(team_id)s)        -- ... or organization
                    AND object_status = %(OBJECT_STATUS_ENABLED)s
                    AND expiration_time >= %(expiration_time)s
                  ORDER BY
                    expiration_time DESC
                  LIMIT 1
                """,
                {
                    'OBJECT_STATUS_ENABLED': ObjectStatus.enabled,
                    'account_id': account_id,
                    'app_id': app_id,
                    'expiration_time': expiration_time,
                    'team_id': team_id,
                })

            row = cursor.fetch_one()
            activation_request = row and row.get_object({
                'expiration_time': cast.string_to_timestamp,
            })

            return activation_request

    def __update_device_app(
            self,
            device_id: str,
            app_id: uuid.UUID,
            app_version: Version,
            connection: RdbmsConnection = None):
        pass

    def __update_device_last_known_location(
            self,
            device_id: str,
            events: list[LocationUpdate],
            connection: RdbmsConnection = None) -> None:
        """
        Update the last known location of a device.


        @todo: Replace the storage of the last known location of the device
            from a persistent storage to a cache memory.  We need a method to
            initialize the cache memory from the history of events reported by
            the devices (when the cache memory server has crashed).


        @param device_id: The identification of a device.

        @param events: A list of location updates.

        @param connection: An existing connection to the device database, with
            the option `auto_commit` enabled.
        """
        if not events:
            return

        events_with_location = list(filter(lambda event: event.location is not None, events))
        if not events_with_location:
            return

        events_with_location.sort(key=lambda event: event.location.fix_time, reverse=True)
        most_recent_location: GeoPoint = events_with_location[0].location

        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            connection.execute(
                """
                UPDATE
                    device
                  SET
                    accuracy = %(accuracy)s,
                    bearing = %(bearing)s,
                    fix_time = %(fix_time)s,
                    location = ST_SetSRID(ST_MakePoint(%(longitude)s, %{latitude)s, %(altitude)s), 4326),
                    provider = %(provider)s,
                    speed = %(speed)s,
                    update_time = current_timestamp
                  WHERE 
                    device_id = %(device_id)s
                    AND (fix_time IS NULL OR fix_time < %(fix_time)s)
                """,
                {
                    'accuracy': most_recent_location.accuracy,
                    'altitude': most_recent_location.altitude,
                    'bearing': most_recent_location.bearing,
                    'device_id': device_id,
                    'fix_time': most_recent_location.fix_time,
                    'latitude': most_recent_location.latitude,
                    'longitude': most_recent_location.longitude,
                    'provider': most_recent_location.provider,
                    'speed': most_recent_location.speed
                }
            )

    def activate_device_app(
            self,
            app_id: uuid.UUID,
            device_id: str,
            activation_code: str,
            connection: RdbmsConnection = None) -> Any:
        """
        Activate a client application running on a mobile device.

        The client application must have sent a handshake to the server
        platform first (cf {@DeviceService.shake_hands}).


        @param app_id: The identification of the client application to be
            activated.

        @param device_id: The identification of the mobile device which the
            client application is running on.

        @param activation_code: An activation code generated by an
            administration application and scanned by the client application
            running on the mobile device.

        @param connection: An existing connection to the device database, with
            the option `auto_commit` enabled.


        @return: An object containing the following attributes:

            - `account: any` (optional): The information about the individual user
              who owns this mobile device (cf. {@link AccountService.get_account}).

            - `activation_time: ISO8601DateTime` (required): The time when the client
              application has been activated on the mobile device.

            - `app_id: uuid.UUID` (required): The identification of the client
              application that has been activated.

            - `app_version: Version` (required): The current version of the client
              application installed on the mobile device.

            - `device_id: str` (required): The identification of the device.

            - `object_status: ObjectStatus` (required): The current status of the
              client application installed on the mobile device.

            - `security_key: str` (required): The security key generated that the
              client application must use to secure the communication with the
              cloud service.

            - `team: any` (optional): The information about the organization that
              owns this mobile device (cf. {@link TeamService.get_team}).

            - `update_time: ISO8601DateTime` (required): The time of the most recent
              modification of one or more attributes of this client application
              installed on the mobile device.


        @raise DeletedObjectException: If the activation code has expired.

        @raise IllegalAccessException: If the individual user or the
            organization that owns this mobile device is not the same as the
            one that has generated the activation code, or if the mobile
            device has been banned by an administrator of the cloud service.

        @raise UndefinedObjectException: If the activation code doesn't exist.
        """
        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            # Check that the client application installed on the mobile device has
            # already shaken hands with the cloud service.
            device_app = self.__get_device_app(
                device_id,
                app_id,
                check_status=False,
                connection=connection)

            # If the client application has been temporarily deactivated, check
            # that the device is linked with either an individual user or an
            # organization.  This is required for security reason, to check that
            # the activation code has been generated by the same individual user or
            # organization.
            if device_app.object_status == ObjectStatus.disabled \
                    and device_app.account_id is None \
                    and device_app.team_id is None:
                raise ValueError(
                    f"The application {device_app.app_id} has been deactivated the device {device_app.device_id}, "
                    "but no individual user nor organization has been specified")

            # Check that the client application has not been banned on this device
            # by an administrator of the cloud service.
            if device_app.object_status == ObjectStatus.deleted:
                raise self.IllegalAccessException(
                    f"The device {device_app.device_id} has been banned by an administrator of the cloud service")

            # Activate the client application installed on the mobile device, and
            # retrieve the updated information about this client application.
            self.__activate_device_app(device_app, activation_code, connection=connection)

            device_app = self.__get_device_app(
                device_app.device_id,
                device_app.app_id,
                check_status=False,
                connection=connection,
                include_security_info=True)

            # Retrieve the information about the organization or the individual user
            # that owns this mobile device.
            if device_app.team_id:
                device_app.team = TeamService().get_team(
                    device_app.team_id,
                    check_status=True,
                    connection=connection,
                    include_contacts=False)
            else:
                device_app.account = AccountService().get_account(
                    device_app.account_id,
                    check_status=True,
                    connection=connection,
                    include_contacts=False)

            del device_app.account_id
            del device_app.team_id

            return device_app

    def generate_activation_code(
            self,
            app_id: uuid.UUID,
            account_id: uuid.UUID,
            activation_code_ttl: int = None,
            connection: RdbmsConnection = None,
            minimum_remaining_ttl_for_reuse: int = None,
            team_id: uuid.UUID = None) -> Any:
        """
        Request a code to activate a client application installed on a mobile
        device.

        The function tries to reuse a previously generated activation code for
        the same individual user or organization.  This allows reusing a same
        code for activating multiple mobile devices without having to generate
        a series of activation codes.


        @param app_id: The identification of the administration application
            that requests to generate a code for activating a client
            application running on a mobile device.

        @param account_id: The identification of the account of the user who
            requests to generate an activation code.

        @param activation_code_ttl: The time-to-live (TTL) in seconds of the
            activation code before it expires.

        @param connection: An existing connection to the database with auto
            commit option enabled.

        @param minimum_remaining_ttl_for_reuse: Minimal remaining time in
            seconds of an activation code to reuse it.  If a previous generated
            activation code is about to expire, the function won't reuse it but
            generated a new one.

        @param team_id: The identification of the organization that is
            responsible for managing this device.  The specified user must be
            an administrator of this organization.


        @return: An object containing the following attributes:

            * `activation_code: ` (required): A code that a device
              needs to send to the cloud service in order to be activated on
              behalf the organization that manages this device.

            * `expiration_time` (required): Time when this activation code is
              going to expire.


        @raise IllegalAccessException: If the specified account is not an
            administrator of the given organization.
        """
        # Validate the specified activation code TTL and remaining TTL for reuse.
        if activation_code_ttl is None:
            activation_code_ttl = self.DEFAULT_DEVICE_ACTIVATION_CODE_TTL
        if minimum_remaining_ttl_for_reuse is None:
            minimum_remaining_ttl_for_reuse = self.DEFAULT_DEVICE_ACTIVATION_CODE_MINIMUM_REMAINING_TTL_FOR_REUSE

        if activation_code_ttl < 0 or minimum_remaining_ttl_for_reuse < 0:
            raise ValueError("Invalid value for activation code TTL and/or remaining TTL for reuse")

        if minimum_remaining_ttl_for_reuse >= activation_code_ttl:
            raise ValueError("The remaining TTL for reuse is above the TTL of the activation code")

        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            # Retrieve the account of the specified user and check its status.
            account = AccountService().get_account(
                account_id,
                check_status=True,
                connection=connection,
                include_contacts=False)

            # If the identification of an organization is specified, check that
            # this organization's status is enabled, and the user is an
            # administrator of this organization.
            if team_id:
                team = TeamService().get_team(
                    team_id,
                    check_status=True,
                    connection=connection,
                    include_extended_info=False,
                    include_contacts=False)

                TeamService().assert_member_role(account_id, team_id, MemberRole.administrator)

            # Try to reuse an activation code that the specified individual user or
            # organization has previously generated.  If none can be reused,
            # generate a new activation code.
            activation_request = self.__reuse_previous_activation_code(
                app_id,
                account_id,
                minimum_remaining_ttl_for_reuse,
                connection=connection,
                team_id=team_id)

            if not activation_request:
                activation_request = self.__generate_new_activation_code(
                    app_id,
                    account_id,
                    activation_code_ttl,
                    connection=connection,
                    team_id=team_id)

            return activation_request

    def report_battery_events(
            self,
            app_id: uuid.UUID,
            device_id: str,
            token: str,
            events: list[BatteryStateChangeEvent],
            account_id: uuid.UUID = None,
            connection: RdbmsConnection = None) -> None:
        """
        Report one or more battery state changes of a device:

        - the device is connected to or disconnected from a power source (such
          as the battery of the vehicle this device is mounted on);

        - the level of the device's battery increases or decreases.

        Only devices equipped with an internal battery can broadcast this
        notification.  A device not equipped with an internal battery is
        indeed less secure as it is not able to send any notification when it
        is disconnected from the power source of the vehicle this device is
        mounted on; the device is immediately shutdown.


        @param app_id: The identification of the client application running on
            the mobile device.

        @param device_id: The identification of the device that reports its
            battery's state changes.

        @param token: A token, which is a "number used once" (nonce) -- also
            known as pseudo-random number -- encrypted with a security key
            shared with the cloud service.  This token is used to verify
            the authentication of the mobile device in order to prevent
            spoofing attack.

        @param events: A list of battery state change events.

        @param account_id: Identification of the account of a user who has
            logged in the mobile device.

        @param connection: An existing connection to the device database, with
            the option `auto_commit` enabled.


        @raise DeletedObjectException: If the mobile device has been banned by
            an administrator of the cloud service.

        @raise DisabledObjectException: If the mobile device has been disabled
            by the individual user or an administrator of the organization
            that owns this device.

        @raise IllegalAccessException: If the given token has not been
            successfully verified, meaning that someone or a program tries to
            masquerade as the specified device (spoofing attack).

        @raise InvalidOperationException: If the mobile device has not been
            activated yet.

        @raise UndefinedObjectException: If the specified identification
            doesn't match a mobile device registered to the cloud service.
        """
        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            # Check whether the client application has been activated on this mobile
            # device.
            device_app = self.__get_device_app(
                device_id,
                app_id,
                check_status=True,
                connection=connection,
                include_security_info=True)  # MAC address & security key

            # Verify that the token has been encrypted with the security key shared
            # with the client application.
            self.verify_token(device_app, token)

            # Remove the battery state update events that have not been already
            # stored.
            events = self.__filter_out_duplicated_battery_events(
                device_app,
                events,
                connection=connection)

            # Store the battery state update events to the device database.
            if not events:
                self.log_warning(
                    f"Ignore battery state update events, sent by the application {app_id} "
                    f"on the device {device_id}, that have been already stored"
                )
                return

            self.__insert_battery_events(
                device_app,
                events,
                account_id=account_id,
                connection=connection)

            # @todo: Update the memory cache with the last battery level and state.

    def report_keepalive_event(
            self,
            app_id: uuid.UUID,
            device_id: str,
            token: str,
            event_time: ISO8601DateTime,
            account_id: uuid.UUID = None,
            connection: RdbmsConnection = None,
            location: GeoPoint = None,
            network_type: str = None) -> None:
        """
        Report a keep-alive (KA) event from a client application running on
        a mobile device.


        @param app_id: The identification of the client application running on
            the mobile device.

        @param device_id: The identification of the mobile device.

        @param token: A token, which is a "number used once" (nonce) -- also
            known as pseudo-random number -- encrypted with a security key
            shared with the cloud service.  This token is used to verify
            the authentication of the mobile device in order to prevent
            spoofing attack.

        @param event_time: The time when the mobile device sent the keep-alive
            message.

        @param account_id: The identification of the account of the user who
            is currently logged in the client application.

        @param battery_level: The current level in percentage of the battery
            of the mobile device.

        @param connection: An existing connection to the device database, with
            the option `auto_commit` enabled.

        @param is_battery_plugged: Indicate if the battery of the mobile
            device is plugged in a power source .

        @param location: The last known location of the mobile device.

        @param mobile_exchanged_byte_count: A string representation of a tuple
            of the form `received:transmitted` where:

            - `received`: Number of bytes received across mobile networks since
              the mobile device boot.

            - `transmitted`: Number of bytes transmitted across mobile networks
              since the mobile device boot.

        @param network_type: A string representation of a tuple of the form
            `operator:type:subtype` where:

            - `operator: string` (required): The identifier of the mobile phone
              operator, composed of the Mobile Country Code (MCC) and the Mobile
              Network Code (MNC) of this carrier.

            - `type: string` (required): A human-readable name that describes the
              type of the current network connection, for example `wifi`, `mobile`,
              `unknown`.

            - `subtype: string` (optional: A human-readable name that describes the
              subtype of this network when applicable, such as the radio technology
              currently in use on the device for data transmission.  Network of type
              `wifi` has no subtype.  Network of type `mobile` can have a subtype
              such as `egde`, `gprs`, `hsdpa`, `hspa`, `hspa+`, `umts`, etc.


        @raise DeletedObjectException: If the mobile device has been banned by
            an administrator of the cloud service.

        @raise DisabledObjectException: If the mobile device has been disabled
            by the individual user or an administrator of the organization
            that owns this device.

        @raise IllegalAccessException: If the given token has not been
            successfully verified, meaning that someone or a program tries to
            masquerade as the specified device (spoofing attack).

        @raise InvalidOperationException: If the mobile device has not been
            activated yet.

        @raise UndefinedObjectException: If the specified identification
            doesn't match a mobile device registered to the cloud service.
        """
        current_time = ISO8601DateTime.now()
        if event_time > current_time + datetime.timedelta(seconds=self.DEFAULT_DEVICE_TIME_APPROXIMATION):
            self.log_warning(
                f"Ignore keep-alive event sent in the future (+{event_time - current_time})"
                f"by the application {app_id} on the device {device_id}"
            )
            return

        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            # Check whether the client application has been activated on this mobile
            # device.
            device_app = self.__get_device_app(
                device_id,
                app_id,
                check_status=True,
                connection=connection,
                include_security_info=True)  # MAC address & security key

            # Verify that the token has been encrypted with the security key shared
            # with the client application.
            self.verify_token(device_app, token)

            # Check that this keep-alive event is not prior to the last keep-alive
            # event sent by this client application running on the mobile device.
            last_keepalive_event = self.__get_last_keepalive_event(
                device_id,
                app_id,
                connection=connection)

            if last_keepalive_event and last_keepalive_event.event_time >= event_time:
                self.log_warning(
                    f"Ignore keep-alive event prior to the last keep-alive event ({last_keepalive_event.event_id}) "
                    f"received from the client application {app_id} on the device {device_id}"
                )
                return

            # Insert the keep-alive event to the database.
            keepalive_event = self.__insert_keepalive_event(
                device_app,
                event_time,
                account_id=account_id,
                connection=connection,
                location=location,
                network_type=network_type)

            # @todo: We may want to send a notification to a message bus to inform
            #     other software components about this event, providing the
            #     identification of this event and the time when it occurs.

    def report_location_events(
            self,
            app_id: uuid.UUID,
            device_id: str,
            token: str,
            events: list[LocationUpdate],
            account_id: uuid.UUID = None,
            connection: RdbmsConnection = None) -> None:
        """
        Report one or more location updates of a mobile device.


        @param app_id: The identification of the client application running on
            the mobile device.

        @param device_id: The identification of the device that reports these
            location updates.

        @param token: A token, which is a "number used once" (nonce) -- also
            known as pseudo-random number -- encrypted with a security key
            shared with the cloud service.  This token is used to verify
            the authentication of the mobile device in order to prevent
            spoofing attack.

        @param events: A list of location updates.

        @param account_id: Identification of the account of a user who has
            logged in the mobile device.

        @param connection: An existing connection to the device database, with
            the option `auto_commit` enabled.


        @raise DeletedObjectException: If the mobile device has been banned by
            an administrator of the cloud service.

        @raise DisabledObjectException: If the mobile device has been disabled
            by the individual user or an administrator of the organization
            that owns this device.

        @raise IllegalAccessException: If the given token has not been
            successfully verified, meaning that someone or a program tries to
            masquerade as the specified device (spoofing attack).

        @raise InvalidOperationException: If the mobile device has not been
            activated yet.

        @raise UndefinedObjectException: If the specified identification
            doesn't match a mobile device registered to the cloud service.
        """
        with self.acquire_rdbms_connection(auto_commit=True, connection=connection) as connection:
            # Check whether the client application has been activated on this mobile
            # device.
            device_app = self.__get_device_app(
                device_id,
                app_id,
                check_status=True,
                connection=connection,
                include_security_info=True)  # MAC address & security key

            # Verify that the token has been encrypted with the security key shared
            # with the client application.
            self.verify_token(device_app, token)

            # Remove the battery state update events that have not been already
            # stored.
            events = self.__filter_out_duplicated_location_events(
                device_app,
                events,
                connection=connection)

            # Store the new location events.
            if events:
                self.__insert_location_events(
                    device_app,
                    events,
                    account_id=account_id,
                    connection=connection)

                self.__update_device_last_known_location(
                    device_id,
                    events,
                    connection=connection)

    def shake_hands(
            self,
            device_id: str,
            app_id: uuid.UUID,
            agent_application: ClientApplication,
            ip_address: str,
            location: GeoPoint = None,
            mac_address: str = None,
            serial_number: str = None,
            connection: RdbmsConnection = None) -> Any:
        """
        Establish the connection between a device and the cloud service
        before normal communication begins.

        If the device is not registered, the function registers the device
        with its system information and the version of the information about
        the client application that initiates this call.  The device's status
        remains pending until an individual user or an administrator of an
        organization activates the device.

        If the device was already registered, but the client application that
        initiates this call is not registered yet, the function registers the
        information about the client application. The application's status
        remains pending until the individual user owning this device or an
        administrator of the organization managing this device activates the
        application.


        @note: If the mobile device shakes hands several times with different
            information about the operating system and the client application,
            the function keeps the information initially sent by the mobile
            device with the first handshake.


        @param device_id: The identification of the device.

        @param app_id: The identification of the client application that
            initiates this call.

        @param agent_application: The information about the client application
            and the device that initiates this call.

        @param ip_address: A dotted-decimal notation of an IPv4 address,
            consisting of four decimal numbers, each ranging from ``0`` to
            ``255``, separated by dots.

        @param location: The geographic location of the device when it sends
            a handshake request.

        @param mac_address: The Media Access Control (MAC) address of the
            mobile device.  A MAC address is a unique identifier assigned to a
            network interface for communications at the data link layer of a
            network segment.

        @param serial_number:  The hardware serial number of the mobile device.
            It corresponds to a unique number assigned by the manufacturer to
            help identify an individual device.

            Because the operating system of the mobile device may restrict
            access to persistent device identifiers, the serial number of the
            mobile device may not be provided.

            Serial number is case-sensitive.

        @param connection: An existing connection to the device database, with
            auto-commit option enabled.


        @return: An object containing the following attributes:

            - `account_id: uuid.UUID` (optional): The identification of the account
              of the user who has activated the device.  This is either an
              individual user (the owner of the device), either an administrator of
              the organization that manages this device.

            - `device_id: str` (required): The identification of the device.

            - `object_status: ObjectStatus` (required): The current status of the
              device:

              - {@link ObjectStatus.deleted}: The device has been banned by an
                administrator of the cloud service.

              - {@link ObjectStatus.disabled}: The device has been suspended by the
                individual user who has activated this device or an administrator of
                the organization that manages this device.

              - {@link ObjectStatus.enabled}: The device is activated and is expected
                to operate properly.

              - {@link ObjectStatus.pending}: The device has not been activated yet.

            - `team: any` (optional): The organization that has activated this
              mobile device.

              - `name: string` (required): The name of the organization.

              - `picture_id: uuid` (required): The identification of the picture
                representing the organization's logo.

              - `picture_url: string` (required): The Uniform Resource Locator (URL)
                of picture representing the organization's logo.

              - `team_id` (required): The identification of the organization on behalf
                of which an administrator has activated the device.

            - `update_time: ISO8601DateTime`: The time of the most recent
              modification of one or more attributes of the device.


        @raise DeletedObjectException: If the team that is managing the device
            has been deleted, or if the account of the individual user who has
            activated this device has been deleted.

        @raise DisabledObjectException: If the team that is managing the device
            has been disabled, or if the account of the individual user who has
            activated this device has been disabled.
        """
        try:
            device = self.__get_device_by_id(device_id, connection=connection)

        except self.UndefinedObjectException:
            device = self.__register_device(
                device_id,
                app_id,
                agent_application,
                ip_address,
                connection=connection,
                location=location,
                mac_address=mac_address,
                serial_number=serial_number
            )

        return device

    def verify_token(
            self,
            device_app: Any,
            token: str) -> None:
        """
        Verify that a token has been generated and encrypted by the specified
        client application installed on a given mobile device.

        A token is composed of two parts concatenated together:

        - A randomly number, also known as a nonce.

        - The nonce encrypted with the security key shared between the server
          platform and the application, and converted to a BASE64 string.


        @param device_app: An object representing a client application
            installed on a mobile device.  This object must contain the
            following attributes:

            - `app_id: uuid.UUID`: The identification of the client application
              that has generated and encrypted the token.

            - `device_id: str`: The identification of the device that sends the
              token.

            - `mac_address: str`: The Media Access Control (MAC) address of the
              device.

            - `security_key: str`: The security key generated by the cloud service
              when the mobile device has been activated, and shared with the client
              application.

        @param token: A token, which is a "number used once" (nonce) -- also
            known as pseudo-random number -- encrypted with a security key
            generated by the cloud service and shared with the device.  A token
            is used to verify the authentication of the device in order to
            prevent spoofing attack.


        @raise IllegalAccessException: If the given token has not been
            successfully verified, meaning that someone or a program tries to
            masquerade as the specified device (spoofing attack).
        """
        encryption_key = f'{device_app.mac_address}{device_app.security_key}'

        # Retrieve the nonce and the encrypted version of the nonce.
        nonce = token[0:self.DEFAULT_TOKEN_NONCE_LENGTH]
        encrypted_nonce = token[self.DEFAULT_TOKEN_NONCE_LENGTH:]

        # Check that the nonce has been correctly encrypted with the security
        # key of the client application installed on the device.
        hashed_message = hmac.new(
            encryption_key.encode(),
            msg=nonce.encode(),
            digestmod=self.DEFAULT_CRYPTOGRAPHIC_HASH_FUNCTION)

        base64encoded_digest = base64 \
            .b64encode(hashed_message.digest()) \
            .decode()[:-1]  # Remove final "=" character

        if encrypted_nonce != base64encoded_digest:
            self.log_warning(
                f"Invalid token encrypted by the application {device_app.app_id} on the device {device_app.device_id}")

            # Display the encrypted nonce that the client application provided and
            # the encrypted nonce that was expected (this debugging information is
            # not logged on production environment).
            if settings.ENVIRONMENT_STAGE != EnvironmentStage.prod:
                self.log_warning(f"- Encrypted nonce received: {encrypted_nonce}")
                self.log_warning(f"- Encrypted nonce expected: {base64encoded_digest}")

            # @todo: TO BE REACTIVATED ASAP
            # if settings.ENVIRONMENT_STAGE != EnvironmentStage.dev:
            #     raise self.IllegalAccessException("Invalid encrypted token")






            # Send a notification to indicate that the battery power increases
            # (charging) or decreases (discharging) by a quarter (25%), or when
            # the battery power reaches a critical level (1%).
#             if device.battery_level and battery_level:
#                 previous_battery_status_index = int(device.battery_level * 100) / 25
#                 current_battery_status_index = int(battery_level * 100) / 25
#
#                 if current_battery_status_index != previous_battery_status_index or \
#                    battery_level == 0.01 and device.battery_level > 0.01:
#                     NotificationService().send_notification(app_id,
#                             XeberusService.XeberusNotification.on_battery_state_changed,
#                             recipient_ids=device.account_id,
#                             team_id=device.team_id,
#                             lifespan=XeberusService.NOTIFICATION_LIFESPAN_BATTERY_STATE_CHANGED,
#                             notification_mode=NotificationService.NotificationMode.push,
#                             package=XeberusService.XEBERUS_MOBILE_APPLICATION_PACKAGE,
#                             payload={
#                                 "battery_level": battery_level,
#                                 "device_id": device_id,
#                                 "event_time": date_util.ISO8601DateTime.now(),
#                                 "event_type": BatteryStateChange.BatteryStateEventType.battery_level_changed,
#                                 "is_battery_plugged": is_battery_plugged })
#