# noinspection PyUnresolvedReferences,PyPackageRequirements
import threading
from time import time

# noinspection PyUnresolvedReferences
from accrocchio.badgeofshame import prototry, compromise, fallacy_method
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 import get_fee_time_window
from conio_sdk.services.conio.sdk.wallet_withdrawals_vo_service import WalletWithdrawalsVOService
from conio_sdk.services.conio.wallet.bitcoin_wallet_service import BitcoinWalletService, BtcTransactionSignature
from etc import settings


@prototry
class _AllTransactionWasNotReallyAllException(Exception):
    pass


class BTCVOService:
    def __init__(
            self,
            bitlurker_client: BitlurkerClient,
            bitcoin_wallet_service: BitcoinWalletService,
            vault_client: VaultClient,
            wallet_withdrawals_vo_service: WalletWithdrawalsVOService
    ):
        self._bitlurker_client = bitlurker_client
        self._bitcoin_wallet_service = bitcoin_wallet_service
        self._vault_client = vault_client
        self._wallet_withdrawals_vo_service = wallet_withdrawals_vo_service

    def request_btc_withdrawal(
            self, msg: v1_pb2.MsgRequestBtcWithdrawal,
            user: ConioUser) -> v1_pb2.MsgRequestBtcWithdrawalResponse:
        response = v1_pb2.MsgRequestBtcWithdrawalResponse()

        if not user.user_info:
            LOGGING_FACTORY.security.warning('Could not find user info for r')
            raise exceptions.NotAuthenticatedException

        if not msg.fee_per_byte:
            time_window = get_fee_time_window()
            min_speed = time_window[0]
            max_speed = time_window[1] > 0 and time_window[1] or min_speed
            _estimation = self._bitlurker_client.get_recommended_fees(
                min_seconds=min_speed, max_seconds=max_speed
            )
            assert not _estimation.HasField('error')
            fee_per_byte = _estimation.data.fee_per_byte
        else:
            fee_per_byte = msg.fee_per_byte

        if msg.amount:
            tx_to_be_signed = self._wallet_withdrawals_vo_service.safe_create_transaction(
                user,
                msg.dest_address,
                fee_per_byte=fee_per_byte,
                bits=msg.amount
            )
        else:
            tx_to_be_signed = self._wallet_withdrawals_vo_service.safe_create_transaction(
                user,
                msg.dest_address,
                fee_per_byte=fee_per_byte
            )

        response.tx = tx_to_be_signed.tx
        response.fees = tx_to_be_signed.fees
        for cur_sign_data in tx_to_be_signed.sign_data:
            response.sign_data.add(
                data_to_sign=cur_sign_data.data_to_sign,
                path=cur_sign_data.path)

        response.date = int(time() * 1000)
        msg_to_sign = response.tx + response.date.to_bytes(8, 'big')
        response.signature = self._vault_client.sign(
            settings.VAULT_RSA_REFERENCE_KEY_ID, msg_to_sign, hash_algo='SHA-256'
        )
        return response

    def withdraw_btc(self, msg_send_payment: v1_pb2.MsgWithdrawBtc, user: ConioUser) \
            -> v1_pb2.MsgWithdrawBtcResponse:
        tx = msg_send_payment.tx
        date = msg_send_payment.date
        request_signature = msg_send_payment.request_signature

        # self.btc_transaction_listener.before_transaction_submitted(
        #     date, user.reference_key_id
        # )

        msg_to_sign = tx + date.to_bytes(8, 'big')
        expected_signature = self._vault_client.sign(
            settings.VAULT_RSA_REFERENCE_KEY_ID, msg_to_sign, hash_algo='SHA-256'
        )
        if expected_signature != request_signature:
            LOGGING_FACTORY.security.warning('Invalid RSA signature')
            raise exceptions.InvalidMessageSignatureException

        btc_tx_hash = self._bitcoin_wallet_service.submit_transaction(
            user.get_wallet(settings.DEFAULT_CRYPTO_CURRENCY)['reference_id'], tx,
            [
                BtcTransactionSignature(
                    bip44_path='/'.join(str(x) for x in s.path),
                    signature=s.signature
                ) for s in msg_send_payment.tx_signatures
            ]
        )
        return v1_pb2.MsgWithdrawBtcResponse(activity_id='__BTC__{}'.format(btc_tx_hash))

    def get_btc_withdrawal_fees(
            self,
            msg_request_payment: v1_pb2.MsgRequestBtcWithdrawalFees,
            user: ConioUser
    ) -> v1_pb2.MsgRequestBtcWithdrawalFeesResponse:
        LOGGING_FACTORY.wallet.debug('request_payment_fees. request: %s', msg_request_payment)
        response = v1_pb2.MsgRequestBtcWithdrawalFeesResponse()

        speed = msg_request_payment.speed \
            if msg_request_payment.WhichOneof('payload') == 'speed' else v1_pb2.TRANSACTION_SPEED_TYPE_3

        if msg_request_payment.get_all_fees_info:
            #  mandatory
            speed = v1_pb2.TRANSACTION_SPEED_TYPE_1

        available_fees = {}

        def _populate_fee_for_speed(ss, fees_dict):
            time_window = get_fee_time_window(ss)
            LOGGING_FACTORY.wallet.debug(
                'request_payment_fees: 1. Populating fee for speed: %s, tw: %s', ss, time_window
            )
            _fee_per_byte = self._bitlurker_client.get_recommended_fees(
                min_seconds=time_window[0],
                max_seconds=time_window[1]
            )
            LOGGING_FACTORY.wallet.debug(
                'request_payment_fees: 2. Populating fee for speed: %s, fpb: %s', ss, _fee_per_byte
            )
            if _fee_per_byte.HasField('error'):
                return
            fees_dict[ss] = {
                'min_time': time_window[0],
                'max_time': time_window[1],
                'fee_per_byte': _fee_per_byte.data.fee_per_byte
            }

        def _populate_transaction_for_fee_range(ss, fees_dict):
            try:
                if msg_request_payment.amount:
                    _tx_to_be_signed = self._wallet_withdrawals_vo_service.safe_create_transaction_for_fees(
                        user,
                        msg_request_payment.dest_address,
                        bits=msg_request_payment.amount,
                        fee_per_byte=fees_dict[ss]['fee_per_byte']
                    )
                else:
                    _tx_to_be_signed = self._wallet_withdrawals_vo_service.safe_create_transaction_for_fees(
                        user,
                        msg_request_payment.dest_address,
                        fee_per_byte=fees_dict[ss]['fee_per_byte']
                    )

                fees_dict[ss]['tx_to_be_signed'] = _tx_to_be_signed
            except exceptions.BFFException:
                LOGGING_FACTORY.fees_info.exception('Could not find fees for range %s', ss)
                fees_dict.pop(ss)
                return

        if msg_request_payment.get_all_fees_info:
            # gather fee costs
            LOGGING_FACTORY.wallet.debug('request_payment_fees, populating multiple fees')
            threads = [
                threading.Thread(target=_populate_fee_for_speed, args=(x, available_fees))
                for x in [speed] + [x for x in range(1, 6) if x != speed]
            ]
            for thread in threads:
                thread.start()
            for thread in threads:
                thread.join()
            LOGGING_FACTORY.wallet.debug('request_payment_fees, populated multiple fees: %s', available_fees)

            # remove duplicates
            processable_speeds = list(available_fees.keys())
            processable_speeds.sort()

            # gather transactions
            to_process = {}
            speeds_by_fee_per_byte = {}
            for s in processable_speeds:
                if available_fees[s]['fee_per_byte'] in speeds_by_fee_per_byte.keys():
                    continue
                to_process[s] = available_fees[s]
                speeds_by_fee_per_byte[available_fees[s]['fee_per_byte']] = s

            threads = [
                threading.Thread(target=_populate_transaction_for_fee_range, args=(x, available_fees))
                for x in to_process
            ]
            for thread in threads:
                thread.start()
            for thread in threads:
                thread.join()
            available_fees = to_process

        else:
            LOGGING_FACTORY.wallet.debug('request_payment_fees, populating single fee')
            # do the same up there, but for a single fee.
            _populate_fee_for_speed(speed, available_fees)
            _populate_transaction_for_fee_range(speed, available_fees)
            LOGGING_FACTORY.wallet.debug('request_payment_fees, populating single fee')
        LOGGING_FACTORY.wallet.debug('request_payment_fees, populated fees, available: %s', available_fees)
        if not available_fees or not available_fees[speed].get('tx_to_be_signed'):
            raise exceptions.NoSuchWithdrawalFeesInfoException

        if not msg_request_payment.amount:
            # The amount we have to send back in the primary view in case
            # of a send all, must be the lower available or the user will not be able to send
            # small amounts in case the speed[1] fee is too high.

            available_speeds = list(available_fees.keys())
            speed = max(available_speeds)

        tx_to_be_signed = available_fees[speed]['tx_to_be_signed']

        response.absolute_fees = tx_to_be_signed.fees
        response.amount = tx_to_be_signed.satoshi_amount
        response.fee_per_byte = tx_to_be_signed.fee_per_byte
        response.transaction_speed.type = speed
        response.transaction_speed.min_time = available_fees[speed]['min_time']
        if available_fees[speed]['max_time'] > 0:
            response.transaction_speed.max_time = available_fees[speed]['max_time']
        if not msg_request_payment.get_all_fees_info:
            LOGGING_FACTORY.wallet.debug('1. request_payment_fees. response: %s', response)
            return response

        processed_fees = []
        available_speeds = list(available_fees.keys())
        available_speeds.sort()

        for sp in available_speeds:
            if available_fees[sp]['tx_to_be_signed'].fees in processed_fees:
                continue
            processed_fees.append(available_fees[sp]['tx_to_be_signed'].fees)
            response.available_fees.add(
                fee_per_byte=available_fees[sp]['fee_per_byte'],
                speed=sp,
                min_time=available_fees[sp]['min_time'],
                max_time=available_fees[sp]['max_time'],
                absolute_fee=available_fees[sp]['tx_to_be_signed'].fees,
                amount=available_fees[sp]['tx_to_be_signed'].satoshi_amount
            )

        LOGGING_FACTORY.wallet.debug('2. request_payment_fees. response: %s', response)
        return response
