package com.braintree.controllers.pages;

import static com.braintree.constants.BraintreeConstants.PropertyConfiguration.BRAINTREE_ACCEPTED_CREDIT_CARD_PAYMENT_METHODS;
import static com.braintree.constants.Braintreeb2baddonWebConstants.US_BANK_ACCOUNT_DATA;
import static com.braintree.controllers.Braintreeb2baddonControllerConstants.CLIENT_TOKEN;
import static com.braintree.constants.BraintreeConstants.US_BANK_ACCOUNT;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
import static de.hybris.platform.servicelayer.util.ServicesUtil.validateParameterNotNull;
import static de.hybris.platform.util.localization.Localization.getLocalizedString;

import com.braintree.configuration.service.BrainTreeConfigService;
import com.braintree.controllers.Braintreeb2baddonControllerConstants;
import com.braintree.exceptions.BrainTreeCardVerifyException;
import com.braintree.exceptions.BrainTreePaymentMethodLimitedException;
import com.braintree.exceptions.BraintreeDeletePaymentDetailsWithNotCapturedOrderException;
import com.braintree.facade.BrainTreeConnectWithPayPalFacade;
import com.braintree.facade.impl.DefaultBrainTreeUserFacade;
import com.braintree.facade.BrainTreeSetUpPasswordFacade;
import com.braintree.facade.BrainTreeUserFacade;
import com.braintree.facade.impl.DefaultBrainTreeCheckoutFacade;
import com.braintree.facade.impl.DefaultBrainTreePaymentFacade;
import com.braintree.form.UpdateCreditCardBillingAddressForm;
import com.braintree.hybris.data.BrainTreeSubscriptionInfoData;
import com.braintree.hybris.data.BraintreeSavePaymentMethodForm;
import com.braintree.hybris.data.ErrorDto;
import com.braintree.hybris.data.ErrorListDto;
import com.braintree.hybris.data.UsBankAccountDetailsData;
import com.braintree.hybris.data.UsBankAccountResponseData;
import com.braintree.method.BrainTreePaymentService;
import com.braintree.converters.BrainTreeUsBankAccountAddressDataConverter;
import com.braintree.util.BrainTreeUtils;
import com.braintree.util.GenericBuilder;
import com.braintree.validators.BrainTreePasswordValidator;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.hybris.platform.acceleratorstorefrontcommons.annotations.RequireHardLogIn;
import de.hybris.platform.acceleratorstorefrontcommons.breadcrumb.Breadcrumb;
import de.hybris.platform.acceleratorstorefrontcommons.breadcrumb.ResourceBreadcrumbBuilder;
import de.hybris.platform.acceleratorstorefrontcommons.controllers.ThirdPartyConstants;
import de.hybris.platform.acceleratorstorefrontcommons.controllers.pages.AbstractPageController;
import de.hybris.platform.acceleratorstorefrontcommons.controllers.util.GlobalMessages;
import de.hybris.platform.acceleratorstorefrontcommons.forms.AddressForm;
import de.hybris.platform.acceleratorstorefrontcommons.forms.UpdatePasswordForm;
import de.hybris.platform.acceleratorstorefrontcommons.forms.validation.AddressValidator;
import de.hybris.platform.acceleratorstorefrontcommons.util.AddressDataUtil;
import de.hybris.platform.cms2.exceptions.CMSItemNotFoundException;
import de.hybris.platform.cms2.model.pages.ContentPageModel;
import de.hybris.platform.commercefacades.i18n.I18NFacade;
import de.hybris.platform.commercefacades.order.CheckoutFacade;
import de.hybris.platform.commercefacades.order.data.CCPaymentInfoData;
import de.hybris.platform.commercefacades.user.data.AddressData;
import de.hybris.platform.commercefacades.user.data.CountryData;
import de.hybris.platform.commercefacades.user.data.TitleData;
import de.hybris.platform.commerceservices.enums.CountryType;
import de.hybris.platform.payment.AdapterException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.mail.Address;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.fest.util.Strings;
import org.springframework.beans.BeanUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.MapBindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;


@Controller
@RequestMapping("/my-account")
public class BrainTreeAccountPageController extends AbstractPageController {

    private static final Logger LOGGER = Logger.getLogger(BrainTreeAccountPageController.class);
    private static final String REDIRECT_TO_PAYMENT_INFO_PAGE = REDIRECT_PREFIX + "/my-account/payment-details";
    private static final String REDIRECT_TO_ADD_PAYMENT_INFO_PAGE = REDIRECT_PREFIX + "/my-account/add-payment-method";
    private static final String COUNTRY_DATA_ATTR = "countryData";
    private static final String ADDRESS_FORM_ATTR = "addressForm";
    private static final String COUNTRY_ATTR = "country";
    private static final String REGIONS_ATTR = "regions";
    private static final String US_ISO_CODE = "US";
    private static final String TITLE_DATA_ATTR = "titleData";
    private static final String ADDRESS_DATA_ATTR = "addressData";
    private static final String FORM_GLOBAL_ERROR = "form.global.error";
    private static final String UPDATE_PASSWORD_CMS_PAGE = "updatePassword";
    private static final String BREADCRUMBS_ATTR = "breadcrumbs";
    private static final String FAILED_VERIFICATION_ERROR_MESSAGE = "Payment method failed verification.";

    // CMS Pages
    private static final String ADD_EDIT_PAYMENT_METHOD_CMS_PAGE = "add-edit-payment-method";
    private static final String UPDATE_PAYMENT_ADDRESS_CMS_PAGE = "update-payment-address";
    private static final String UPDATE_PROFILE_CMS_PAGE = "update-profile";
    private static final String REDIRECT_TO_PASSWORD_UPDATE_PAGE = REDIRECT_PREFIX + "/my-account/update-password";

    @Resource(name = "userFacade")
    protected BrainTreeUserFacade userFacade;

    @Resource(name = "i18NFacade")
    private I18NFacade i18NFacade;

    @Resource(name = "acceleratorCheckoutFacade")
    private CheckoutFacade checkoutFacade;

    @Resource(name = "brainTreeCheckoutFacade")
    private DefaultBrainTreeCheckoutFacade brainTreeCheckoutFacade;

    @Resource(name = "brainTreeSetUpPasswordFacade")
    private BrainTreeSetUpPasswordFacade brainTreeSetUpPasswordFacade;

    @Resource(name = "addressDataUtil")
    private AddressDataUtil addressDataUtil;

    @Resource(name = "usBankAccountAddressDataConverter")
    private BrainTreeUsBankAccountAddressDataConverter usBankAddressDataConverter;

    @Resource(name = "addressValidator")
    private AddressValidator addressValidator;

    @Resource(name = "brainTreePasswordValidator")
    private BrainTreePasswordValidator brainTreePasswordValidator;

    @Resource(name = "brainTreeConnectWithPayPalFacade")
    private BrainTreeConnectWithPayPalFacade brainTreeConnectWithPayPalFacade;

    @Resource(name = "accountBreadcrumbBuilder")
    private ResourceBreadcrumbBuilder accountBreadcrumbBuilder;

    @Resource(name = "brainTreePaymentService")
    private BrainTreePaymentService brainTreePaymentService;

    @Resource(name = "brainTreePaymentFacadeImpl")
    private DefaultBrainTreePaymentFacade brainTreePaymentFacade;

    @Resource(name = "brainTreeUserFacade")
    private DefaultBrainTreeUserFacade brainTreeUserFacade;

    @Resource(name = "brainTreeConfigService")
    private BrainTreeConfigService configService;

    @ModelAttribute("countries")
    public Collection<CountryData> getCountries()
    {
        return checkoutFacade.getCountries(CountryType.SHIPPING);
    }

    @ModelAttribute("titles")
    public Collection<TitleData> getTitles()
    {
        return userFacade.getTitles();
    }

    @ModelAttribute("countryDataMap")
    public Map<String, CountryData> getCountryDataMap()
    {
        final Map<String, CountryData> countryDataMap = new HashMap<>();
        for (final CountryData countryData : getCountries())
        {
            countryDataMap.put(countryData.getIsocode(), countryData);
        }
        return countryDataMap;
    }

    @RequestMapping(value = "/unlink-paypal", method = RequestMethod.GET)
    @RequireHardLogIn
    public String getConnectWithPayPalUnlinkPage(final Model model, final RedirectAttributes redirectAttributes,
                                                 final HttpServletRequest request) throws CMSItemNotFoundException {

        boolean isSetUpPasswordForm = brainTreeSetUpPasswordFacade.isSetUpPasswordForm();
        if (isSetUpPasswordForm) {
            GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.INFO_MESSAGES_HOLDER,
                    Braintreeb2baddonControllerConstants.UNSUCCESS_UNLINK_NOTIFICATION);
            return REDIRECT_TO_PASSWORD_UPDATE_PAGE;
        }

        storeCmsPageInModel(model, getContentPageForLabelOrId(UPDATE_PROFILE_CMS_PAGE));
        setUpMetaDataForContentPage(model, getContentPageForLabelOrId(UPDATE_PROFILE_CMS_PAGE));

        return Braintreeb2baddonControllerConstants.ADDON_PREFIX +
                Braintreeb2baddonControllerConstants.Views.Pages.Account.ACCOUNT_UNLINK_PAGE;
    }

    @RequestMapping(value = "/unlink-paypal", method = RequestMethod.POST)
    @RequireHardLogIn
    public String postConnectWithPayPalUnlinkPage(final RedirectAttributes redirectAttributes) {

        boolean isUnlinkedPayPalAccount = brainTreeConnectWithPayPalFacade.unlinkConnectWithPayPalAccount();

        if (isUnlinkedPayPalAccount) {
            GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.INFO_MESSAGES_HOLDER,
                    Braintreeb2baddonControllerConstants.SUCCESS_UNLINK_NOTIFICATION);
        } else {
            GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.INFO_MESSAGES_HOLDER,
                    Braintreeb2baddonControllerConstants.BRAINTREE_TEXT_NOT_AVAILABLE);
        }

        return REDIRECT_PREFIX + ROOT;
    }

    @RequestMapping(value = "/remove-payment-method-bt", method = RequestMethod.POST)
    @RequireHardLogIn
    public String removePaymentMethod(@RequestParam(value = "paymentInfoId") final String paymentInfoId,
        final RedirectAttributes redirectAttributes)
        throws CMSItemNotFoundException {
        try {
            userFacade.removeCCPaymentInfo(paymentInfoId);
        } catch (BraintreeDeletePaymentDetailsWithNotCapturedOrderException ex) {
            GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.ERROR_MESSAGES_HOLDER,
                getLocalizedString("braintree.delete.payment.details.with.not.captured.order.error"));
            LOGGER.error(ex);
            return REDIRECT_TO_PAYMENT_INFO_PAGE;
        }
        GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.CONF_MESSAGES_HOLDER,
            getLocalizedString("text.account.profile.paymentCart.remove"));

        return REDIRECT_TO_PAYMENT_INFO_PAGE;
    }

    @RequestMapping(value = "/set-password", method = RequestMethod.POST)
    @RequireHardLogIn
    public String setPassword(final UpdatePasswordForm updatePasswordForm, final BindingResult bindingResult,
                              final Model model,
                              final RedirectAttributes redirectAttributes) throws CMSItemNotFoundException {
        brainTreePasswordValidator.validate(updatePasswordForm, bindingResult);
        if (!bindingResult.hasErrors()) {
            if (updatePasswordForm.getNewPassword().equals(updatePasswordForm.getCheckNewPassword())) {
                brainTreeSetUpPasswordFacade.setUpPassword(updatePasswordForm.getNewPassword());
            } else {
                bindingResult.rejectValue("checkNewPassword", "validation.checkPwd.equals", new Object[]{},
                        "validation.checkPwd.equals");
            }
        }
        if (bindingResult.hasErrors()) {
            GlobalMessages.addErrorMessage(model, FORM_GLOBAL_ERROR);
            final ContentPageModel updatePasswordPage = getContentPageForLabelOrId(UPDATE_PASSWORD_CMS_PAGE);
            storeCmsPageInModel(model, updatePasswordPage);
            setUpMetaDataForContentPage(model, updatePasswordPage);
            model.addAttribute(BREADCRUMBS_ATTR,
                    accountBreadcrumbBuilder.getBreadcrumbs("text.account.profile.updatePasswordForm"));
            return getViewForPage(model);
        } else {
            GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.CONF_MESSAGES_HOLDER,
                    "text.account.confirmation.password.set", null);
            return REDIRECT_TO_PASSWORD_UPDATE_PAGE;
        }
    }

    @RequestMapping(value = "/update-payment-address", method = RequestMethod.GET)
    @RequireHardLogIn
    public String updateAddress(@RequestParam(value = "paymentInfoId") final String paymentMethodId, final Model model)
            throws CMSItemNotFoundException
    {
        final AddressForm addressForm = new AddressForm();
        model.addAttribute(COUNTRY_DATA_ATTR, checkoutFacade.getCountries(CountryType.SHIPPING));
        model.addAttribute(TITLE_DATA_ATTR, userFacade.getTitles());
        model.addAttribute(ADDRESS_FORM_ATTR, addressForm);
        prefillAddressForm(paymentMethodId, addressForm, model);

        final ContentPageModel addEditAddressPage = getContentPageForLabelOrId(UPDATE_PAYMENT_ADDRESS_CMS_PAGE);
        storeCmsPageInModel(model, addEditAddressPage);
        setUpMetaDataForContentPage(model, addEditAddressPage);

        final List<Breadcrumb> breadcrumbs = accountBreadcrumbBuilder.getBreadcrumbs(null);
        breadcrumbs.add(new Breadcrumb("/my-account/payment-details",
                getMessageSource()
                        .getMessage("text.account.paymentDetails", null, getI18nService().getCurrentLocale()), null));
        breadcrumbs.add(new Breadcrumb("#",
                getMessageSource()
                        .getMessage("text.account.profile.paymentCart.update", null, getI18nService().getCurrentLocale()),
                null));

        model.addAttribute("paymentInfoId", paymentMethodId);
        model.addAttribute(BREADCRUMBS_ATTR, breadcrumbs);
        model.addAttribute(ThirdPartyConstants.SeoRobots.META_ROBOTS, ThirdPartyConstants.SeoRobots.NOINDEX_NOFOLLOW);
        return getViewForPage(model);
    }

    @RequestMapping(value = "/update-payment-address", method = RequestMethod.POST)
    @RequireHardLogIn
    public String updatePaymentAddress(@RequestParam(value = "paymentInfoId") final String paymentMethodId,
                                       final Model model, final AddressForm addressForm,
                                       final BindingResult bindingResult, final RedirectAttributes redirectAttributes) throws CMSItemNotFoundException {
        getAddressValidator().validate(addressForm, bindingResult);
        populateModelRegionAndCountry(model, addressForm.getCountryIso());
        CCPaymentInfoData paymentInfoData = userFacade.getCCPaymentInfoForCode(paymentMethodId);
        AddressData currentAddress = paymentInfoData.getBillingAddress();
        AddressData newAddress = addressDataUtil.convertToVisibleAddressData(addressForm);
        if (bindingResult.hasErrors()) {
            return handleAddressInputErrors(model, addressForm, paymentMethodId,"text.account.profile.address.update.error");
        } else if (areAddressesIdentical(currentAddress, newAddress)) {
            return handleAddressInputErrors(model, addressForm, paymentMethodId,"text.account.profile.address.update.sameAddressError");
        }
        try {
            UpdateCreditCardBillingAddressForm updateCreditCardBillingAddressForm = new UpdateCreditCardBillingAddressForm(newAddress, paymentMethodId);
            userFacade.editPaymentMethod(updateCreditCardBillingAddressForm);
        } catch (final AdapterException e) {
            LOGGER.error(
                    "Error occurred while payment address update: " + e.getMessage(), e);
            GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.ERROR_MESSAGES_HOLDER,
                    getLocalizedString("text.account.profile.paymentCart.addPaymentMethod.general.error"));
            return REDIRECT_TO_PAYMENT_INFO_PAGE;
        }
        GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.CONF_MESSAGES_HOLDER,
                getLocalizedString("text.account.profile.paymentCart.update.success"));
        return REDIRECT_TO_PAYMENT_INFO_PAGE;
    }

    @RequestMapping(value = "/add-payment-method", method = RequestMethod.GET)
    @RequireHardLogIn
    public String addPaymentMethod(final Model model)
        throws CMSItemNotFoundException {

        final List<Breadcrumb> breadcrumbs = accountBreadcrumbBuilder.getBreadcrumbs(null);
        breadcrumbs.add(new Breadcrumb("/my-account/payment-details",
            getMessageSource().getMessage("text.account.paymentDetails", null, getI18nService().getCurrentLocale()),
            null));
        breadcrumbs.add(new Breadcrumb("#", getLocalizedString("text.account.paymentMethod.addPaymentMethod"), null));

        storeCmsPageInModel(model, getContentPageForLabelOrId(ADD_EDIT_PAYMENT_METHOD_CMS_PAGE));
        setUpMetaDataForContentPage(model, getContentPageForLabelOrId(ADD_EDIT_PAYMENT_METHOD_CMS_PAGE));
        setupAdditionalFields(model);
        final List<AddressData> addressBook = userFacade.getAddressBook();
        model.addAttribute("deliveryAddresses", addressBook);
        model.addAttribute("achBillingAddresses", addressBook.stream()
                .filter(address -> address.getCountry().getIsocode().equalsIgnoreCase("US"))
                .collect(Collectors.toList()));
        model.addAttribute("addressForm", new AddressForm());
        model.addAttribute("breadcrumbs", breadcrumbs);
        model.addAttribute("metaRobots", "noindex,nofollow");
        model.addAttribute(REGIONS_ATTR, getI18NFacade().getRegionsForCountryIso(US_ISO_CODE));
        return getViewForPage(model);
    }

    @RequestMapping(value = "/add-us-bank-account-payment-method", method = RequestMethod.POST)
    public String addUsBankAccountPaymentMethod(HttpServletRequest request, final RedirectAttributes redirectAttributes, @RequestParam(defaultValue = "false") boolean shouldSaveAddress) {
        final UsBankAccountResponseData usBankAccountData = handleUsBankAccountResponse(request);
        final UsBankAccountDetailsData usBankDetails = usBankAccountData.getDetails();
        final AddressData billingAddress = usBankAddressDataConverter.convert(usBankDetails.getBillingAddress());
        boolean shouldDisplayAddressDuplicateMessage = false;
        if (shouldSaveAddress) {
            if (!brainTreeUserFacade.isDuplicateAddressForCurrentUser(billingAddress)) {
                brainTreeUserFacade.saveAddressForCurrentCustomer(billingAddress);
            } else {
                shouldDisplayAddressDuplicateMessage = true;
            }
        }

        final BrainTreeSubscriptionInfoData paymentInfoData = GenericBuilder.of(BrainTreeSubscriptionInfoData::new)
                .with(BrainTreeSubscriptionInfoData::setPaymentProvider, US_BANK_ACCOUNT)
                .with(BrainTreeSubscriptionInfoData::setNonce, usBankAccountData.getNonce())
                .with(BrainTreeSubscriptionInfoData::setRoutingNumber, usBankDetails.getRoutingNumber())
                .with(BrainTreeSubscriptionInfoData::setLast4, usBankDetails.getAccountNumber()
                        .substring(usBankDetails.getAccountNumber().length() - 4))
                .with(BrainTreeSubscriptionInfoData::setShouldBeSaved, Boolean.TRUE)
                .with(BrainTreeSubscriptionInfoData::setSavePaymentInfo, Boolean.TRUE)
                .with(BrainTreeSubscriptionInfoData::setDeviceData, usBankAccountData.getDeviceData())
                .with(BrainTreeSubscriptionInfoData::setAddressData, billingAddress)
                .build();
        try {
            brainTreePaymentFacade.completeCreateSubscriptionForMyAccount(paymentInfoData);
            if(shouldDisplayAddressDuplicateMessage){
                GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.INFO_MESSAGES_HOLDER,
                        getLocalizedString("text.account.profile.paymentCart.addPaymentMethod.duplicateAddress"));
            }
        } catch (BrainTreePaymentMethodLimitedException ex) {
            LOGGER.error("Failed to create payment method. The maximum number of payment methods is reached." + ex.getMessage());
            GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.ERROR_MESSAGES_HOLDER,
                    getLocalizedString("text.account.profile.paymentCart.addPaymentMethod.limitation.error"));
            return REDIRECT_TO_PAYMENT_INFO_PAGE;
        } catch (final BrainTreeCardVerifyException e) {
            LOGGER.error("Error occurred while card verification: " + e.getMessage(), e);
            GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.ERROR_MESSAGES_HOLDER,
                    getLocalizedString("text.account.profile.paymentCart.addPaymentMethod.failed.verification.error"));
            return REDIRECT_TO_PAYMENT_INFO_PAGE;
        }
        catch (final  AdapterException e) {
            LOGGER.error("Error occurred while US Bank Account payment method creation: " + e.getMessage(), e);
            GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.ERROR_MESSAGES_HOLDER,
                    getLocalizedString("text.account.profile.paymentCart.addPaymentMethod.general.error"));
            return REDIRECT_TO_PAYMENT_INFO_PAGE;
        }
        GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.CONF_MESSAGES_HOLDER,
                getLocalizedString("text.account.profile.paymentCart.addPaymentMethod.success"));
        return REDIRECT_TO_PAYMENT_INFO_PAGE;
    }

    @RequestMapping(value = "/get-address/{addressId}", method = RequestMethod.GET)
    @ResponseBody
    @RequireHardLogIn
    public AddressData getAddressById(@PathVariable final String addressId) {
        return userFacade.getAddressForCode(addressId);
    }


    @RequestMapping(value = "/add-payment-method", method = RequestMethod.POST)
    @RequireHardLogIn
    public String addPaymentMethod(@ModelAttribute(ADDRESS_FORM_ATTR) final BraintreeSavePaymentMethodForm savePaymentMethodForm,
                                   final RedirectAttributes redirectAttributes) {
        if (StringUtils.isEmpty(savePaymentMethodForm.getNonce())) {
            LOGGER.error("Failed to create payment method. Error occurred while contacting external payment services.");
            GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.ERROR_MESSAGES_HOLDER,
                getLocalizedString("text.account.profile.paymentCart.addPaymentMethod.noNonce"));
            return REDIRECT_TO_ADD_PAYMENT_INFO_PAGE;
        }

        final boolean storeInVault = configService.getBrainTreePluginConfiguration().getBraintreeGeneralConfiguration().isStoreInVault();
        if (!storeInVault) {
            LOGGER.error(
                "Failed to create payment method. Store in vault is forbidden. For adding new payment methods enable store in vault.");
            GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.ERROR_MESSAGES_HOLDER,
                getLocalizedString("text.account.profile.paymentCart.addPaymentMethod.forbidden"));
            return REDIRECT_TO_PAYMENT_INFO_PAGE;
        }

        AddressData addressData = null;
        boolean shouldDisplayAddressDuplicateMessage = false;
        if (StringUtils.isNotEmpty(savePaymentMethodForm.getSelectedAddressCode())) {
            addressData = userFacade.getAddressForCode(savePaymentMethodForm.getSelectedAddressCode());
            if (addressData == null) {
                LOGGER.error("Failed to create payment method. Error occurred while address selection");
                GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.ERROR_MESSAGES_HOLDER,
                        getLocalizedString("text.account.profile.paymentCart.addPaymentMethod.noAddress"));
                return REDIRECT_TO_ADD_PAYMENT_INFO_PAGE;
            }
        } else if (!configService.isDropInEnabled()){
            AddressForm addressForm = new AddressForm();
            BeanUtils.copyProperties(savePaymentMethodForm, addressForm);
            addressData = addressDataUtil.convertToAddressData(addressForm);
            boolean isAddressDuplicated = brainTreeUserFacade.isDuplicateAddressForCurrentUser(addressData);
            shouldDisplayAddressDuplicateMessage = savePaymentMethodForm.isShouldSaveAddress() && isAddressDuplicated;
            if (savePaymentMethodForm.isShouldSaveAddress() && !isAddressDuplicated) {
                brainTreeUserFacade.saveAddressForCurrentCustomer(addressData);
            }
        }

        final BrainTreeSubscriptionInfoData paymentInfoData = GenericBuilder.of(BrainTreeSubscriptionInfoData::new)
                .with(BrainTreeSubscriptionInfoData::setCardType, savePaymentMethodForm.getCardType())
                .with(BrainTreeSubscriptionInfoData::setPaymentProvider, savePaymentMethodForm.getPaymentProvider())
                .with(BrainTreeSubscriptionInfoData::setCardNumber,
                        BrainTreeUtils.formatCardNumber(savePaymentMethodForm.getCardNumber()))
                .with(BrainTreeSubscriptionInfoData::setNonce, savePaymentMethodForm.getNonce())
                .with(BrainTreeSubscriptionInfoData::setExpirationMonth, savePaymentMethodForm.getExpiryMonth())
                .with(BrainTreeSubscriptionInfoData::setExpirationYear, savePaymentMethodForm.getExpiryYear())
                .with(BrainTreeSubscriptionInfoData::setCardholder, savePaymentMethodForm.getCardholder())
                .with(BrainTreeSubscriptionInfoData::setEmail, savePaymentMethodForm.getPayPalEmail())
                .with(BrainTreeSubscriptionInfoData::setLiabilityShifted, Boolean.valueOf(savePaymentMethodForm.getLiabilityShifted()))
                .with(BrainTreeSubscriptionInfoData::setSavePaymentInfo, true)
                .with(BrainTreeSubscriptionInfoData::setDeviceData, savePaymentMethodForm.getDeviceData())
                .with(BrainTreeSubscriptionInfoData::setImageSource,
                        brainTreePaymentService.getImageSourceForPaymentMethod(savePaymentMethodForm.getCardType(),
                                BRAINTREE_ACCEPTED_CREDIT_CARD_PAYMENT_METHODS))
                .with(BrainTreeSubscriptionInfoData::setAddressData, addressData)
                .build();

        try {
            final CCPaymentInfoData paymentMethod = brainTreePaymentFacade.completeCreateSubscriptionForMyAccount(paymentInfoData);

            if (paymentMethod == null) {
                LOGGER.error("Failed to create payment method. Error occurred while payment method creation");
                GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.ERROR_MESSAGES_HOLDER,
                    getLocalizedString("text.account.profile.paymentCart.addPaymentMethod.error"));
                return REDIRECT_TO_ADD_PAYMENT_INFO_PAGE;
            }

            if(shouldDisplayAddressDuplicateMessage){
                GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.INFO_MESSAGES_HOLDER,
                        getLocalizedString("text.account.profile.paymentCart.addPaymentMethod.duplicateAddress"));
            }
        } catch (BrainTreePaymentMethodLimitedException ex) {
            LOGGER.error("Failed to create payment method. The maximum number of payment methods is reached." + ex.getMessage());
            GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.ERROR_MESSAGES_HOLDER,
                    getLocalizedString("text.account.profile.paymentCart.addPaymentMethod.limitation.error"));
            return REDIRECT_TO_PAYMENT_INFO_PAGE;
        } catch (final Exception e) {
            String error;
            if (e.getMessage().equals(FAILED_VERIFICATION_ERROR_MESSAGE)) {
                error = "text.account.profile.paymentCart.addPaymentMethod.failed.verification.error";
            } else {
                error = "text.account.profile.paymentCart.addPaymentMethod.general.error";
            }
            LOGGER.error(
                    "Failed to create payment method. Error occurred while payment method creation: " + e.getMessage(), e);
            GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.ERROR_MESSAGES_HOLDER,
                    getLocalizedString(error));
            return REDIRECT_TO_ADD_PAYMENT_INFO_PAGE;
        }

        GlobalMessages.addFlashMessage(redirectAttributes, GlobalMessages.CONF_MESSAGES_HOLDER,
            getLocalizedString("text.account.profile.paymentCart.addPaymentMethod.success"));
        return REDIRECT_TO_PAYMENT_INFO_PAGE;
    }

    private UsBankAccountResponseData handleUsBankAccountResponse(final HttpServletRequest request) throws IllegalArgumentException {
        validateParameterNotNull(request, "request cannot be null");
        final String[] jsonResponseArray = request.getParameterMap().get(US_BANK_ACCOUNT_DATA);
        final String responseJson = jsonResponseArray[0];
        UsBankAccountResponseData response = null;
        final ObjectMapper mapper = new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES);
        try {
            response = mapper.readValue(responseJson, UsBankAccountResponseData.class);
        } catch (final Exception e) {
            throw new IllegalArgumentException(
                    "Us Bank Account response is invalid!. Please try again later or contact with payment provider support.", e);
        }
        return response;
    }

    @RequestMapping(value = "/add-payment-method/validate", method = RequestMethod.POST,
            consumes = MediaType.APPLICATION_JSON_VALUE)
    @RequireHardLogIn
    public @ResponseBody ResponseEntity<ErrorListDto> addPaymentMethod(@RequestBody final AddressForm addressForm) {
        final Errors bindingResult = new MapBindingResult(new HashMap<>(), ADDRESS_FORM_ATTR);
        getAddressValidator().validate(addressForm, bindingResult);
        ErrorListDto errorList = new ErrorListDto();

        if (bindingResult.hasErrors()) {
            List<ErrorDto> errors = bindingResult.getFieldErrors().stream().map(errorDetail -> {
                ErrorDto error = new ErrorDto();
                error.setSubject(errorDetail.getField());
                error.setMessage(getMessageSource().getMessage(errorDetail.getCode(),
                        null, getI18nService().getCurrentLocale()));
                error.setType(errorDetail.getCode());
                return error;
            }).collect(Collectors.toList());
            errorList.setErrors(errors);
            return new ResponseEntity<>(errorList, HttpStatus.BAD_REQUEST);
        }

        return new ResponseEntity<>(HttpStatus.OK);
    }


    protected void setUpAddressFormAfterError(AddressForm addressForm, Model model, String paymentMethodId) {
        model.addAttribute(COUNTRY_DATA_ATTR, checkoutFacade.getCountries(CountryType.SHIPPING));
        model.addAttribute("metaRobots", "noindex,nofollow");
        model.addAttribute("paymentInfoId", paymentMethodId);
        model.addAttribute("titles", userFacade.getTitles());
        model.addAttribute("titleData", userFacade.getTitles());
        if (addressForm.getCountryIso() != null)
        {
            populateModelRegionAndCountry(model, addressForm.getCountryIso());
        }
    }

    protected void prefillAddressForm(String paymentMethodId, AddressForm addressForm, Model model) {
        CCPaymentInfoData paymentInfoData = userFacade.getCCPaymentInfoForCode(paymentMethodId);
        AddressData addressData = paymentInfoData.getBillingAddress();
        if (addressData != null) {
            model.addAttribute(REGIONS_ATTR, getI18NFacade().getRegionsForCountryIso(addressData.getCountry().getIsocode()));
            model.addAttribute(COUNTRY_ATTR, addressData.getCountry().getIsocode());
            model.addAttribute(ADDRESS_DATA_ATTR, addressData);
            addressDataUtil.convert(addressData, addressForm);
            addressForm.setDefaultAddress(Boolean.TRUE);
            model.addAttribute("addressBookEmpty", Boolean.TRUE);
        }
    }

    private String handleAddressInputErrors(Model model, AddressForm addressForm, String paymentMethodId, String messageKey) throws CMSItemNotFoundException {
        GlobalMessages.addErrorMessage(model, messageKey);
        final ContentPageModel addEditAddressPage = getContentPageForLabelOrId(UPDATE_PAYMENT_ADDRESS_CMS_PAGE);
        storeCmsPageInModel(model, addEditAddressPage);
        setUpMetaDataForContentPage(model, addEditAddressPage);
        setUpAddressFormAfterError(addressForm, model, paymentMethodId);
        return getViewForPage(model);
    }

    protected void populateModelRegionAndCountry(final Model model, final String countryIsoCode) {
        model.addAttribute(REGIONS_ATTR, getI18NFacade().getRegionsForCountryIso(countryIsoCode));
        model.addAttribute(COUNTRY_ATTR, countryIsoCode);
    }

    private void setupAdditionalFields(final Model model) {
        String clientToken = StringUtils.EMPTY;

        try {
            clientToken = brainTreeCheckoutFacade.generateClientToken(Boolean.TRUE);
        } catch (final AdapterException exception) {
            LOGGER.error("[Brain Tree Controller] Error during token generation!", exception);
        }

        model.addAttribute(CLIENT_TOKEN, clientToken);
    }

    private boolean areAddressesIdentical(AddressData currentAddress, AddressData newAddress) {
        return currentAddress != null && newAddress != null &&
                currentAddress.getCountry().getIsocode().equals(newAddress.getCountry().getIsocode()) &&
                currentAddress.getFirstName().equals(newAddress.getFirstName()) &&
                currentAddress.getLastName().equals(newAddress.getLastName()) &&
                currentAddress.getLine1().equals(newAddress.getLine1()) &&
                StringUtils.equals(currentAddress.getLine2(), newAddress.getLine2()) &&
                currentAddress.getTown().equals(newAddress.getTown()) &&
                currentAddress.getPostalCode().equals(newAddress.getPostalCode()) &&
                areRegionsIdentical(currentAddress, newAddress);
    }

    private boolean areRegionsIdentical(AddressData currentAddress, AddressData newAddress) {
        boolean isCurrentRegionPresent = Objects.nonNull(currentAddress.getRegion());
        boolean isNewRegionPresent = Objects.nonNull(newAddress.getRegion());
        if (isCurrentRegionPresent && isNewRegionPresent) {
            return StringUtils.equals(currentAddress.getRegion().getIsocode(), newAddress.getRegion().getIsocode());
        } else if (!isCurrentRegionPresent && !isNewRegionPresent) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }

    private AddressData getAddressForPaymentInfo(String selectedAddressCode, List<AddressData> addressBook) {
        if (StringUtils.isNotBlank(selectedAddressCode)) {
            for (final AddressData addressData : addressBook) {
                if (selectedAddressCode.equals(addressData.getId())) {
                    return addressData;
                }
            }
        }
        return null;
    }

    protected AddressValidator getAddressValidator() {
        return addressValidator;
    }

    protected I18NFacade getI18NFacade() {
        return i18NFacade;
    }
}
