import asyncio
import random

from .client import TraderionClient


BID, ASK = (0, 1)


class TraderionBot(object):
    """
    Base class to create a custom trading bot.
    """

    def __init__(self, username, password, room_id, loop_sleep=1):
        """
        Initialize Traderion bot.

        On instantiation it will create a :class:`.TraderionClient` with the provided credentials and
        will store it in a member called *api*. Through this member you will call the API methods.

        After creating a :class:`.TraderionBot` object, you should call its
        :func:`run <traderion2.bot.TraderionBot.run>` method in your main event loop. See the example for more details.

        :param str username: traderion username
        :param str password: traderion password
        :param str room_id: id of the current playing room
        :param float loop_sleep: the frequency (in seconds) of the main loop, defaults to 1

        :raises LoginError: if the credentials are not correct
        :raises BadRequest: this most likely means an incorrect room id
        :raises ServerError: if some error occured on the server
        """
        self.loop_sleep = loop_sleep
        self.api = TraderionClient(username, password, room_id, self)

    def on_room_status_change(self, old_status, new_status):
        """
        Room status change callback. Implement this method to receive notifications when the room status changes.
        Take notice that this is not a coroutine.

        :param str old_status: one of ``'pending'`` | ``'playing'`` | ``'paused'`` | ``'finished'``
        :param str new_status: one of ``'pending'`` | ``'playing'`` | ``'paused'`` | ``'finished'``
        """
        pass

    async def on_room_param_change(self, category, name, old_value, new_value):
        """
        Room parameter change callback. Implement this method to receive notifications when a room parameter changes.

        :param str category: category of the parameter (is either ``'general'`` or a swift id (as string))
        :param str name: parameter name
        :param int old_value: parameter old value
        :param int new_value: parameter new value
        """
        pass

    async def on_new_client_call(self, client_call):
        """
        New client call callback. Implement this method to receive notifications when a new client call is available.

        If the *client_price* attribute in the client_call is *None*, then the client needs a quote,
        otherwise you have to either accept or decline the call. In the first case, you should call
        :func:`self.api.quote_client_call <traderion2.client.TraderionClient.quote_client_call>`. In the second case,
        you should call one of
        :func:`self.api.accept_client_call <traderion2.client.TraderionClient.accept_client_call>` or
        :func:`self.api.decline_client_call <traderion2.client.TraderionClient.decline_client_call>`.

        If you don't respond to the call, it will expire after some time.

        :param dict client_call: a dictionary with information about the call

        **Client call structure**:
            * **id** (*int*): call id
            * **client** (*str*): name of the client
            * **swift_id** (*int*): swift id
            * **direction** (*int*): 0/1 (``BID``/``ASK``) (client buys/client sells)
            * **amount** (*int*): amount the client wants to buy/sell
            * **client_max_spread** (*int*): the maximum spread you can quote to the client (can be *None*)
            * **client_price** (*float*): the price at which the client wants to buy/sell (*None* if the client requests
              quote)
            * **trader_quote**: always *None*
        """
        pass

    async def on_client_call_accepted(self, client_call):
        """
        Client call accepted callback. Implement this method to receive notifications when clients accept your quotes.

        :param dict client_call: a dictionary with information about the call

        **Client call structure**:
            * **id** (*int*): call id
            * **client** (*str*): name of the client
            * **swift_id** (*int*): swift id
            * **direction** (*int*): 0/1 (``BID``/``ASK``) (client buys/client sells)
            * **amount** (*int*): amount the client bought/sold
            * **client_max_spread** (*int*): the maximum spread you can quote to the client (can be *None*)
            * **client_price** (*float*): always *None*
            * **trader_quote** (*float*): the price you previously offered to the client
        """
        pass

    async def on_client_call_declined(self, client_call):
        """
        Client call declined callback. Implement this method to receive notifications when clients decline your quotes.

        :param dict client_call: a dictionary with information about the call

        **Client call structure**:
            * **id** (*int*): call id
            * **client** (*str*): name of the client
            * **swift_id** (*int*): swift id
            * **direction** (*int*): 0/1 (``BID``/``ASK``) (client buys/client sells)
            * **amount** (*int*): amount the client wanted to buy/sell
            * **client_max_spread** (*int*): the maximum spread you can quote to the client (can be *None*)
            * **client_price** (*float*): always *None*
            * **trader_quote** (*float*): the price you previously offered to the client
        """
        pass

    async def on_client_call_expired(self, client_call):
        """
        Client call expired callback. Implement this method to receive notifications when client calls expire.

        :param dict client_call: a dictionary with information about the call

        See :func:`on_new_client_call <traderion2.bot.TraderionBot.on_new_client_call>` for the structure of the
        *client_call* object.
        """
        pass

    async def on_new_ib_call(self, ib_call):
        """
        New IB call callback. Implement this method to receive notifications when a new IB call is available.

        You will only receive notifications about calls where you are the market maker (some bank asks you
        for bid and ask prices). For responding to the call, you should call
        :func:`self.api.quote_ib_call <traderion2.client.TraderionClient.quote_ib_call>`.

        :param dict ib_call: a dictionary with information about the call

        **IB call structure:**
            * **id** (*int*): call id
            * **market_taker** (*str*): name of the market taker
            * **market_maker** (*str*): name of the market maker (which is always your username)
            * **swift_id** (*int*): swift id
            * **amount** (*int*): amount the client bank wants to buy/sell
            * **direction**: always *None*
            * **bid**: always *None*
            * **ask**: always *None*
        """
        pass

    async def on_ib_call_answered(self, ib_call):
        """
        IB call answered callback. Implement this method to receive notifications when your outgoing IB calls are
        answered. This coroutine will be called once for every offer you receive after you make an IB call.

        You can respond to the offer in 2 ways:
            * hit the bid or ask price by calling :func:`self.api.hit_ib_call
              <traderion2.client.TraderionClient.hit_ib_call>`
            * do nothing by calling :func:`self.api.ntg_ib_call <traderion2.client.TraderionClient.ntg_ib_call>`

        If you don't respond to an offer, it will expire on its own after some time, but you will be unable to
        make other IB calls until that happens.

        :param dict ib_call: a dictionary with information about the call

        **IB call structure:**
            * **id** (*int*): call id
            * **market_taker** (*str*): name of the market taker (which is always your username)
            * **market_maker** (*str*): name of the market maker
            * **swift_id** (*int*): swift id
            * **amount** (*int*): amount you wanted to buy/sell
            * **direction**: always *None*
            * **bid** (*float*): the bid offer
            * **ask** (*float*): the ask offer
        """
        pass

    async def on_ib_call_hit(self, ib_call):
        """
        IB call hit callback. Implement this method to receive notifications when a client bank hits your offer.

        You can determine which one of bid or ask price was hit based on the direction parameter.

        :param dict ib_call: a dictionary with information about the call

        **IB call structure:**
            * **id** (*int*): call id
            * **market_taker** (*str*): name of the market taker
            * **market_maker** (*str*): name of the market maker (which is always your username)
            * **swift_id** (*int*): swift id
            * **amount** (*int*): amount the client bank bought/sold
            * **direction** (*int*): which price the client bank hit: 0/1 (``BID``/``ASK``) (client bank sells/buys)
            * **bid** (*float*): your bid offer
            * **ask** (*float*): your ask offer
        """
        pass

    async def on_ib_call_ntg(self, ib_call):
        """
        IB call NTG callback. Implement this method to receive notifications when a client bank responds with NTG on
        your offer.

        :param dict ib_call: a dictionary with information about the call

        **IB call structure:**
            * **id** (*int*): call id
            * **market_taker** (*str*): name of the market taker
            * **market_maker** (*str*): name of the market maker (which is always your username)
            * **swift_id** (*int*): swift id
            * **amount** (*int*): amount the client bank wanted to buy/sell
            * **direction**: always *None*
            * **bid**: your bid offer
            * **ask**: your ask offer
        """
        pass

    async def on_ib_call_expired(self, ib_call):
        """
        IB call expired callback. Implement this callback to receive notifications when an IB call expires.

        You will receive this notification in 4 cases:
            1. you haven't quoted an incoming call
            2. you have quoted an incoming call, but the other party din't respond (by hitting or NTG)
            3. some other trader didn't quote your outgoing call
            4. some other trader quoted your outgoing call, but you didn't respond (by hitting or NTG)

        You can differentiate between these cases by checking the parameters in the following way: ::

            1. market_maker == your_username and bid is None
            2. market_maker == your_username and bid is not None
            3. market_taker == your_username and bid is None
            4. market_taker == your_username and bid is not None

        :param ib_call: a dictionary with information about the call

        **IB call structure:**
            * **id** (*int*): call id
            * **market_taker** (*str*): name of the market taker
            * **market_maker** (*str*): name of the market maker
            * **swift_id** (*int*): swift id
            * **amount** (*int*): amount
            * **direction**: always *None*
            * **bid** (*None* or *float*): the bid offer (can be *None* if the call wasn't quoted)
            * **ask** (*None* or *float*): the ask offer (can be *None* if the call wasn't quoted)
        """
        pass

    async def on_dp_order_hit(self, other_party, hit_amount, price, dp_order):
        """
        Dark pool order hit callback. Implement this method to receive notifications when one of your DP orders is
        hit.

        :param str other_party: name of the other party
        :param int hit_amount: hit amount
        :param float price: price at which the order was executed
        :param dict eb_order: a dictionary with information about the DP order that was hit

        **DP order structure:**
            * **id** (*int*): order id
            * **room_time** (*int*): room time when the order was added
            * **swift_id** (*int*): swift id
            * **order_type** (*str*): one of ``'limit'`` | ``'market'``
            * **direction** (*int*): 0/1 (``BID``/``ASK``) (you buy/you sell) order direction
            * **amount** (*int*): left order amount (if 0, the order status will be ``'completed'``)
            * **price** (*None* or *float*): order price for limit orders. For market orders, this is always *None*.
            * **status** (*str*): one of ``'pending'`` | ``'completed'``
        """
        pass

    async def on_eb_order_hit(self, market_taker, hit_amount, price, eb_order):
        """
        EB order hit callback. Implement this method to receive notifications when one of your EB orders is hit.

        :param str market_taker: name of the market taker
        :param int hit_amount: hit amount
        :param float price: price at which the order was executed (this might not be equal to the order price for plus
                            orders)
        :param dict eb_order: a dictionary with information about the EB order that was hit

        **EB order structure:**
            * **id** (*int*): order id
            * **room_time** (*int*): room time when the order was added
            * **swift_id** (*int*): swift id
            * **order_type** (*str*): one of ``'limit'`` | ``'iceberg'`` | ``'limit_plus'`` | ``'iceberg_plus'``
            * **direction** (*int*): 0/1 (``BID``/``ASK``) (you buy/you sell) order direction
            * **amount** (*int*): left order amount (if 0, the order status will be ``'completed'``)
            * **chunk** (*None* or *int*): visible chunk for an Iceberg order. Is *None* if the order is not an Iceberg
                                           one.
            * **visible_amount** (*int*): the visible amount of the order. For Limit orders, this is equal to the order
                                          *amount*. For Iceberg orders, this will always be ``min(amount, chunk)``.
            * **price** (*float*): order price
            * **status** (*str*): one of ``'pending'`` | ``'completed'``
        """
        pass

    async def on_eb_depth_change(self, swift_id, direction, old_depth, new_depth):
        """
        EB depth change callback. Implement this method to receive notifications when the EB depth changes.

        :param int swift_id: swift id of the depth
        :param int direction: 0/1 (``BID``/``ASK``) depth direction
        :param list old_depth: old depth (list of EB depth order objects)
        :param list new_depth: new depth (list of EB depth order objects)

        **EB depth order structure:**
            * **count** (*int*): the number of atomic orders at this price
            * **amount** (*int*): total amount at this price
            * **price** (*float*): price

        **Note**: An EB depth order can contain several atomic orders having the same price. A price appears
        only once in the depth and the amounts of all the atomic orders at that price are summed up. Prices
        are decreasing for ``BID`` depth and increasing for ``ASK`` depth.
        """
        pass

    async def on_market_price_change(self, old_price, new_price):
        """
        Market price change callback. Implement this method to receive notifications when market prices change.

        :param dict old_price: old state of the prices (market price object)
        :param dict new_price: new state of the prices (market price object)

        **Market price object structure:**
            * **swift_id** (*int*): swift id for which the price changed (has the same value for *old_prices* and
              *new_prices*)
            * **open** (*float*): the open price of the scenario
            * **bid** (*float*): market bid price
            * **ask** (*float*): market ask price
            * **price** (*float*): market mid price (average of *bid* and *ask*)
        """
        pass

    async def on_position_change(self, old_position, new_position):
        """
        Player position change callback. Implement this callback to receive notifications when your position changes.

        :param dict old_position: old state of the position (position object)
        :param dict new_position: new state of the position (position object)

        **Position object structure:**
            * **swift_id** (*int*): swift id for which the position changed (has the same value for *old_position*
              and *new_position*)
            * **amount** (*int*): amount of the position
            * **rate** (*float*): average rate of the position
            * **pnl** (*int*): the PnL expressed in the reporting currency of the swift
            * **converted_pnl** (*int*): the PnL expressed in USD
            * **pnl_percentage** (*float*): the PnL relative to the position limit
            * **return_on_volume** (*float*): the PnL relative to the total traded volume
            * **max_position** (*int*): maximum position (amount) you had since the beginning of the game
            * **total_volume** (*int*): total traded volume
            * **yield** (*float* or *None*): bond yield (*None* if the asset type is not ``FI``)
            * **dv01** (*int* or *None*): bond DV01 (*None* if the asset type is not ``FI``)
            * **day_change** (*float*): difference between the current market price and the open price
        """
        pass

    async def on_macro_event(self, macro_event):
        """
        Macro event callback. Implement this callback to receive notifications when macro events happen.

        :param dict macro_event: details about the macro event

        **Macro event structure:**
            * **event_type** (*str*): type of the event. It is either ``'news'`` or ``'forecast'``
            * **event_data** (*dict*): dictionary with extra details about the event (depends on the event type)

        **News event data:**
            * **headline** (*str*): event headline text (a brief description of what happened)
            * **body** (*str*): event body text (a more detailed description of what happened)

        **Forecast event data:**
            * **country** (*str*): country in question
            * **indicator** (*str*): forecast indicator (e.g. ``'Average Weekly Hours'``)
            * **description** (*str*): detailed description of the indicator
            * **previous** (*str*): previous value of the indicator (as string)
            * **forecast** (*str*): forecasted value of the indicator (as string) (can be ``''``)
            * **actual** (*str*): actual value of the indicator (current value) (as string)
            * **unit** (*str*): unit of measurement for the indicator (e.g. ``'%'``, ``'K'``) (can be ``''``)
        """
        pass

    async def main_loop(self):
        """
        Bot main loop. Implement this method to add some custom logic that should be run periodically.

        This method will run every ``self.loop_sleep`` seconds **while** the room is playing. The first execution
        of the main loop will happen immediately (after the client connects to the room).

        Check the example for more details.
        """
        pass

    async def run(self):
        """
        Starts the bot.  Call this method in your main event loop to start the bot (check the example for more
        details).

        **You should not override this method.**

        The bot will run until the room is finished. If the room is already finished, it will return immediately.
        You can still query the API after the room is finished (for example to get your final PnL), but some methods
        will obviously not work (for example, you cannot make an IB call in a finished room).

        If you need to stop the bot early, use the :func:`stop <traderion2.bot.TraderionBot.stop>`
        method.
        """
        await self.api._run_until_room_finished()

    def stop(self):
        """
        Stops the bot early, before the room is finished.

        **You should not override this method.**

        Upon calling this method, the :func:`run <traderion2.bot.TraderionBot.run>` method will complete and the bot
        :func:`main_loop <traderion2.bot.TraderionBot.main_loop>` will no longer be called. However, event callbacks
        will still get called if your python script continues to run.
        """
        self.api._stop_early()
