"""

    AUTHOR:

        Khushal P Soonderji

    DATE:

        Friday, 13th Dec., 2024

    OBJECTIVE:

        To handle all SMS related behaviour 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("..")

# For Quart:
from quart import current_app

# 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, AsyncMongoStorage

# Data models:
from models.core.user import CoreUserInfoModel
from models.core.auth_token import CoreAuthTokenModel
from models.core.payment import CorePaymentModel, PaymentEvent, CustomerDetails
from models.api.finstitutions.payments.request import (
    PGPaymentRequestData,
    PaymentRequestOneResult,
)

# Payment Clients:
from utils_v2.payments.safaricom.models.auth import MPesaExpressAuthorization
from utils_v2.payments.safaricom.controllers.m_pesa_express import SafaricomMPesaExpress

# To work with MongoDB:
from bson import ObjectId
from pymongo import InsertOne

# To work with datatypes:
from typing import Literal, List, Dict, Any, Union

# To make API calls:
import httpx

# For asynchronous activities:
import asyncio


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


# --- Nothing Yet


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


# --- Nothing Yet


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


# --- Nothing Yet


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


class PaymentController:

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

    pass

    # ┓┏  ┓
    # ┣┫┏┓┃┏┓┏┓┏┓┏
    # ┛┗┗ ┗┣┛┗ ┛ ┛
    #      ┛

    pass

    # ┏┓   ┓
    # ┣┫┓┏╋┣┓
    # ┛┗┗┻┗┛┗

    @staticmethod
    async def set_token_direct(
            db_conn: AsyncMySQL,
            mongo_conn: AsyncMongo,
            auth_token: CoreAuthTokenModel,
            session_token: str = None
    ) -> bool:

        # Start by assuming failure:
        success = False

        # Get a token id:
        token_key = await current_app.core_auth_token_controller.get_token_key(
           db_conn = db_conn,
           mongo_conn = mongo_conn,
           auth_token = auth_token,
           token_notes = {},
           session_token = session_token
       )

        # Immediately save the details against that token id:
        success = await current_app.core_auth_token_controller.set_token(
            db_conn = db_conn,
            mongo_conn = mongo_conn,
            token_key = token_key,
            auth_token = auth_token,
            token_notes = {},
            session_token = session_token
        )

        # Done here:
        return success

    @staticmethod
    async def get_token(
            mongo_conn: AsyncMongo,
            token_key: ObjectId | str = None,
    ) -> CoreAuthTokenModel | None:

        # Simply call the core model:
        return await current_app.core_auth_token_controller.get_token(
            mongo_conn = mongo_conn,
            token_key = token_key
        )

    @staticmethod
    async def get_tokens(
            mongo_conn: AsyncMongo,
            token_keys: ObjectId | str = None,
    ) -> List[CoreAuthTokenModel] | None:

        # Simply call the core model:
        return await current_app.core_auth_token_controller.get_tokens(
            mongo_conn = mongo_conn,
            token_keys = token_keys
        )

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

    async def __request_from_safaricom_m_pesa_express(
            self,
            mongo_conn: AsyncMongo,
            http_client: httpx.AsyncClient,
            auth_token: CoreAuthTokenModel,
            payment_request: PGPaymentRequestData,
            payment_id: ObjectId,
            callback_url: str
    ) -> PaymentRequestOneResult:

        # Start by assuming failure:
        result = PaymentRequestOneResult()

        # Create the client:
        client = SafaricomMPesaExpress(
            auth = MPesaExpressAuthorization(
                consumerKey = auth_token.auth["consumerKey"],
                consumerSecret = auth_token.auth["consumerSecret"],
                businessShortCode = auth_token.auth["businessShortCode"],
                appPasskey = auth_token.auth["appPasskey"]
            ),
            http_client = http_client
        )

        # Make the payment request:
        client_response = await client.request_payment(
            amount = payment_request.amount,
            party_a = payment_request.customerNo,
            type = "CustomerPayBillOnline",
            reference = payment_request.reference,
            description = payment_request.description,
            callback_url = callback_url,
            payer_no = payment_request.payerNo,
            party_b = auth_token.auth["businessShortCode"]
        )

        # Add this event to the payment's document:
        event_note_success = await self.add_event_by_payment_id(
            mongo_conn = mongo_conn,
            payment_id = payment_id,
            event = PaymentEvent(
                paymentStatus = "initiated" if client_response.success else "initFailed",
                initByPG = False,
                httpCode = client_response.httpCode,
                headers = await client_response.get_headers(),
                payload = await client_response.get_json()
            ),
            client_reference_id = client_response.referenceId
        )

        # Done here:
        result.success = client_response.success and event_note_success
        result.message = "; ".join([
            "payment requested successfully" if client_response.success
            else f"payment request failed (PG: {client_response.message})",
            "event noted successfully" if event_note_success
            else "event noting failed",
        ])
        return result

    async def request_payment(
            self,
            mongo_conn: AsyncMongo,
            http_client: httpx.AsyncClient,
            auth_token: CoreAuthTokenModel,
            user_info: CoreUserInfoModel,
            payment_request: PGPaymentRequestData,
    ) -> PaymentRequestOneResult:

        # Start by assuming failure:
        request_result = PaymentRequestOneResult()

        # Create a basic database entry to note down the intended payment:
        payment_id = await current_app.core_payment_controller.init(
            mongo_conn = mongo_conn,
            payment = CorePaymentModel(
                user = user_info,
                customer = CustomerDetails(
                    name = payment_request.customerName,
                    contactNo = payment_request.customerNo,
                    payerNo = payment_request.payerNo,
                    email = payment_request.email
                ),
                lastPaymentStatus = "queued",
                tokenId = auth_token.authTokenId,
                amount = payment_request.amount,
                currencyCode = payment_request.currencyCode,
                metadata = payment_request.metadata.model_dump(),
                tags = ["payment", "safaricom", "mPesaExpress", "kenya"],
                serviceType = "paymentGateway",
                client = auth_token.client,
                clientPaymentReferenceId = None,
                events = []
            )
        )
        if payment_id is None:
            request_result.message = "failed to queue the payment"
            return request_result
        else: request_result.message = "payment has been queued for processing"

        # Now we route the request to the appropriate client:
        match auth_token.client:
            case "safaricomMPesaExpress":
                request_result = await self.__request_from_safaricom_m_pesa_express(
                    http_client = http_client,
                    mongo_conn = mongo_conn,
                    auth_token = auth_token,
                    payment_request = payment_request,
                    callback_url = f"https://api.thecaoffice.com/finstitutions/payments/callback/safaricom/mpesaexpress",
                    payment_id = payment_id
                )
            case _:
                request_result = None

        # Done here:
        return request_result

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

    # These are simply for retrieving payment records.
    # You need to already have them saved to the database.

    @staticmethod
    async def list_payments(
            mongo_conn: AsyncMongo,
            token_ids: List[ObjectId | str],
            limit: int = 100,
            skip: int = 0,
            additional_filter: dict = None
    ) -> List[CorePaymentModel] | None:

        # Regardless of what additional filter is provided from outside,
        # we add a payment-selecting filter here:
        if additional_filter is None: additional_filter = {}
        # additional_filter["serviceType"] = "paymentGateway"

        # Simply call the core model:
        return await current_app.core_payment_controller.get_payment_previews(
            mongo_conn = mongo_conn,
            token_ids = token_ids,
            limit = limit,
            skip = skip,
            additional_filter = additional_filter
        )

    @staticmethod
    async def get_payment(
            mongo_conn: AsyncMongo,
            token_id: ObjectId | str,
            payment_id: ObjectId | str
    ) -> CorePaymentModel | None:

        # Simply call the core model:
        return await current_app.core_payment_controller.get_payment(
            mongo_conn = mongo_conn,
            token_id = token_id,
            payment_id = payment_id
        )

    @staticmethod
    async def get_payment_internal(
            mongo_conn: AsyncMongo,
            payment_id: ObjectId | str
    ) -> CorePaymentModel | None:

        # Simply call the core model:
        return await current_app.core_payment_controller.get_payment_internal(
            mongo_conn = mongo_conn,
            payment_id = payment_id
        )

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

    @staticmethod
    async def add_event_by_payment_id(
            mongo_conn: AsyncMongo,
            payment_id: ObjectId | str,
            event: PaymentEvent,
            client_reference_id: str = None
    ) -> bool:

        # Simply call the core model:
        return await current_app.core_payment_controller.add_event_by_payment_id(
            mongo_conn = mongo_conn,
            payment_id = payment_id,
            event = event,
            client_reference_id = client_reference_id
        )

    @staticmethod
    async def add_event_by_client_reference_id(
            mongo_conn: AsyncMongo,
            client_reference_id: str,
            event: PaymentEvent,
    ) -> bool:

        # Simply call the core model:
        return await current_app.core_payment_controller.add_event_by_client_reference_id(
            mongo_conn = mongo_conn,
            event = event,
            client_reference_id = client_reference_id
        )

    @staticmethod
    async def update_tags(
            mongo_conn: AsyncMongo,
            token_id: ObjectId | str,
            payment_id: ObjectId | str,
            unset_tags: List[str] = None,
            set_tags: List[str] = None
    ) -> bool:

        # Simply call the core model:
        return await current_app.core_payment_controller.update_tags(
            mongo_conn = mongo_conn,
            token_id = token_id,
            payment_id = payment_id,
            unset_tags = unset_tags,
            set_tags = set_tags
        )


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


if __name__ == "__main__":

    pass

    # from utils_v2.string import json
    #
    # file_options = [
    #     r"/home/developer/Downloads/recursive parts parse - 20241210.json",
    #     r"/home/developer/Downloads/recursive parts parse (no attachment) - 20241210.json",
    # ]
    #
    # raw_mail_json = json.from_file(file_options[1])
    # print("FROM FILE:", json.to_string(raw_mail_json["payload"]))
    # print("\n\n---------\n\n")
    # mail_controller = MailController()
    # print(json.to_string(mail_controller.drop_attachments(raw_mail_json["payload"])))
