import decimal
import sys
import typing

from bithustler_client.client.client import BithustlerClient
from bithustler_client.generated_protobuf import bithustler_pb2
from bittrade_client.client.client import BittradeClient
from bittrade_client.generated_protobuf import bittrade_pb2
from cards_client.client.client import CardsClient
from cards_client.generated_protobuf import base_pb2
from vault_client.client.conio_user import ConioUser

from conio_sdk.generated_protobuf import v1_pb2


class TradingLimitsVOService:
    _CURRENCY_TO_CARDS_CURRENCY = {
        'EUR': base_pb2.EUR,
        'USD': base_pb2.USD
    }
    _CURRENCY_TO_BITHUSTLER_CURRENCY = {
        'EUR': bithustler_pb2.EUR,
        'USD': bithustler_pb2.USD
    }
    _BUY_LIMITS_TYPES = {
        base_pb2.DAILY: v1_pb2.DAILY,
        base_pb2.MONTHLY: v1_pb2.MONTHLY,
        base_pb2.YEARLY: v1_pb2.YEARLY
    }

    _SELL_LIMITS_TYPES = {
        bithustler_pb2.DAILY: v1_pb2.DAILY,
        bithustler_pb2.MONTHLY: v1_pb2.MONTHLY,
        bithustler_pb2.YEARLY: v1_pb2.YEARLY
    }

    def __init__(
            self, cards_client: CardsClient,
            bithustler_client: BithustlerClient,
            bittrade_client: BittradeClient):
        self._cards_client, self._bithustler_client = cards_client, bithustler_client
        self._bittrade_client = bittrade_client

    def get_trading_limits(self, currency: str, user: ConioUser, use_iban: bool) \
            -> v1_pb2.MsgGetTradingLimitsResponse:
        resp_msg = v1_pb2.MsgGetTradingLimitsResponse()
        self._build_buy_limits(resp_msg, currency, user, use_iban)
        self._build_sell_limits(resp_msg, currency, user)
        self._build_min_limits(resp_msg, currency, user)
        return resp_msg

    def _build_min_limits(self, resp_msg: v1_pb2.MsgGetTradingLimitsResponse, currency: str, user: ConioUser):
        min_values = dict()
        currency = currency or 'EUR'
        for trading_min_fiat in filter(
                lambda d: d.currency == currency,
                self._bittrade_client.get_trading_settings().trading_min_fiat
        ):
            if trading_min_fiat.trading_min_fiat_type == bittrade_pb2.MIN_FIAT_TYPE_BID:
                field = 'buy_min_fiat_limits'
            elif trading_min_fiat.trading_min_fiat_type == bittrade_pb2.MIN_FIAT_TYPE_ASK:
                field = 'sell_min_fiat_limits'
            else:
                raise ValueError('Unexpected {}, message: {}'.format(
                    trading_min_fiat.trading_min_fiat_type, str(trading_min_fiat))
                )
            new_value = \
                decimal.Decimal(trading_min_fiat.value.fiat_value) / \
                decimal.Decimal('1' + ('0' * trading_min_fiat.value.decimal_digits))
            min_values[field] = max(min_values.get(field, new_value), new_value)

        min_values.setdefault('buy_min_fiat_limits', decimal.Decimal(0))
        min_values.setdefault('sell_min_fiat_limits', decimal.Decimal(0))
        for k, v in min_values.items():
            setattr(resp_msg, k, float(v))

    def _build_sell_limits(self, resp_msg: v1_pb2.MsgGetTradingLimitsResponse, currency: str, user: ConioUser):
        sell_fiat_limits = self._bithustler_client.get_limits_by_seller_id(user.seller_id)
        if sell_fiat_limits.HasField('error'):
            raise ValueError(str(sell_fiat_limits))
        sell_fiat_limits = sell_fiat_limits.data
        sell_limit = None
        expenses = {
            bithustler_pb2.DAILY: sell_fiat_limits.current_daily_expense,
            bithustler_pb2.MONTHLY: sell_fiat_limits.current_monthly_expense,
            bithustler_pb2.YEARLY: sell_fiat_limits.current_yearly_expense
        }
        limits_by_type = dict()  # type: typing.Dict[int, float]
        for l in filter(
                lambda d: d.currency == self._CURRENCY_TO_BITHUSTLER_CURRENCY[currency],
                sell_fiat_limits.all_limits
        ):
            resp_msg.sell_fiat_limits.all_limits.add(
                limit_type=self._SELL_LIMITS_TYPES[l.limit_type],
                limit=float(l.limit)
            )
            current_limit = l.limit - expenses[l.limit_type]
            limits_by_type[l.limit_type] = min(
                limits_by_type.get(l.limit_type, current_limit),
                current_limit
            )
        for limit_type, limit in limits_by_type.items():
            sell_limit = min(sell_limit or sys.maxsize, limit)
            resp_msg.sell_fiat_limits.current_limits_by_type.add(
                limit_type=self._SELL_LIMITS_TYPES[limit_type],
                limit=limit
            )
        resp_msg.sell_fiat_limits.current_limit = sell_limit or 0.0

    def _build_buy_limits(
            self, resp_msg: v1_pb2.MsgGetTradingLimitsResponse, currency: str, user: ConioUser,
            use_iban: bool) -> None:
        payment_method_family = base_pb2.PAYMENT_METHOD_FAMILY_WIRETRANSFER if use_iban \
            else base_pb2.PAYMENT_METHOD_FAMILY_CREDIT_CARD
        def _filter_payment_method(p: base_pb2.MsgPaymentMethod) -> bool:
            return p.payment_method_family == payment_method_family

        buy_limit = None
        cards_limits_msg = base_pb2.MsgGetFiatLimits(
            payer_id=user.cards_integration_id, currency=self._CURRENCY_TO_CARDS_CURRENCY[currency])

        buy_fiat_limits = self._cards_client.get_fiat_limits(cards_limits_msg)
        if buy_fiat_limits.error.code != base_pb2.ERR_OK:
            raise ValueError(str(buy_fiat_limits))
        buy_fiat_limits = buy_fiat_limits.data
        for l in filter(_filter_payment_method, buy_fiat_limits.all_limits):
            resp_msg.buy_fiat_limits.all_limits.add(
                limit_type=self._BUY_LIMITS_TYPES[l.limit_type],
                limit=l.limit
            )
        for fiat_limit_by_type in filter(_filter_payment_method, buy_fiat_limits.current_limits_by_type):
            buy_limit = min(buy_limit or sys.maxsize, fiat_limit_by_type.limit)
            resp_msg.buy_fiat_limits.current_limits_by_type.add(
                limit_type=self._BUY_LIMITS_TYPES[fiat_limit_by_type.limit_type],
                limit=fiat_limit_by_type.limit
            )
        resp_msg.buy_fiat_limits.current_limit = buy_limit or 0.0
