from quantplay.broker.finvasia_utils.shoonya import ShoonyaApiPy
from quantplay.utils.constant import Constants, OrderType
# from quantplay.broker.generics.broker import Broker
from quantplay.exception.exceptions import InvalidArgumentException
import pyotp

from quantplay.utils.pickle_utils import PickleUtils

import pandas as pd
import numpy as np
from retrying import retry
import time
import json
import traceback
import copy

logger = Constants.logger


class FinvAsia(Broker):

    def __init__(self,
                 order_updates=None,
                 api_secret=None,
                 imei=None,
                 password=None,
                 totp_key=None,
                 user_id=None,
                 vendor_code=None,
                 load_instruments=True):
        super(FinvAsia, self).__init__()
        self.order_updates = order_updates

        self.api = ShoonyaApiPy()
        totp = pyotp.TOTP(totp_key).now()
        response = self.api.login(userid=user_id,
                                  password=password,
                                  twoFA=totp,
                                  vendor_code=vendor_code,
                                  api_secret=api_secret,
                                  imei=imei)
        print("finvasia login successful email {} account_id {}".format(response['email'], response['actid']))

        self.set_attributes(response)

        if load_instruments:
            self.load_instrument()

        self.order_type_sl = "SL-LMT"

    def set_attributes(self, response):
        self.email = response['email']
        self.user_id = response['actid']
        self.full_name = response['uname']

    def initialize_symbol_data(self):
        instruments = self.instrument_data
        instruments = instruments.to_dict('records')
        self.symbol_data = {}
        for instrument in instruments:
            exchange = instrument['exchange']
            tradingsymbol = instrument['TradingSymbol']

            instrument_data = copy.deepcopy(instrument)
            self.symbol_data["{}:{}".format(exchange, tradingsymbol)] = instrument_data

        PickleUtils.save_data(self.symbol_data, "shoonya_instruments")

    def load_instrument(self):
        try:
            self.symbol_data = PickleUtils.load_data("shoonya_instruments")
            Constants.logger.info("[LOADING_INSTRUMENTS] loading data from cache")
        except Exception as e:
            Constants.logger.info("[LOADING_INSTRUMENTS] loading data from server")
            instrument_file_EQ = self.get_df_from_zip("https://api.shoonya.com/NSE_symbols.txt.zip")
            instrument_file_FO = self.get_df_from_zip("https://api.shoonya.com/NFO_symbols.txt.zip")
            instrument_file_MCX = self.get_df_from_zip("https://api.shoonya.com/MCX_symbols.txt.zip")

            self.instrument_data = pd.concat([instrument_file_MCX, instrument_file_FO, instrument_file_EQ])
            self.instrument_data.loc[:, 'instrument_symbol'] = self.instrument_data.Symbol
            self.instrument_data.loc[:, 'instrument_expiry'] = self.instrument_data.Expiry
            self.instrument_data.loc[:, 'strike_price'] = self.instrument_data.StrikePrice
            self.instrument_data.loc[:, 'exchange'] = self.instrument_data.Exchange
            self.instrument_data.loc[:, 'option_type'] = self.instrument_data.OptionType
            self.instrument_data.loc[:, 'instrument'] = self.instrument_data.Instrument
            self.instrument_data.loc[:, 'lot_size'] = self.instrument_data.LotSize

            self.initialize_expiry_fields()
            self.add_quantplay_opt_tradingsymbol()
            self.add_quantplay_fut_tradingsymbol()

            self.instrument_data = self.instrument_data[['Token', 'TradingSymbol', 'strike_price', 'exchange',
                                                          'option_type', 'instrument', 'tradingsymbol', 'expiry']]

            self.initialize_symbol_data()

        self.fno_symbol_map = {}
        for a in self.symbol_data:
            self.fno_symbol_map[self.symbol_data[a]['TradingSymbol']] = self.symbol_data[a]['tradingsymbol']
        self.quantplay_symbol_map = {v: k for k, v in self.fno_symbol_map.items()}


    def get_symbol(self, symbol):
        if symbol not in self.quantplay_symbol_map:
            return symbol
        return self.quantplay_symbol_map[symbol]

    def get_transaction_type(self, transaction_type):
        if transaction_type == "BUY":
            return "B"
        elif transaction_type == "SELL":
            return "S"

        raise InvalidArgumentException("transaction type {} not supported for trading".format(transaction_type))

    def get_order_type(self, order_type):
        if order_type == OrderType.market:
            return "MKT"
        elif order_type == OrderType.sl:
            return "SL-LMT"
        elif order_type == OrderType.slm:
            return "SL-MKT"
        elif order_type == OrderType.limit:
            return "LMT"
        elif order_type in [self.order_type_sl]:
            return order_type

        raise InvalidArgumentException("order type {} not supported for trading".format(order_type))

    def get_product(self, product):
        if product == "NRML":
            return "M"
        elif product == "CNC":
            return "C"
        elif product == "MIS":
            return "I"
        elif product in ["M", "C", "I"]:
            return product

        raise InvalidArgumentException("Product {} not supported for trading".format(product))

    def event_handler_order_update(self, order):
        try:
            order['placed_by'] = order['actid']
            order['tag'] = order['actid']
            order['order_id'] = order['norenordno']
            order['exchange_order_id'] = order['exchordid']
            order['exchange'] = order['exch']

            # TODO translate symbol
            # -EQ should be removed
            # F&O symbol translation
            order['tradingsymbol'] = order['tsym']

            if order['exchange'] == "NSE":
                order['tradingsymbol'] = order['tradingsymbol'].replace("-EQ", "")
            elif order['exchange'] in ["NFO", "MCX"]:
                order["tradingsymbol"] = self.fno_symbol_map[order["tradingsymbol"]]

            order['order_type'] = order['prctyp']
            if order['order_type'] == "LMT":
                order['order_type'] = "LIMIT"
            elif order['order_type'] == "MKT":
                order['order_type'] = "MARKET"
            elif order['order_type'] == "SL-LMT":
                order['order_type'] = "SL"

            if order['pcode'] == "M":
                order['product'] = "NRML"
            elif order['pcode'] == "C":
                order['product'] = "CNC"
            elif order['pcode'] == "I":
                order['product'] = "MIS"

            if order['trantype'] == "S":
                order['transaction_type'] = "SELL"
            elif order['trantype'] == "B":
                order['transaction_type'] = "BUY"
            else:
                logger.error("[UNKNOW_VALUE] finvasia transaction type {} not supported".format(order['trantype']))

            order['quantity'] = int(order['qty'])

            if 'trgprc' in order:
                order['trigger_price'] = float(order['trgprc'])
            else:
                order['trigger_price'] = None

            order['price'] = float(order['prc'])

            if order["status"] == "TRIGGER_PENDING":
                order["status"] = "TRIGGER PENDING"
            elif order["status"] == "CANCELED":
                order["status"] = "CANCELLED"

            print(f"order feed {order}")
            self.order_updates.put(order)
        except Exception as e:
            logger.error("[ORDER_UPDATE_PROCESSING_FAILED] {}".format(e))

    def place_order(self, tradingsymbol=None, exchange=None, quantity=None, order_type=None, transaction_type=None,
                    tag=None, product=None, price=None, trigger_price=None):
        try:
            if trigger_price == 0:
                trigger_price = None

            transaction_type = self.get_transaction_type(transaction_type)
            order_type = self.get_order_type(order_type)
            product = self.get_product(product)
            tradingsymbol = self.get_symbol(tradingsymbol)

            data = {
                "product_type": product,
                "buy_or_sell": transaction_type,
                "exchange": exchange,
                "tradingsymbol": tradingsymbol,
                "quantity": quantity,
                "price_type": order_type,
                "price": price,
                "trigger_price": trigger_price,
                "remarks": tag
            }
            Constants.logger.info("[PLACING_ORDER] {}".format(json.dumps(data)))
            response = self.api.place_order(buy_or_sell=transaction_type,
                                            product_type=product,
                                            exchange=exchange,
                                            tradingsymbol=tradingsymbol,
                                            quantity=quantity,
                                            discloseqty=0,
                                            price_type=order_type,
                                            price=price,
                                            trigger_price=trigger_price,
                                            retention='DAY',
                                            remarks=tag)
            Constants.logger.info("[PLACE_ORDER_RESPONSE] {} input {}".format(response, json.dumps(data)))
            if 'norenordno' in response:
                return response['norenordno']
            else:
                raise Exception(response)
        except Exception as e:
            print(traceback.print_exc())
            exception_message = "Order placement failed with error [{}]".format(str(e))
            print(exception_message)

    def get_orders(self):
        return self.api.get_order_book()

    def get_ltp(self, exchange, tradingsymbol):
        tradingsymbol = self.get_symbol(tradingsymbol)
        token = self.symbol_data["{}:{}".format(exchange, tradingsymbol)]['Token']
        return float(self.api.get_quotes(exchange, str(token))['lp'])

    @retry(wait_exponential_multiplier=3000, wait_exponential_max=10000, stop_max_attempt_number=3)
    def modify_order(self, data):
        try:
            logger.info("Modifying order [{}] new price [{}]".format(data['norenordno'], data['prc']))
            response = self.api.modify_order(orderno=data['norenordno'],
                                             exchange=data['exch'],
                                             tradingsymbol=data['tsym'],
                                             newprice_type=data['prctyp'],
                                             newquantity=data['qty'],
                                             newprice=data['prc'])
            logger.info("[MODIFY_ORDER_RESPONSE] order id [{}] response [{}]".format(data['norenordno'], response))
            return response
        except Exception as e:
            exception_message = "OrderModificationFailed for {} failed with exception {}".format(data['norenordno'], e)
            Constants.logger.error("{}".format(exception_message))

    def modify_price(self, order_id, price, trigger_price=None, order_type=None):
        data = {}
        order_history = self.api.single_order_history(order_id)
        order_details = order_history[0]
        data['norenordno'] = order_id
        data['prc'] = price
        data['prctyp'] = order_details['prctyp']
        data['exch'] = order_details['exch']
        data['qty'] = order_details['qty']
        data['tsym'] = order_details['tsym']
        if trigger_price != None and trigger_price > 0:
            data['newtrigger_price'] = trigger_price
        else:
            data['newtrigger_price'] = None

        self.modify_order(data)

    def cancel_order(self, order_id):
        self.api.cancel_order(order_id)

    def modify_orders_till_complete(self, orders_placed, sleep_time=10):
        modification_count = {}
        while 1:
            time.sleep(sleep_time)
            orders = pd.DataFrame(self.get_orders())

            orders = orders[orders.norenordno.isin(orders_placed)]
            orders = orders[~orders.status.isin(["REJECTED", "CANCELED", "COMPLETE"])]

            if len(orders) == 0:
                Constants.logger.info("ALL orders have been completed")
                break

            orders = orders.to_dict('records')
            for order in orders:
                order_id = order['order_id']

                ltp = self.get_ltp(order['exchange'], order['tradingsymbol'])
                order['prc'] = ltp
                self.modify_order(order)

                if order_id not in modification_count:
                    modification_count[order_id] = 1
                else:
                    modification_count[order_id] += 1

                time.sleep(.1)

                if modification_count[order_id] > 5:
                    order['prctyp'] = "MKT"
                    order['prc'] = 0
                    Constants.logger.info("Placing MARKET order [{}]".format(order))
                    self.modify_order(order)

    def stream_order_data(self):
        self.api.start_websocket(order_update_callback=self.event_handler_order_update)

    def profile(self):

        response = {
            'user_id': self.user_id,
            'full_name': self.full_name,
            'email' : self.email
        }

        return response

    def positions(self):
        positions = self.api.get_positions()
        positions = pd.DataFrame(positions)

        positions.loc[:, 'tradingsymbol'] = positions.tsym
        positions.loc[:, 'ltp'] = positions.lp.astype(float)
        positions.loc[:, 'pnl'] = positions.rpnl.astype(float)

        return positions

    def orders(self, tag=None):
        orders = pd.DataFrame(self.api.get_order_book())
        positions = self.positions()
        if len(orders) == 0:
            orders = pd.DataFrame(columns=self.orders_column_list)

        orders.loc[:, 'tradingsymbol'] = orders.tsym
        orders = pd.merge(orders, positions[['tradingsymbol', 'ltp']],
                          how="left",
                          left_on=['tradingsymbol'],
                          right_on=['tradingsymbol'])

        orders.rename(
            columns={
                'norenordno' : 'order_id',
                'uid': 'user_id',
                'exch' : 'exchange',
                'remarks' : 'tag',
                'avgprc' : 'average_price',
                'prd' : 'product',
                'trantype' : 'transaction_type',
                'qty' : 'quantity',
                'fillshares' : 'filled_quantity',
                'rorgqty' : 'pending_quantity'

            }, inplace=True)

        orders = orders[self.orders_column_list]
        orders.loc[:, 'filled_quantity'] = orders.filled_quantity.astype(float)
        orders.loc[:, 'average_price'] = orders.average_price.astype(float)
        orders.loc[:, 'pnl'] = orders.ltp * orders.filled_quantity - orders.average_price * orders.filled_quantity
        orders.loc[:, 'pnl'] = np.where(orders.transaction_type == "S", -orders.pnl, orders.pnl)

        if tag:
            orders = orders[orders.tag == tag]

        return orders

    def account_summary(self):
        margins = self.api.get_limits()
        margin_available = float(margins['cash']) - float(margins['marginused'])
        response = {
            'margin_used': float(margins['marginused']),
            'total_balance' : float(margins['cash']),
            'margin_available': margin_available,
            'pnl': 0
        }
        return response