import typing

from bithustler_client.client.client import BithustlerClient
from bithustler_client.client.exceptions import InvalidIBANException
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.common import exceptions, ACCEPTANCE_TYPES_PB2_BY_NAME
from conio_sdk.common.decorators import xray_this
from conio_sdk.generated_protobuf import v1_pb2
from conio_sdk.logging.factory import LOGGING_FACTORY
from conio_sdk.services.conio.explicit_fees.explicit_fees_service import ExplicitFeesService
from conio_sdk.services.conio.fiat_limits.fiat_limits_service import FiatLimitsService
from conio_sdk.services.conio.sdk.crypto_proof.crypto_proof_verification_strategy import CryptoProofVerificationStrategy
from conio_sdk.services.conio.user.user_service import UserService, UserCredentials, TermsAndConditionsAcceptances, \
    AuthenticationData, UserLanguage
from conio_sdk.services.conio.wallet.bitcoin_wallet_service import WalletEncryptedData
from conio_sdk.services.conio.wallet.core_wallet_service import CoreWalletService


class SignupVOServices:
    def __init__(
            self, user_service: UserService, core_wallet_service: CoreWalletService,
            fiat_limits_service: FiatLimitsService,
            explicit_fees_service: ExplicitFeesService,
            bithustler_client: BithustlerClient,
            cards_client: CardsClient,
            expiry_crypto_proof_verification_strategy: CryptoProofVerificationStrategy,
            signature_crypto_proof_verification_strategy: CryptoProofVerificationStrategy):
        self._user_service = user_service
        self._bithustler_client = bithustler_client
        self._core_wallet_service = core_wallet_service
        self._fiat_limits_service = fiat_limits_service
        self._explicit_fees_service = explicit_fees_service
        self._expiry_crypto_proof_verification_strategy = expiry_crypto_proof_verification_strategy
        self._signature_crypto_proof_verification_strategy = signature_crypto_proof_verification_strategy
        self._cards_client = cards_client

    @xray_this
    def signup(self, msg: v1_pb2.Signup, user_language: typing.Optional[UserLanguage]) -> typing.Tuple[v1_pb2.SignupResponse, AuthenticationData]:
        LOGGING_FACTORY.testing.info('signup.expiry_crypto_proof_verification')
        if not self._expiry_crypto_proof_verification_strategy.verify(msg.cryptoRequest):
            LOGGING_FACTORY.security.warning('Crypto proof expired. Signup message: %s', str(msg))
            raise exceptions.CryptoProofExpiredException
        LOGGING_FACTORY.testing.info('signup.signature_crypto_proof_verification')
        if not self._signature_crypto_proof_verification_strategy.verify(msg.cryptoRequest):
            LOGGING_FACTORY.security.warning('Invalid crypto proof. Signup message: %s', str(msg))
            raise exceptions.InvalidCryptoProofException

        user_credentials = UserCredentials(
            msg.conioCredentials.externalUserID,
            msg.conioCredentials.hashedConioPassword
        )
        LOGGING_FACTORY.testing.info('signup.create_user_if_does_not_exist')
        conio_user = self._user_service.create_user_if_does_not_exist(
            user_credentials, msg.cryptoRequest.email,
            msg.cryptoRequest.firstName, msg.cryptoRequest.lastName)
        wallet_encrypted_data = WalletEncryptedData(
            encrypted_mnemonic=msg.encryptedUserKey.encryptedMnemonic,
            encrypted_seed=msg.encryptedUserKey.encryptedSeed,
            encrypted_private_key=msg.encryptedUserKey.encryptedPrivateKey,
            bip32_public_key=msg.encryptedUserKey.bip32PublicKey,
            wallet_id=None
        )

        LOGGING_FACTORY.testing.info('signup.create_wallet_if_not_exists')
        wallet_encrypted_data = self._core_wallet_service.create_wallet_if_not_exists(
            conio_user,
            wallet_encrypted_data
        )

        LOGGING_FACTORY.testing.info('signup.associate_fiat_limits')
        self._fiat_limits_service.associate_fiat_limits(conio_user, msg.cryptoRequest.userLevel)
        LOGGING_FACTORY.testing.info('signup.associate_explicit_fees')
        self._explicit_fees_service.associate_explicit_fees(conio_user, msg.cryptoRequest.userLevel)
        # Reloading user to get seller and payer ids.
        LOGGING_FACTORY.testing.info('signup.get_user')
        conio_user = self._user_service.get_user(msg.conioCredentials.externalUserID)
        LOGGING_FACTORY.testing.info('signup._ensure_sell_method')
        self._ensure_sell_method(conio_user, msg.cryptoRequest.iban)
        LOGGING_FACTORY.testing.info('signup._ensure_payment_method')
        self._ensure_payment_method(conio_user, msg.cryptoRequest.iban)
        LOGGING_FACTORY.testing.info('signup.activate_user_if_not_yet_active')
        # This MUST be the last action before completing signup process.
        accepted_client_support = accepted_app_improvement = False
        for a in msg.tc.acceptances:
            if ACCEPTANCE_TYPES_PB2_BY_NAME[a.acceptance_type] == v1_pb2.ACCEPTANCE_APP_IMPROVEMENT:
                accepted_app_improvement |= a.acceptance_choice == v1_pb2.ACC_CHOICE_ACCEPTED
            elif ACCEPTANCE_TYPES_PB2_BY_NAME[a.acceptance_type] == v1_pb2.ACCEPTANCE_CLIENT_SUPPORT:
                accepted_client_support |= a.acceptance_choice == v1_pb2.ACC_CHOICE_ACCEPTED
        if not accepted_client_support:
            raise exceptions.ClientSupportAcceptanceNotAcceptedException
        if not accepted_app_improvement:
            raise exceptions.AppImprovementAcceptanceNotAcceptedException

        authenticated_user = self._user_service.activate_user_if_not_yet_active(
            user_credentials,
            TermsAndConditionsAcceptances(
                accepted_marketing=False,
                accepted_client_support=accepted_client_support,
                accepted_app_improvement=accepted_app_improvement
            ),
            user_language
        )

        LOGGING_FACTORY.testing.info('signup.response')
        response = v1_pb2.SignupResponse()
        response.encryptedUserKey.encryptedPrivateKey = wallet_encrypted_data.encrypted_private_key
        response.encryptedUserKey.encryptedMnemonic = wallet_encrypted_data.encrypted_mnemonic
        response.encryptedUserKey.encryptedSeed = wallet_encrypted_data.encrypted_seed
        response.encryptedUserKey.bip32PublicKey = wallet_encrypted_data.bip32_public_key

        response.tc.MergeFrom(msg.tc)
        return response, authenticated_user.authentication_data

    def _ensure_payment_method(self, user: ConioUser, iban: str):
        payment_methods = self._cards_client.get_payment_methods(
            base_pb2.MsgGetPaymentMethods(payer_id=user.cards_integration_id)
        )
        if payment_methods.HasField('error'):
            raise ValueError(payment_methods)
        if any(
                filter(
                    lambda p: p.iban_data.iban == iban,
                    payment_methods.data.payment_methods
                )
        ):
            LOGGING_FACTORY.user.warning(
                'Payment method with IBAN %s for user %s already added',
                iban, user.reference_key_id
            )
            return

        # Here we do not double check iban validity, as it was already checked
        # in `_ensure_sell_method`.
        payment_method_id = self._cards_client.create_new_payment_method_wiretransfer(
            user.cards_integration_id, iban
        )
        LOGGING_FACTORY.user.info(
            'Payment method for IBAN %s created for user %s, payment method id %s',
            iban, user.reference_key_id, payment_method_id
        )

    def _ensure_sell_method(self, user: ConioUser, iban: str):
        sell_methods = self._bithustler_client.get_sell_methods(user.seller_id)
        sell_methods_to_be_deleted = list(
            filter(
                lambda d: d.iban_bank_account.iban != iban,
                sell_methods
            )
        )
        if sell_methods_to_be_deleted:
            LOGGING_FACTORY.user.warning(
                'Found sell methods to be removed: %s',
                sell_methods_to_be_deleted
            )
            for sell_method in sell_methods_to_be_deleted:
                self._bithustler_client.remove_sell_method(
                    user.seller_id, sell_method.id
                )
        if len(sell_methods) == len(sell_methods_to_be_deleted):
            try:
                self._bithustler_client.add_sell_method(iban, user.seller_id)
            except InvalidIBANException:
                LOGGING_FACTORY.validation.warning(
                    'Invalid IBAN %s for user %s', iban, user.reference_key_id
                )
                raise exceptions.InvalidIbanException
        else:
            LOGGING_FACTORY.user.warning('Sell method for user %s already added', user.reference_key_id)
