from collections import defaultdict
from collections import OrderedDict
from concurrent.futures import as_completed
from concurrent.futures import ThreadPoolExecutor
from typing import Any
from typing import DefaultDict
from typing import Dict
from typing import Iterator
from typing import List
from typing import Optional
from typing import Tuple

from asciitree import LeftAligned
from terminaltables import AsciiTable

from data.watchlist import ALL_REVERSED
from report.symbolhistory.symbolhistorycontext import SymbolHistoryContext
from report.symbolhistory.symbolhistoryevent import symbol_history_event_from_option_event
from report.symbolhistory.symbolhistoryevent import symbol_history_event_from_option_order
from report.symbolhistory.symbolhistoryevent import symbol_history_event_from_stock_order
from report.symbolhistory.symbolhistoryevent import SymbolHistoryEvent
from robinhood.lib.account.account import account
from robinhood.lib.account.optionevent.optionevent import OptionEvent
from robinhood.lib.account.optionorder.optionorder import OptionOrder
from robinhood.lib.account.stockorder.stockorder import StockOrder
from robinhood.lib.helpers import format_optional_greek
from robinhood.lib.helpers import format_optional_price
from robinhood.lib.helpers import format_percentage
from robinhood.lib.helpers import format_price
from robinhood.lib.helpers import format_quantity
from robinhood.lib.marketplace.equity import Equity
from robinhood.lib.marketplace.marketplace import marketplace
from robinhood.lib.robinhoodclient import rhc


class SymbolHistory(object):

    def __init__(
        self,
        stock_orders: List[StockOrder],
        option_orders: List[OptionOrder],
        option_events: List[OptionEvent],
    ) -> None:
        self._events_by_symbol = self._load_events_by_symbol(
            stock_orders=stock_orders,
            option_orders=option_orders,
            option_events=option_events,
        )

    def get_events(self, symbol: str) -> List[SymbolHistoryEvent]:
        return self._events_by_symbol[symbol]

    def show_current_positions(self) -> None:
        active_symbol = account.active_symbols
        equaities = marketplace.get_equities(symbols=active_symbol)

        with ThreadPoolExecutor() as executor:
            futures = [
                executor.submit(
                    self.get_current_position_report,
                    symbol=symbol,
                    equity=equaities[symbol],
                )
                for symbol in active_symbol
            ]

        reports_by_symbol: Dict[str, str] = {}
        for future in as_completed(futures):
            symbol, report = future.result()
            reports_by_symbol.update({symbol: report})

        for symbol in sorted(reports_by_symbol.keys()):
            print(reports_by_symbol[symbol])
            print()

    def get_current_position_report(self, symbol: str, equity: Equity) -> Tuple[str, str]:
        events = self.get_events(symbol=symbol)
        if len(events) == 0:
            report = f'No event for {symbol}...'
            return (symbol, report)

        event = events[-1]

        # Stock
        symbol_with_price = '{symbol} {last_price} {dod_change}'.format(
            symbol=symbol,
            last_price=format_price(equity.last_price),
            dod_change=format_percentage(equity.dod_percentage),
        )
        symbol_data: OrderedDict[str, Any] = OrderedDict()

        stock_key, stock_details = self._get_current_stock_holding(
            event=event,
            last_price=equity.last_price,
        )
        symbol_data[stock_key] = stock_details

        # Option Buy and Sell
        for buy_or_sell in ['Buy', 'Sell']:
            for option_key, option_details in self._get_current_option_buy_or_sell_holding(
                event=event,
                last_price=equity.last_price,
                buy_or_sell=buy_or_sell,
            ):
                symbol_data[option_key] = option_details

        data = {symbol_with_price: symbol_data}

        tree = LeftAligned()
        return (symbol, tree(data))

    def _get_current_stock_holding(
        self,
        event: SymbolHistoryEvent,
        last_price: float,
    ) -> 'Tuple[str, OrderedDict[str, Any]]':
        stock_quantity = event.context.stock_holding.quantity
        stock_avg_price = event.context.stock_holding.avg_unit_price
        stock_total_value = stock_quantity * stock_avg_price
        if stock_quantity == 0:
            stock_holding = 'none'
        else:
            stock_holding = '{quantity} x {avg_price} = {total_value}'.format(
                quantity=stock_quantity,
                avg_price=format_price(stock_avg_price),
                total_value=format_price(stock_total_value),
            )
        stock_key = f'Stock Holding: {stock_holding}'
        stock_details: OrderedDict[str, Any] = OrderedDict()

        if stock_quantity > 0:
            current_value = last_price * stock_quantity
            stock_details['Current Value: {value} ({change})'.format(
                value=format_price(current_value),
                change=format_percentage(current_value / stock_total_value - 1),
            )] = {}

        return (stock_key, stock_details)

    def _get_current_option_buy_or_sell_holding(
        self,
        event: SymbolHistoryEvent,
        last_price: float,
        buy_or_sell: str,
    ) -> 'Iterator[Tuple[str, OrderedDict[str, Any]]]':
        if buy_or_sell == 'Buy':
            holding_dict = event.context.option_buy_holding
        else:
            holding_dict = event.context.option_sell_holding

        for _, item in holding_dict.items():
            total_base_price = item.avg_unit_price * item.quantity * 100
            if item.option_instrument.instrument_type == 'call':
                if last_price > item.option_instrument.strike_price:
                    diff = last_price - item.option_instrument.strike_price
                    above_or_below = 'above'
                    breached = 'BREACHED'
                else:
                    diff = item.option_instrument.strike_price - last_price
                    above_or_below = 'below'
                    breached = 'NOT BREACHED'
            else:
                assert item.option_instrument.instrument_type == 'put'
                if last_price < item.option_instrument.strike_price:
                    diff = item.option_instrument.strike_price - last_price
                    above_or_below = 'below'
                    breached = 'BREACHED'
                else:
                    diff = last_price - item.option_instrument.strike_price
                    above_or_below = 'above'
                    breached = 'NOT BREACHED'
            breached_key = '{breached} - {above_or_below} strike price {diff_dollar} or {diff_percentage}'.format(
                above_or_below=above_or_below,
                diff_dollar=format_price(diff),
                diff_percentage=format_percentage(diff / last_price),
                breached=breached,
            )

            option_sell_key = (
                '{buy_or_sell} {symbol} ({days_to_expire}): {quantity} x {avg_price} = {base_price}'.format(
                    buy_or_sell=buy_or_sell,
                    symbol=item.option_instrument.symbol,
                    strike_price=format_price(item.option_instrument.strike_price),
                    exp_date=item.option_instrument.exp_date,
                    days_to_expire=format_quantity(item.option_instrument.days_to_expire),
                    quantity=item.quantity,
                    avg_price=format_price(item.avg_unit_price),
                    base_price=format_price(total_base_price),
                )
            )
            market_data = item.option_instrument.market_data
            if market_data is None:
                current_price = '-'
                theta = '-'
                delta = '-'
                gamma = '-'
                vega = '-'
                rho = '-'
            else:
                current_price = format_optional_price(market_data.medium_fill_rate_sell_price)
                theta = format_optional_greek(market_data.theta)
                delta = format_optional_greek(market_data.delta)
                gamma = format_optional_greek(market_data.gamma)
                vega = format_optional_greek(market_data.vega)
                rho = format_optional_greek(market_data.rho)

            current_price_key = f'Current Price: {current_price}'
            greek_key = f'Theta: {theta} Delta: {delta} Gamma: {gamma} Vega: {vega} Rho: {rho}'

            option_sell_details: OrderedDict[str, Any] = OrderedDict()
            option_sell_details[current_price_key] = {}
            option_sell_details[breached_key] = {}
            option_sell_details[greek_key] = {}

            yield (option_sell_key, option_sell_details)

    def show(self, symbol: str, last_n_events: Optional[int] = 10) -> None:
        data = self._gen_ascii_tbl_data(symbol=symbol)
        if last_n_events is None:
            applicable_data = data
        else:
            data_no_head = data[1:]
            applicable_data = [data[0]] + data_no_head[-last_n_events - 1:]
        t = AsciiTable(applicable_data)
        t.inner_column_border = True
        t.inner_footing_row_border = False
        t.inner_heading_row_border = True
        t.inner_row_border = False
        t.outer_border = False
        t.justify_columns = {
            0: 'center',
            1: 'center',
            2: 'center',
            3: 'right',
            4: 'right',
            5: 'right',
            6: 'right',
            7: 'right',
        }
        print(t.table)

    def show_holdings_by_events(self, symbol: str) -> None:
        events = self.get_events(symbol=symbol)
        for event in events:
            print(event.event_name)
            quantity = event.context.stock_holding.quantity
            avg_price = event.context.stock_holding.avg_unit_price
            print('>>> Stock Holding: {quantity} x {avg_price} = {total_value}'.format(
                quantity=format_quantity(quantity),
                avg_price=format_price(avg_price),
                total_value=format_price(quantity * avg_price),
            ))

            for _, item in event.context.option_buy_holding.items():
                print('>>> Option Buy Holding: {quantity} x {avg_price} = {total_value}'.format(
                    quantity=format_quantity(item.quantity),
                    avg_price=format_price(item.avg_unit_price),
                    total_value=format_price(item.quantity * item.avg_unit_price),
                ))

            for _, item in event.context.option_sell_holding.items():
                print('>>> Option Sell Holding: {quantity} x {avg_price} = {total_value}'.format(
                    quantity=format_quantity(item.quantity),
                    avg_price=format_price(item.avg_unit_price),
                    total_value=format_price(item.quantity * item.avg_unit_price),
                ))

            print()

    def _gen_ascii_tbl_data(self, symbol: str) -> List[List[str]]:
        data = [[
            'Date',
            'Event',
            'Symbol',
            'Quantity',
            'Price',
            '# of Shares',
            'Avg Price',
            'Cumulative Profit',
        ]]

        for event in self._events_by_symbol[symbol]:
            # could be option legs from vertical
            if event.context is None:
                continue

            data.append(
                [
                    event.formatted_ts,
                    event.event_name,
                    event.symbol,
                    format_quantity(event.quantity),
                    format_optional_price(event.unit_price),
                    format_quantity(event.context.stock_holding.quantity),
                    format_price(event.context.stock_holding.avg_unit_price),
                    format_price(event.context.profit),
                ],
            )

        return data

    def _load_events_by_symbol(
        self,
        stock_orders: List[StockOrder],
        option_orders: List[OptionOrder],
        option_events: List[OptionEvent],
    ) -> DefaultDict[str, List[SymbolHistoryEvent]]:
        events_by_symbol: DefaultDict[str, List[SymbolHistoryEvent]] = defaultdict(list)

        self._convert_stock_orders_to_symbol_history_events(
            stock_orders=stock_orders,
            events_by_symbol=events_by_symbol,
        )

        self._convert_option_orders_to_symbol_history_events(
            option_orders=option_orders,
            events_by_symbol=events_by_symbol,
        )

        self._convert_option_events_to_symbol_history_events(
            option_events=option_events,
            events_by_symbol=events_by_symbol,
        )

        for symbol, symbol_events in events_by_symbol.items():
            events_by_symbol[symbol] = sorted(symbol_events, key=lambda e: e.event_ts)

        for symbol, symbol_events in events_by_symbol.items():
            context = SymbolHistoryContext(symbol=symbol)
            for event in symbol_events:
                if event.event_name in ('SHORT PUT', 'SHORT CALL'):
                    assert event.unit_price is not None
                    assert event.option_order.num_of_legs == 1
                    leg = event.option_order.legs[0]
                    context.sell_to_open(
                        symbol=event.symbol,
                        quantity=int(event.quantity) * leg.ratio_quantity,
                        price=abs(event.unit_price),
                        option_instrument=leg.option_instrument,
                    )
                elif event.event_name in ('LONG PUT', 'LONG CALL'):
                    assert event.unit_price is not None
                    assert len(event.option_order.opening_legs) == 1
                    context.buy_to_open(
                        symbol=event.symbol,
                        quantity=int(event.quantity),
                        price=event.unit_price,
                        option_instrument=event.option_order.legs[0].option_instrument,
                    )
                elif event.event_name in {'ROLLING CALL', 'ROLLING PUT'}:
                    assert event.unit_price is not None
                    closing_option_instrument = event.option_rolling_order.closing_leg.option_instrument
                    unit_price = context.option_sell_holding[closing_option_instrument.symbol].avg_unit_price
                    context.buy_to_close(
                        symbol=closing_option_instrument.symbol,
                        quantity=int(event.quantity),
                        price=unit_price,
                    )

                    leg = event.option_rolling_order.opening_leg
                    context.sell_to_open(
                        symbol=leg.option_instrument.symbol,
                        quantity=int(event.quantity) * leg.ratio_quantity,
                        price=abs(event.unit_price) + unit_price,
                        option_instrument=leg.option_instrument,
                    )
                elif event.event_name == 'SHORT CALL SPREAD':
                    for leg in event.option_order.legs:
                        if leg.side == 'buy':
                            context.buy_to_open(
                                symbol=leg.option_instrument.symbol,
                                quantity=int(leg.quantity),
                                price=leg.price,
                                option_instrument=leg.option_instrument,
                            )
                        else:
                            context.sell_to_open(
                                symbol=leg.option_instrument.symbol,
                                quantity=int(leg.quantity),
                                price=leg.price,
                                option_instrument=leg.option_instrument,
                            )
                elif event.event_name == 'OPTION EXPIRATION':
                    context.option_expire(
                        symbol=event.symbol,
                        quantity=int(event.quantity),
                    )
                elif event.event_name == 'BUY TO CLOSE':
                    assert event.unit_price is not None
                    context.buy_to_close(
                        symbol=event.symbol,
                        quantity=int(event.quantity),
                        price=event.unit_price,
                    )
                elif event.event_name == 'SELL TO CLOSE':
                    assert event.unit_price is not None
                    context.sell_to_close(
                        symbol=event.symbol,
                        quantity=int(event.quantity),
                        price=event.unit_price,
                    )
                elif event.event_name == 'CALL ASSIGNMENT':
                    assert event.unit_price is not None
                    context.call_assignment(
                        symbol=event.symbol,
                        quantity=int(event.quantity),
                        price=event.unit_price,
                    )
                elif event.event_name == 'PUT ASSIGNMENT':
                    instrument = event.option_event.option_instrument
                    assert event.unit_price is not None
                    context.put_asignment(
                        symbol=event.symbol,
                        quantity=int(event.quantity),
                        price=event.unit_price,
                        strike_price=instrument.strike_price,
                    )
                elif event.event_name == 'OPTION EXERCISE':
                    option_instrument = event.option_event.option_instrument
                    option_avg_price = context.option_buy_holding[event.symbol].avg_unit_price
                    event.unit_price = option_instrument.strike_price + option_avg_price
                    context.call_exercise(
                        symbol=event.symbol,
                        quantity=int(event.quantity),
                        price=option_avg_price,
                        strike_price=option_instrument.strike_price,
                    )
                elif event.event_name in ('LIMIT BUY', 'MARKET BUY'):
                    assert event.unit_price is not None
                    context.limit_or_market_buy_stock(
                        quantity=int(event.quantity),
                        price=event.unit_price,
                    )
                elif event.event_name in ('LIMIT SELL', 'MARKET SELL'):
                    assert event.unit_price is not None
                    context.limit_or_market_sell_stock(
                        quantity=int(event.quantity),
                        price=event.unit_price,
                    )
                elif event.event_name in {
                    'FIG LEAF',
                    'LONG CALL SPREAD',
                    'LONG PUT SPREAD',
                    'SHORT PUT SPREAD',
                    'CUSTOM',
                    'IRON CONDOR',
                }:
                    for leg in event.option_order.legs:
                        quantity = int(event.option_order.processed_quantity) * leg.ratio_quantity
                        if leg.is_buy and leg.is_open:
                            context.buy_to_open(
                                symbol=leg.option_instrument.symbol,
                                quantity=quantity,
                                price=leg.price,
                                option_instrument=leg.option_instrument,
                            )
                        elif leg.is_buy and leg.is_close:
                            context.buy_to_close(
                                symbol=leg.option_instrument.symbol,
                                quantity=quantity,
                                price=leg.price,
                            )
                        elif leg.is_sell and leg.is_open:
                            context.sell_to_open(
                                symbol=leg.option_instrument.symbol,
                                quantity=quantity,
                                price=leg.price,
                                option_instrument=leg.option_instrument,
                            )
                        elif leg.is_sell and leg.is_close:
                            context.sell_to_close(
                                symbol=leg.option_instrument.symbol,
                                quantity=quantity,
                                price=leg.price,
                            )

                elif event.event_name in {
                    'FIG LEAF CLOSE',
                    'LONG CALL SPREAD CLOSE',
                    'LONG CALL CLOSE',
                    'LONG PUT CLOSE',
                    'LONG PUT SPREAD CLOSE',
                    'SHORT PUT SPREAD CLOSE',
                    'CUSTOM CLOSE',
                    'IRON CONDOR CLOSE',
                }:
                    for leg in event.option_order.legs:
                        if leg.side == 'buy':
                            context.buy_to_close(
                                symbol=leg.option_instrument.symbol,
                                quantity=int(event.option_order.processed_quantity) * leg.ratio_quantity,
                                price=leg.price,
                            )
                        else:
                            context.sell_to_close(
                                symbol=leg.option_instrument.symbol,
                                quantity=int(event.option_order.processed_quantity) * leg.ratio_quantity,
                                price=leg.price,
                            )
                else:
                    raise NotImplementedError(event.event_name)

                snapshot = context.take_snapshot()
                event.add_context(symbol_history_context_snapshot=snapshot)

        return events_by_symbol

    def _convert_stock_orders_to_symbol_history_events(
        self,
        stock_orders: List[StockOrder],
        events_by_symbol: DefaultDict[str, List[SymbolHistoryEvent]],
    ) -> None:
        for stock_order in stock_orders:
            if stock_order.instrument_id not in ALL_REVERSED:
                data = rhc.get_url(stock_order.instrument_url)
                for k, v in data.items():
                    print(f'{k}: {v}')

            symbol = ALL_REVERSED[stock_order.instrument_id]
            symbol_history_event = symbol_history_event_from_stock_order(
                symbol=symbol,
                stock_order=stock_order,
            )
            if symbol_history_event is not None:
                events_by_symbol[symbol].append(symbol_history_event)

    def _convert_option_orders_to_symbol_history_events(
        self,
        option_orders: List[OptionOrder],
        events_by_symbol: DefaultDict[str, List[SymbolHistoryEvent]],
    ) -> None:
        for option_order in option_orders:
            symbol = option_order.chain_symbol
            symbol_history_event = symbol_history_event_from_option_order(
                symbol=symbol,
                option_order=option_order,
            )
            if symbol_history_event is not None:
                events_by_symbol[symbol].append(symbol_history_event)

    def _convert_option_events_to_symbol_history_events(
        self,
        option_events: List[OptionEvent],
        events_by_symbol: DefaultDict[str, List[SymbolHistoryEvent]],
    ) -> None:
        for option_event in option_events:
            symbol = option_event.option_instrument.chain_symbol
            symbol_history_event = symbol_history_event_from_option_event(option_event=option_event)
            if symbol_history_event is not None:
                events_by_symbol[symbol].append(symbol_history_event)
