import decimal
import time
import typing

# noinspection PyUnresolvedReferences
from accrocchio.badgeofshame import compromise, fallacy_method, detonator
from bithustler_client.client.client import BithustlerClient
from bithustler_client.client.types import Limits
from bitlurker_client.client.client import BitlurkerClient
from vault_client.client.client_definition import VaultClient
from vault_client.client.conio_user import ConioUser

from conio_sdk.common import exceptions
from conio_sdk.generated_protobuf import v1_pb2
from conio_sdk.logging.factory import LOGGING_FACTORY
from conio_sdk.services.conio.sdk.crypto_proof.crypto_proof_verification_strategy import CryptoProofVerificationStrategy
from conio_sdk.services.conio.sdk.wallet_withdrawals_vo_service import WalletWithdrawalsVOService
from conio_sdk.services.conio.trading.trading_service import TradingService, FiatCurrency, Ask, AskID
from conio_sdk.services.conio.wallet.bitcoin_wallet_service import SatoshiAmount, BitcoinWalletService, BtcAddressID, \
    SatoshiAmountWithFees
from etc.settings import DEFAULT_CRYPTO_CURRENCY


class TradingVOServices:
    _CURRENCIES_BY_ID = {
        i[1]: i[0] for i in v1_pb2.CurrencyCode.items()
    }

    def __init__(
            self, bitlurker_client: BitlurkerClient,
            bithustler_client: BithustlerClient,
            trading_service: TradingService,
            bitcoin_wallet_service: BitcoinWalletService,
            vault_client: VaultClient,
            crypto_proof_verification_strategy: CryptoProofVerificationStrategy,
            fake_btc_address: BtcAddressID,
            rsa_reference_key: str,
            max_wait_time_to_charged_bids_msecs: int,
            wallet_withdrawals_vo_service: WalletWithdrawalsVOService):
        self._bithustler_client = bithustler_client
        self._bitlurker_client = bitlurker_client
        self._bitcoin_wallet_service = bitcoin_wallet_service
        self._trading_service = trading_service
        self._fake_btc_address = fake_btc_address
        self._vault_client = vault_client
        self._crypto_proof_verification_strategy = crypto_proof_verification_strategy
        self._rsa_reference_key = rsa_reference_key
        self._max_wait_time_to_charged_bids_msecs = max_wait_time_to_charged_bids_msecs
        self._wallet_withdrawals_vo_service = wallet_withdrawals_vo_service

    def create_or_refresh_ask(
            self, msg: v1_pb2.MsgCreateOrRefreshAsk,
            user: ConioUser) -> v1_pb2.MsgCreateOrRefreshAskResponse:
        response = v1_pb2.MsgCreateOrRefreshAskResponse()
        currency_code = FiatCurrency(msg.currency)
        if not user.user_info.get('seller_id'):
            LOGGING_FACTORY.trading.warning('No such seller for user %s', user.reference_key_id)
            raise exceptions.NoSuchSellerException
        if not user.get_wallet(DEFAULT_CRYPTO_CURRENCY) or \
           not user.get_wallet(DEFAULT_CRYPTO_CURRENCY).get('reference_id'):
            LOGGING_FACTORY.trading.warning('No trading data ')
            raise exceptions.NoSuchWalletException

        with detonator:
            blocks = self._trading_service.get_ask_tx_inclusion_blocks()
            min_seconds = int(blocks * 600)
            max_seconds = min_seconds

        fee_estimation = self._bitlurker_client.get_recommended_fees(
            min_seconds=min_seconds, max_seconds=max_seconds
        )

        mining_fees = 0
        if msg.satoshi:
            if msg.ask_id:
                ask = self._trading_service.refresh_ask_price_by_satoshi(
                    user, msg.ask_id, msg.satoshi, currency_code
                )
            else:
                ask = self._trading_service.create_ask_by_satoshi(user, msg.satoshi, currency_code)
        elif msg.fiat_amount:
            if msg.ask_id:
                ask = self._trading_service.refresh_ask_satoshi_by_fiat(
                    user, msg.ask_id, msg.fiat_amount, currency_code
                )
            else:
                ask = self._trading_service.create_ask_satoshi_by_fiat(
                    user, msg.fiat_amount, currency_code
                )
        else:
            assert not fee_estimation.HasField('error')
            ask, mining_fees = self._create_max_ask(
                msg.ask_id,
                user, currency_code, fee_estimation.data.fee_per_byte
            )
        if not mining_fees:
            try:
                mining_fees = self._wallet_withdrawals_vo_service.safe_create_transaction_for_fees(
                    user, self._fake_btc_address,
                    fee_estimation.data.fee_per_byte,
                    ask.satoshi
                ).fees
            except Exception:
                LOGGING_FACTORY.asks.warning('Unable to determinate mining fees, ask: %s', ask.ask_id, exc_info=True)
                raise
        
        response.ask_id = ask.ask_id
        response.satoshi = ask.satoshi
        response.fiat_amount = float(ask.fiat_amount)
        response.currency = ask.currency.value
        response.expiration_timestamp = ask.expiration_timestamp
        response.fee = float(ask.explicit_fees)
        response.miner_fee = mining_fees

        return response

    def pay_for_ask(
            self, msg: v1_pb2.MsgPayForAsk, user: ConioUser) -> v1_pb2.MsgPayForAskResponse:
        LOGGING_FACTORY.wallet_bittrade.debug('pay_for_ask: %s', str(msg))
        payload = self._wallet_withdrawals_vo_service.safe_pay_for_ask(user, msg.ask_id)

        response = v1_pb2.MsgPayForAskResponse()
        response.tx = payload.tx
        response.date = int(time.time() * 1000)
        response.sign_data.extend(
            [
                v1_pb2.TransactionSignData(path=list(x.path), data_to_sign=x.data_to_sign)
                for x in payload.sign_data
            ]
        )
        response.ask_id = msg.ask_id
        response.fees = payload.fees
        msg_to_sign = response.tx + response.date.to_bytes(8, 'big')
        signature = self._vault_client.sign(self._rsa_reference_key, msg_to_sign, hash_algo='SHA-256')
        response.request_signature = signature
        LOGGING_FACTORY.trading.debug('pay_for_ask response: %s', str(response))
        return response

    def finalize_payment_for_ask(self, msg: v1_pb2.MsgFinalizePaymentForAsk) -> v1_pb2.MsgFinalizePaymentForAskResponse:
        response = v1_pb2.MsgFinalizePaymentForAskResponse(ask_id=msg.ask_id)
        self._trading_service.finalize_payment_for_ask(AskID(msg.ask_id), msg.tx, *msg.signatures)
        return response

    def create_or_refresh_bid(
            self, msg: v1_pb2.MsgCreateOrRefreshBid, user: ConioUser, use_iban: bool) \
            -> v1_pb2.MsgCreateOrRefreshBidResponse:
        currency = FiatCurrency(msg.currency)
        response = v1_pb2.MsgCreateOrRefreshBidResponse()
        if not user.user_info.get('cards_integration_id') or \
                not user.get_wallet(DEFAULT_CRYPTO_CURRENCY) or \
                not user.get_wallet(DEFAULT_CRYPTO_CURRENCY).get('reference_id'):
            LOGGING_FACTORY.security.warning(
                'Cannot create bid with payer %s, wallet %s',
                user.user_info.get('cards_integration_id'),
                user.get_wallet(DEFAULT_CRYPTO_CURRENCY)
            )
            raise exceptions.NotAuthenticatedException

        if msg.satoshi:
            if msg.bid_id:
                bid = self._trading_service.refresh_bid_price_by_satoshi(
                    user, msg.bid_id, msg.satoshi, currency, use_iban
                )
            else:
                bid = self._trading_service.create_bid_by_satoshi(
                    user, msg.satoshi, currency, use_iban
                )
        else:
            if msg.bid_id:
                bid = self._trading_service.refresh_bid_satoshi_by_fiat(
                    user, msg.bid_id, msg.fiat_amount, currency, use_iban
                )
            else:
                bid = self._trading_service.create_bid_satoshi_by_fiat(
                    user, msg.fiat_amount, currency, use_iban)

        response.bid_id = bid.bid_id
        response.satoshi = bid.satoshi
        response.fiat_amount = float(bid.fiat_amount)
        response.currency = getattr(v1_pb2, bid.currency.name)
        response.expiration_timestamp = bid.expiration_timestamp
        response.fee = float(bid.explicit_fees)
        response.wiretransfer_payee_info.description = bid.wiretransfer_payee_info.description
        response.wiretransfer_payee_info.holder = bid.wiretransfer_payee_info.holder
        response.wiretransfer_payee_info.holder_iban = bid.wiretransfer_payee_info.holder_iban
        return response

    def pay_for_bid(self, msg: v1_pb2.MsgPayForBid, user: ConioUser) -> v1_pb2.MsgPayForBidResponse:
        LOGGING_FACTORY.wallet_bittrade.debug('pay_for_bid: %s', str(msg))
        if not self._crypto_proof_verification_strategy.verify_pay_for_bid(msg.crypto_proof, user):
            LOGGING_FACTORY.security.warning('Invalid crypto proof. Message: %s', str(msg))
            raise exceptions.InvalidCryptoProofException
        response = v1_pb2.MsgPayForBidResponse()
        response.bid_id = msg.bid_id
        payment_method_id = self._trading_service.get_payment_method_id_by_external_reference(
            user, msg.crypto_proof.external_reference_id
        )
        if payment_method_id:
            payload = self._trading_service.pay_for_bid_with_existing_payment_method(
                user, msg.bid_id, payment_method_id
            )
            maskedpan = payload.maskedpan
        else:
            payload = self._trading_service.pay_for_bid_with_new_payment_method(user, msg.bid_id)
            maskedpan = None
        response.payment_token = payload.payment_token
        response.gp_id = payload.gp_id
        response.gp_url = payload.gp_url
        response.bid_id = msg.bid_id
        if maskedpan:
            response.maskedpan = maskedpan
        LOGGING_FACTORY.wallet_bittrade.debug('pay_for_bid response: %s', str(response))
        return response

    def finalize_bid(self, msg: v1_pb2.MsgFinalizePaymentForBid, user: ConioUser) \
            -> v1_pb2.MsgFinalizePaymentForBidResponse:
        LOGGING_FACTORY.wallet_bittrade.debug('finalize_bid: %s', str(msg))
        if not self._crypto_proof_verification_strategy.verify_pay_for_bid(msg.crypto_proof, user):
            LOGGING_FACTORY.security.warning('Invalid crypto proof. Signup message: %s', str(msg))
            raise exceptions.InvalidCryptoProofException
        if msg.maskedpan:
            self._trading_service.finalize_bid_with_new_payment_method(
                user, msg.bid_id, msg.maskedpan, msg.crypto_proof.external_reference_id
            )
        else:
            self._trading_service.finalize_bid_with_existing_payment_method(user, msg.bid_id)
        return v1_pb2.MsgFinalizePaymentForBidResponse(bid_id=msg.bid_id)

    def pay_for_bid_using_wiretransfers(
            self, msg: v1_pb2.MsgPayForBidUsingWiretransfer, user: ConioUser
    ) -> v1_pb2.MsgPayForBidUsingWiretransferResponse:
        LOGGING_FACTORY.wallet_bittrade.debug('pay_for_bid_using_wiretransfers: %s', str(msg))
        if not self._crypto_proof_verification_strategy.verify_pay_for_bid_using_wiretransfer(
                msg.bid_id, msg.crypto_proof, user
        ):
            LOGGING_FACTORY.security.warning('Invalid crypto proof. Message: %s', str(msg))
            raise exceptions.InvalidCryptoProofException
        self._trading_service.trigger_wiretransfer_bid(msg.bid_id)
        return v1_pb2.MsgPayForBidUsingWiretransferResponse(bid_id=msg.bid_id)

    def finalize_bid_using_wiretransfer(self, msg: v1_pb2.MsgFinalizePaymentForBidUsingWiretransfer)\
            -> v1_pb2.MsgFinalizePaymentForBidUsingWiretransferResponse:
        self._trading_service.wait_for_bid_finalization(msg.bid_id)
        return v1_pb2.MsgFinalizePaymentForBidUsingWiretransferResponse(bid_id=msg.bid_id)

    def _create_max_ask(
            self, ask_id: typing.Optional[AskID], user: ConioUser,
            fiat_currency: FiatCurrency, fee_per_byte: SatoshiAmount) -> typing.Tuple[Ask, SatoshiAmount]:
        satoshi_amount_with_mining_fees = self._get_max_satoshi(user, fee_per_byte)
        max_satoshi_amount, mining_fees = satoshi_amount_with_mining_fees
        max_fiat_amount = self._get_max_fiat_for_ask(user.seller_id)
        if not max_fiat_amount or not max_satoshi_amount:
            LOGGING_FACTORY.asks.warning(
                'Could not create max ask, max_fiat_amount: %s, max_satoshi_amount: %s',
                max_fiat_amount, max_satoshi_amount)
            if not max_satoshi_amount:
                raise exceptions.TradingLimitsExceededException
            raise exceptions.NotEnoughBtcAmountException

        if ask_id:
            ask = self._trading_service.refresh_ask_price_by_satoshi(
                user, ask_id, max_satoshi_amount, fiat_currency
            )
        else:
            ask = self._trading_service.create_ask_by_satoshi(
                user, max_satoshi_amount, fiat_currency
            )
        if ask.fiat_amount > max_fiat_amount:
            ask = self._trading_service.refresh_ask_satoshi_by_fiat(
                user, AskID(ask.ask_id), decimal.Decimal(max_fiat_amount), fiat_currency
            )
        return ask, mining_fees

    def _get_max_satoshi(self, user: ConioUser, fee_per_byte: SatoshiAmount) -> SatoshiAmountWithFees:
        result = self._wallet_withdrawals_vo_service.safe_create_transaction_for_fees(
            user,
            self._fake_btc_address,
            fee_per_byte=fee_per_byte
        )
        return SatoshiAmountWithFees(
                SatoshiAmount(result.satoshi_amount),
                SatoshiAmount(result.fees)
            )

    def _get_max_fiat_for_ask(self, seller_id: str):
        return Limits.from_protobuf(
            self._bithustler_client.get_limits_by_seller_id(seller_id)
        ).current_limit
