import json
import requests
import threading
import websocket
import logging
import enum
import datetime
from time import sleep
from bs4 import BeautifulSoup
from collections import OrderedDict
from protlib import CUInt, CStruct, CULong, CUChar, CArray, CUShort, CString
from collections import namedtuple

Instrument = namedtuple('Instrument', ['exchange', 'token', 'symbol',
                                       'name', 'expiry', 'lot_size'])
logger = logging.getLogger(__name__)

class Requests(enum.Enum):
    PUT = 1
    DELETE = 2
    GET = 3
    POST = 4

class TransactionType(enum.Enum):
    Buy = 'BUY'
    Sell = 'SELL'

class OrderType(enum.Enum):
    Market = 'MARKET'
    Limit = 'LIMIT'
    StopLossLimit = 'SL'
    StopLossMarket = 'SL-M'

class ProductType(enum.Enum):
    Intraday = 'I'
    Delivery = 'D'
    CoverOrder = 'CO'
    BracketOrder = 'BO'

class LiveFeedType(enum.Enum):
    MARKET_DATA = 1
    COMPACT = 2
    SNAPQUOTE = 3
    FULL_SNAPQUOTE = 4

class WsFrameMode(enum.IntEnum):
    MARKETDATA          =    1
    COMPACT_MARKETDATA  =    2
    SNAPQUOTE           =    3
    FULL_SNAPQUOTE      =    4
    SPREADDATA          =    5
    SPREAD_SNAPQUOTE    =    6
    DPR                 =    7
    OI                  =    8
    MARKET_STATUS       =    9
    EXCHANGE_MESSAGES   =    10
    
class MarketData(CStruct):
    exchange = CUChar()
    token = CUInt()
    ltp = CUInt()
    ltt = CUInt()
    ltq = CUInt()
    volume = CUInt()
    best_bid_price = CUInt()
    best_bid_quantity = CUInt()
    best_ask_price = CUInt()
    best_ask_quantity = CUInt()
    total_buy_quantity = CULong()
    total_sell_quantity = CULong()
    atp = CUInt()
    exchange_time_stamp = CUInt()
    open = CUInt()
    high = CUInt()
    low = CUInt()
    close = CUInt()
    yearly_high = CUInt()
    yearly_low = CUInt()
        
class CompactData(CStruct):
    exchange = CUChar()
    token = CUInt()
    ltp = CUInt()
    change = CUInt()
    exchange_time_stamp = CUInt()
    volume = CUInt()
     
class SnapQuote(CStruct):
    exchange = CUChar()
    token = CUInt()
    buyers = CArray(5, CUInt)
    bid_prices = CArray(5, CUInt)
    bid_quantities = CArray(5, CUInt)
    sellers = CArray(5, CUInt)
    ask_prices = CArray(5, CUInt)
    ask_quantities = CArray(5, CUInt)
    exchange_time_stamp = CUInt()
 
class FullSnapQuote(CStruct):
    exchange = CUChar()
    token = CUInt()
    buyers = CArray(5, CUInt)
    bid_prices = CArray(5, CUInt)
    bid_quantities = CArray(5, CUInt)
    sellers = CArray(5, CUInt)
    ask_prices = CArray(5, CUInt)
    ask_quantities = CArray(5, CUInt)
    atp = CUInt()
    open = CUInt()
    high = CUInt()
    low = CUInt()
    close = CUInt()
    total_buy_quantity = CULong()
    total_sell_quantity = CULong()
    volume = CUInt()

class DPR(CStruct):
    exchange = CUChar()
    token = CUInt()
    exchange_time_stamp = CUInt()
    high = CUInt()
    low = CUInt()

class OpenInterest(CStruct):
    exchange = CUChar()
    token = CUInt()
    current_open_interest = CUChar()
    initial_open_interest = CUChar()
    exchange_time_stamp = CUInt()

class ExchangeMessage(CStruct):
    exchange = CUChar()
    length = CUShort()
    message = CString(length = "length")
    exchange_time_stamp = CUInt()
 
class MarketStatus(CStruct):
    exchange = CUChar()
    length_of_market_type = CUShort()
    market_type = CString(length = "length_of_market_type")
    length_of_status = CUShort()
    status = CString(length = "length_of_status")

class AliceBlue:
    # dictionary object to hold settings
    __service_config = {
      'host': 'https://ant.aliceblueonline.com',
      'routes': {
          'authorize': '/oauth2/auth',
          'access_token': '/oauth2/token',
          'profile': '/api/v2/profile',
          'master_contract': '/api/v2/contracts.json?exchanges={exchange}',
          'holdings': '/api/v2/holdings',
          'balance': '/api/v2/cashposition',
          'positions_daywise': '/api/v2/positions?type=daywise',
          'positions_netwise': '/api/v2/positions?type=netwise',
          'positions_holdings': '/api/v2/holdings',
          'place_order': '/api/v2/order',
          'place_amo': '/api/v2/amo',
          'place_bracket_order': '/api/v2/bracketorder',
          'place_basket_order' : '/api/v2/basketorder',
          'get_orders': '/api/v2/order',
          'get_order_info': '/api/v2/order/{order_id}',
          'modify_order': '/api/v2/order',
          'cancel_order': '/api/v2/order?oms_order_id={order_id}&order_status=open',
          'cancel_bo_order': '/api/v2/order?oms_order_id={order_id}&order_status=open&leg_order_indicator={leg_order_id}',
          'cancel_co_order': '/api/v2/coverorder?oms_order_id={order_id}&order_status=open&leg_order_indicator={leg_order_id}',
          'trade_book': '/api/v2/trade',
          'scripinfo': '/api/v2/scripinfo?exchange={exchange}&instrument_token={token}',
      },
      'socket_endpoint': 'wss://ant.aliceblueonline.com/hydrasocket/v2/websocket?access_token={access_token}'
    }

    def __init__(self, username, password, access_token, master_contracts_to_download = None):
        """ logs in and gets enabled exchanges and products for user """
        self.__access_token = access_token
        self.__username = username
        self.__password = password
        self.__websocket = None
        self.__websocket_connected = False
        self.__ws_mutex = threading.Lock()
        self.__on_error = None
        self.__on_disconnect = None
        self.__on_open = None
        self.__subscribe_callback = None
        self.__order_update_callback = None
        self.__market_status_messages_callback = None
        self.__exchange_messages_callback = None
        self.__oi_callback = None
        self.__dpr_callback = None
        self.__subscribers = {}
        self.__market_status_messages = []
        self.__exchange_messages = []
        self.__exchange_codes = {'NSE' : 1,
                                 'NFO' : 2,
                                 'CDS' : 3,
                                 'MCX' : 4,
                                 'BSE' : 6,
                                 'BFO' : 7}
        self.__exchange_price_multipliers = {1: 100,
                                             2: 100,
                                             3: 10000000,
                                             4: 100,
                                             6: 100,
                                             7: 100}

        try:
            profile = self.get_profile()
        except Exception as e:
            raise Exception(f"Couldn't get profile info with credentials provided '{e}'")
        if(profile['status'] == 'error'):
            if(profile['message'] == 'Not able to retrieve AccountInfoService'):     # Don't know why this error comes, but it safe to proceed further.
                logger.warning("Couldn't get profile info - 'Not able to retrieve AccountInfoService'")
            else:
                raise Exception(f"Couldn't get profile info '{profile['message']}'")
        self.__master_contracts_by_token = {}
        self.__master_contracts_by_symbol = {}
        if(master_contracts_to_download == None):
            for e in self.__enabled_exchanges:
                self.__get_master_contract(e)
        else:
            for e in master_contracts_to_download:
                self.__get_master_contract(e)
        self.ws_thread = None

    @staticmethod
    def login_and_get_access_token(username, password, twoFA, api_secret, redirect_url='https://ant.aliceblueonline.com/plugin/callback', app_id=None):
        """ Login and get access token """
        #Get the Code
        if(app_id is None):
            app_id = username
        r = requests.Session()
        config = AliceBlue.__service_config
        url = f"{config['host']}{config['routes']['authorize']}?response_type=code&state=test_state&client_id={app_id}&redirect_uri={redirect_url}" 
        resp = r.get(url)
        if('OAuth 2.0 Error' in resp.text):
            logger.error("OAuth 2.0 Error occurred. Please verify your api_secret")
            return None
        page = BeautifulSoup(resp.text, features="html.parser")
        csrf_token = page.find('input', attrs = {'name':'_csrf_token'})['value']
        login_challenge = page.find('input', attrs = {'name' : 'login_challenge'})['value']
        resp = r.post(resp.url,data={'client_id':username,'password':password,'login_challenge':login_challenge,'_csrf_token':csrf_token})
        if('Please Enter Valid Password' in resp.text):
            logger.error("Please enter a valid password")
            return
        if('Internal server error' in resp.text):
            logger.error("Got Internal server error, please try again after sometimes")
            return
        question_ids = []
        page = BeautifulSoup(resp.text, features="html.parser")
        err = page.find('p', attrs={'class':'error'})
        if(len(err) > 0):
            logger.error(f"Couldn't login {err}")
            return
        for i in page.find_all('input', attrs={'name':'question_id1'}):
            question_ids.append(i['value'])
        logger.info(f"Assuming answers for all 2FA questions are '{twoFA}', Please change it to '{twoFA}' if not")
        resp = r.post(resp.url,data={'answer1':twoFA,'question_id1':question_ids,'answer2':twoFA,'login_challenge':login_challenge,'_csrf_token':csrf_token})
        if('consent_challenge' in resp.url):
            logger.info("Authorizing app for the first time")
            page = BeautifulSoup(resp.text, features="html.parser")
            csrf_token = page.find('input', attrs = {'name':'_csrf_token'})['value']
            resp = r.post(url=resp.url,data={'_csrf_token':csrf_token, 'consent': "Authorize", "scopes": ""})
            if('Internal server error' in resp.text):
                logger.error(f"Getting 'Internal server error' while authorizing the app for the first time. Please login manually using the following url '{url}'")
                return
        code = resp.url[resp.url.index('=')+1:resp.url.index('&')]

        #Get Access Token
        params = {'code': code, 'redirect_uri': redirect_url, 'grant_type': 'authorization_code', 'client_secret' : api_secret, "cliend_id": username}
        url = f"{config['host']}{config['routes']['access_token']}?client_id={app_id}&client_secret={api_secret}&grant_type=authorization_code&code={code}&redirect_uri={redirect_url}&authorization_response={resp.url}"
        resp = r.post(url,auth=(app_id, api_secret),data=params)
        resp = json.loads(resp.text)
        if('access_token' in resp):
            access_token = resp['access_token']
            logger.info(f'access_token - {access_token}')
            return access_token
        else:
            logger.error(f"Couldn't get access token {resp}")
        return None

    def __convert_prices(self, dictionary, multiplier):
        keys = ['ltp', 
                'best_bid_price',
                'best_ask_price',
                'atp',
                'open',
                'high',
                'low',
                'close',
                'yearly_high',
                'yearly_low']
        for key in keys:
            if(key in dictionary):
                dictionary[key] = dictionary[key]/multiplier
        multiple_value_keys = ['bid_prices', 'ask_prices']
        for key in multiple_value_keys:
            if(key in dictionary):
                new_values = []
                for value in dictionary[key]:
                    new_values.append(value/multiplier)
                dictionary[key] = new_values
        return dictionary
    
    def __conver_exchanges(self, dictionary):
        if('exchange' in dictionary):
            d = self.__exchange_codes
            dictionary['exchange'] = list(d.keys())[list(d.values()).index(dictionary['exchange'])]
        return dictionary

    def __convert_instrument(self, dictionary):
        if('exchange' in dictionary) and ('token' in dictionary):
            dictionary['instrument'] = self.get_instrument_by_token(dictionary['exchange'], dictionary['token'])
        return dictionary
        
    def __modify_human_readable_values(self, dictionary):
        dictionary = self.__convert_prices(dictionary, self.__exchange_price_multipliers[dictionary['exchange']])
        dictionary = self.__conver_exchanges(dictionary)
        dictionary = self.__convert_instrument(dictionary)
        return dictionary

    def __on_data_callback(self, ws=None, message=None, data_type=None, continue_flag=None):
        if(type(ws) is not websocket.WebSocketApp): # This workaround is to solve the websocket_client's compatiblity issue of older versions. ie.0.40.0 which is used in upstox. Now this will work in both 0.40.0 & newer version of websocket_client
            message = ws
        if(message[0] == WsFrameMode.MARKETDATA):
            p = MarketData.parse(message[1:]).__dict__
            res = self.__modify_human_readable_values(p) 
            if(self.__subscribe_callback is not None):
                self.__subscribe_callback(res)
        elif(message[0] == WsFrameMode.COMPACT_MARKETDATA):
            p = CompactData.parse(message[1:]).__dict__
            res = self.__modify_human_readable_values(p) 
            if(self.__subscribe_callback is not None):
                self.__subscribe_callback(res)
        elif(message[0] == WsFrameMode.SNAPQUOTE):
            p = SnapQuote.parse(message[1:]).__dict__
            res = self.__modify_human_readable_values(p) 
            if(self.__subscribe_callback is not None):
                self.__subscribe_callback(res)
        elif(message[0] == WsFrameMode.FULL_SNAPQUOTE):
            p = FullSnapQuote.parse(message[1:]).__dict__
            res = self.__modify_human_readable_values(p)
            if(self.__subscribe_callback is not None):
                self.__subscribe_callback(res)
        elif(message[0] == WsFrameMode.DPR):
            p = DPR.parse(message[1:]).__dict__
            res = self.__modify_human_readable_values(p) 
            if(self.__dpr_callback is not None):
                self.__dpr_callback(res)
        elif(message[0] == WsFrameMode.OI):
            p = OpenInterest.parse(message[1:]).__dict__
            res = self.__modify_human_readable_values(p) 
            if(self.__oi_callback is not None):
                self.__oi_callback(res)
        elif(message[0] == WsFrameMode.MARKET_STATUS):
            p = MarketStatus.parse(message[1:]).__dict__
            res = self.__modify_human_readable_values(p)
            self.__market_status_messages.append(res) 
            if(self.__market_status_messages_callback is not None):
                self.__market_status_messages_callback(res)
        elif(message[0] == WsFrameMode.EXCHANGE_MESSAGES):
            p = ExchangeMessage.parse(message[1:]).__dict__
            res = self.__modify_human_readable_values(p)
            self.__exchange_messages.append(res) 
            if(self.__exchange_messages_callback is not None):
                self.__exchange_messages_callback(res)
         
    def __on_close_callback(self, ws=None):
        self.__websocket_connected = False
        if self.__on_disconnect:
            self.__on_disconnect()

    def __on_open_callback(self, ws=None):
        self.__websocket_connected = True
        self.__resubscribe()
        if self.__on_open:
            self.__on_open()

    def __on_error_callback(self, ws=None, error=None):
        if(type(ws) is not websocket.WebSocketApp): # This workaround is to solve the websocket_client's compatiblity issue of older versions. ie.0.40.0 which is used in upstox. Now this will work in both 0.40.0 & newer version of websocket_client
            error = ws
        if self.__on_error:
            self.__on_error(error)

    def __send_heartbeat(self):
        heart_beat = {"a": "h", "v": [], "m": ""}
        while True:
            sleep(5)
            self.__ws_send(json.dumps(heart_beat), opcode = websocket._abnf.ABNF.OPCODE_PING)

    def __ws_run_forever(self):
        while True:
            try:
                self.__websocket.run_forever()
            except Exception as e:
                logger.warning(f"websocket run forever ended in exception, {e}")
            sleep(0.1) # Sleep for 100ms between reconnection.

    def __ws_send(self, *args, **kwargs):
        while self.__websocket_connected == False:
            sleep(0.05)  # sleep for 50ms if websocket is not connected, wait for reconnection
        with self.__ws_mutex:
            ret = self.__websocket.send(*args, **kwargs)
        return ret

    def start_websocket(self, subscribe_callback = None, 
                                order_update_callback = None,
                                socket_open_callback = None,
                                socket_close_callback = None,
                                socket_error_callback = None,
                                run_in_background=False,
                                market_status_messages_callback = None,
                                exchange_messages_callback = None,
                                oi_callback = None,
                                dpr_callback = None):
        """ Start a websocket connection for getting live data """
        self.__on_open = socket_open_callback
        self.__on_disconnect = socket_close_callback
        self.__on_error = socket_error_callback
        self.__subscribe_callback = subscribe_callback
        self.__order_update_callback = order_update_callback
        self.__market_status_messages_callback = market_status_messages_callback
        self.__exchange_messages_callback = exchange_messages_callback
        self.__oi_callback = oi_callback
        self.__dpr_callback = dpr_callback
        
        url = self.__service_config['socket_endpoint'].format(access_token=self.__access_token)
        self.__websocket = websocket.WebSocketApp(url,
                                                on_data=self.__on_data_callback,
                                                on_error=self.__on_error_callback,
                                                on_close=self.__on_close_callback,
                                                on_open=self.__on_open_callback)
        th = threading.Thread(target=self.__send_heartbeat)
        th.daemon = True
        th.start()
        if run_in_background is True:
            self.__ws_thread = threading.Thread(target=self.__ws_run_forever)
            self.__ws_thread.daemon = True
            self.__ws_thread.start()
        else:
            self.__ws_run_forever()

    def get_profile(self):
        """ Get profile """
        profile = self.__api_call_helper('profile', Requests.GET, None, None)
        if(profile['status'] != 'error'):
            self.__enabled_exchanges = profile['data']['exchanges']
        return profile

    def get_balance(self):
        """ Get balance/margins """
        return self.__api_call_helper('balance', Requests.GET, None, None)

    def get_daywise_positions(self):
        """ Get daywise positions """
        return self.__api_call_helper('positions_daywise', Requests.GET, None, None)

    def get_netwise_positions(self):
        """ Get netwise positions """
        return self.__api_call_helper('positions_netwise', Requests.GET, None, None)

    def get_holding_positions(self):
        """ Get holding positions """
        return self.__api_call_helper('positions_holdings', Requests.GET, None, None)

    def get_order_history(self, order_id=None):
        """ leave order_id as None to get all entire order history """
        if order_id is None:
            return self.__api_call_helper('get_orders', Requests.GET, None, None);
        else:
            return self.__api_call_helper('get_order_info', Requests.GET, {'order_id': order_id}, None);
    
    def get_scrip_info(self, instrument):
        """ Get scrip information """
        params = {'exchange': instrument.exchange, 'token': instrument.token}
        return self.__api_call_helper('scripinfo', Requests.GET, params, None)

    def get_trade_book(self):
        """ get all trades """
        return self.__api_call_helper('trade_book', Requests.GET, None, None)

    def get_exchanges(self):
        """ Get enabled exchanges """
        return self.__enabled_exchanges

    def place_order(self, transaction_type, instrument, quantity, order_type,
                    product_type, price=0.0, trigger_price=None,
                    stop_loss=None, square_off=None, trailing_sl=None,
                    is_amo = False,
                    order_tag = 'order1'):
        """ placing an order, many fields are optional and are not required
            for all order types
        """
        if transaction_type is None:
            raise TypeError("Required parameter transaction_type not of type TransactionType")

        if not isinstance(instrument, Instrument):
            raise TypeError("Required parameter instrument not of type Instrument")

        if not isinstance(quantity, int):
            raise TypeError("Required parameter quantity not of type int")

        if order_type is None:
            raise TypeError("Required parameter order_type not of type OrderType")

        if product_type is None:
            raise TypeError("Required parameter product_type not of type ProductType")

        if price is not None and not isinstance(price, float):
            raise TypeError("Optional parameter price not of type float")

        if trigger_price is not None and not isinstance(trigger_price, float):
            raise TypeError("Optional parameter trigger_price not of type float")

        if(product_type == ProductType.Intraday):
            prod_type = 'MIS'
        elif(product_type == ProductType.Delivery):
            if(instrument.exchange == 'NFO') or (instrument.exchange == 'MCX') or (instrument.exchange == 'CDS'):
                prod_type = 'NRML'
            else:
                prod_type = 'CNC'
        elif(product_type == ProductType.CoverOrder):
            prod_type = 'CO'
        elif(product_type == ProductType.BracketOrder):
            prod_type = None
        # construct order object after all required parameters are met
        order = {  'exchange': instrument.exchange,
                   'order_type': order_type.value,
                   'instrument_token': instrument.token,
                   'quantity':quantity,
                   'disclosed_quantity':0,
                   'price':price,
                   'transaction_type':transaction_type.value,
                   'trigger_price':trigger_price,
                   'validity':'DAY',
                   'product':prod_type,
                   'source':'web',
                   'order_tag': order_tag}

        if stop_loss is not None and not isinstance(stop_loss, float):
            raise TypeError("Optional parameter stop_loss not of type float")
        elif stop_loss is not None:
            order['stop_loss_value'] = stop_loss

        if square_off is not None and not isinstance(square_off, float):
            raise TypeError("Optional parameter square_off not of type float")
        elif square_off is not None:
            order['square_off_value'] = square_off

        if trailing_sl is not None and not isinstance(trailing_sl, int):
            raise TypeError("Optional parameter trailing_sl not of type int")
        elif trailing_sl is not None:
            order['trailing_stop_loss'] = trailing_sl

        if product_type is ProductType.CoverOrder:
            if not isinstance(trigger_price, float):
                raise TypeError("Required parameter trigger_price not of type float")

        if(is_amo == True):
            helper = 'place_amo'
        else:
            helper = 'place_order'

        if product_type is ProductType.BracketOrder:
            helper = 'place_bracket_order'
            del order['product'] 
            if not isinstance(stop_loss, float):
                raise TypeError("Required parameter stop_loss not of type float")

            if not isinstance(square_off, float):
                raise TypeError("Required parameter square_off not of type float")
            
        return self.__api_call_helper(helper, Requests.POST, None, order)

    def place_basket_order(self, orders):
        """ placing a basket order, 
            Argument orders should be a list of all orders that should be sent
            each element in order should be a dictionary containing the following key.
            "instrument", "order_type", "quantity", "price" (only if its a limit order), 
            "transaction_type", "product_type"
        """
        keys = {"instrument"        : Instrument, 
                "order_type"        : OrderType, 
                "quantity"          : int,
                "transaction_type"  : TransactionType,
                "product_type"      : ProductType}
        if not isinstance(orders, list):
            raise TypeError("Required parameter orders is not of type list")

        if len(orders) <= 0:
            raise TypeError("Length of orders should be greater than 0")

        for i in orders:
            if not isinstance(i, dict):
                raise TypeError("Each element in orders should be of type dict")
            for s in keys:
                if s not in i:
                    raise TypeError(f"Each element in orders should have key {s}")
                if type(i[s]) is not keys[s]:  
                    raise TypeError(f"Element '{s}' in orders should be of type {keys[s]}")
            if i['order_type'] == OrderType.Limit:
                if "price" not in i:
                    raise TypeError("Each element in orders should have key 'price' if its a limit order ")
                if not isinstance(i['price'], float):
                    raise TypeError("Element price in orders should be of type float")
            else:
                i['price'] = 0.0
            if(i['product_type'] == ProductType.Intraday):
                i['product_type'] = 'MIS'
            elif(i['product_type'] == ProductType.Delivery):
                if(i['instrument'].exchange == 'NFO'):
                    i['product_type'] = 'NRML'
                else:
                    i['product_type'] = 'CNC'
            elif(i['product_type'] == ProductType.CoverOrder):
                raise TypeError("Product Type BO or CO is not supported in basket order")
            elif(i['product_type'] == ProductType.BracketOrder):
                raise TypeError("Product Type BO or CO is not supported in basket order")
            if i['quantity'] <= 0:
                raise TypeError("Quantity should be greater than 0")

        data = {'source':'web',
                'orders' : []}
        for i in orders:
            # construct order object after all required parameters are met
            data['orders'].append({'exchange'           : i['instrument'].exchange,
                                   'order_type'         : i['order_type'].value,
                                   'instrument_token'   : i['instrument'].token,
                                   'quantity'           : i['quantity'],
                                   'disclosed_quantity' : 0,
                                   'price'              : i['price'],
                                   'transaction_type'   : i['transaction_type'].value,
                                   'trigger_price'      : 0,
                                   'validity'           : 'DAY',
                                   'product'            : i['product_type']})

        helper = 'place_basket_order'
        return self.__api_call_helper(helper, Requests.POST, None, data)

    def modify_order(self, transaction_type, instrument, product_type, order_id, order_type, quantity=None, price=0.0,
                     trigger_price=0.0):
        """ modify an order, transaction_type, instrument, product_type, order_id & order_type is required, 
            rest are optional, use only when when you want to change that attribute.
        """
        if not isinstance(instrument, Instrument):
            raise TypeError("Required parameter instrument not of type Instrument")

        if not isinstance(order_id, str):
            raise TypeError("Required parameter order_id not of type str")

        if quantity is not None and not isinstance(quantity, int):
            raise TypeError("Optional parameter quantity not of type int")

        if type(order_type) is not OrderType:
            raise TypeError("Optional parameter order_type not of type OrderType")

        if ProductType is None:
            raise TypeError("Required parameter product_type not of type ProductType")

        if price is not None and not isinstance(price, float):
            raise TypeError("Optional parameter price not of type float")

        if trigger_price is not None and not isinstance(trigger_price, float):
            raise TypeError("Optional parameter trigger_price not of type float")

        if(product_type == ProductType.Intraday):
            product_type = 'MIS'
        elif(product_type == ProductType.Delivery):
            if(instrument.exchange == 'NFO'):
                product_type = 'NRML'
            else:
                product_type = 'CNC'
        elif(product_type == ProductType.CoverOrder):
            product_type = 'CO'
        elif(product_type == ProductType.BracketOrder):
            product_type = None
        # construct order object with order id
        order = {  'oms_order_id': str(order_id),  
                   'instrument_token': int(instrument.token),
                   'exchange': instrument.exchange,
                   'transaction_type':transaction_type.value,
                   'product':product_type,
                   'validity':'DAY',
                   'order_type': order_type.value,
                   'price':price,
                   'trigger_price':trigger_price,
                   'quantity':quantity,
                   'disclosed_quantity':0,
                   'nest_request_id' : '1'}
        return self.__api_call_helper('modify_order', Requests.PUT, None, order)

    def cancel_order(self, order_id, leg_order_id=None, is_co=False):
        """ Cancel single order """
        if(is_co == False):
            if(leg_order_id == None):
                ret = self.__api_call_helper('cancel_order', Requests.DELETE, {'order_id': order_id}, None)
            else:
                ret = self.__api_call_helper('cancel_bo_order', Requests.DELETE, {'order_id': order_id, 'leg_order_id': leg_order_id}, None)
        else:
            ret = self.__api_call_helper('cancel_co_order', Requests.DELETE, {'order_id': order_id, 'leg_order_id': leg_order_id}, None)
        return ret

    def cancel_all_orders(self):
        """ Cancel all orders """
        ret = []
        orders = self.get_order_history()['data']
        if not orders:
            return
        for c_order in orders['pending_orders']:
            if(c_order['product'] == 'BO' and c_order['leg_order_indicator']):
                r = self.cancel_order(c_order['leg_order_indicator'], leg_order_id = c_order['leg_order_indicator'])
            elif(c_order['product'] == 'CO'):
                r = self.cancel_order(c_order['oms_order_id'], leg_order_id = c_order['leg_order_indicator'], is_co = True)
            else:
                r = self.cancel_order(c_order['oms_order_id'])
            ret.append(r)
        return ret

    def subscribe_market_status_messages(self):
        """ Subscribe to market messages """
        return self.__ws_send(json.dumps({"a": "subscribe", "v": [1,2,3,4,6], "m": "market_status"}))

    def get_market_status_messages(self):
        """ Get market messages """
        return self.__market_status_messages
    
    def subscribe_exchange_messages(self):
        """ Subscribe to exchange messages """
        return self.__ws_send(json.dumps({"a": "subscribe", "v": [1,2,3,4,6], "m": "exchange_messages"}))

    def get_exchange_messages(self):
        """ Get stored exchange messages """
        return self.__exchange_messages
    
    def subscribe(self, instrument, live_feed_type):
        """ subscribe to the current feed of an instrument """
        if(type(live_feed_type) is not LiveFeedType):
            raise TypeError("Required parameter live_feed_type not of type LiveFeedType")
        arr = []
        if (isinstance(instrument, list)):
            for _instrument in instrument:
                if not isinstance(_instrument, Instrument):
                    raise TypeError("Required parameter instrument not of type Instrument")
                exchange = self.__exchange_codes[_instrument.exchange] 
                arr.append([exchange, int(_instrument.token)])
                self.__subscribers[_instrument] = live_feed_type
        else:
            if not isinstance(instrument, Instrument):
                raise TypeError("Required parameter instrument not of type Instrument")
            exchange = self.__exchange_codes[instrument.exchange]
            arr = [[exchange, int(instrument.token)]]
            self.__subscribers[instrument] = live_feed_type
        if(live_feed_type == LiveFeedType.MARKET_DATA):
            mode = 'marketdata' 
        elif(live_feed_type == LiveFeedType.COMPACT):
            mode = 'compact_marketdata' 
        elif(live_feed_type == LiveFeedType.SNAPQUOTE):
            mode = 'snapquote' 
        elif(live_feed_type == LiveFeedType.FULL_SNAPQUOTE):
            mode = 'full_snapquote' 
        data = json.dumps({'a' : 'subscribe', 'v' : arr, 'm' : mode})
        return self.__ws_send(data)

    def unsubscribe(self, instrument, live_feed_type):
        """ unsubscribe to the current feed of an instrument """
        if(type(live_feed_type) is not LiveFeedType):
            raise TypeError("Required parameter live_feed_type not of type LiveFeedType")
        arr = []
        if (isinstance(instrument, list)):
            for _instrument in instrument:
                if not isinstance(_instrument, Instrument):
                    raise TypeError("Required parameter instrument not of type Instrument")
                exchange = self.__exchange_codes[_instrument.exchange] 
                arr.append([exchange, int(_instrument.token)])
                if(_instrument in self.__subscribers): del self.__subscribers[_instrument]
        else:
            if not isinstance(instrument, Instrument):
                raise TypeError("Required parameter instrument not of type Instrument")
            exchange = self.__exchange_codes[instrument.exchange]
            arr = [[exchange, int(instrument.token)]]
            if(instrument in self.__subscribers): del self.__subscribers[instrument]
        if(live_feed_type == LiveFeedType.MARKET_DATA):
            mode = 'marketdata' 
        elif(live_feed_type == LiveFeedType.COMPACT):
            mode = 'compact_marketdata' 
        elif(live_feed_type == LiveFeedType.SNAPQUOTE):
            mode = 'snapquote' 
        elif(live_feed_type == LiveFeedType.FULL_SNAPQUOTE):
            mode = 'full_snapquote' 
        data = json.dumps({'a' : 'unsubscribe', 'v' : arr, 'm' : mode})
        return self.__ws_send(data)

    def get_all_subscriptions(self):
        """ get the all subscribed instruments """
        return self.__subscribers
    
    def __resubscribe(self):
        market = []
        compact = []
        snap = []
        full = []
        for key, value in self.get_all_subscriptions().items():
            if(value == LiveFeedType.MARKET_DATA):
                market.append(key) 
            elif(value == LiveFeedType.COMPACT):
                compact.append(key) 
            elif(value == LiveFeedType.SNAPQUOTE):
                snap.append(key) 
            elif(value == LiveFeedType.FULL_SNAPQUOTE):
                full.append(key)
        if(len(market) > 0):
            self.subscribe(market, LiveFeedType.MARKET_DATA)
        if(len(compact) > 0):
            self.subscribe(compact, LiveFeedType.COMPACT)
        if(len(snap) > 0):
            self.subscribe(snap, LiveFeedType.SNAPQUOTE)
        if(len(full) > 0):
            self.subscribe(full, LiveFeedType.FULL_SNAPQUOTE)

    def get_instrument_by_symbol(self, exchange, symbol):
        """ get instrument by providing symbol """
        # get instrument given exchange and symbol
        exchange = exchange.upper()
        # check if master contract exists
        if exchange not in self.__master_contracts_by_symbol:
            logger.warning(f"Cannot find exchange {exchange} in master contract. "
                            "Please ensure if that exchange is enabled in your profile and downloaded the master contract for the same")
            return None
        master_contract = self.__master_contracts_by_symbol[exchange]
        if symbol not in master_contract:
            logger.warning(f"Cannot find symbol {exchange} {symbol} in master contract")
            return None
        return master_contract[symbol]
    
    def get_instrument_for_fno(self, symbol, expiry_date, is_fut=False, strike=None, is_CE = False, exchange = 'NFO'):
        """ get instrument for FNO """
        res = self.search_instruments(exchange, symbol)
        if(res == None):
            return
        matches = []
        for i in res:
            if(i.expiry == expiry_date):
                matches.append(i)
        for i in matches:
            if(is_fut == True):
                if('FUT' in i.symbol):
                    return i
            else:
                sp = i.symbol.split(' ')
                if((sp[-1] == 'CE') or (sp[-1] == 'PE')):           # Only option scrips 
                    if(float(sp[-2]) == float(strike)):
                        if(is_CE == True):
                            if(sp[-1] == 'CE'):
                                return i
                        else:
                            if(sp[-1] == 'PE'):
                                return i
                            
    def search_instruments(self, exchange, symbol):
        """ Search instrument by symbol match """
        # search instrument given exchange and symbol
        exchange = exchange.upper()
        matches = []
        # check if master contract exists
        if exchange not in self.__master_contracts_by_token:
            logger.warning(f"Cannot find exchange {exchange} in master contract. "
                "Please ensure if that exchange is enabled in your profile and downloaded the master contract for the same")
            return None
        master_contract = self.__master_contracts_by_token[exchange]
        for contract in master_contract:
            if (isinstance(symbol, list)):
                for sym in symbol:
                    if sym.lower() in master_contract[contract].symbol.split(' ')[0].lower():
                        matches.append(master_contract[contract])
            else:
                if symbol.lower() in master_contract[contract].symbol.split(' ')[0].lower():
                    matches.append(master_contract[contract])
        return matches

    def get_instrument_by_token(self, exchange, token):
        """ Get instrument by providing token """
        # get instrument given exchange and token
        exchange = exchange.upper()
        token = int(token)
        # check if master contract exists
        if exchange not in self.__master_contracts_by_symbol:
            logger.warning(f"Cannot find exchange {exchange} in master contract. "
                            "Please ensure if that exchange is enabled in your profile and downloaded the master contract for the same")
            return None
        master_contract = self.__master_contracts_by_token[exchange]
        if token not in master_contract:
            logger.warning(f"Cannot find symbol {exchange} {token} in master contract")
            return None
        return master_contract[token]

    def get_master_contract(self, exchange):
        """ Get master contract """
        return self.__master_contracts_by_symbol[exchange]

    def __get_master_contract(self, exchange):
        """ returns all the tradable contracts of an exchange
            placed in an OrderedDict and the key is the token
        """
        logger.info(f'Downloading master contracts for exchange: {exchange}')
        body = self.__api_call_helper('master_contract', Requests.GET, {'exchange': exchange}, None)
        master_contract_by_token = OrderedDict()
        master_contract_by_symbol = OrderedDict()
        for sub in body:
            for scrip in body[sub]:
                # convert token
                token = int(scrip['code'])
    
                # convert symbol to upper
                symbol = scrip['symbol']
    
                # convert expiry to none if it's non-existent
                if('expiry' in scrip):
                    expiry = datetime.datetime.fromtimestamp(scrip['expiry']).date()
                else:
                    expiry = None
    
                # convert lot size to int
                if('lotSize' in scrip):
                    lot_size = scrip['lotSize']
                else:
                    lot_size = None
                    
                # Name & Exchange 
                name = scrip['company'] 
                exch = scrip['exchange']
    
                instrument = Instrument(exch, token, symbol, name, expiry, lot_size)
                master_contract_by_token[token] = instrument
                master_contract_by_symbol[symbol] = instrument
        self.__master_contracts_by_token[exchange] = master_contract_by_token
        self.__master_contracts_by_symbol[exchange] = master_contract_by_symbol

    def __api_call_helper(self, name, http_method, params, data):
        # helper formats the url and reads error codes nicely
        config = self.__service_config
        url = f"{config['host']}{config['routes'][name]}"
        if params is not None:
            url = url.format(**params)
        response = self.__api_call(url, http_method, data)
        if response.status_code != 200:
            raise requests.HTTPError(response.text)
        return json.loads(response.text)

    def __api_call(self, url, http_method, data):
        #logger.debug('url:: %s http_method:: %s data:: %s headers:: %s', url, http_method, data, headers)
        headers = {"Content-Type": "application/json"} 
        if(len(self.__access_token) > 100):
            headers['X-Authorization-Token'] = self.__access_token
            headers['Connection'] = 'keep-alive'
        else:
            headers['client_id'] = self.__username
            headers['authorization'] = f"Bearer {self.__access_token}"
        r = None
        if http_method is Requests.POST:
            r = requests.post(url, data=json.dumps(data), headers=headers)
        elif http_method is Requests.DELETE:
            r = requests.delete(url, headers=headers)
        elif http_method is Requests.PUT:
            r = requests.put(url, data=json.dumps(data), headers=headers)
        elif http_method is Requests.GET:
            r = requests.get(url, headers=headers)
        return r