"""The sample demonstrates how to request options chain by commodity and output strike properties with updates."""

import logging
import threading
from datetime import datetime

from dateutil.relativedelta import relativedelta
from tabulate import tabulate
from win32com.client import Dispatch, constants

import CELEnvironment
from CELEnvironment import AssertMessage

# region Settings

# Replace it with enabled symbol
COMMODITY = "EP"

# Possible filters: only calls (itOptionCall), only puts (itOptionPut) or both (itAllOptions).
TYPE: int = constants.itOptionCall

# First option month
RANGE_START = datetime.today()
# Last option month
RANGE_END = RANGE_START + relativedelta(months=3)

# how many strikes (rows) to show
CHUNK_SIZE = 20

# show strikes that equal or more to the provided one:
START_WITH_STRIKE = 5775_0

# properties to show in the output
PROPERTY_TYPES = [constants.ipCOI, constants.ipTCOI, constants.ipPCOI]

# the value for CQGInstrumentsGroupRequest.SubscriptionLevel property; shall be eGroupSubscriptionLevel enum value
SUBSCRIPTION_LEVEL = constants.gslFullUpdates

# the value for CQGInstrumentsGroupRequest.PropertyLevel property; shall be mask of EGroupPropertySubscriptionLevel
# enum values
PROPERTY_LEVEL = constants.gpsContractOpenInterestExtended

# 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 InstrumentsGroupRequest(CELEnvironment.CELSinkBase):
    def __init__(self):
        self.eventDone = threading.Event()
        self.instruments = None
        self.lastStrike = START_WITH_STRIKE
        self.propertyNames = {}

    def Init(self, celEnvironment):
        self.celEnvironment: CELEnvironment.CELEnvironment = celEnvironment

    def Start(self):
        request = Dispatch(self.celEnvironment.cqgCEL.CreateInstrumentsGroupRequest())
        request.Commodity = COMMODITY
        request.SubscriptionLevel = SUBSCRIPTION_LEVEL
        # next 3 lines are optional, see default values on CQGInstrumentsGroupRequest doc page
        request.InstrumentTypes = TYPE
        request.RangeStart = RANGE_START
        request.RangeEnd = RANGE_END
        request.PropertyLevel = PROPERTY_LEVEL

        LOG.info("Request options chain for %s", COMMODITY)
        self.celEnvironment.cqgCEL.RequestInstrumentsGroup(request, False)
        AssertMessage(self.eventDone.wait(120), "Timeout on instruments group resolution")
        if self.celEnvironment.cqgCEL.AllInstrumentsGroups.Count == 0:
            # resolution failed, error is reported in OnInstrumentsGroupResolved
            return

        input()

    def OnDataError(self, cqgError, errorDescription):
        if cqgError is not None:
            error = Dispatch(cqgError)
            LOG.error("OnDataError: Code: %d Description: %s", error.Code, error.Description)

        self.eventDone.set()

    def OnInstrumentsGroupResolved(self, cqgInstrumentsGroup, cqgError):
        if cqgError is not None:
            error = Dispatch(cqgError)
            LOG.error("OnInstrumentsGroupResolved error: Error code: %d Description: %s", error.Code, error.Description)
            self.eventDone.set()
            return

        instrumentGroup = Dispatch(cqgInstrumentsGroup)
        LOG.info("Resolved %d contracts.", instrumentGroup.Instruments.Count)

        self.instruments = sorted(instrumentGroup.Instruments, key=lambda i: i.Strike)
        self.instruments = [i for i in self.instruments if i.Strike >= START_WITH_STRIKE][:CHUNK_SIZE]
        self.lastStrike = self.instruments[-1].Strike
        self.propertyNames = {
            propType: self.instruments[0].Properties.Item(propType).Name for propType in PROPERTY_TYPES
        }
        self.PrintProperties()
        self.eventDone.set()

    def OnInstrumentsGroupChanged(self, changeType, cqgInstrumentsGroup, instrumentsNames):
        LOG.info(
            "OnInstrumentsGroupChanged: changeType=%d. Instruments that changed: %s", changeType, list(instrumentsNames)
        )

    def OnInstrumentChanged(self, cqgInstrument, cqgQuotes, cqgInstrumentProperties):
        if not self.eventDone.is_set():
            # ignore updates until we resolve the whole group
            return

        instrument = Dispatch(cqgInstrument)
        if not START_WITH_STRIKE <= instrument.Strike <= self.lastStrike:
            # the strike is not in our tracked range
            return

        properties = Dispatch(cqgInstrumentProperties)
        if any(prop.Type in PROPERTY_TYPES for prop in properties):
            self.PrintProperties()

    def PrintProperties(self):
        def _getDisplayValue(value):
            return value if self.celEnvironment.cqgCELForCallback.IsValid(value) else "N/A"

        data = {"Contract": [instrument.FullName for instrument in self.instruments]}

        for propType in PROPERTY_TYPES:
            data[f"{self.propertyNames[propType]} Time"] = [
                _getDisplayValue(i.Properties.Item(propType).ServerTimestamp) for i in self.instruments
            ]
            data[f"{self.propertyNames[propType]} Value"] = [
                _getDisplayValue(i.Properties.Item(propType).Value) for i in self.instruments
            ]

        LOG.info("Properties:\n%s\nPress ENTER to exit\n", tabulate(data, headers=data.keys()))


if __name__ == "__main__":
    CELEnvironment.Start(InstrumentsGroupRequest)
