package com.braintreeocc.controllers;

import static com.braintree.constants.BraintreeConstants.PAYPAL_FUNDING_SOURCE;

import com.braintree.configuration.service.BrainTreeConfigService;
import com.braintree.hybris.data.ws.BraintreeUserVerificationStatus;
import com.braintree.hybris.data.ws.BraintreeUserVerificationWsDTO;
import com.braintree.hybris.data.ws.BraintreeUserVerificationResultWsDTO;
import com.braintree.exceptions.PaypalAccessTokenNotFoundException;
import com.braintree.hybris.data.ws.PayPalCheckoutWsDTO;
import com.braintreeocc.converters.BraintreePayPalAddressConverterForOCC;
import com.braintree.facade.BrainTreeConnectWithPayPalFacade;
import com.braintree.facade.BrainTreeRegistrationUserFacade;
import com.braintree.facade.BrainTreeUserFacade;
import com.braintree.facade.BraintreeConnectWithPayPalNotificationFacade;
import com.braintree.facade.impl.DefaultBrainTreeCheckoutFacade;
import com.braintree.hybris.data.ws.PayPalConnectTokenWsDTO;
import com.braintree.facade.impl.DefaultBrainTreePaymentFacade;
import com.braintree.hybris.data.BrainTreeConnectWithPayPalUserData;
import com.braintree.hybris.data.BrainTreeSubscriptionInfoData;
import com.braintree.hybris.data.ws.PayPalAddressWsDTO;
import com.braintree.hybris.data.ws.PayPalConnectLoginDTO;
import com.braintree.hybris.data.ws.PayPalConnectWsDTO;
import com.braintree.hybris.data.ws.PayPalDetailsWsDTO;
import com.braintree.hybris.data.ws.PayPalPaymentRequestWsDTO;
import com.braintree.util.GenericBuilder;
import com.google.gson.Gson;
import com.paypal.hybris.data.ws.AddingPaymentMethodAndAddressStatusWsDTO;
import de.hybris.platform.commercefacades.user.data.AddressData;
import de.hybris.platform.commercefacades.user.data.CustomerData;
import de.hybris.platform.commerceservices.customer.DuplicateUidException;
import de.hybris.platform.servicelayer.user.UserService;
import de.hybris.platform.site.BaseSiteService;
import de.hybris.platform.webservicescommons.cache.CacheControl;
import de.hybris.platform.webservicescommons.cache.CacheControlDirective;
import de.hybris.platform.webservicescommons.swagger.ApiBaseSiteIdAndUserIdParam;
import de.hybris.platform.webservicescommons.swagger.ApiBaseSiteIdParam;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import java.util.Optional;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import com.braintree.model.PaypalAccessTokenModel;
import com.braintree.service.BrainTreeAccessTokenService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
import org.springframework.validation.Validator;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import com.braintree.hybris.data.BraintreeCheckoutData;

import javax.annotation.Resource;

@RestController
@RequestMapping(value = "/{baseSiteId}/users/{userId}/connect")
@CacheControl(directive = CacheControlDirective.NO_CACHE)
@Tag(name = "PayPal Connect")
public class BraintreeConnectWithPaypalController extends BraintreeBaseController {

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

    private final Gson gson = new Gson();

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

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

    @Resource(name = "braintreeConnectWithPayPalNotificationFacade")
    private BraintreeConnectWithPayPalNotificationFacade braintreeConnectWithPayPalNotificationFacade;

    @Resource(name = "userService")
    private UserService userService;

    @Resource(name = "brainTreeRegistrationUserFacade")
    private BrainTreeRegistrationUserFacade userFacade;

    @Resource(name = "userFacade")
    private BrainTreeUserFacade brainTreeUserFacade;

    @Resource(name = "payPalPaymentRequestValidator")
    private Validator payPalPaymentRequestValidator;

    @Resource(name = "payPalDetailsValidator")
    private Validator payPalDetailsValidator;

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

    @Resource(name = "payPalOCCAddressConverter")
    private BraintreePayPalAddressConverterForOCC payPalOCCAddressConverter;

    @Resource(name = "brainTreeConfigService")
    private BrainTreeConfigService brainTreeConfigService;
    
    @Resource
    private BrainTreeAccessTokenService braintreeAccessTokenService;

    @GetMapping("/paypal")
    @ResponseStatus(value = HttpStatus.OK)
    @ResponseBody
    @ApiBaseSiteIdParam
    @Operation(summary = " Get the Braintree data for client-side payment initialization",
        description = "Get a config data for Connect with PayPal button")
    public PayPalCheckoutWsDTO getPayPalCheckoutData(
        @Parameter(description = "Page type", schema = @Schema (defaultValue = "LOGIN"))
        @RequestParam(defaultValue = "LOGIN", required = false) final String pageType) {
        final BraintreeCheckoutData braintreeCheckoutData = brainTreeCheckoutFacade.getBraintreeConfigurationDataForConnectWithPayPalButton();
        return getDataMapper().map(braintreeCheckoutData, PayPalCheckoutWsDTO.class, "configData");
    }

    @Secured({"ROLE_CLIENT", "ROLE_CUSTOMERGROUP", "ROLE_GUEST", "ROLE_CUSTOMERMANAGERGROUP", "ROLE_TRUSTED_CLIENT",
        "ROLE_ANONYMOUS"})
    @PostMapping(value = "/exchange")
    @ResponseStatus(HttpStatus.OK)
    @Operation(operationId = "exchange authorization code", summary = "exchange authorization code to access token")
    @ApiBaseSiteIdAndUserIdParam
    public PayPalConnectWsDTO exchangeAuthorizationCode(
        @Parameter(description = "JSON object which contains authorization code", required = true)
        @RequestBody PayPalConnectTokenWsDTO payPalConnectTokenWsDTO) {
        final PayPalConnectWsDTO connectWsData = new PayPalConnectWsDTO();
        final String accessToken = brainTreeConnectWithPayPalFacade
            .exchangeAuthorizationCodeToAccessToken(payPalConnectTokenWsDTO.getAuthorizationCode());
        final BrainTreeConnectWithPayPalUserData userData = brainTreeConnectWithPayPalFacade.getUserDataByAccessToken(
            accessToken);
        String email = userFacade.getEmailFromPayPalUserData(userData);
        if (userService.isUserExisting(email)) {
            connectWsData.setIsRegistered(true);
            connectWsData.setShouldSaveAddress(
                !brainTreeConnectWithPayPalFacade.isPayPalAddressPresent(email, userData.getAddress()));
            connectWsData.setShouldSavePaymentInfo(!brainTreeConnectWithPayPalFacade.isPayPalPaymentMethodPresentForConnectWithPayPal(email));
            connectWsData.setEmail(email);
            connectWsData.setBaseSiteName(baseSiteService.getCurrentBaseSite().getName());
        } else {
            final boolean shutSavePaymentInfo = brainTreeConnectWithPayPalFacade.shouldSavePayPalPaymentInfo();
            connectWsData.setShouldSaveAddress(true);
            connectWsData.setShouldSavePaymentInfo(shutSavePaymentInfo);
            connectWsData.setIsRegistered(false);
        }

        connectWsData.setIsUserAlreadyHaveAccount(
            brainTreeUserFacade.isCustomerWithPayerIdExist(userData.getPayerId()));
        String accessTokenGuid = braintreeAccessTokenService.savePaypalAccessToken(accessToken);
        connectWsData.setAccessTokenGuid(accessTokenGuid);
        connectWsData.setPayerId(userData.getPayerId());
        return connectWsData;
    }

    @Secured({"ROLE_CLIENT", "ROLE_CUSTOMERGROUP", "ROLE_GUEST", "ROLE_CUSTOMERMANAGERGROUP", "ROLE_TRUSTED_CLIENT",
        "ROLE_ANONYMOUS"})
    @PostMapping(value = "/login")
    @ResponseStatus(HttpStatus.OK)
    @Operation(operationId = "login endpoint for connect with PayPal", summary = "Generate login data for user by accessToken")
    @ApiBaseSiteIdAndUserIdParam
    public PayPalConnectLoginDTO login(
        @Parameter(description = "JSON object which contains accessToken", required = true)
        @RequestBody PayPalConnectTokenWsDTO payPalConnectTokenWsDTO) {
        String accessTokenGuid = payPalConnectTokenWsDTO.getAccessTokenGuid();
        String accessToken = braintreeAccessTokenService.getPaypalAccessToken(accessTokenGuid)
                .map(PaypalAccessTokenModel::getAccessToken)
                .orElseThrow(PaypalAccessTokenNotFoundException::new);
        final BrainTreeConnectWithPayPalUserData userData = brainTreeConnectWithPayPalFacade.getUserDataByAccessToken(
            accessToken);
        final String email = userFacade.getEmailFromPayPalUserData(userData);
        if (!brainTreeUserFacade.isCustomerWithPayerIdExist(userData.getPayerId())) {
            userFacade.setPayerIdToUser(email, userData.getPayerId());
            braintreeConnectWithPayPalNotificationFacade.sendLinkCustomerEmailNotification(email);
        }
        final CustomerData customer = brainTreeUserFacade.getCustomerDataByUid(email);
        final PayPalConnectLoginDTO loginData = new PayPalConnectLoginDTO();
        brainTreeConnectWithPayPalFacade.setAccessTokenForCustomer(accessTokenGuid, userData.getPayerId());
        loginData.setLogin(customer.getUid());
        loginData.setAccessTokenGuid(accessTokenGuid);
        return loginData;
    }

    @Secured({"ROLE_CLIENT", "ROLE_CUSTOMERGROUP", "ROLE_GUEST", "ROLE_CUSTOMERMANAGERGROUP", "ROLE_TRUSTED_CLIENT",
        "ROLE_ANONYMOUS"})
    @PostMapping(value = "/afterLogin")
    @ResponseStatus(HttpStatus.OK)
    @Operation(operationId = "After login action", summary = "clear password field")
    @ApiBaseSiteIdAndUserIdParam
    public HttpStatus afterLogin(
        @Parameter(description = "JSON object which contains access token", required = true)
        @RequestBody PayPalConnectTokenWsDTO payPalConnectTokenWsDTO) {
        String accessTokenGuid = payPalConnectTokenWsDTO.getAccessTokenGuid();

        String accessToken = braintreeAccessTokenService.getPaypalAccessToken(accessTokenGuid)
                .map(PaypalAccessTokenModel::getAccessToken)
                .orElseThrow(PaypalAccessTokenNotFoundException::new);
        final BrainTreeConnectWithPayPalUserData userData = brainTreeConnectWithPayPalFacade
            .getUserDataByAccessToken(accessToken);
        brainTreeConnectWithPayPalFacade.setDefaultAccessToken(userData.getPayerId());

        braintreeAccessTokenService.removePaypalAccessToken(accessTokenGuid);
        
        return HttpStatus.OK;
    }

    @Secured({"ROLE_CLIENT", "ROLE_CUSTOMERGROUP", "ROLE_GUEST", "ROLE_CUSTOMERMANAGERGROUP", "ROLE_TRUSTED_CLIENT",
        "ROLE_ANONYMOUS"})
    @PostMapping(value = "/register")
    @ResponseStatus(HttpStatus.OK)
    @Operation(operationId = "register endpoint for connect with PayPal", summary = "register user by accessToken")
    @ApiBaseSiteIdAndUserIdParam
    public ResponseEntity<String> doRegister(
        @Parameter(description = "JSON object which contains access token", required = true)
        @RequestBody final PayPalConnectTokenWsDTO registerData)
        throws DuplicateUidException {
        String accessToken = braintreeAccessTokenService.getPaypalAccessToken(registerData.getAccessTokenGuid())
                .map(PaypalAccessTokenModel::getAccessToken)
                .orElseThrow(PaypalAccessTokenNotFoundException::new);
        String payerId = userFacade.registerPayPalUser(accessToken);
        return new ResponseEntity<>(gson.toJson(payerId), HttpStatus.OK);
    }

    @Secured({"ROLE_CLIENT", "ROLE_CUSTOMERGROUP", "ROLE_GUEST", "ROLE_CUSTOMERMANAGERGROUP", "ROLE_TRUSTED_CLIENT",
            "ROLE_ANONYMOUS"})
    @PostMapping(value = "/verifyUser")
    @ResponseStatus(HttpStatus.OK)
    @Operation(operationId = "Endpoint for user account verification", summary = "Returns the user verification result")
    @ApiBaseSiteIdAndUserIdParam
    public BraintreeUserVerificationResultWsDTO verifyUser(
            @Parameter(description = "JSON object which contains username, password", required = true)
            @RequestBody BraintreeUserVerificationWsDTO braintreeUserVerificationWsDTO) {

        final String username = braintreeUserVerificationWsDTO.getUsername();
        final String password = braintreeUserVerificationWsDTO.getPassword();

        BraintreeUserVerificationResultWsDTO result = new BraintreeUserVerificationResultWsDTO();

        if (brainTreeUserFacade.isLoginDisabledForCurrentEmail(username)) {
            result.setVerificationStatus(BraintreeUserVerificationStatus.BLOCKED.toString());
        } else if (brainTreeUserFacade.isPasswordValid(username, password)) {
            result.setVerificationStatus(BraintreeUserVerificationStatus.VERIFIED.toString());
        } else {
            result.setVerificationStatus(BraintreeUserVerificationStatus.REJECTED.toString());
        }

        return result;
    }

    @Secured({"ROLE_CLIENT", "ROLE_CUSTOMERGROUP", "ROLE_GUEST", "ROLE_CUSTOMERMANAGERGROUP", "ROLE_TRUSTED_CLIENT",
        "ROLE_ANONYMOUS"})
    @PostMapping(value = "/createPaymentDetails")
    @ResponseStatus(HttpStatus.CREATED)
    @Operation(operationId = "addPayPalPayment", summary = "Save new PayPal payment info", description = "Adding PayPal payment info into customer wallet")
    @ApiBaseSiteIdAndUserIdParam
    public AddingPaymentMethodAndAddressStatusWsDTO addPayPalPaymentAndAddressForNewUser(
        @Parameter(description = "Request body parameter that contains" +
            "payPalPaymentResponse(payment method nonce (nonce), payment type (type), email, " +
            "countryCode, payerId, phone, firstName, lastName " +
            "and the billing address (First line of the address, " +
            "billingAddress.lastName, billingAddress.titleCode, billingAddress.country.isocode, " +
            "\n\nThe DTO is in XML or .json format.", required = true) @RequestBody final PayPalPaymentRequestWsDTO payPalRequest,
        @Parameter(description = "Data collected from client.") @RequestParam(required = false) final String deviceData,
        @Parameter(description = "Connect with PP access token.") @RequestParam final String accessTokenGuid,
        @Parameter(description = "Funding source. Detect type of paypal payment")
        @RequestParam(defaultValue = PAYPAL_FUNDING_SOURCE) final String fundingSource,
        @RequestParam final boolean shouldSaveAddressInfo,
        @RequestParam final boolean shouldSavePaymentInfo) {
        AddingPaymentMethodAndAddressStatusWsDTO result = new AddingPaymentMethodAndAddressStatusWsDTO();
        String accessToken = braintreeAccessTokenService.getPaypalAccessToken(accessTokenGuid)
                .map(PaypalAccessTokenModel::getAccessToken)
                .orElseThrow(PaypalAccessTokenNotFoundException::new);
        final BrainTreeConnectWithPayPalUserData userData = brainTreeConnectWithPayPalFacade.getUserDataByAccessToken(
            accessToken);
        final String email = userFacade.getEmailFromPayPalUserData(userData);
        result.setIsAddressAdded(false);
        result.setIsPaymentMethodAdded(false);

        if (shouldSavePaymentInfo) {
            validate(payPalRequest, "payPalRequest", payPalPaymentRequestValidator);
            final String payPalEmail = payPalRequest.getDetails().getEmail();
            final PayPalDetailsWsDTO brainTreeDetails = payPalRequest.getDetails();
            final AddressData addressData = resolveAddress(brainTreeDetails);

            validate(brainTreeDetails, "brainTreeDetails", payPalDetailsValidator);

            final BrainTreeSubscriptionInfoData paymentInfoData = GenericBuilder.of(BrainTreeSubscriptionInfoData::new)
                .with(BrainTreeSubscriptionInfoData::setPaymentProvider, payPalRequest.getType())
                .with(BrainTreeSubscriptionInfoData::setEmail, payPalEmail)
                .with(BrainTreeSubscriptionInfoData::setNonce, payPalRequest.getNonce())
                .with(BrainTreeSubscriptionInfoData::setSavePaymentInfo, brainTreeConnectWithPayPalFacade.shouldSavePayPalPaymentInfo())
                .with(BrainTreeSubscriptionInfoData::setPaypalFundingSource, fundingSource)
                .with(BrainTreeSubscriptionInfoData::setDeviceData, deviceData)
                .with(BrainTreeSubscriptionInfoData::setAddressData, addressData)
                .build();
            try {
                brainTreePaymentFacade.createPaymentInfoForLogin(paymentInfoData, email);
                result.setIsPaymentMethodAdded(true);
            } catch (Exception ex) {
                LOG.error("Error during adding payment method for new customer", ex);
                result.setErrorMessagePaymentMethod(ex.getMessage());
            }
        }

        if (shouldSaveAddressInfo && userData.getAddress() != null) {
                try {
                    brainTreeConnectWithPayPalFacade.addAddressForNewUser(userData.getAddress(), email);
                    result.setIsAddressAdded(true);
                } catch (Exception ex) {
                    LOG.error("Error during adding address for new customer", ex);
                    result.setErrorMessageAddress(ex.getMessage());
                }
        }

        return result;
    }

    private AddressData resolveAddress(PayPalDetailsWsDTO brainTreeDetails) {
        final PayPalAddressWsDTO billingAddress = brainTreeDetails.getBillingAddress();

        AddressData addressData = Optional.ofNullable(billingAddress)
            .map(payPalOCCAddressConverter::convert)
            .or(() -> Optional.ofNullable(payPalOCCAddressConverter.convert(brainTreeDetails.getShippingAddress())))
            .get();

        addressData.setEmail(brainTreeDetails.getEmail());

        if (StringUtils.isBlank(brainTreeDetails.getFirstName()) && StringUtils.isNotBlank(
            addressData.getFirstName())) {
            brainTreeDetails.setFirstName(addressData.getFirstName());
            brainTreeDetails.setLastName(addressData.getLastName());
            brainTreeDetails.setCountryCode(addressData.getPostalCode());
        }

        if (StringUtils.isBlank(addressData.getFirstName())) {
            addressData.setFirstName(brainTreeDetails.getFirstName());
            addressData.setLastName(brainTreeDetails.getLastName());
            addressData.setPhone(brainTreeDetails.getPhone());
        }
        return addressData;
    }
}
