"""

    AUTHOR:

        Khushal P Soonderji

    DATE:

        Friday, 24th Jan., 2025.

    OBJECTIVE:

        To infer EoD data from the ticks collection, and then feed it in the SQL database.

    REFERENCES:

        N/A

    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.string import regex
from utils_v2.system import files
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

# To make HTTP calls:
import httpx
import socket

# To work with date and time:
import datetime
import time

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

# For handling NaN values:
import pandas as pd

# For scheduling and cron:
from scheduler.asyncio import Scheduler

# For debugging:
from icecream import IceCreamDebugger

# For asynchronous operations:
import asyncio


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


# To make API calls:
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 = 2.5,  # ... 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.
    )
)

# For debugging:
printer = IceCreamDebugger(prefix = "EoD (Ticks) | ", includeContext = True)
no_context_printer = IceCreamDebugger(prefix = "EoD (Ticks) | ", includeContext = False)


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


# This script's config.:
SERVER_HOSTNAME = str(socket.gethostname())
SCRIPT_DATA = {}

# For the database:
sql_writer: AsyncMySQL | None = None
data_mongo: AsyncMongo | None = None

# To construct a report:
failed_to_parse_dates = []
failed_eq_dates = []
failed_fo_dates = []


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


async def init(
        script_id: str,
        debug: bool
) -> bool:

    """
    To initialize all credentials, instances, and connectivity for this whole script.
    :param script_id: The id to use to load cred and data from the internal service.
    :param debug: Whether, or not, you would like to print the debug messages.
    :return: True if initialized successfully, else False.
    """

    # Declare the required global variables:
    global SCRIPT_DATA
    global sql_writer
    global data_mongo

    # Basic stuff:
    if not debug: printer.disable()

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

    # Get the script credentials:
    response = await http_client.get(
        url = r"https://nexcom.ditscentre.in/internal/cred/get",
        headers = {"X-Script-Id": script_id}
    )
    if response.status_code not in [200]:
        print("FATAL: SCRIPT CREDENTIALS LOADING FAILED!")
        return False
    script_cred = response.json().get("data")

    # Get the script data:
    response = await http_client.get(
        url = r"https://nexcom.ditscentre.in/internal/data/get",
        headers = {"X-Script-Id": script_id}
    )
    if response.status_code not in [200]:
        print("FATAL: SCRIPT DATA LOADING FAILED!")
        return False
    SCRIPT_DATA = response.json().get("data")

    # Done with this step:
    printer("Cred and Data loaded.")

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

    sql_writer = AsyncMySQL(
        pool_size = script_cred["mariaDb"]["write"]["poolSize"],
        host = script_cred["mariaDb"]["write"]["host"],
        user = script_cred["mariaDb"]["write"]["user"],
        password = script_cred["mariaDb"]["write"]["password"],
        database = script_cred["mariaDb"]["write"]["database"]
    )
    if not await sql_writer.connect():
        print("FATAL: MARIA-DB CONNECTION FAILED!")
        return False

    printer("MariaDB connected.")

    # ┳┳┓
    # ┃┃┃┏┓┏┓┏┓┏┓
    # ┛ ┗┗┛┛┗┗┫┗┛
    #         ┛

    data_mongo = AsyncMongo(
        connection_string = script_cred["mongoDb"]["data"]["connectionString"],
        # database_name = script_cred["mongoDb"]["data"]["dbName"],
        database_name = "markets",
        max_connections = script_cred["mongoDb"]["data"]["poolSize"],
        debug = debug
    )
    if not await data_mongo.connect():
        print("FATAL: MONGO-DB NOT CONNECTED!")
        return False
    no_context_printer("MongoDB ready.")

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

    # If everything went well, we return with success:
    printer("Initialization done.")
    return True


# ---------------------------------------------------------------------------------------------------------------------


async def send_telegram(
        message: str,
        chat_id: str = None,
        message_type: Literal["info", "warning", "error"] = "info"
) -> None:

    """
    To send out alerts and heartbeats to inform about th script being alive.
    :param message: The text to send.
    :param chat_id: The destination chat identifier.
    :param message_type: The kind of message to send. Decides the presentation of the header.
    :return: None.
    """

    try:

        # Create the JSON for sending to the API endpoint:
        json_input = {
            "chatClient": "telegram",
            "message": message,
            "type": message_type
        }
        if chat_id: json_input["chatId"] = json_input

        # Make the API call to send the ticks:
        response = await http_client.post(
            url = r"https://api.thecaoffice.com/converse/tech/alert/chat/backend",
            json = json_input
        )

        # Raise an exception if the call was not successful:
        response.raise_for_status()

    # If something goes wrong:
    except Exception as exception:
        printer(message_type, message, exception)


# ---------------------------------------------------------------------------------------------------------------------


async def get_latest_eod_data(target_date: datetime.datetime = None) -> dict:

    """
    To fetch the latest EoD (daily candle) data from the ticks database.
    :param target_date: The date (UTC) whose EoD ticks are desired.
    :return: The inferred daily candle data.
    """

    no_context_printer("Getting EoD data from ticks.")

    # Prepare the inputs needed for the aggregation:
    if not isinstance(target_date, datetime.datetime): target_date = date_time.get_current_utc_date_time()
    else: target_date = date_time.to_timezone(target_date, date_time.TIMEZONE_UTC)
    start_ts = target_date.replace(hour = 3, minute = 45, second = 0, microsecond = 0)
    end_ts = target_date.replace(hour = 10, minute = 0, second = 0, microsecond = 0)

    # Construct the aggregation pipeline:
    # Consider only the target date's ticks:
    stage_0 = {
        "$match": {
            "tradeTs": {
                "$gte": start_ts,
                "$lte": end_ts
            }
        }
    }

    # Add a field that has the rounded timestamp.
    # We round it to one day for EoD data:
    stage_1 = {
        "$addFields": {
            "roundTs": {
                "$dateTrunc": {
                    "date": "$tradeTs",
                    "unit": "day",
                    "binSize": 1
                }
            }
        }
    }

    # Now we convert tick to candlesticks:
    stage_2 = {
        "$group": {
            "_id": {
                "roundTs": "$roundTs",
                "symbol": "$symbol"
            },
            "roundTs": {"$last": "$roundTs"},
            "symbol": {"$last": "$symbol"},
            "name": {"$last": "$name"},
            "exchange": {"$last": "$exchange"},
            "segment": {"$last": "$segment"},
            "type": {"$last": "$type"},
            "expiry": {"$last": "$expiry"},
            "strike": {"$last": "$strike"},
            "open": {"$first": "$ltp"},
            "high": {"$max": "$ltp"},
            "low": {"$min": "$ltp"},
            "close": {"$last": "$ltp"},
            "vwap": {"$last": "$vwap"},
            "chg": {"$last": "$chg"},
            "pChg": {"$last": "$pChg"},
            "volume": {"$last": "$totVol"},
            "dayHigh": {"$last": "$h"},
            "dayLow": {"$last": "$l"},
            "ticks": {"$sum": 1},
            "broker": {"$last": "$broker"},
            "brokerToken": {"$last": "$brokerToken"},
        }
    }

    # Finally we organize and present the data:
    stage_3 = {
        "$sort": {
            "symbol": 1,
            "roundTs": 1
        }
    }
    stage_4 = {
        "$project": {
            "_id": False
        }
    }

    # Now we run the aggregation:
    eod_data = await data_mongo.aggregate(
        collection = "__hot_zerodhaTicks",
        pipeline = [
            stage_0,
            stage_1,
            stage_2,
            stage_3,
            stage_4
        ],
        limit = None,
        raise_exception = False
    )

    # Done here:
    return eod_data


# ---------------------------------------------------------------------------------------------------------------------


async def save_latest_eod_data(eod_data: dict) -> bool:

    """
    To save the loaded data to the SQL database.
    :param eod_data: The dict of the EoD data received from the ticks database.
    :return: True if successful, else False
    """

    # Note down the "scraping time":
    scrape_ts = date_time.get_current_utc_date_time()

    # Build the query and data content:
    query_str = (
        "INSERT INTO `eod_market_data_today` (exchange, symbol, segment, type, underlying, is_index, expiry, "
        "strike, prev_close, open, high, low, close, ltp, vwap, tot_vol, tot_cash, delivery_vol, delivery_pct, oi, "
        "oi_chg, date, ts, tz, scrape_ts) "
        "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);"
    )
    query_data = []
    for data in eod_data:
        total_cash = data["vwap"] * data["volume"]
        target_date = data["roundTs"].strftime("%Y-%m-%d")
        curr_close = data["close"]
        prev_close = curr_close - data["chg"]
        one_query_data = (
            data["exchange"],  # ......................... exchange
            data["symbol"],  # ........................... symbol
            data["segment"],  # .......................... segment
            data["type"],  # ............................. type
            data["name"],  # ............................. underlying
            None,  # ..................................... is_index
            data["expiry"],  # ........................... expiry
            data["strike"],  # ........................... strike
            prev_close,  # ............................... prev_close
            data["open"],  # ............................. open
            data["high"],  # ............................. high
            data["low"],  # .............................. low
            curr_close,  # ............................... close
            curr_close,  # ............................... ltp
            data["vwap"],  # ............................. vwap
            data["volume"],  # ........................... tot_vol
            total_cash,  # ............................... tot_cash
            None,  # ..................................... delivery_vol
            None,  # ..................................... delivery_pct
            None,  # ..................................... oi
            None,  # ..................................... oi_chg
            target_date,  # .............................. date
            data["roundTs"].replace(tzinfo = None),  # ... ts
            "Asia/Kolkata",  # ........................... tz
            scrape_ts.replace(tzinfo = None),  # ......... scrape_ts
        )
        one_query_data = [None if pd.isna(d) else d for d in one_query_data]
        query_data.append(one_query_data)

    # Run the commands:
    no_context_printer("Saving data to SQL DB.")
    rows_affected, db_response, db_exception = await sql_writer.execute_many(
        query = query_str,
        data = query_data,
        return_exception = True
    )
    if db_exception: await send_telegram(
        message = (
            f"*EoD From Ticks:*\n\n"
            "Message: `SQL database threw an exception.`\n\n"
            f"Exception: `{db_exception}`"
        ),
        message_type = "error"
    )

    # Done here:
    success = True if rows_affected else False
    no_context_printer(success)
    return success


# ---------------------------------------------------------------------------------------------------------------------


async def run_once() -> bool:

    """
    Run the job once where data is grabbed from the ticks database and then fed into the SQL database.
    :return: True if successful, else False.
    """

    # Get the data from the ticks database:
    eod_data = await get_latest_eod_data()
    if not eod_data:
        await send_telegram(
            message = (
                f"*EoD From Ticks:*\n\n"
                "Message: `Failed to get EoD candles from ticks.`"
            ),
            message_type = "error"
        )
        return False

    # If there is no data to save:
    if not eod_data:
        await send_telegram(
            message = (
                f"*EoD From Ticks:*\n\n"
                "Message: `No EoD data to save to SQL.`"
            ),
            message_type = "error"
        )
        return False

    # Save the data to the SQL database:
    success = await save_latest_eod_data(eod_data)
    if not success:
        await send_telegram(
            message = (
                f"*EoD From Ticks:*\n\n"
                "Message: `Failed to save EoD data to SQL.`"
            ),
            message_type = "error"
        )
        return False

    # If both th steps succeeded, we are good to go:
    return True


# ---------------------------------------------------------------------------------------------------------------------


async def main(
    start_time: datetime.datetime,
    end_time: datetime.datetime,
    interval_seconds: int = 300,
):

    """
    The main scheduler that manages jobs.
    :param start_time: The time of the day at which messages can start going out.
    :param end_time: The time of the day after which new messages should not go out.
    :param interval_seconds: The time (in seconds) between two reminder jobs.
    :return: None.
    """

    # Start configuring the scheduler:
    printer("Configuring the schedule-manager.")
    schedule_manager = Scheduler()

    # Create all the timestamps at which the job must be done:
    all_job_ts = []
    offset_seconds = 0
    while True:
        ts = start_time + datetime.timedelta(seconds = offset_seconds)
        if ts > end_time: break
        all_job_ts.append(ts.time())
        offset_seconds += interval_seconds

    # Add the jobs:
    for ts in all_job_ts: schedule_manager.daily(ts, run_once)
    printer(len(all_job_ts))

    # Infinite loop to keep doing the tasks:
    printer("Schedule-manager ready.")
    while True: await asyncio.sleep(3_600)


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


if __name__ == "__main__":

    # To get args. from the terminal:
    import argparse

    # Get the config. from the command-line:
    parser = argparse.ArgumentParser(
        description = (
            "To periodically infer EoD data from tick-by-tick data and feed it into the SQL database."
        )
    )
    parser.add_argument(
        "-s", "--script-id",
        dest = "script_id",
        type = str,
        help = "The id of this script (will affect the loaded config)."
    )
    parser.add_argument(
        "--start-time",
        type = str,
        help = "The 24-hr time of the day (in 'HH:MM:SS' format) from which the data can be refreshed."
    )
    parser.add_argument(
        "--end-time",
        type = str,
        help = "The 24-hr time of the day (in 'HH:MM:SS' format) till which the data must be refreshed."
    )
    parser.add_argument(
        "--interval",
        type = int,
        help = "The no. of seconds after which you would like to refresh the data.",
        default = 300
    )
    parser.add_argument(
        "-d", "--debug",
        dest = "debug",
        action = "store_true",
        help = "Whether, or not, you want to see debugging messages in the terminal.",
        default = False
    )
    args = parser.parse_args()


    async def runner():

        # Parse the inputs:
        start_time = datetime.datetime.strptime(args.start_time, "%H:%M:%S")
        end_time = datetime.datetime.strptime(args.end_time, "%H:%M:%S")

        # Initialize and run the main code:
        if await init(
            script_id = args.script_id,
            debug = args.debug
        ): await main(
            start_time = start_time,
            end_time = end_time,
            interval_seconds = args.interval,
        )

        # Disconnect from the database:
        disconnected = await sql_writer.disconnect()
        # disconnected = await data_mongo.disconnect()


    asyncio.run(runner())
