"""

    AUTHOR:

        Khushal P Soonderji

    DATE:

        Thursday, 12th Dec., 2024

    OBJECTIVE:

        To handle all auth-tokens from one place.

    REFERENCES:

        N/A

    DOWNLOADS:

        N/A

"""

#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                   IMPORT                                                      ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")

# My async utils:
from utils_v2.string import json
from utils_v2.date_time import date_time
from utils_v2.database.async_mysql_v2 import AsyncMySQL
from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache

# Base model:
from controllers_v2.core.base import CoreBaseModel

# Data models:
from models.core.auth_token import CoreAuthTokenModel

# To work with datatypes:
from typing import List

# To make API calls:
import httpx

# To work with MongoDB:
from bson import ObjectId


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                           MACROS / ONE-TIME INIT                                              ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                VARIABLES                                                      ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                  FUNCTIONS                                                    ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                   CLASSES                                                     ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


class CoreAuthTokenController(CoreBaseModel):

    # ┏┓┓      ┓┏
    # ┃ ┃┏┓┏┏  ┃┃┏┓┏┓┏
    # ┗┛┗┗┻┛┛  ┗┛┗┻┛ ┛

    # For MongoDB:
    AUTH_COLLECTION = "_authTokens"

    # Other variables:
    _service_type = None
    _client = None

    # ┏┓
    # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓
    # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛

    def __init__(
            self,
            cache: AsyncRedisCache = None,
            http_client: httpx.AsyncClient = None,
            alert_url: str = None,
            base_filter: dict = None,
            debug = True,
            debug_prefix = "Auth Token (C) | ",
            debug_only_errors = True
    ):

        """
        This is the core controller for all authorization and token activities.
        :param cache: The object to use for caching results from database calls.
        :param debug: Whether, or not, you would like to print debugging messages:
        :param debug_prefix: The prefix to print with the debugging messages.
        :param debug_only_errors: Whether you would like to print only error messages or all messages.
        :return: None.
        """

        # Accept the base filter:
        self._base_filter = base_filter or {}

        # Invoke the parent's constructor:
        CoreBaseModel.__init__(
            self,
            cache = cache,
            http_client = http_client,
            alert_url = alert_url,
            debug = debug,
            debug_prefix = debug_prefix,
            debug_only_errors = debug_only_errors
        )

    # ┏┓┳┓┳┳┳┓      ┏┓                ┓  ┳┳   ┓
    # ┃ ┣┫┃┃┃┃  ━━  ┃ ┏┓┏┓┏┓╋┏┓  ┏┓┏┓┏┫  ┃┃┏┓┏┫┏┓╋┏┓
    # ┗┛┛┗┗┛┻┛      ┗┛┛ ┗ ┗┻┗┗   ┗┻┛┗┗┻  ┗┛┣┛┗┻┗┻┗┗
    #                                      ┛

    async def generate_token_key(
            self,
            sql_conn: AsyncMySQL,
            mongo_data_conn: AsyncMongo,
            auth_token: CoreAuthTokenModel,
            token_notes: dict,
            display_name: str = None,
            display_picture: str = None,
            session_token: str = None,
    ) -> ObjectId:

        """
        Stores params from the session info and gives an identifier to use in the authorization URL. Use this when the
        user requests an authorization URL to link your service to another service (like GMail).
        :param sql_conn: The database connection (MariaDB) to use to perform the action.
        :param mongo_data_conn: The database connection (MongoDB) to use to perform the action.
        :param auth_token: An instance of the core auth-token model that holds data in the database.
        :param token_notes: Any notes to feed into MariaDB with the token identifier.
        :param display_name: The name of the user to user as their display name.
        :param display_picture: The URL at which you will find a display picture of the user.
        :param session_token: The session token of the user who requested this service.
        :return: An ObjectId to later store the granted tokens.
        """

        # Safety check for consistency:
        if self._service_type is not None and self._service_type != auth_token.serviceType:
            raise ValueError(f"Expected service type '{self._service_type}', got '{auth_token.serviceType}'")
        if self._client is not None and self._client != auth_token.client:
            raise ValueError(f"Expected service type '{self._client}', got '{auth_token.client}'")

        # Note down the timestamp at which this event occurred:
        request_ts = date_time.get_current_utc_date_time(as_string = False)

        # Get the identifier from the database if it already exists, else create one.
        # BE CAREFUL WITH THE KEYS HERE, THEY SHOULD MATCH THE FIELDS OF THE CORE AUTH-TOKEN MODEL:
        mongo_json = await mongo_data_conn.find_one_and_update(
            collection = self.AUTH_COLLECTION,
            filter = mongo_data_conn.dict_to_dot_notation({
                "serviceType": auth_token.serviceType,
                "user": {
                    "entityId": auth_token.user.entityId,
                    "billingAccountId": auth_token.user.billingAccountId
                },
                "clientUserId": auth_token.clientUserId
            }),
            update = {
                "$set": {
                    "lastRequestTs": auth_token.lastRequestTs,
                    "status": auth_token.status,
                    "syncFreq": auth_token.syncFreq
                },
                "$setOnInsert": {
                    "key": auth_token.key,
                    "serviceType": auth_token.serviceType,
                    "client": auth_token.client,
                    "authType": auth_token.authType,
                    "user": auth_token.user.model_dump(),
                    "clientUserId": auth_token.clientUserId,
                    "auth": auth_token.auth,
                    "token": auth_token.token,
                    "firstRefreshTs": auth_token.firstRefreshTs,
                    "lastRefreshTs": auth_token.lastRefreshTs,
                    "firstRequestTs": auth_token.firstRequestTs or request_ts,
                }
            },
            projection = {
                "_id": True,
                "key": True
            },
            upsert = True,
            return_updated = True
        )

        # Tell MariaDB that an authorization request was initiated:
        db_json = {}
        if mongo_json is not None:
            db_json = await self.call_procedure(
                sql_conn = sql_conn,
                proc_name = "entity_integration_save",
                proc_args = (
                    auth_token.user.entityId,  # ..................................... 'p_entity_id'
                    auth_token.client,  # ............................................ 'p_provider'
                    auth_token.status,  # ............................................ 'p_current_status'
                    "Auth Requested",  # ............................................. 'p_last_action'
                    display_name,  # ................................................. 'p_display_name'
                    display_picture,  # .............................................. 'p_display_picture'
                    mongo_json["key"],  # ............................................ 'p_token_id'
                    json.to_string(python_data = token_notes, no_space = True),  # ... 'p_notes'
                    auth_token.user.userId  # ........................................ 'p_created_by'
                ),
                session_token = session_token
            )

        # Done here:
        return mongo_json["key"] if mongo_json and db_json.get("status") == 1 else None

    async def set_token(
            self,
            sql_conn: AsyncMySQL,
            mongo_data_conn: AsyncMongo,
            token_key: ObjectId | str,
            auth_token: CoreAuthTokenModel,
            token_notes: dict,
            display_name: str = None,
            display_picture: str = None,
            session_token: str = None
    ) -> bool:

        """
        This method is to be called when the end user authorizes your service to connect to his third-party account. For
        example, when the end user allows you to access his GMail account. USE THIS FOR UPDATING (REFRESHING) TOKENS
        ALSO.
        :param sql_conn: The database connection (MariaDB) to use to perform the action.
        :param mongo_data_conn: The database connection (MongoDB) to use to perform the action.
        :param token_key: The identifier granted by the 'generate_token_key' method.
        :param auth_token: The actual auth/token data to be saved to the database.
        :param token_notes: Any notes to feed into MariaDB with the token identifier.
        :param display_name: The name of the user to user as their display name.
        :param display_picture: The URL at which you will find a display picture of the user.
        :param session_token: The session token of the user who requested this service.
        :return: True if saved, False if failed.
        """

        # Safety check for consistency:
        if self._service_type is not None and self._service_type != auth_token.serviceType:
            raise ValueError(f"Expected service type '{self._service_type}', got '{auth_token.serviceType}'")
        if self._client is not None and self._client != auth_token.client:
            raise ValueError(f"Expected service type '{self._client}', got '{auth_token.client}'")

        # Start by assuming failure:
        token_saved = False

        # Note down the timestamp at which this event occurred:
        request_ts = date_time.get_current_utc_date_time(as_string = False)

        # Save the token to MongoDB.
        # BE CAREFUL WITH THE KEYS HERE, THEY SHOULD MATCH THE FIELDS OF THE CORE AUTH-TOKEN MODEL:
        mongo_json = await mongo_data_conn.find_one_and_update(
            collection = self.AUTH_COLLECTION,
            filter = mongo_data_conn.dict_to_dot_notation({
                "key": ObjectId(token_key),
                "clientUserId": auth_token.clientUserId
            }),
            update = [{
                "$set": {
                    "auth": auth_token.auth,
                    "token": auth_token.token,
                    "status": auth_token.status,
                    "lastRefreshTs": request_ts,
                    "firstRefreshTs": {
                        "$cond": {
                            "if": {
                                "$or": [
                                    {"$eq": ["$firstRefreshTs", None]},
                                    {"$eq": [{"$type": "$firstRefreshTs"}, "missing"]}
                                ]
                            },
                            "then": request_ts,
                            "else": "$firstRefreshTs"
                        }
                    }
                }
            }],
            projection = {"token": False},
            return_updated = True,
            upsert = False
        )

        # Tell MariaDB that the token was saved:
        if mongo_json is not None:
            db_json = await self.call_procedure(
                sql_conn = sql_conn,
                proc_name = "entity_integration_save",
                proc_args = (
                    mongo_json["user"]["entityId"],  # ............................... 'p_entity_id'
                    mongo_json["client"],  # ......................................... 'p_provider'
                    auth_token.status,  # ............................................ 'p_current_status'
                    "Auth Granted",  # ............................................... 'p_last_action'
                    display_name,  # ................................................. 'p_display_name'
                    display_picture,  # .............................................. 'p_display_picture'
                    token_key,  # .................................................... 'p_token_id'
                    json.to_string(python_data = token_notes, no_space = True),  # ... 'p_notes'
                    auth_token.user.userId  # ........................................ 'p_created_by'
                ),
                session_token = session_token
            )
            if db_json["status"] == 1: token_saved = True

        # Done here:
        return token_saved

    async def set_token_direct(
            self,
            sql_conn: AsyncMySQL,
            mongo_data_conn: AsyncMongo,
            auth_token: CoreAuthTokenModel,
            token_notes: dict,
            display_name: str = None,
            display_picture: str = None,
            session_token: str = None
    ) -> bool:

        """
        Some authorizations don't need two steps, but our core system works on the 2-step approach that was developed to
        work with Google's GMail OAuth2.0 mechanism.
        :param sql_conn: The database connection (MariaDB) to use to perform the action.
        :param mongo_data_conn: The database connection (MongoDB) to use to perform the action.
        :param auth_token: The actual auth/token data to be saved to the database.
        :param token_notes: Any notes to feed into MariaDB with the token identifier.
        :param display_name: The name of the user to user as their display name.
        :param display_picture: The URL at which you will find a display picture of the user.
        :param session_token: The session token of the user who requested this service.
        :return: True if saved, False if failed.
        """

        # Start by assuming failure:
        success = False

        # Get a token id (and receive its key):
        token_key = await self.generate_token_key(
            sql_conn = sql_conn,
            mongo_data_conn = mongo_data_conn,
            auth_token = auth_token,
            token_notes = token_notes,
            display_name = display_name,
            display_picture = display_picture,
            session_token = session_token
        )

        # Immediately save the details against that token id:
        success = await self.set_token(
            sql_conn = sql_conn,
            mongo_data_conn = mongo_data_conn,
            token_key = token_key,
            auth_token = auth_token,
            token_notes = token_notes,
            display_name = display_name,
            display_picture = display_picture,
            session_token = session_token
        )

        # Done here:
        return success

    # ┏┓┳┓┳┳┳┓      ┳┓     •
    # ┃ ┣┫┃┃┃┃  ━━  ┣┫┏┓╋┏┓┓┏┓┓┏┏┓
    # ┗┛┛┗┗┛┻┛      ┛┗┗ ┗┛ ┗┗ ┗┛┗

    async def get_token_from_id(
            self,
            mongo_data_conn: AsyncMongo,
            token_id: ObjectId | str = None,
            additional_filter: dict = None
    ) -> CoreAuthTokenModel | None:

        """
        To retrieve stored tokens from the database. One token at a time.
        :param mongo_data_conn: The database connection (MongoDB) to use to perform the action.
        :param token_id: The identifier of the document that holds the token's details.
        :param additional_filter: Any addition filters to use.
        :return: The retrieved record that has the token, and information about the service and client if found, else
            None when there is no matching record.
        """

        # Prepare the filter:
        filter_json = {"_id": ObjectId(token_id)}
        if self._base_filter:
            for k, v in self._base_filter.items(): filter_json[k] = v
        if additional_filter:
            for k, v in additional_filter.items(): filter_json[k] = v

        # If there is some filtering possible, we fetch the token:
        token = await mongo_data_conn.find_one(
            collection = self.AUTH_COLLECTION,
            filter = filter_json
        )

        # Done here:
        return CoreAuthTokenModel(**token) if token else None

    async def get_token_from_key(
            self,
            mongo_data_conn: AsyncMongo,
            token_key: ObjectId | str = None,
            additional_filter: dict = None
    ) -> CoreAuthTokenModel | None:

        """
        To retrieve stored tokens from the database. One token at a time.
        :param mongo_data_conn: The database connection (MongoDB) to use to perform the action.
        :param token_key: The identifier granted by the 'generate_token_key' method.
        :param additional_filter: Any addition filters to use.
        :return: The retrieved record that has the token, and information about the service and client if found, else
            None when there is no matching record.
        """

        # Prepare the filter:
        filter_json = {"key": ObjectId(token_key)}
        if self._base_filter:
            for k, v in self._base_filter.items(): filter_json[k] = v
        if additional_filter:
            for k, v in additional_filter.items(): filter_json[k] = v

        # If there is some filtering possible, we fetch the token:
        token = await mongo_data_conn.find_one(
            collection = self.AUTH_COLLECTION,
            filter = filter_json
        )

        # Done here:
        return CoreAuthTokenModel(**token) if token else None

    async def get_token_from_filter(
            self,
            mongo_data_conn: AsyncMongo,
            filter_json: dict
    ) -> CoreAuthTokenModel | None:

        """
        To retrieve stored tokens from the database. One token at a time.
        :param mongo_data_conn: The database connection (MongoDB) to use to perform the action.
        :param filter_json: The filter conditions to use.
        :return: The retrieved record that has the token, and information about the service and client if found, else
            None when there is no matching record.
        """

        # Prepare the filter:
        if self._base_filter:
            for k, v in self._base_filter.items(): filter_json[k] = v

        # If there is some filtering possible, we fetch the token:
        token = await mongo_data_conn.find_one(
            collection = self.AUTH_COLLECTION,
            filter = filter_json
        )

        # Done here:
        return CoreAuthTokenModel(**token) if token else None

    async def get_tokens_from_ids(
            self,
            mongo_data_conn: AsyncMongo,
            token_ids: List[ObjectId | str] = None,
            limit: int = 100,
            additional_filter: dict = None
    ) -> List[CoreAuthTokenModel]:

        """
        To retrieve stored tokens from the database. Multiple tokens at a time.
        :param mongo_data_conn: The database connection (MongoDB) to use to perform the action.
        :param token_ids: The identifier of the document that holds the token's details.
        :param limit: The max. no. of records to pick.
        :param additional_filter: Any addition filters to use.
        :return: The retrieved record that has the token, and information about the service and client if found, else
            None when there is no matching record.
        """

        # Prepare the filter:
        filter_json = {"_id": {"$in": [ObjectId(k) for k in token_ids]}}
        if self._base_filter:
            for k, v in self._base_filter.items(): filter_json[k] = v
        if additional_filter:
            for k, v in additional_filter.items(): filter_json[k] = v

        # If there is some filtering possible, we fetch the token:
        tokens = await mongo_data_conn.find_many(
            collection = self.AUTH_COLLECTION,
            filter = filter_json,
            limit = limit
        )

        # Done here:
        return [CoreAuthTokenModel(**token) for token in tokens]

    async def get_tokens_from_keys(
            self,
            mongo_data_conn: AsyncMongo,
            token_keys: List[ObjectId | str] = None,
            limit: int = 100,
            additional_filter: dict = None
    ) -> List[CoreAuthTokenModel]:

        """
        To retrieve stored tokens from the database. Multiple tokens at a time.
        :param mongo_data_conn: The database connection (MongoDB) to use to perform the action.
        :param token_keys: the identifiers granted by the 'generate_token_key' method.
        :param limit: The max. no. of records to pick.
        :param additional_filter: Any addition filters to use.
        :return: The retrieved record that has the token, and information about the service and client if found, else
            None when there is no matching record.
        """

        # Prepare the filter:
        filter_json = {"key": {"$in": [ObjectId(k) for k in token_keys]}}
        if self._base_filter:
            for k, v in self._base_filter.items(): filter_json[k] = v
        if additional_filter:
            for k, v in additional_filter.items(): filter_json[k] = v

        # If there is some filtering possible, we fetch the token:
        tokens = await mongo_data_conn.find_many(
            collection = self.AUTH_COLLECTION,
            filter = filter_json,
            limit = limit
        )

        # Done here:
        return [CoreAuthTokenModel(**token) for token in tokens]

    # ┏┓┳┓┳┳┳┓      ┳┓  ┓
    # ┃ ┣┫┃┃┃┃  ━━  ┃┃┏┓┃┏┓╋┏┓
    # ┗┛┛┗┗┛┻┛      ┻┛┗ ┗┗ ┗┗

    pass


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                               MAIN PROGRAM                                                    ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


if __name__ == "__main__":

    pass
