"""

    AUTHOR:

        Khushal P Soonderji

    DATE:

        Saturday, 21st Dec. 2024

    OBJECTIVE:

        To simulate stock market updates to test on SocketIO.

    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

# For pseudo-random simulations:
import random

# To work with SocketIO
import socketio
from aiohttp import web

# To make HTTP calls:
import httpx

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

# For asynchronous behaviour:
import asyncio


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


# For SocketIO:
# Create a Socket.IO server instance
sio = socketio.AsyncServer(cors_allowed_origins = "*")
app = web.Application()
sio.attach(app)

# A list of stocks to simulate:
SYMBOL_TO_PRICE_MAP = {
    "HDFCBANK": {
        "prevClose": 1_763.95,
        "ltp": 1_771.50,
        "totVol": 55_96_931,
        "buyVol": 16_79_079,
        "sellVol": 39_17_852,
    },
    "RELIANCE": {
        "prevClose": 1_213.35,
        "ltp": 1_205.30,
        "totVol": 7_34_568,
        "buyVol": 1_04_873,
        "sellVol": 6_29_695,
    },
    "INFY": {
        "prevClose": 1_925.70,
        "ltp": 1_922.15,
        "totVol": 5_54_108,
        "buyVol": 2_61_593,
        "sellVol": 2_92_515,
    },
    "TCS": {
        "prevClose": 4_203.50,
        "ltp": 4_170.30,
        "totVol": 7_24_932,
        "buyVol": 1_34_666,
        "sellVol": 5_90_266,
    },
    "HINDUNILVR": {
        "prevClose": 2_312.95,
        "ltp": 2_333.90,
        "totVol": 5_04_533,
        "buyVol": 9_252,
        "sellVol": 4_95_281,
    },
    "ITC": {
        "prevClose": 463.20,
        "ltp": 464.65,
        "totVol": 7_07_905,
        "buyVol": 3_27_422,
        "sellVol": 3_80_483,
    },
    "KOTAKBANK": {
        "prevClose": 1_751.65,
        "ltp": 1_743.55,
        "totVol": 4_49_104,
        "buyVol": 2_47_489,
        "sellVol": 2_01_615,
    }
}


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


tg_update = False


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


@sio.event
async def before_connect(sid, environ):

    print(f"Checking connection attempt from {sid}.")

    # # Simulate a failed connection based on some conditions (for example, invalid IP or header)
    # user_agent = environ.get('HTTP_USER_AGENT', '')
    # if 'BadUserAgent' in user_agent:
    #     print(f"Rejected connection from {sid} due to invalid User-Agent.")
    #     return False  # This will reject the connection attempt

    return True  # Allow connection


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


@sio.event
async def connect(sid, environ):
    print(f"Client {sid} connected")
    if tg_update:
        async with httpx.AsyncClient() as client:
            try: await client.post(
                url = r"https://api.thecaoffice.com/converse/tech/alert/chat/backend",
                json = {
                    "type": "info",
                    "chatClient": "telegram",
                    "chatId": "-4206946032",
                    # "chatId": "1275560043",
                    "message": f"*SocketIO Connected!*\n👍 SID: {sid}"
                }
            )
            except: pass


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


@sio.event
async def disconnect(sid):
    print(f"Client {sid} disconnected")
    if tg_update:
        async with httpx.AsyncClient() as client:
            try: await client.post(
                url = r"https://api.thecaoffice.com/converse/tech/alert/chat/backend",
                json = {
                    "type": "info",
                    "chatClient": "telegram",
                    "chatId": "-4206946032",
                    # "chatId": "1275560043",
                    "message": f"*SocketIO Disconnected!*\n❌ SID: {sid}"
                }
            )
            except: pass


def round_tick(price):
    return round(price * 20) / 20


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


def simulate_one_stock(symbol, price):

    global SYMBOL_TO_PRICE_MAP

    # Simulate a change in the price:
    pos_bias = [1] * 10
    no_bias = [0] * 1
    neg_bias = [-1] * 10
    bias = random.choice(pos_bias + no_bias + neg_bias)
    change_factor = random.random() / 100.0
    change = price * change_factor * bias
    ltp = round_tick(price + change)

    # Simulate the volume.
    # Assume a trade qty. worth 1L to 10L rupees:
    traded_amt = random.uniform(1_00_000, 10_00_000)
    ltq = int(traded_amt / price)
    SYMBOL_TO_PRICE_MAP[symbol]["totVol"] += ltq
    if bias >= 0: SYMBOL_TO_PRICE_MAP[symbol]["buyVol"] += ltq
    else: SYMBOL_TO_PRICE_MAP[symbol]["sellVol"] += ltq

    # Create the basic JSON payload:
    stock_json = {
        "symbol": symbol,
        "last_traded_quantity": ltq,
        "average_traded_price": round_tick(price + (bias * price * (random.random() / 100.0))),
        "volume_traded": SYMBOL_TO_PRICE_MAP[symbol]["totVol"],
        "total_buy_quantity": SYMBOL_TO_PRICE_MAP[symbol]["buyVol"],
        "total_sell_quantity": SYMBOL_TO_PRICE_MAP[symbol]["sellVol"],
        "ohlc": {
            "open": round_tick(price + (price * 0.005)),
            "high": round_tick(price + (price * 0.015)),
            "low": round_tick(price - (price * 0.015)),
            "close": ltp
        },
        "change": ((ltp - SYMBOL_TO_PRICE_MAP[symbol]["prevClose"]) / SYMBOL_TO_PRICE_MAP[symbol]["prevClose"]) * 100,
        "last_trade_time": (datetime.datetime.now() - datetime.timedelta(seconds = random.uniform(0.0, 2.5))).strftime("%Y-%m-%d %H:%M:%S"),
        "oi": 0,
        "oi_day_high": 0,
        "oi_day_low": 0,
        "exchange_timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "depth": {
            "buy": [
                {
                    "quantity": random.randint(0, 100),
                    "price": round(ltp - 0.05, 2),
                    "orders": random.randint(0, 10)
                },
                {
                    "quantity": random.randint(0, 100),
                    "price": round(ltp - 0.10, 2),
                    "orders": random.randint(0, 10)
                },
                {
                    "quantity": random.randint(0, 100),
                    "price": round(ltp - 0.15, 2),
                    "orders": random.randint(0, 10)
                },
                {
                    "quantity": random.randint(0, 100),
                    "price": round(ltp - 0.20, 2),
                    "orders": random.randint(0, 10)
                },
                {
                    "quantity": random.randint(0, 100),
                    "price": round(ltp - 0.25, 2),
                    "orders": random.randint(0, 10)
                }
            ],
            "sell": [
                {
                    "quantity": random.randint(0, 100),
                    "price": round(ltp + 0.05, 2),
                    "orders": random.randint(0, 10)
                },
                {
                    "quantity": random.randint(0, 100),
                    "price": round(ltp + 0.10, 2),
                    "orders": random.randint(0, 10)
                },
                {
                    "quantity": random.randint(0, 100),
                    "price": round(ltp + 0.15, 2),
                    "orders": random.randint(0, 10)
                },
                {
                    "quantity": random.randint(0, 100),
                    "price": round(ltp + 0.20, 2),
                    "orders": random.randint(0, 10)
                },
                {
                    "quantity": random.randint(0, 100),
                    "price": round(ltp + 0.25, 2),
                    "orders": random.randint(0, 10)
                }
            ]
        }
    }

    # Done here:
    return stock_json


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


def simulate_ticks_once():

    # Pick a no. of stocks to simulate:
    count = random.randint(1, len(SYMBOL_TO_PRICE_MAP))
    symbols = random.sample(list(SYMBOL_TO_PRICE_MAP.keys()), count)

    # Create the tick JSON:
    tick_json = [
        simulate_one_stock(
            symbol = symbol,
            price = SYMBOL_TO_PRICE_MAP[symbol]["ltp"]
        ) for symbol in symbols
    ]

    # Done here:
    return tick_json


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


async def broadcast_random_data():
    while True:
        await sio.emit("ticks", simulate_ticks_once())
        await asyncio.sleep(random.uniform(0.15, 1.0))


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


if __name__ == "__main__":

    async def server():

        # Start broadcasting random data in the background
        asyncio.create_task(broadcast_random_data())

        # Run the web server
        runner = web.AppRunner(app)
        await runner.setup()
        site = web.TCPSite(runner, "0.0.0.0", 5214)
        await site.start()

        # Keep the server running
        while True:
            await asyncio.sleep(3600)

    asyncio.run(server())
