import asyncio
from typing import Dict, List, Optional, Tuple

from bfxapi import Order

from bfxbot.bfxwrapper import BfxWrapper
from bfxbot.currency import Symbol
from bfxbot.models import SymbolStatus, Ticker, EventHandler, Strategy, Event, EventKind, OFFER_PERC, PositionWrapper


class BfxBot:
    def __init__(self, api_key: str, api_secret: str, symbols: List[Symbol], tick_duration: int = 1, ):
        if api_key is None:
            print("API_KEY is not set!")
            raise ValueError

        if api_secret is None:
            print("API_SECRET is not set!")
            raise ValueError

        self.__bfx: BfxWrapper = BfxWrapper(api_key, api_secret)
        self.__ticker: Ticker = Ticker(tick_duration)
        self.__status: Dict[Symbol, SymbolStatus] = {}

        if isinstance(symbols, Symbol):
            symbols = [symbols]

        self.symbols: List[Symbol] = symbols

        # init symbol statuses
        for s in self.symbols:
            self.__status[s] = SymbolStatus(s)

    def __position_wrapper_from_id(self, position_id) -> Tuple[Optional[PositionWrapper], Optional[SymbolStatus]]:
        for s in self.__status.values():
            pw = s.active_position_wrapper_from_id(position_id)

            if pw:
                return pw, s
        return None, None

    async def __update_status__(self):
        active_positions = await self.__bfx.get_active_position()

        for symbol in self.__status:
            # updating tick
            self.__status[symbol].__init_tick__(self.__ticker.current_tick)

            # updating last price
            last_price = await self.__bfx.get_current_prices(symbol)
            last_price = last_price[0]

            self.__status[symbol].set_tick_price(self.__ticker.current_tick, last_price)

            # updating positions
            symbol_positions = [x for x in active_positions if x.symbol == str(symbol)]
            for p in symbol_positions:
                await self.__status[Symbol.from_str(p.symbol)].add_position(p)

            # updating orders
            active_orders = await self.__bfx.get_active_orders(symbol)

            for o in active_orders:
                self.__status[symbol].add_order(o)

            # emitting new tick event
            # TODO: handle _on_new_tick() from Strategy
            await self.__status[symbol].add_event(Event(EventKind.NEW_TICK, self.__ticker.current_tick))

    async def best_position_closing_price(self, position_id: int) -> Optional[float]:
        pw, _ = self.__position_wrapper_from_id(position_id)

        if not pw:
            return None

        is_long_pos = pw.position.amount < 0

        pub_tick = await self.__bfx.get_public_ticker(pw.position.symbol)

        bid_price = pub_tick[0]
        ask_price = pub_tick[2]

        if is_long_pos:
            closing_price = bid_price * (1 - OFFER_PERC / 100)
        else:
            closing_price = ask_price * (1 + OFFER_PERC / 100)

        return closing_price

    def close_order(self, symbol: Symbol, order_id: int):
        print(f"I would have closed order {order_id} for {symbol}")

    async def close_position(self, position_id: int):
        pw, ss = self.__position_wrapper_from_id(position_id)

        if not pw:
            print("Could not find open position!")
            return

        closing_price = await self.best_position_closing_price(pw.position.id)

        amount = pw.position.amount * -1

        open_orders = await self.__bfx.get_active_orders(pw.position.symbol)

        if not open_orders:
            await self.__bfx.submit_order(pw.position.symbol, closing_price, amount, Order.Type.LIMIT)
            await ss.add_event(Event(EventKind.ORDER_SUBMITTED, ss.current_tick))

    def set_strategy(self, symbol, strategy: Strategy):
        if symbol in self.__status:
            self.__status[symbol].strategy = strategy
        else:
            self.__status[symbol] = SymbolStatus(symbol, strategy)

    async def start(self):
        await self.__update_status__()

    def symbol_event_handler(self, symbol) -> Optional[EventHandler]:
        if symbol not in self.__status:
            return None

        return self.__status[symbol].eh

    def symbol_status(self, symbol: Symbol) -> Optional[SymbolStatus]:
        if symbol not in self.__status:
            return None

        return self.__status[symbol]

    async def update(self):
        await asyncio.sleep(self.__ticker.seconds)
        self.__ticker.inc()
        await self.__update_status__()