import itertools
import typing

from bitquote_client.client.client import BitquoteClient
from bittrade_client.client.client import BittradeClient
from bittrade_client.generated_protobuf import bittrade_pb2
from core_wallet_client.client.client import CoreWalletClient
from core_wallet_client.generated_protobuf import core_wallet_pb2
from vault_client.client.conio_user import ConioUser

from conio_sdk.generated_protobuf import v1_pb2
from conio_sdk.services.conio.sdk import BidBO, AskBO
from etc.settings import DEFAULT_CRYPTO_CURRENCY


class ActivityListVOService:
    _CURRENCIES_BY_PROTO = {
        i[1]: i[0] for i in bittrade_pb2.Currency.items()
    }
    _MIN_CONFIRMATIONS = 3

    def __init__(
            self,
            core_wallet_client: CoreWalletClient,
            quoting_client: BitquoteClient,
            trading_client: BittradeClient):
        self._core_wallet_client = core_wallet_client
        self._quoting_client = quoting_client
        self._trading_client = trading_client

    def get_activities(self, msg_txs_info: v1_pb2.MsgActivitiesInfo, user: ConioUser) \
            -> v1_pb2.MsgActivitiesInfoResponse:
        response = v1_pb2.MsgActivitiesInfoResponse()
        wallet_reference_id = user.get_wallet(DEFAULT_CRYPTO_CURRENCY)['reference_id']

        if msg_txs_info.get_previous:
            first_index = 0
            last_index = msg_txs_info.from_index
        else:
            first_index = msg_txs_info.from_index
            last_index = msg_txs_info.from_index + 6
        expected = last_index - first_index

        txs = self._core_wallet_client.get_transactions_info(wallet_reference_id, last_index, True)
        bids = [
            BidBO(bid.id, bid.status, bid.satoshi,
                  bid.fiat_amount.fiat_value / 10 ** bid.fiat_amount.decimal_digits,
                  self._CURRENCIES_BY_PROTO[bid.currency],
                  bid.created_at, bid.updated_at,
                  bid.payment_id, bid.btc_transaction_id,
                  bid.explicit_fees.fiat_value / 10 ** bid.explicit_fees.decimal_digits,
                  bid.address_id) for bid in self._trading_client.get_bids(user.reference_key_id, last_index + 1).bids
        ]

        # noinspection PyUnresolvedReferences
        asks = [
            AskBO(ask.id, ask.status, ask.satoshi,
                  ask.fiat_amount.fiat_value / 10 ** ask.fiat_amount.decimal_digits,
                  self._CURRENCIES_BY_PROTO[ask.currency],
                  ask.created_at, ask.updated_at,
                  ask.sell_id, ask.sell_method_id, ask.btc_transaction_id,
                  fee=ask.explicit_fees.fiat_value / 10 ** ask.explicit_fees.decimal_digits)
            for ask in self._trading_client.get_asks(user.reference_key_id, last_index + 1).asks
        ]
        self._merge_data_into_activities(
            wallet_reference_id, msg_txs_info.fiat, txs, bids, asks,
            response.activities, expected, first_index, msg_txs_info.activity_types)

        return response

    def _merge_data_into_activities(
            self, wallet_reference_id: str,
            currency: str,
            txs: typing.List[core_wallet_pb2.Transaction],
            bids: typing.List[BidBO],
            asks: typing.List[AskBO],
            activities_list, expected_size: int, start_idx: int = 0,
            activity_types=None):

        # WARNING !!! This method makes impossible to change the cards_integration_id once an activity is done.
        tx_hashes_associated_to_a_bid = set()
        bids_without_tx_hash = []
        for bid in bids:
            if bid.btc_transaction_id:
                tx_hashes_associated_to_a_bid.add(bid.btc_transaction_id)
            else:
                bids_without_tx_hash.append(bid)
        asks_by_tx_hash = {}
        asks_without_tx_hash = []
        for ask in asks:
            if ask.btc_transaction_id:
                asks_by_tx_hash[ask.btc_transaction_id] = ask
            else:
                asks_without_tx_hash.append(ask)
        txs_by_tx_hash = {tx.hash: tx for tx in txs}

        txs_that_are_not_bids_or_asks = [
            tx for tx in txs_by_tx_hash.values()
            if tx.hash not in tx_hashes_associated_to_a_bid and tx.hash not in asks_by_tx_hash
        ]

        all_activities = list(
            itertools.chain(
                bids, asks_by_tx_hash.values(), txs_that_are_not_bids_or_asks, asks_without_tx_hash
            )
        )

        def _extract_timestamp(tx_or_bid_or_ask: typing.Union[AskBO, BidBO, core_wallet_pb2.Transaction]):
            if isinstance(tx_or_bid_or_ask, core_wallet_pb2.Transaction):
                return tx_or_bid_or_ask.timestamp
            if not isinstance(tx_or_bid_or_ask, BidBO) and not isinstance(tx_or_bid_or_ask, AskBO):
                raise AssertionError('Unexpected {} ({})'.format(type(tx_or_bid_or_ask), str(tx_or_bid_or_ask)))
            # noinspection PyUnresolvedReferences
            return tx_or_bid_or_ask.created_at

        all_activities.sort(key=_extract_timestamp, reverse=True)
        filtered_activities = self._filter_activities(all_activities, activity_types)
        filtered_selected_activities = filtered_activities[start_idx:expected_size + start_idx]
        # noinspection PyUnresolvedReferences
        missing_tx_hashes_by_bid = [
            bid.btc_transaction_id for bid in filtered_selected_activities
            if isinstance(bid, BidBO) and bid.btc_transaction_id and bid.btc_transaction_id not in txs_by_tx_hash
        ]
        # noinspection PyUnresolvedReferences
        missing_tx_hashes_by_ask = [
            ask.btc_transaction_id for ask in filtered_selected_activities
            if isinstance(ask, AskBO) and ask.btc_transaction_id and ask.btc_transaction_id not in txs_by_tx_hash
        ]
        missing_tx_hashes = missing_tx_hashes_by_bid + missing_tx_hashes_by_ask

        if missing_tx_hashes:
            txs_by_tx_hash.update(
                {tx.hash: tx for tx in
                 self._core_wallet_client.get_transactions(wallet_reference_id, missing_tx_hashes)}
            )
        for d in filtered_selected_activities:
            if isinstance(d, BidBO):
                bid = d  # type: BidBO
                activity_type = v1_pb2.ACTIVITY_BUY
                can_be_spent = True
                activity_id = '__BID__{}'.format(bid.id)
                satoshi = bid.satoshi
                fiat_amount = bid.fiat_amount - bid.fee
                btc_tx = txs_by_tx_hash.get(bid.btc_transaction_id)
            elif isinstance(d, AskBO):
                ask = d  # type: AskBO
                activity_type = v1_pb2.ACTIVITY_SELL
                can_be_spent = True
                activity_id = '__SELL__{}'.format(ask.id)
                satoshi = ask.satoshi
                fiat_amount = ask.fiat_amount + ask.fee
                btc_tx = txs_by_tx_hash.get(ask.btc_transaction_id)
            else:
                btc_tx = d  # type: core_wallet_pb2.Transaction
                if btc_tx.is_local:
                    can_be_spent = True
                elif d.incoming:
                    can_be_spent = btc_tx.confirmations >= self._MIN_CONFIRMATIONS
                else:
                    # outgoing transaction
                    if btc_tx.type in (
                            core_wallet_pb2.TRANSACTION_TYPE_RBF_CURRENT,
                            core_wallet_pb2.TRANSACTION_TYPE_CPFP_CHILD):
                        can_be_spent = btc_tx.confirmations >= self._MIN_CONFIRMATIONS
                    else:
                        can_be_spent = btc_tx.confirmations >= 1
                activity_type = d.incoming and v1_pb2.ACTIVITY_RECV or v1_pb2.ACTIVITY_SEND
                activity_id = '__BTC__{}'.format(d.hash)
                satoshi = btc_tx.amount
                fiat_amount = float(
                    self._quoting_client.get_historical_price(btc_tx.amount, btc_tx.timestamp, currency)
                ) * (btc_tx.amount * 10 ** -8)

            has_been_accelerated = btc_tx.type in (
                core_wallet_pb2.TRANSACTION_TYPE_CPFP_PARENT,
                core_wallet_pb2.TRANSACTION_TYPE_CPFP_CHILD,
                core_wallet_pb2.TRANSACTION_TYPE_RBF_CURRENT
            ) if btc_tx else False
            activities_list.add(
                activity_id=activity_id,
                timestamp=_extract_timestamp(d),
                activity_type=activity_type,
                satoshi=satoshi,
                fiat_amount=fiat_amount,
                can_be_spent=can_be_spent,
                has_been_accelerated=has_been_accelerated,
                status=btc_tx.status if btc_tx else 0
            )

    @classmethod
    def _filter_activities(cls, activities: list, activity_types) -> list:
        if activity_types is None or not len(activity_types):
            filtered_activities = activities
        else:
            def _filter_only_incoming_tx(tx_or_bid_or_ask):
                return isinstance(tx_or_bid_or_ask, core_wallet_pb2.Transaction) and tx_or_bid_or_ask.incoming

            def _filter_only_outgoing_tx(tx_or_bid_or_ask):
                return isinstance(tx_or_bid_or_ask, core_wallet_pb2.Transaction) and not tx_or_bid_or_ask.incoming

            def _filter_only_bids(tx_or_bid_or_ask):
                return isinstance(tx_or_bid_or_ask, BidBO)

            def _filter_only_asks(tx_or_bid_or_ask):
                return isinstance(tx_or_bid_or_ask, AskBO)

            delegates = []
            if v1_pb2.ACTIVITY_BUY in activity_types:
                delegates.append(_filter_only_bids)
            if v1_pb2.ACTIVITY_SELL in activity_types:
                delegates.append(_filter_only_asks)
            if v1_pb2.ACTIVITY_RECV in activity_types:
                delegates.append(_filter_only_incoming_tx)
            if v1_pb2.ACTIVITY_SEND in activity_types:
                delegates.append(_filter_only_outgoing_tx)

            def _filter(tx_or_bid):
                return any(d(tx_or_bid) for d in delegates)
            filtered_activities = list(filter(_filter, activities))

        return filtered_activities

    @classmethod
    def _core_wallet_transaction_type_to_bff_transaction_type(cls, txtype: int) -> int:
        txtypes = {
            core_wallet_pb2.TRANSACTION_TYPE_GENERIC: v1_pb2.TRANSACTION_TYPE_GENERIC,
            core_wallet_pb2.TRANSACTION_TYPE_CPFP_CHILD: v1_pb2.TRANSACTION_TYPE_CPFP_CHILD,
            core_wallet_pb2.TRANSACTION_TYPE_CPFP_PARENT: v1_pb2.TRANSACTION_TYPE_CPFP_PARENT,
            core_wallet_pb2.TRANSACTION_TYPE_RBF_CURRENT: v1_pb2.TRANSACTION_TYPE_RBF,
            core_wallet_pb2.TRANSACTION_TYPE_REDEPOSIT: v1_pb2.TRANSACTION_TYPE_REDEPOSIT
        }
        return txtypes[txtype]
