import typing

import retrying
from core_wallet_client.client import exceptions as core_wallet_exceptions
from core_wallet_client.client.client import FakeOutput
from core_wallet_client.generated_protobuf import core_wallet_pb2
from vault_client.client.conio_user import ConioUser

from conio_sdk.common import exceptions
from conio_sdk.logging.factory import LOGGING_FACTORY
from conio_sdk.services.conio.trading.trading_service import TradingService, AskID, AskBtcTransaction
from conio_sdk.services.conio.wallet.bitcoin_wallet_service import BitcoinWalletService, WalletID, BtcAddressID, \
    SatoshiAmount
from etc import settings
from etc.settings import DEFAULT_CRYPTO_CURRENCY


class _CreateTransactionStatus:
    _MAX_RETRIES_WITHOUT_PENDING_OPERATIONS = 6

    def __init__(self):
        self.triggered_charge_btc = False
        self.retried_one_last_time_after_trigger = 0
        self.pending_operations = False
        self.balance_stability_double_check = False

    @property
    def should_retry_no_bids(self) -> bool:
        print('retried_one_last_time_after_trigger', self.retried_one_last_time_after_trigger)
        return self.retried_one_last_time_after_trigger <= self._MAX_RETRIES_WITHOUT_PENDING_OPERATIONS


class _RetryException(Exception):
    pass


def _retry_if_not_enough_amount(exx: Exception):
    return isinstance(exx, _RetryException)


class WalletWithdrawalsVOService:
    def __init__(
            self,
            bitcoin_wallet_service: BitcoinWalletService,
            trading_service: TradingService):
        self._bitcoin_wallet_service = bitcoin_wallet_service
        self._trading_service = trading_service
        self._logger = LOGGING_FACTORY.safe_withdrawal

    def _trigger_charge_btc(self, user: ConioUser, status: _CreateTransactionStatus):
        self._logger.info('Starting _trigger_charge_btc, user %s', user.reference_key_id)
        if status.triggered_charge_btc:
            self._logger.info('Trigger already executed')
            if not status.pending_operations:
                self._logger.info('No such pending operations in last trigger')
                if not status.should_retry_no_bids:
                    self._logger.info('I tried too many times after trigger, not enough amount')
                    raise exceptions.NotEnoughBtcAmountException()
                self._logger.info('Retry again after trigger')
                status.retried_one_last_time_after_trigger += 1
        else:
            self._logger.info('Triggering bids')
            found_pending_operations = self._trading_service.trigger_charge_btc(user.reference_key_id)
            self._logger.info('Triggered bids, found_pending_operations? %s', found_pending_operations)
            if not found_pending_operations:
                # This is the case of a potential race condition: if the bid has been charged but
                # I am not seeing the btc transaction yet, I have to retry. Unfortunately I do not
                # know if there is a btc transaction to expect or not.
                # Luckily, it is expected that the client will never request a payment if the
                # balance is not enough, AND the balance includes pending bids as well.
                self._logger.warning('No such pending operations, potential race condition. Retrying...')
            status.triggered_charge_btc = True
            status.pending_operations = found_pending_operations
        self._logger.info('Retrying operation')
        raise _RetryException

    def _get_wallet_balance(self, wallet_id: WalletID) -> int:
        return self._bitcoin_wallet_service.get_wallet_balance(
            wallet_id, invalidata_cache=True).confirmed_satoshi_amount

    def safe_create_transaction(
            self, user: ConioUser, dest_address: BtcAddressID,
            bits: typing.Optional[SatoshiAmount] = None, fee_per_byte: typing.Optional[int] = None) \
            -> core_wallet_pb2.TransactionToBeSignedV1:
        wallet_reference_id = user.get_wallet(DEFAULT_CRYPTO_CURRENCY)['reference_id']
        assert fee_per_byte

        @retrying.retry(
            wait_exponential_multiplier=200,
            wait_exponential_max=5000,
            stop_max_delay=settings.MAX_WAIT_TIME_TO_CHARGED_BIDS_MSECS,
            retry_on_exception=_retry_if_not_enough_amount)
        def _inner(status: _CreateTransactionStatus):
            while True:
                core_wallet_balance = self._get_wallet_balance(wallet_reference_id)
                bids_balance = self._trading_service.get_processing_bids_bits(user.reference_key_id)
                if core_wallet_balance == self._get_wallet_balance(wallet_reference_id):
                    break
                self._logger.warning('Wallet balance not stable, checking again...')
            i_dont_have_enough_amount = bits and core_wallet_balance <= bits
            i_need_to_spend_all_my_pending_bids = bids_balance and not bits
            if bids_balance and (i_dont_have_enough_amount or i_need_to_spend_all_my_pending_bids):
                self._logger.info(
                    'I dont have enough amount? %s\n'
                    'I need to spend all my pending bids?%s\n'
                    'Core wallet balance: %s\n'
                    'Bids balance: %s',
                    i_dont_have_enough_amount,
                    i_need_to_spend_all_my_pending_bids,
                    core_wallet_balance,
                    bids_balance
                )
                self._trigger_charge_btc(user, status)

            try:
                result = self._bitcoin_wallet_service.create_transaction(
                    wallet_reference_id,
                    dest_address,
                    bits,
                    fee_per_byte=fee_per_byte
                ) if bits else self._bitcoin_wallet_service.create_all_transaction(
                    wallet_reference_id, dest_address, fee_per_byte=fee_per_byte
                )
                return result

            except (
                    core_wallet_exceptions.DustTransactionException,
                    core_wallet_exceptions.NotEnoughAmountException) as e:
                if bids_balance:
                    self._trigger_charge_btc(user, status)
                elif self._get_wallet_balance(wallet_reference_id) > core_wallet_balance:
                    raise _RetryException
                else:
                    raise exceptions.DustTransactionException() \
                        if isinstance(e, core_wallet_exceptions.DustTransactionException)\
                        else exceptions.NotEnoughBtcAmountException
        try:
            return _inner(_CreateTransactionStatus())
        except _RetryException:
            LOGGING_FACTORY.safe_withdrawal.exception('Retries expired')
            raise exceptions.NotEnoughBtcAmountException

    def safe_create_transaction_for_fees(
            self, user: ConioUser, dest_address: BtcAddressID, fee_per_byte: SatoshiAmount,
            bits: typing.Optional[SatoshiAmount] = None) \
            -> core_wallet_pb2.TransactionToBeSignedV1:
        wallet_reference_id = user.get_wallet(DEFAULT_CRYPTO_CURRENCY)['reference_id']
        fake_outputs = tuple(
            FakeOutput(x.address_id, x.satoshi)
            for x in self._trading_service.get_paid_bids(user)
        )
        try:
            return self._bitcoin_wallet_service.create_transaction(
                wallet_reference_id, dest_address, bits, fee_per_byte=fee_per_byte,
                fake_outputs=fake_outputs
            ) if bits else self._bitcoin_wallet_service.create_all_transaction(
                wallet_reference_id,
                dest_address,
                fee_per_byte=fee_per_byte,
                fake_outputs=fake_outputs
            )
        except core_wallet_exceptions.DustTransactionException:
            raise exceptions.DustTransactionException
        except core_wallet_exceptions.NotEnoughAmountException:
            raise exceptions.NotEnoughBtcAmountException

    def safe_pay_for_ask(self, user: ConioUser, ask_id: AskID) -> AskBtcTransaction:
        wallet_reference_id = user.get_wallet(DEFAULT_CRYPTO_CURRENCY)['reference_id']

        @retrying.retry(
            wait_exponential_multiplier=50,
            wait_exponential_max=1000,
            stop_max_delay=settings.MAX_WAIT_TIME_TO_CHARGED_BIDS_MSECS,
            retry_on_exception=_retry_if_not_enough_amount)
        def _inner(status: _CreateTransactionStatus):
            try:
                return self._trading_service.pay_for_ask(user, ask_id)
            except exceptions.NotEnoughBtcAmountException:
                core_wallet_balance = self._get_wallet_balance(wallet_reference_id)
                bids_balance = self._trading_service.get_processing_bids_bits(user.reference_key_id)
                if bids_balance:
                    try:
                        self._trigger_charge_btc(user, status)
                    except exceptions.NotEnoughBtcAmountException:
                        LOGGING_FACTORY.safe_withdrawal.warning(
                            'Despite the trigger charge, I could not pay for ask %s', ask_id
                        )
                        raise
                    raise _RetryException
                elif not status.balance_stability_double_check \
                        or core_wallet_balance < self._get_wallet_balance(wallet_reference_id):
                    status.balance_stability_double_check = True
                    raise _RetryException
                raise
        try:
            return _inner(_CreateTransactionStatus())
        except _RetryException:
            LOGGING_FACTORY.safe_withdrawal.exception('Retries expired')
            raise exceptions.NotEnoughBtcAmountException
