package com.braintree.method.impl;

import com.braintree.command.request.BrainTreeAddressRequest;
import com.braintree.command.request.BrainTreeAdjustAuthorizationRequest;
import com.braintree.command.request.BrainTreeAuthorizationRequest;
import com.braintree.command.request.BrainTreeCloneTransactionRequest;
import com.braintree.command.request.BrainTreeCreateCreditCardPaymentMethodRequest;
import com.braintree.command.request.BrainTreeCreatePaymentMethodRequest;
import com.braintree.command.request.BrainTreeCustomerRequest;
import com.braintree.command.request.BrainTreeDeletePaymentMethodRequest;
import com.braintree.command.request.BrainTreeFindMerchantAccountRequest;
import com.braintree.command.request.BrainTreeFindTransactionRequest;
import com.braintree.command.request.BrainTreeGenerateClientTokenRequest;
import com.braintree.command.request.BrainTreeOptionsRequest;
import com.braintree.command.request.BrainTreeRefundTransactionRequest;
import com.braintree.command.request.BrainTreeSaleTransactionRequest;
import com.braintree.command.request.BrainTreeSubmitForSettlementTransactionRequest;
import com.braintree.command.request.BrainTreeTokenizeCreditCardRequest;
import com.braintree.command.request.BrainTreeUpdateCreditCardRequest;
import com.braintree.command.request.BrainTreeUpdateCustomerRequest;
import com.braintree.command.result.BrainTreeAddressResult;
import com.braintree.command.result.BrainTreeAdjustedAuthorizationResult;
import com.braintree.command.result.BrainTreeCloneTransactionResult;
import com.braintree.command.result.BrainTreeCreatePaymentMethodResult;
import com.braintree.command.result.BrainTreeCustomerResult;
import com.braintree.command.result.BrainTreeFindCustomerResult;
import com.braintree.command.result.BrainTreeFindCustomersResult;
import com.braintree.command.result.BrainTreeFindMerchantAccountResult;
import com.braintree.command.result.BrainTreeFindTransactionResult;
import com.braintree.command.result.BrainTreeGenerateClientTokenResult;
import com.braintree.command.result.BrainTreePaymentDetailsResult;
import com.braintree.command.result.BrainTreePaymentMethodNonceResult;
import com.braintree.command.result.BrainTreePaymentMethodResult;
import com.braintree.command.result.BrainTreeRefundTransactionResult;
import com.braintree.command.result.BrainTreeSaleTransactionResult;
import com.braintree.command.result.BrainTreeSubmitForSettlementTransactionResult;
import com.braintree.command.result.BrainTreeTokenizeCardResult;
import com.braintree.command.result.BrainTreeTransactionResult;
import com.braintree.command.result.BrainTreeUpdateCreditCardBillingAddressResult;
import com.braintree.command.result.BrainTreeUpdateCustomerResult;
import com.braintree.command.result.BrainTreeVoidResult;
import com.braintree.commands.BrainTreeCloneCommand;
import com.braintree.commands.BrainTreeCreateAddressCommand;
import com.braintree.commands.BrainTreeCreateCreditCardPaymentMethodCommand;
import com.braintree.commands.BrainTreeCreatePaymentMethodCommand;
import com.braintree.commands.BrainTreeCreatePaymentMethodNonceCommand;
import com.braintree.commands.BrainTreeCreateUsBankAccountPaymentMethodCommand;
import com.braintree.commands.BrainTreeDeletePaymentMethodCommand;
import com.braintree.commands.BrainTreeFindCustomerCommand;
import com.braintree.commands.BrainTreeFindMerchantAccountCommand;
import com.braintree.commands.BrainTreeFindRefundCommand;
import com.braintree.commands.BrainTreeFindTransactionByIdCommand;
import com.braintree.commands.BrainTreeFindTransactionCommand;
import com.braintree.commands.BrainTreeGenerateClientTokenCommand;
import com.braintree.commands.BrainTreeGetPaymentDetailsByNonce;
import com.braintree.commands.BrainTreeGetPaymentMethodByTokenCommand;
import com.braintree.commands.BrainTreePartialCaptureCommand;
import com.braintree.commands.BrainTreeRefundCommand;
import com.braintree.commands.BrainTreeRemoveAddressCommand;
import com.braintree.commands.BrainTreeRemoveCustomerCommand;
import com.braintree.commands.BrainTreeSaleCommand;
import com.braintree.commands.BrainTreeSetDefaultPaymentInfoCommand;
import com.braintree.commands.BrainTreeSubmitForSettlementCommand;
import com.braintree.commands.BrainTreeUpdateAddressCommand;
import com.braintree.commands.BrainTreeUpdateCreditCardBillingAddressCommand;
import com.braintree.commands.BrainTreeUpdateCustomerCommand;
import com.braintree.commands.BrainTreeVerifyCreditCardCommand;
import com.braintree.commands.BrainTreeVoidCommand;
import com.braintree.commands.BrainTreeWebhookNotificationCommand;
import com.braintree.commands.BrainTreePingCommand;
import com.braintree.configuration.service.BrainTreeConfigService;
import com.braintree.constants.BraintreeConstants;
import com.braintree.customer.service.BrainTreeCustomerAccountService;
import com.braintree.customfield.service.BraintreeCustomFieldsService;
import com.braintree.enums.BrainTreeCardType;
import com.braintree.exceptions.BrainTreeCVVVerificationException;
import com.braintree.exceptions.BraintreeCreatePaymentMethodException;
import com.braintree.graphql.commands.BrainTreeGraphQLExchangeLegacyIdCommand;
import com.braintree.graphql.commands.BrainTreeGraphQLTokenizeCreditCardCommand;
import com.braintree.graphql.commands.BraintreeGraphQLAdjustAuthorizationCommand;
import com.braintree.hybris.data.BrainTreeSubscriptionInfoData;
import com.braintree.hybris.data.graphql.commands.request.BrainTreeExchangeLegacyIdRequest;
import com.braintree.hybris.data.graphql.commands.response.BraintreePaymentMethodData;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeThreeDSecure;
import com.braintree.method.BrainTreePaymentService;
import com.braintree.model.BrainTreePaymentInfoModel;
import com.braintree.payment.info.dao.BrainTreePaymentInfoDao;
import com.braintree.payment.info.service.BraintreePaymentInfoService;
import com.braintree.paypal.converters.impl.BraintreeBillingAddressConverter;
import com.braintree.transaction.service.BrainTreePaymentTransactionService;
import com.braintree.util.BrainTreeDuplicateCheckUtils;
import com.braintree.util.BrainTreeUtils;
import com.braintreegateway.WebhookNotification;
import com.braintreegateway.exceptions.NotFoundException;
import de.hybris.platform.braintree.data.BrainTreeWebhookNotificationRequest;
import de.hybris.platform.commerceservices.strategies.CheckoutCustomerStrategy;
import de.hybris.platform.commerceservices.strategies.CustomerNameStrategy;
import de.hybris.platform.core.model.order.CartModel;
import de.hybris.platform.core.model.order.payment.PaymentInfoModel;
import de.hybris.platform.core.model.user.AddressModel;
import de.hybris.platform.core.model.user.CustomerModel;
import de.hybris.platform.order.CartService;
import de.hybris.platform.payment.AdapterException;
import de.hybris.platform.payment.commands.AuthorizationCommand;
import de.hybris.platform.payment.commands.CreateSubscriptionCommand;
import de.hybris.platform.payment.commands.factory.CommandFactory;
import de.hybris.platform.payment.commands.factory.CommandFactoryRegistry;
import de.hybris.platform.payment.commands.factory.CommandNotSupportedException;
import de.hybris.platform.payment.commands.request.AuthorizationRequest;
import de.hybris.platform.payment.commands.request.CreateSubscriptionRequest;
import de.hybris.platform.payment.commands.request.VoidRequest;
import de.hybris.platform.payment.commands.result.AuthorizationResult;
import de.hybris.platform.payment.commands.result.SubscriptionResult;
import de.hybris.platform.payment.dto.BillingInfo;
import de.hybris.platform.payment.enums.PaymentTransactionType;
import de.hybris.platform.payment.model.PaymentTransactionEntryModel;
import de.hybris.platform.payment.model.PaymentTransactionModel;
import de.hybris.platform.servicelayer.config.ConfigurationService;
import de.hybris.platform.servicelayer.model.ModelService;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Supplier;

import static com.braintree.constants.BraintreeConstants.BRAINTREE_PROVIDER_NAME;
import static com.braintree.constants.BraintreeConstants.CARD_NUMBER_MASK;
import static com.braintree.constants.BraintreeConstants.CVV_FAILED_VERIFICATION_ERROR_MESSAGE;
import static com.braintree.constants.BraintreeConstants.NETWORK_CHECK;
import static com.braintree.constants.BraintreeConstants.PropertyConfiguration.BRAINTREE_AUTHENTICATION_TOKEN;
import static com.braintree.constants.BraintreeConstants.US_BANK_ACCOUNT;
import static java.util.Objects.nonNull;


public class DefaultBrainTreePaymentService implements BrainTreePaymentService {

    private static final Logger LOG = Logger.getLogger(DefaultBrainTreePaymentService.class);

    private ModelService modelService;
    private CartService cartService;
    private BrainTreeCustomerAccountService brainTreeCustomerAccountService;
    private BrainTreeConfigService brainTreeConfigService;
    private ConfigurationService configurationService;
    private CommandFactoryRegistry commandFactoryRegistry;
    private BraintreeBillingAddressConverter billingAddressConverter;
    private CheckoutCustomerStrategy checkoutCustomerStrategy;
    private BrainTreePaymentTransactionService brainTreePaymentTransactionService;
    private BraintreePaymentInfoService braintreePaymentInfoService;
    private CustomerNameStrategy customerNameStrategy;

    @Override
    public AuthorizationResult authorize(final AuthorizationRequest authorizationRequest) {
        final CustomerModel customer = checkoutCustomerStrategy.getCurrentUserForCheckout();
        return authorize((BrainTreeAuthorizationRequest) authorizationRequest, customer);
    }

    @Override
    public AuthorizationResult authorize(final BrainTreeAuthorizationRequest authorizationRequest,
        final CustomerModel customer) {
        LOG.info("authorize, authorizationRequest.getTotalAmount: " + authorizationRequest.getTotalAmount());

        try {
            if (brainTreeConfigService.isStoreInVault() &&
                !checkIfBrainTreeCustomerExist(customer.getBraintreeCustomerId())) {
                saveBraintreeCustomerId(customer, getCart().getDeliveryAddress());
                authorizationRequest.setCustomerId(customer.getBraintreeCustomerId());
            }

            LOG.info("authorizationRequest: " + authorizationRequest);

            final AuthorizationCommand command = getCommandFactory().createCommand(AuthorizationCommand.class);

            return command.perform(authorizationRequest);
        } catch (final NotFoundException exception) {
            LOG.error("[BT Payment Service] Errors occured not fount some item in BrainTree(throws NotFoundException)",
                exception);
            throw new AdapterException(
                "Problem occurred in Payment Provider configuration. Please contact with store support.");
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during authorization: " + exception.getMessage(), exception);
            throw new AdapterException(exception.getMessage());
        }
    }

    @Override
    public BrainTreeVoidResult voidTransaction(final VoidRequest voidRequest) {
        try {
            final BrainTreeVoidCommand command = getCommandFactory().createCommand(BrainTreeVoidCommand.class);

            return command.perform(voidRequest);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during trying to void transaction: " + exception.getMessage(),
                exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public BrainTreeCloneTransactionResult cloneTransaction(final BrainTreeCloneTransactionRequest request) {
        try {
            final BrainTreeCloneCommand command = getCommandFactory().createCommand(BrainTreeCloneCommand.class);
            return command.perform(request);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during trying to clone transaction: " + exception.getMessage(),
                exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public BrainTreeRefundTransactionResult refundTransaction(final BrainTreeRefundTransactionRequest request) {
        try {
            final BrainTreeRefundCommand command = getCommandFactory().createCommand(BrainTreeRefundCommand.class);

            return command.perform(request);
        } catch (final CommandNotSupportedException exception) {
            LOG.error("[BT Payment Service] Errors during trying to refund transaction: " + exception.getMessage(),
                exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public BrainTreeUpdateCustomerResult updateCustomer(final BrainTreeUpdateCustomerRequest request) {
        try {
            final BrainTreeUpdateCustomerCommand command = getCommandFactory()
                .createCommand(BrainTreeUpdateCustomerCommand.class);

            return command.perform(request);
        } catch (final CommandNotSupportedException exception) {
            LOG.error("[BT Payment Service] Errors during trying to update customer: " + exception.getMessage(),
                exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public BrainTreeSaleTransactionResult saleTransaction(final BrainTreeSaleTransactionRequest request) {
        try {
            final BrainTreeSaleCommand command = getCommandFactory().createCommand(BrainTreeSaleCommand.class);

            return command.perform(request);
        } catch (final CommandNotSupportedException exception) {
            LOG.error("[BT Payment Service] Errors during trying to sate transaction: " + exception.getMessage(),
                exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public BrainTreeSaleTransactionResult partialCaptureTransaction(final BrainTreeSaleTransactionRequest request) {
        try {
            final BrainTreePartialCaptureCommand command = getCommandFactory()
                .createCommand(BrainTreePartialCaptureCommand.class);

            return command.perform(request);
        } catch (final CommandNotSupportedException exception) {
            LOG.error("[BT Payment Service] Errors during trying to sate transaction: " + exception.getMessage(),
                exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public BrainTreeCustomerResult removeCustomer(final BrainTreeCustomerRequest request) {
        try {
            final BrainTreeRemoveCustomerCommand command = getCommandFactory()
                .createCommand(BrainTreeRemoveCustomerCommand.class);

            return command.perform(request);
        } catch (final CommandNotSupportedException exception) {
            LOG.error("[BT Payment Service] Errors during trying to remove customer: " + exception.getMessage(),
                exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public BrainTreeUpdateCreditCardBillingAddressResult updatePaymentMethod(final BrainTreeUpdateCreditCardRequest request) {
        try {
            final BrainTreeUpdateCreditCardBillingAddressCommand command = getCommandFactory().createCommand(
                BrainTreeUpdateCreditCardBillingAddressCommand.class);

            return command.perform(request);
        } catch (final CommandNotSupportedException exception) {
            LOG.error("[BT Payment Service] Errors during trying to update payment Method: " + exception.getMessage(),
                exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public BrainTreeUpdateCreditCardBillingAddressResult verifyCreditCard(final BrainTreeUpdateCreditCardRequest request) {
        try {
            final BrainTreeVerifyCreditCardCommand command = getCommandFactory().createCommand(
                    BrainTreeVerifyCreditCardCommand.class);

            return command.perform(request);
        } catch (final CommandNotSupportedException exception) {
            LOG.error("[BT Payment Service] Errors during trying to verify credit card: " + exception.getMessage(),
                    exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public SubscriptionResult createCustomerSubscription(final CreateSubscriptionRequest subscriptionRequest)
        throws AdapterException {
        try {
            final CreateSubscriptionCommand command = getCommandFactory()
                .createCommand(CreateSubscriptionCommand.class);

            return command.perform(subscriptionRequest);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during customer creation: " + exception.getMessage(), exception);
            throw new AdapterException(exception.getMessage());
        }
    }

    @Override
    public BrainTreeGenerateClientTokenResult generateClientToken(
        final BrainTreeGenerateClientTokenRequest clientTokenRequest)
        throws AdapterException {
        try {
            final BrainTreeGenerateClientTokenCommand command = getCommandFactory()
                .createCommand(BrainTreeGenerateClientTokenCommand.class);

            return command.perform(clientTokenRequest);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during token generation: " + exception.getMessage(), exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public BrainTreeFindCustomerResult findCustomer(final BrainTreeCustomerRequest findCustomerRequest)
        throws AdapterException {
        try {
            final BrainTreeFindCustomerCommand command = getCommandFactory()
                .createCommand(BrainTreeFindCustomerCommand.class);
            return command.perform(findCustomerRequest);
        } catch (final Exception exception) {
            LOG.error(
                "[BT Payment Service] Errors during trying to find customer generation: " + exception.getMessage(),
                exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public BrainTreeFindCustomersResult findCustomers(BrainTreeCustomerRequest findCustomerRequest)
        throws AdapterException {
        try {
            final BrainTreeFindCustomerCommand command = getCommandFactory()
                .createCommand(BrainTreeFindCustomerCommand.class);
            return command.process(findCustomerRequest);
        } catch (final Exception exception) {
            LOG.error(
                "[BT Payment Service] Errors during trying to find customer generation: " + exception.getMessage(),
                exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public BrainTreeSubmitForSettlementTransactionResult submitForSettlementTransaction(
        final BrainTreeSubmitForSettlementTransactionRequest request) {
        LOG.info("submitForSettlementTransaction");
        try {
            final BrainTreeSubmitForSettlementCommand command = getCommandFactory()
                .createCommand(BrainTreeSubmitForSettlementCommand.class);
            final BrainTreeSubmitForSettlementTransactionResult result = command.perform(request);

            updateCaptureTransaction(result, request);

            return result;
        } catch (final CommandNotSupportedException exception) {
            LOG.error("[BT Payment Service] Errors during trying to submit for settlement transaction: " + exception
                    .getMessage(),
                exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public String getBraintreePing() throws AdapterException {
        try {
            final BrainTreePingCommand command = getCommandFactory()
                    .createCommand(BrainTreePingCommand.class);
            return command.perform(null);
        } catch (final Exception exception) {
            LOG.error(
                    "[BT Payment Service] Errors during trying to get ping: " + exception.getMessage(),
                    exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    private void updateCaptureTransaction(final BrainTreeSubmitForSettlementTransactionResult result,
        final BrainTreeSubmitForSettlementTransactionRequest request) {
        List<PaymentTransactionModel> transactions = brainTreePaymentTransactionService
            .getTransactionsByRequestIdAndPaymentProvider(request.getTransactionId(),
                BraintreeConstants.BRAINTREE_PROVIDER_NAME);
        for (PaymentTransactionModel transactionModel : transactions) {
            for (PaymentTransactionEntryModel transactionEntryModel : transactionModel.getEntries()) {
                if (PaymentTransactionType.CAPTURE.equals(transactionEntryModel.getType())) {
                    transactionEntryModel.setAmount(request.getAmount());
                    modelService.save(transactionEntryModel);
                }
            }
        }
    }

    @Override
    public BrainTreeAddressResult createAddress(final BrainTreeAddressRequest addressRequest,
        final CustomerModel customer) {
        try {
            populateCustomerID(addressRequest, customer);
            final BrainTreeCreateAddressCommand command = getCommandFactory()
                .createCommand(BrainTreeCreateAddressCommand.class);
            return command.perform(addressRequest);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during address creation: " + exception.getMessage(), exception);
            return null;
        }
    }

    private void populateCustomerID(final BrainTreeAddressRequest addressRequest, final CustomerModel customer) {
        if (addressRequest.getCustomerId() == null) {
            if (customer.getBraintreeCustomerId() == null) {
                saveBraintreeCustomerId(customer);
                addressRequest.setCustomerId(customer.getBraintreeCustomerId());
            } else {
                addressRequest.setCustomerId(customer.getBraintreeCustomerId());
            }
        }
    }

    @Override
    public BrainTreeAddressResult updateAddress(final BrainTreeAddressRequest addressRequest) {
        try {
            final BrainTreeUpdateAddressCommand command = getCommandFactory()
                .createCommand(BrainTreeUpdateAddressCommand.class);
            return command.perform(addressRequest);
        } catch (final Exception exception) {
            LOG.error(
                "[BT Payment Service] Errors during address[" + addressRequest.getAddressId() + "] update: " + exception
                    .getMessage(), exception);
        }
        return null;
    }

    @Override
    public BrainTreeAdjustedAuthorizationResult adjustAuthorization(final BrainTreeAdjustAuthorizationRequest adjustAuthorizationRequest) {
        BrainTreeAdjustedAuthorizationResult result;
        try {
            final BraintreeGraphQLAdjustAuthorizationCommand command = getCommandFactory()
                    .createCommand(BraintreeGraphQLAdjustAuthorizationCommand.class);
            result = command.perform(adjustAuthorizationRequest);
        } catch (final Exception exception) {
            LOG.error(
                    "[BT Payment Service] Errors during transaction [" + adjustAuthorizationRequest.getTransactionId() +
                            "] amount update: " + exception.getMessage(), exception);
            result = new BrainTreeAdjustedAuthorizationResult();
        }
        return result;
    }

    @Override
    public BrainTreeAddressResult removeAddress(final BrainTreeAddressRequest addressRequest) {
        try {
            final BrainTreeRemoveAddressCommand command = getCommandFactory()
                .createCommand(BrainTreeRemoveAddressCommand.class);

            return command.perform(addressRequest);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during address[" + addressRequest.getAddressId() + "] creation: "
                + exception
                .getMessage(), exception);
        }
        return new BrainTreeAddressResult();
    }

    @Override
    public BrainTreePaymentInfoModel completeCreateSubscription(final CustomerModel customer,
        final String paymentInfoId) {
        final CartModel card = getCart();

        final BrainTreePaymentInfoModel paymentInfo = brainTreeCustomerAccountService
            .getBrainTreePaymentInfoForCode(customer,
                paymentInfoId);

        if (paymentInfo != null) {
            paymentInfo.setMerchantAccountIdForCurrentSite(brainTreeConfigService.getMerchantAccountIdForCurrentSiteAndCurrency());

            saveBillingAddressIfNotExistForVenmoPaymentMethod(paymentInfo);
            modelService.save(paymentInfo);
            card.setPaymentInfo(paymentInfo);
            modelService.save(card);

            return paymentInfo;
        } else {
            return null;
        }
    }

    @Override
    public void saveBillingAddressIfNotExistForVenmoPaymentMethod(final BrainTreePaymentInfoModel paymentInfo) {
        if (BrainTreeUtils.isVenmoPayment(paymentInfo.getPaymentProvider())
                && Optional.ofNullable(paymentInfo.getBillingAddress()).isEmpty()) {
            AddressModel addressModel = modelService.clone(getCart().getDeliveryAddress());
            addressModel.setShippingAddress(false);
            addressModel.setBillingAddress(true);
            modelService.save(addressModel);
            paymentInfo.setBillingAddress(addressModel);
        }
    }

    @Override
    public BrainTreePaymentInfoModel completeCreateSubscriptionWithCvvVerification(
            final CustomerModel customer, final String paymentInfoId, final String cvvNonce) {
        final BrainTreePaymentInfoModel paymentInfo = completeCreateSubscription(customer, paymentInfoId);

        if (paymentInfo != null) {
            if (brainTreeConfigService.isBraintreeVerifyFlow()
                    && BrainTreeUtils.isCreditCardPayment(paymentInfo.getPaymentProvider())) {

                BrainTreeUpdateCreditCardRequest request =
                        new BrainTreeUpdateCreditCardRequest(paymentInfo.getPaymentMethodToken());
                request.setToken(paymentInfo.getPaymentMethodToken());
                request.setCvvNonce(cvvNonce);
                BrainTreeOptionsRequest options = new BrainTreeOptionsRequest();
                options.setVerifyCard(true);
                request.setOptions(options);
                BrainTreeUpdateCreditCardBillingAddressResult result = verifyCreditCard(request);
                if (!result.isSuccess()) {
                    CartModel cart = getCart();
                    cart.setPaymentInfo(null);
                    modelService.save(cart);
                    throw new BrainTreeCVVVerificationException(CVV_FAILED_VERIFICATION_ERROR_MESSAGE);
                }
            }
            return paymentInfo;
        } else {
            return null;
        }
    }

    @Override
    public String generateClientToken(final String braintreeCustomerId) {
        final String merchantAccountId = brainTreeConfigService.getMerchantAccountIdForCurrentSiteAndCurrency();
        return getClientToken(merchantAccountId, braintreeCustomerId);
    }

    @Override
    public String generateClientToken(final String site, final String currency) {
        final String merchantAccountId = brainTreeConfigService.getMerchantAccountIdForSiteAndCurrencyIsoCode(site, currency);
        return getClientToken(merchantAccountId, StringUtils.EMPTY);
    }

    @Override
    public BrainTreeCreatePaymentMethodResult createPaymentMethod(final BrainTreeCreatePaymentMethodRequest request) {
        try {
            final BrainTreeCreatePaymentMethodCommand command = getCommandFactory()
                .createCommand(BrainTreeCreatePaymentMethodCommand.class);
            final BrainTreeCreatePaymentMethodResult result = command.perform(request);
            LOG.info("Created PaymentMethod, result: " + result);
            return result;
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors payment method creating: " + exception.getMessage(), exception);
            throw new BraintreeCreatePaymentMethodException();
        }
    }

    @Override
    public BrainTreePaymentMethodResult createCreditCardPaymentMethod(
        final BrainTreeCreateCreditCardPaymentMethodRequest request) {
        try {
            final BrainTreeCreateCreditCardPaymentMethodCommand command = getCommandFactory().createCommand(
                BrainTreeCreateCreditCardPaymentMethodCommand.class);
            return command.perform(request);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors payment method creating: " + exception.getMessage(), exception);
            return null;
        }
    }

    @Override
    public BrainTreePaymentMethodNonceResult createPaymentMethodNonce(String request) {
        BrainTreePaymentMethodNonceResult result = new BrainTreePaymentMethodNonceResult();
        LOG.info("createPaymentMethodNonce");
        try {
            BrainTreeCreatePaymentMethodNonceCommand command = getCommandFactory()
                .createCommand(BrainTreeCreatePaymentMethodNonceCommand.class);
            result = command.perform(request);
        } catch (Exception exception) {
            LOG.error(
                "[BT Payment Service] Error when trying to create payment method nonce: " + exception.getMessage(),
                exception);
        }
        return result;
    }

    @Override
    public BrainTreePaymentMethodResult deletePaymentMethod(final BrainTreeDeletePaymentMethodRequest request) {
        try {
            final BrainTreeDeletePaymentMethodCommand command = getCommandFactory().createCommand(
                BrainTreeDeletePaymentMethodCommand.class);
            return command.perform(request);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during trying to delete payment method: " + exception.getMessage(),
                exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public BrainTreeFindMerchantAccountResult findMerchantAccount(
        final BrainTreeFindMerchantAccountRequest brainTreeFindMerchantAccountRequest) {
        try {
            final BrainTreeFindMerchantAccountCommand command = getCommandFactory().createCommand(
                BrainTreeFindMerchantAccountCommand.class);
            return command.perform(brainTreeFindMerchantAccountRequest);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during trying to find merchant account: " + exception.getMessage(),
                exception);
            throw new AdapterException("Request failed!", exception);
        }
    }

    @Override
    public BrainTreeFindTransactionResult findTransactions(BrainTreeFindTransactionRequest request) {
        try {
            final BrainTreeFindTransactionCommand command = getCommandFactory().createCommand(
                BrainTreeFindTransactionCommand.class);
            return command.perform(request);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during trying to find merchant account: " + exception.getMessage(),
                exception);
            throw new AdapterException("Request failed!", exception);
        }
    }

    @Override
    public BrainTreeFindTransactionResult findRefundedTransactions(BrainTreeFindTransactionRequest request) {
        try {
            final BrainTreeFindRefundCommand command = getCommandFactory().createCommand(
                    BrainTreeFindRefundCommand.class);
            return command.perform(request);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during trying to find merchant account: " + exception.getMessage(),
                    exception);
            throw new AdapterException("Request failed!", exception);
        }
    }

    @Override
    public BrainTreeTransactionResult findTransactionById(String transactionId) {
        try {
            BrainTreeFindTransactionByIdCommand command = getCommandFactory().createCommand(
                    BrainTreeFindTransactionByIdCommand.class);
            return command.perform(transactionId);
        } catch (Exception exception) {
            LOG.error("[BT Payment Service] Errors during trying to find transaction by id: " + exception.getMessage(),
                    exception);
            throw new AdapterException("Request failed!", exception);
        }
    }

    private CartModel getCart() {
        return cartService.getSessionCart();
    }

    private String getClientToken(final String merchantAccountId, final String braintreeCustomerId) {
        final String authenticationToken = getAuthenticationToken();
        if (authenticationToken != null && !StringUtils.EMPTY.equals(authenticationToken)) {
            return authenticationToken;
        }

        BrainTreeGenerateClientTokenRequest brainTreeGenerateClientTokenRequest = new BrainTreeGenerateClientTokenRequest(
            StringUtils.EMPTY, StringUtils.EMPTY);
        if (StringUtils.isNotEmpty(merchantAccountId)) {
            brainTreeGenerateClientTokenRequest.setMerchantAccount(merchantAccountId);
        }

        if (StringUtils.isNotEmpty(braintreeCustomerId)) {
            brainTreeGenerateClientTokenRequest.setCustomerId(braintreeCustomerId);
        }

        final BrainTreeGenerateClientTokenResult response = generateClientToken(brainTreeGenerateClientTokenRequest);
        return response.getClientToken();
    }

    private String getAuthenticationToken() {
        return configurationService.getConfiguration().getString(BRAINTREE_AUTHENTICATION_TOKEN);
    }

    private boolean checkIfBrainTreeCustomerExist(final String braintreeCustomerId) {
        return findBrainTreeCustomer(braintreeCustomerId).isCustomerExist();
    }

    private SubscriptionResult createCustomerSubscription(final CustomerModel customer,
        final AddressModel shippingAddress) {
        final BillingInfo billingInfo = new BillingInfo();
        if (shippingAddress != null) {
            billingAddressConverter.convert(shippingAddress, billingInfo);
        } else {
            String[] names = customerNameStrategy.splitName(customer.getName());
            if (names != null) {
                billingInfo.setFirstName(names[0]);
                billingInfo.setLastName(names[1]);
            }
            billingInfo.setEmail(customer.getContactEmail());
        }

        final CreateSubscriptionRequest createSubscriptionRequest = new CreateSubscriptionRequest(null, billingInfo,
            null, null,
            null, null, null);

        return createCustomerSubscription(createSubscriptionRequest);
    }


    private String saveBraintreeCustomerId(final CustomerModel customer, final AddressModel shippingAddress) {
        final SubscriptionResult result = createCustomerSubscription(customer, shippingAddress);
        customer.setBraintreeCustomerId(result.getMerchantTransactionCode());
        modelService.save(customer);
        return result.getMerchantTransactionCode();
    }

    private String saveBraintreeCustomerId(final CustomerModel customer) {
        final SubscriptionResult result = createCustomerSubscription(customer, null);
        customer.setBraintreeCustomerId(result.getMerchantTransactionCode());
        modelService.save(customer);
        return result.getMerchantTransactionCode();
    }

    private CommandFactory getCommandFactory() {
        return commandFactoryRegistry.getFactory(BRAINTREE_PROVIDER_NAME);
    }

    @Override
    public BrainTreeCreatePaymentMethodResult createPaymentMethodOnBraintreeOrGetExisting(CustomerModel customer,
        AddressModel billingAddress, final BrainTreeSubscriptionInfoData subscriptionInfoData) {

        final BrainTreeCreatePaymentMethodRequest request = createRequest(customer,
                 billingAddress, subscriptionInfoData);

        final BrainTreeFindCustomerResult findCustomerResult = findBrainTreeCustomer(customer.getBraintreeCustomerId());

        if (!findCustomerResult.isCustomerExist()) {
            final String customerId = saveBraintreeCustomerId(customer, billingAddress);
            request.setCustomerId(customerId);
        }

        final BrainTreeCreatePaymentMethodResult createResult;
        if (BrainTreeUtils.isPayPalPayment(subscriptionInfoData.getPaymentProvider())
            && !brainTreeConfigService.isDropInEnabled()) {
            createResult = createPaymentMethod(request);
        } else {
            createResult = findPaymentMethod(findCustomerResult, billingAddress, subscriptionInfoData)
                .map(BrainTreeUtils::convertPaymentDetailsToPaymentMethodResult)
                .orElseGet(getCreatePaymentMethod(request));
        }

        return createResult;
    }

    private Supplier<BrainTreeCreatePaymentMethodResult> getCreatePaymentMethod(final BrainTreeCreatePaymentMethodRequest request){
        return () -> {
            if(request instanceof BrainTreeCreateCreditCardPaymentMethodRequest){
                return createCreditCardPaymentMethod((BrainTreeCreateCreditCardPaymentMethodRequest)request);
            } else if (request.getPaymentProvider() != null && request.getPaymentProvider().equalsIgnoreCase(US_BANK_ACCOUNT)) {
                return createUsBankAccountPaymentMethod(request);
            } else {
                return createPaymentMethod(request);
            }
        };
    }

    private BrainTreeCreatePaymentMethodRequest createRequest(CustomerModel customer, AddressModel billingAddress, BrainTreeSubscriptionInfoData subscriptionInfoData) {
        BillingInfo billingInfo = null;

        if (nonNull(billingAddress)) {
            billingInfo = billingAddressConverter.convert(billingAddress);
        }
        BrainTreeCreatePaymentMethodRequest request;
        final boolean isAdvancedFraudTools = brainTreeConfigService.isAdvancedFraudTools();
        if (BrainTreeUtils.isCreditCardPayment(subscriptionInfoData.getPaymentProvider())){
            request = new BrainTreeCreateCreditCardPaymentMethodRequest(null,
                    subscriptionInfoData.getNonce(),
                    subscriptionInfoData.getCardholder(), customer.getBraintreeCustomerId(),
                    nonNull(billingAddress) ? billingAddress.getBrainTreeAddressId() : null,
                    billingInfo, subscriptionInfoData.getAmount(), isAdvancedFraudTools, subscriptionInfoData.getDeviceData(), subscriptionInfoData.isIs3dSecureFlow());

        } else if (BrainTreeUtils.isUsBankAccountPayment(subscriptionInfoData.getPaymentProvider())) {
            request = new BrainTreeCreatePaymentMethodRequest(null, subscriptionInfoData.getNonce(), customer.getBraintreeCustomerId());
            request.setDeviceData(subscriptionInfoData.getDeviceData());
            request.setAdvancedFraudTools(isAdvancedFraudTools);
            request.setPaymentProvider(subscriptionInfoData.getPaymentProvider());
            BrainTreeOptionsRequest optionsRequest = new BrainTreeOptionsRequest();
            optionsRequest.setUsBankAccountVerificationMethod(NETWORK_CHECK);
            request.setOptions(optionsRequest);

        } else {
            request = new BrainTreeCreatePaymentMethodRequest(null,
                    subscriptionInfoData.getNonce(),
                    subscriptionInfoData.getCardholder(), customer.getBraintreeCustomerId(),
                    Optional.ofNullable(billingAddress).map(AddressModel::getBrainTreeAddressId).orElse(null),
                    billingInfo, subscriptionInfoData.getAmount(), isAdvancedFraudTools, subscriptionInfoData.getDeviceData());

        }
        return request;
    }

    @Override
    public String createCustomer(CustomerModel customer, AddressModel billingAddress) {
        return saveBraintreeCustomerId(customer, billingAddress);
    }

    private BrainTreeFindCustomerResult findBrainTreeCustomer(String braintreeCustomerId) {
        if (StringUtils.isEmpty(braintreeCustomerId)) {
            return new BrainTreeFindCustomerResult(false);
        }
        final BrainTreeCustomerRequest findCustomerRequest = new BrainTreeCustomerRequest(braintreeCustomerId);
        findCustomerRequest.setCustomerId(braintreeCustomerId);

        return findCustomer(findCustomerRequest);
    }

    @Override
    public Optional<BrainTreePaymentDetailsResult> findPaymentMethod(BrainTreeFindCustomerResult customerResult,
            AddressModel billingAddress, BrainTreeSubscriptionInfoData subscriptionData) {
        return Optional.ofNullable(customerResult)
            .map(BrainTreeFindCustomerResult::getPaymentMethods)
            .orElse(Collections.emptyList())
            .stream()
            .filter(detailsResult -> detailsResult.getPaymentProvider()
                .equalsIgnoreCase(subscriptionData.getPaymentProvider()))
            .filter(detailsResult -> {
                if (BrainTreeUtils.isVenmoPayment(subscriptionData.getPaymentProvider())
                    || BrainTreeUtils.isPayPalPayment(subscriptionData.getPaymentProvider())) {
                    return BrainTreeDuplicateCheckUtils.emailBasedCheck(detailsResult, subscriptionData);
                } else if (BrainTreeUtils.isSrcPayment(subscriptionData.getPaymentProvider())
                    || BrainTreeUtils.isGooglePayment(subscriptionData.getPaymentProvider())) {
                    return BrainTreeDuplicateCheckUtils.checkForGoogleAndSrc(detailsResult, subscriptionData);
                } else if (BrainTreeUtils.isCreditCardPayment(subscriptionData.getPaymentProvider())) {
                    return BrainTreeDuplicateCheckUtils.checkForCreditCard(detailsResult, subscriptionData,
                        billingAddress);
                } else if (BrainTreeUtils.isUsBankAccountPayment(subscriptionData.getPaymentProvider())) {
                    return BrainTreeDuplicateCheckUtils.checkForUsBankAccount(detailsResult, subscriptionData);
                }
                return false;
            }).findFirst();
    }

    @Override
    public BrainTreeUpdateCustomerResult setBrainTreeDefaultPaymentInfo(BrainTreeUpdateCustomerRequest request) {
        try {
            final BrainTreeSetDefaultPaymentInfoCommand command = getCommandFactory()
                    .createCommand(BrainTreeSetDefaultPaymentInfoCommand.class);

            return command.perform(request);
        } catch (final CommandNotSupportedException exception) {
            LOG.error("[BT Payment Service] Errors during trying to set default payment info: " + exception.getMessage(),
                    exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public BrainTreeCreatePaymentMethodResult createUsBankAccountPaymentMethod(BrainTreeCreatePaymentMethodRequest request) {
        LOG.info("createUsBankAccountPaymentMethod");
        try {
            final BrainTreeCreateUsBankAccountPaymentMethodCommand command = getCommandFactory()
                    .createCommand(BrainTreeCreateUsBankAccountPaymentMethodCommand.class);
            final BrainTreeCreatePaymentMethodResult result = command.perform(request);
            LOG.info("Created PaymentMethod, result: " + result);
            return result;
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors payment method creating: " + exception.getMessage(), exception);
            return null;
        }
    }

    @Override
    public BraintreePaymentMethodData getPaymentDetailsByNonce(String nonce) {
        try {
            final BrainTreeGetPaymentDetailsByNonce command = getCommandFactory()
                    .createCommand(BrainTreeGetPaymentDetailsByNonce.class);
            return command.perform(nonce);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors getting payment by nonce: " + exception.getMessage(), exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public WebhookNotification getWebhookNotification(BrainTreeWebhookNotificationRequest webhookNotificationRequest) {
        BrainTreeWebhookNotificationCommand command;
        try {
            command = getCommandFactory().createCommand(BrainTreeWebhookNotificationCommand.class);
            return command.perform(webhookNotificationRequest);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during trying to get webhook notification " + exception.getMessage(),
                exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public void updatePaymentInfo(PaymentInfoModel paymentInfo, BrainTreePaymentDetailsResult paymentDetails) {
        if (paymentDetails == null || paymentDetails.getPaymentMethodToken() == null) {
            return;
        }
        if (paymentInfo instanceof BrainTreePaymentInfoModel) {
            BrainTreePaymentInfoModel brainTreePaymentInfoModel = (BrainTreePaymentInfoModel) paymentInfo;
            if (brainTreePaymentInfoModel.isShouldBeSaved()) {
                paymentInfo.setSaved(true);
            }
            if (brainTreeConfigService.isStoreInVault()) {
                brainTreePaymentInfoModel.setUsePaymentMethodToken(true);
                brainTreePaymentInfoModel.setPaymentMethodToken(paymentDetails.getPaymentMethodToken());
                brainTreePaymentInfoModel.setPaymentMethodGraphQLToken(paymentDetails.getPaymentMethodGraphQLToken());
                brainTreePaymentInfoModel.setExpirationMonth(paymentDetails.getExpirationMonth());
                brainTreePaymentInfoModel.setExpirationYear(paymentDetails.getExpirationYear());
                brainTreePaymentInfoModel.setImageSource(paymentDetails.getImageUrl());
                if (brainTreePaymentInfoModel.getCardNumber() == null &&
                        paymentDetails.getLast4() != null) {
                    brainTreePaymentInfoModel.setCardNumber(String.format(CARD_NUMBER_MASK, paymentDetails.getLast4()));
                    brainTreePaymentInfoModel.setCardType(BrainTreeCardType.valueOf(paymentDetails.getCardType()));
                }
            }
            modelService.save(brainTreePaymentInfoModel);
            braintreePaymentInfoService.checkOnDuplicatePaymentMethod(brainTreePaymentInfoModel, (CustomerModel) brainTreePaymentInfoModel.getUser(), paymentInfo.getBillingAddress());
        }
    }

    public BrainTreePaymentDetailsResult getPaymentMethodFromBTByToken(final String paymentMethodToken) {
        BrainTreeGetPaymentMethodByTokenCommand command;
        try {
            command = getCommandFactory().createCommand(BrainTreeGetPaymentMethodByTokenCommand.class);
            return command.perform(paymentMethodToken);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during trying to get webhook payment method by token " + exception
                .getMessage(), exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public String exchangeLegacyIdToGraphQLId(BrainTreeExchangeLegacyIdRequest request) {
        try {
            final BrainTreeGraphQLExchangeLegacyIdCommand command = getCommandFactory()
                .createCommand(BrainTreeGraphQLExchangeLegacyIdCommand.class);

            return command.perform(request);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during exchanging legacy id to graphQL id: "
                + exception.getMessage(), exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public BrainTreeTokenizeCardResult tokenizeCreditCard(BrainTreeTokenizeCreditCardRequest request) {
        try {
            final BrainTreeGraphQLTokenizeCreditCardCommand command = getCommandFactory()
                .createCommand(BrainTreeGraphQLTokenizeCreditCardCommand.class);

            return command.perform(request);
        } catch (final Exception exception) {
            LOG.error("[BT Payment Service] Errors during exchanging legacy id to graphQL id: "
                + exception.getMessage(), exception);
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    @Override
    public String getImageSourceForPaymentMethod(String cardType, final String paymentMethodsString) {
        if(cardType != null) {
            cardType = cardType.replaceAll("[\\_\\s]", "");
            Map<String, String> methodImages = brainTreeConfigService.getAcceptedPaymentMethodImages(paymentMethodsString);
            for (Entry<String, String> entry : methodImages.entrySet()) {
                if (entry.getKey().equalsIgnoreCase(cardType)) {
                    return entry.getValue();
                }
            }
        }
        return null;
    }

    public void setBraintreePaymentInfoService(BraintreePaymentInfoService braintreePaymentInfoService) {
        this.braintreePaymentInfoService = braintreePaymentInfoService;
    }

    public void setModelService(final ModelService modelService) {
        this.modelService = modelService;
    }

    public void setCartService(final CartService cartService) {
        this.cartService = cartService;
    }

    public void setBrainTreeCustomerAccountService(
        final BrainTreeCustomerAccountService brainTreeCustomerAccountService) {
        this.brainTreeCustomerAccountService = brainTreeCustomerAccountService;
    }

    public void setBrainTreeConfigService(final BrainTreeConfigService brainTreeConfigService) {
        this.brainTreeConfigService = brainTreeConfigService;
    }

    public void setConfigurationService(final ConfigurationService configurationService) {
        this.configurationService = configurationService;
    }

    public void setCommandFactoryRegistry(final CommandFactoryRegistry commandFactoryRegistry) {
        this.commandFactoryRegistry = commandFactoryRegistry;
    }

    public void setBillingAddressConverter(final BraintreeBillingAddressConverter billingAddressConverter) {
        this.billingAddressConverter = billingAddressConverter;
    }

    public void setCheckoutCustomerStrategy(final CheckoutCustomerStrategy checkoutCustomerStrategy) {
        this.checkoutCustomerStrategy = checkoutCustomerStrategy;
    }

    public void setBrainTreePaymentTransactionService(
        BrainTreePaymentTransactionService brainTreePaymentTransactionService) {
        this.brainTreePaymentTransactionService = brainTreePaymentTransactionService;
    }

    public void setCustomerNameStrategy(CustomerNameStrategy customerNameStrategy) {
        this.customerNameStrategy = customerNameStrategy;
    }
}
