""" The sample demonstrates how to place a compound order with 2 legs, modify it by adding another leg and wait for the
order filling/canceling.
"""

import logging
import threading
from typing import Set

import win32com.client
from win32com.client import constants

import CELEnvironment
from CELEnvironment import AssertMessage

#region Settings

# Replace it with real login before run
LOGIN = "your_gw_login"
PASSWORD = "your_gw_password"

# Replace it with enabled symbol
SYMBOL = "EP"

# Compound order type. You may change between OCO and OPO
CO_TYPE = constants.coOCO

# Events waiting time
TIMEOUT = 10

# Logging settings: specify log level and log format here.
# Set log level to DEBUG to see more internal messages.
# See more information about logging lib usage: https://docs.python.org/3/howto/logging-cookbook.html
CELEnvironment.LOG_LEVEL = logging.INFO
# CELEnvironment.LOG_FORMAT = ...

# CELEnvironment.SinkInternal uses own LOG_LEVEL (which is inherited from the global one) and own LOG_FORMAT.
# You can redefine them as follows:
# CELEnvironment.SinkInternal.LOG_LEVEL = logging.DEBUG
# CELEnvironment.SinkInternal.LOG_FORMAT = ...

LOG = CELEnvironment.GetLogger()

#endregion


class CompoundOrdersPlacingSample(CELEnvironment.CELSinkBase):
    """Class is simplified CEL client implemented as script.
    """
    def __init__(self):
        self.account = None  # CQGAccount that will be used to place orders
        self.instruments = []  # list of resolved instruments
        self.orders = []  # list of leg orders (all orders that we've placed via compound order)
        self.eventGatewayIsUp = threading.Event()
        self.eventGatewayIsDown = threading.Event()
        self.generalEvent = threading.Event()
        self.pendingInstruments: Set[str] = set()  # requested and not resolved symbols
        self.compoundOrderGUID = ""  # used to filter order updates
        self.pendingOrders: Set[str] = set()  # set of orders (GUIDS) we wait to be delivered to GW
        self.activeOrders: Set[str] = set()  # set of orders (GUIDS) that we wait to be filled or cancelled
        self.aborting = False  # Helper flag, is set to True if user has aborted wait for compound order fill

    def Init(self, celEnvironment):
        """Used from outside to pass initialized CQGCEL.
        """
        self.celEnvironment = celEnvironment

    def createOrder(self, instrument):
        """For simplification we create only limit sell orders with quantity == 1, but you can
        add any order to your CQGOrderChain.

        Args:
            instrument (CQGInstrument): Order's instrument (not dispatched).

        Returns:
            CQGOrder: Created and dispatched order.
        """
        dispatchedInstrument = win32com.client.Dispatch(instrument)

        LOG.info("Preparing order for %s", dispatchedInstrument.FullName)
        bestBid = dispatchedInstrument.Bid
        AssertMessage(bestBid.IsValid, "Error! Can't set an order price due to invalid BBA")

        LOG.info("Best bid value is %f", bestBid.Price)
        LOG.info("Calculating a price that below the market by 10 tick sizes...")
        price = bestBid.Price - 10 * dispatchedInstrument.TickSize
        LOG.info("It is %f", price)

        LOG.info("Created limit buy order for %s with price %f", dispatchedInstrument.FullName, price)
        order = self.celEnvironment.cqgCEL.CreateOrder(constants.otLimit, dispatchedInstrument, self.account,
                                                       1, constants.osdUndefined, price)
        dispatchedOrder = win32com.client.Dispatch(order)
        self.orders.append(dispatchedOrder)
        self.pendingOrders.add(dispatchedOrder.GUID)

        return dispatchedOrder

    def Start(self):
        """Actual sample script: creates and places compound order with three limit buy legs
        (same contract, different months), then adds one more via compound order modification
        interface and waits to user's input to exit.
        """
        LOG.info("Connecting to GW")
        self.celEnvironment.cqgCEL.GWLogon(LOGIN, PASSWORD)

        LOG.info("Waiting for GW connection...")
        AssertMessage(self.eventGatewayIsUp.wait(TIMEOUT), "GW connection timeout!")

        self.celEnvironment.cqgCEL.AccountSubscriptionLevel = constants.aslAccountUpdatesAndOrders
        LOG.info("Waiting for accounts coming...")
        AssertMessage(self.generalEvent.wait(TIMEOUT), "Accounts coming timeout!")
        self.generalEvent.clear()

        LOG.info("Select the first account")
        accounts = win32com.client.Dispatch(self.celEnvironment.cqgCEL.Accounts)
        self.account = win32com.client.Dispatch(accounts.ItemByIndex(0))

        for i in range(1, 4):
            symbol = f"{SYMBOL}?{i}"
            LOG.info("%s instrument requesting...", symbol)
            self.pendingInstruments.add(symbol)
            self.celEnvironment.cqgCEL.NewInstrument(symbol)

        LOG.info("Waiting for instruments to resolve...")
        AssertMessage(self.generalEvent.wait(TIMEOUT), "Instruments resolution timeout!")
        self.generalEvent.clear()

        LOG.info("Creating an order chain for initial compound order...")
        orderChain = win32com.client.Dispatch(self.celEnvironment.cqgCEL.CreateOrderChain())
        self.compoundOrderGUID = orderChain.GUID

        LOG.info("Adding orders to chain")
        for instrument in self.instruments:
            order = self.createOrder(instrument)
            orderChain.AddOrder(order)

        LOG.info("Place the compound order with GUID=%s", orderChain.GUID)
        self.celEnvironment.cqgCEL.PlaceCompoundOrderEx(CO_TYPE, orderChain)
        LOG.info("Waiting for compound order legs placing...")
        AssertMessage(self.generalEvent.wait(TIMEOUT), "Order placing timeout!")
        self.generalEvent.clear()

        LOG.info("Extend compound order")
        symbol = f"{SYMBOL}?4"
        LOG.info("%s instrument requesting...", symbol)
        self.pendingInstruments.add(symbol)
        self.celEnvironment.cqgCEL.NewInstrument(symbol)

        LOG.info("Waiting for %s to resolve...", symbol)
        AssertMessage(self.generalEvent.wait(TIMEOUT), f"{symbol} resolution timeout!")
        self.generalEvent.clear()

        LOG.info("Create an order chain for the compound order extending")
        newOrderChain = self.celEnvironment.cqgCEL.CreateOrderChain()
        order = self.createOrder(self.instruments[-1])
        newOrderChain.AddOrder(order)

        # get compound order ID from active order
        properties = win32com.client.Dispatch(self.orders[0].Properties)
        compoundOrderID = properties(constants.opCompoundOrderID)

        LOG.info("Modifying the compound order...")
        # do not change CO_TYPE for modification operation, otherwise Gateway will reject it
        self.celEnvironment.cqgCEL.ModifyCompoundOrderEx(compoundOrderID, CO_TYPE, newOrderChain)

        LOG.info("Waiting for the compound order to be modified...")
        AssertMessage(self.generalEvent.wait(TIMEOUT), "Order modification timeout!")
        self.generalEvent.clear()

        LOG.info("Wait for orders to be filled or cancelled. Press ENTER to stop and exit...")
        input()
        if self.activeOrders:
            # cancel orders of any left
            self.aborting = True
            for guid in self.activeOrders:
                order = win32com.client.Dispatch(self.celEnvironment.cqgCEL.InternalOrders.ItemByGuid(guid))
                if order.CanBeCanceled:
                    order.Cancel()

            LOG.info("Waiting for orders cancelling...")
            AssertMessage(self.generalEvent.wait(TIMEOUT), "Orders cancelling timeout!")

        LOG.info("Logoff from GW")
        self.eventGatewayIsDown.clear()
        self.celEnvironment.cqgCEL.GWLogoff()
        AssertMessage(self.eventGatewayIsDown.wait(TIMEOUT), "GW disconnection timeout!")

        LOG.info("Done!")

    def OnGWConnectionStatusChanged(self, newStatus):
        if newStatus == constants.csConnectionUp:
            LOG.info("GW connection is UP!")
            self.eventGatewayIsUp.set()
        if newStatus == constants.csConnectionDown:
            LOG.info("GW connection is DOWN!")
            self.eventGatewayIsDown.set()

    def OnAccountChanged(self, changeType, cqgAccount, cqgPosition):
        if changeType != constants.actAccountsReloaded:
            return

        LOG.info("Accounts are ready!")
        self.generalEvent.set()

    def OnInstrumentResolved(self, symbol, cqgInstrument, cqgError):
        if cqgError is not None:
            error = win32com.client.Dispatch(cqgError)
            LOG.error("OnInstrumentResolved error: Error code: %d Description: '%s'",
                      error.Code, error.Description)
            return

        assert symbol in self.pendingInstruments
        self.pendingInstruments.remove(symbol)
        self.instruments.append(cqgInstrument)
        LOG.info("Symbol '%s' is resolved!", symbol)
        if not self.pendingInstruments:
            self.generalEvent.set()

    def OnOrderChanged(self, changeType, cqgOrder, oldProperties, cqgFill, cqgError):
        if cqgError is not None:
            error = win32com.client.Dispatch(cqgError)
            LOG.error("OnOrderChanged error: Code: %d Description: %s", error.Code, error.Description)
            return

        order = win32com.client.Dispatch(cqgOrder)
        props = win32com.client.Dispatch(order.Properties)
        compoundOrderID = props(constants.opCompoundOrderID).Value if props(constants.opCompoundOrderID) else ""
        compoundOrderGUID = props(constants.opCompoundOrderGUID).Value if props(constants.opCompoundOrderGUID) else ""

        LOG.info("OnOrderChanged: ID: '%s'; CO_ID: '%s'; GUID: '%s', CO_GUID: '%s', "
                 "change type: %d; GW status: %d; Quantity: %d; Instrument: '%s'",
                 order.GWOrderID, compoundOrderID, order.GUID, compoundOrderGUID,
                 changeType, order.GWStatus, order.Quantity, order.InstrumentName)

        if compoundOrderGUID != self.compoundOrderGUID:
            # skip order updates that are not related to our compound order
            return

        if order.GWStatus in (constants.osInOrderBook, constants.osContingent):
            LOG.info("Compound order leg is placed!")
            if not self.pendingOrders or order.GUID not in self.pendingOrders:
                return

            self.activeOrders.add(order.GUID)
            self.pendingOrders.remove(order.GUID)
            if not self.pendingOrders:
                # we've just removed last one
                self.generalEvent.set()
        elif order.GWStatus in (constants.osFilled, constants.osCanceled):
            action = "filled" if order.GWStatus == constants.osFilled else "cancelled"
            LOG.info("Compound order leg is %s!", action)
            if not self.activeOrders or order.GUID not in self.activeOrders:
                return

            self.activeOrders.remove(order.GUID)
            if not self.pendingOrders and not self.activeOrders:
                # we've just removed last active order and we don't have any pending ones
                LOG.info("All orders has been filled or cancelled.")
                if not self.aborting:
                    # compound order filled successfully, we are still in input() call
                    LOG.info("Press ENTER to exit...")
                self.generalEvent.set()


if __name__ == "__main__":
    CELEnvironment.Start(CompoundOrdersPlacingSample)
