"""

    AUTHOR:

        Khushal P Soonderji

    DATE:

        Friday, 27th Dec., 2024

    OBJECTIVE:

        To provide a way to interface with Zerodha's Kite API in an asynchronous manner. Zerodha provides a great client
        library, but it works only in sync mode. Here we will try to build an asynchronous version of the same for
        more advance use cases.

    REFERENCES:

        01. Official documentation: https://kite.trade/docs/connect/v3/

    DOWNLOADS:

        N/A

"""


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


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

# System-level activities:
import io
import os

# My utils:
from utils_v2.string import json
from utils_v2.system import files
from utils_v2.security.hash import Hasher
from utils_v2.date_time import date_time

# Data models:
from utils_v2.trading.zerodha_kite.models.api_call import ZerodhaKiteApiResponse

# To make API calls:
import httpx

# To work with date and time:
import datetime

# For working with datatypes:
from typing import Literal, List

# For debugging:
from icecream import IceCreamDebugger
import inspect


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


# --- Nothing Yet


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


# --- Nothing Yet


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


# --- Nothing Yet


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


class AsyncZerodhaKite:

    def __init__(
            self,
            api_key: str,
            api_secret: str,
            http_client: httpx.AsyncClient = None,
            debug = True,
            debug_prefix = "Z-Kite | ",
            debug_only_errors = True
    ):

        """
        To initialize the instance of this Telegram messenger.
        :param api_key: The API key that identifies your app. Note that this doesn't change in the lifecycle of the app.
        :param api_secret: The API secret to access your app. Note that this can be changed from the API portal. This is
            meant to be kept more secure than the simple API key.
        :param http_client: An instance of 'httpx' library's AsyncClient. If not given, one will be instantiated
            internally. It is recommended that, for multi-bot use cases, you provide a common HTTP client from outside.
        :param debug: Whether, or not, you would like to show debugging messages on the terminal.
        :param debug_prefix: The prefix string to identify the debugging messages.
        :param debug_only_errors: Whether you would like to show all debugging messages or just error messages.
        """

        # Prepare the debugging utility:
        self._debug_prefix = debug_prefix
        self._printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
        if not debug: self._printer.disable()
        self._debug_only_errors = debug_only_errors

        # Accept the input configuration:
        self.__api_key = api_key
        self.__api_secret = api_secret
        self.__request_token = None
        self.__access_token = None
        self.__checksum = None

        # Accept/create an HTTP client to work with:
        if http_client: self.__http_client = http_client
        else: self.__http_client = httpx.AsyncClient(
            limits = httpx.Limits(
                max_connections = 100,  # ............ Maximum number of connections allowed in the pool.
                max_keepalive_connections = 50,  # ... Maximum number of connections that can be kept alive.
            ),
            timeout = httpx.Timeout(
                pool = 120.0,  # .... Time to wait for a free connection from the pool.
                connect = 5.0,  # ... Time to wait for establishing a connection to the server.
                write = 10.0,  # .... Time to wait for sending data.
                read = 120.0  # ..... Time to wait for receiving data.
            )
        )

    def enable_debug(self):
        self._printer.enable()

    def disable_debug(self):
        self._printer.disable()

    def debug_only_errors(self):
        self._debug_only_errors = True

    def debug_everything(self):
        self._debug_only_errors = False

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

    async def __get(
            self,
            url: str,
            headers: dict = None,
            params: dict = None
    ) -> ZerodhaKiteApiResponse:

        """
        To call an API using the GET method.
        :param url: The URL to call.
        :param headers: The headers to pass.
        :param params: The params to send in the query string itself.
        :return: A structured response that includes the raw response, the exception (if any), and so on.
        """

        # Prepare the structure of the response:
        api_response = ZerodhaKiteApiResponse(
            action = inspect.stack()[1].function,
            url = url,
            method = "GET"
        )

        try:

            # Make the API call:
            response = await self.__http_client.get(
                url = url,
                headers = headers,
                params = params
            )

            # Note down the results:
            api_response.response = response
            api_response.httpCode = response.status_code
            api_response.message = response.reason_phrase

            # Check for errors:
            await api_response.note_error()

        # If something goes wrong:
        except Exception as exception:
            api_response.exception = exception
            api_response.message = str(exception)
            self._printer(exception, api_response.url, api_response.method, headers, params)

        # Done here:
        return api_response

    async def __post(
            self,
            url: str,
            headers: dict = None,
            json: dict = None,
            data: dict = None,
            params: dict = None,
            content: str | bytes = None,
            files: dict = None
    ) -> ZerodhaKiteApiResponse:

        """
        To call an API using the POST method.
        :param url: The URL to call.
        :param headers: The headers to pass.
        :param json: The params to send in the JSON body.
        :param data: The params to send in the form-data in the body.
        :param params: The params to send in the query string itself.
        :param content: The raw content to be sent in the body (typically as an octet-stream).
        :param files: Any file that you may want to send.
        :return: A structured response that includes the raw response, the exception (if any), and so on.
        """

        # Prepare the structure of the response:
        api_response = ZerodhaKiteApiResponse(
            action = inspect.stack()[1].function,
            url = url,
            method = "POST"
        )

        try:

            # Make the API call:
            response = await self.__http_client.post(
                url = url,
                headers = headers,
                json = json,
                data = data,
                params = params,
                content = content,
                files = files
            )

            # Note down the results:
            api_response.response = response
            api_response.httpCode = response.status_code
            api_response.message = response.reason_phrase

            # Check for errors:
            await api_response.note_error()

        # If something goes wrong:
        except Exception as exception:
            api_response.exception = exception
            api_response.message = str(exception)
            self._printer(exception, api_response.url, api_response.method, headers, json, data)

        # Done here:
        return api_response

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

    @property
    def login_url(self) -> str:

        """
        Generate the login URL that the user can use to log in to his Zerodha account for this app.
        DOCUMENTATION:
            01. https://kite.trade/docs/connect/v3/user/
        :return: The login URL that the user must use.
        """

        return f"https://kite.zerodha.com/connect/login?api_key={self.__api_key}"

    def set_request_token(
            self,
            request_token: str
    ) -> None:

        """
        When the user authorizes the login flow, Zerodha's serve will send you a GET request on the callback URL that
        you set on the PI portal for your app. This callback will have, among other things, a 'request_toke'. The
        request token is valid only for a very short period, and must be used to get a longer token called
        'access_token' for actual activities.
        DOCUMENTATION:
            01. https://kite.trade/docs/connect/v3/user/
        :param request_token: The request token received from Zerodha when the user logs in.
        :return: None.
        """

        self.__request_token = request_token
        hasher = Hasher()
        hasher.update(self.__api_key + request_token + self.__api_secret)
        self.__checksum = hasher.digest().decode()
        print(self.__checksum)

    # async def get_access_token(self):
    #
    #     client_response = self.__get(
    #         url = r"https://kite.zerodha.com/session/token",
    #
    #     )


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


if __name__ == "__main__":

    import asyncio

    async def main():

        # Read the credentials:
        creds = json.from_file(r"../../../../creds/zerodha/api.json")

        # Create the client:
        my_kite = AsyncZerodhaKite(
            api_key = creds["apiKey"],
            api_secret = creds["apiSecret"]
        )

        # Login flow:
        print("LOGIN URL:", my_kite.login_url)
        my_kite.set_request_token(input("Request Token: "))

    asyncio.run(main())
