package com.braintree.graphql.commands.impl;

import com.braintree.command.request.BrainTreeCustomerRequest;
import com.braintree.command.result.BrainTreeFindCustomerResult;
import com.braintree.command.result.BrainTreeFindCustomersResult;
import com.braintree.command.result.BrainTreePaymentDetailsResult;
import com.braintree.commands.BrainTreeFindCustomerCommand;
import com.braintree.commands.impl.AbstractCommand;
import com.braintree.constants.BrainTreeSearchTextOptions;
import com.braintree.customer.service.BrainTreeCustomerAccountService;
import com.braintree.hybris.data.BrainTreeGraphQLCustomerData;
import com.braintree.hybris.data.graphql.commands.request.BrainTreeSearchPaymentCustomerInput;
import com.braintree.hybris.data.graphql.commands.request.BrainTreeSearchTextInput;
import com.braintree.hybris.data.graphql.commands.request.BrainTreeSearchValueInput;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeCustomer;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeCustomerConnection;
import com.braintree.hybris.data.graphql.commands.response.BraintreePaymentMethodData;
import com.braintree.hybris.data.graphql.commands.response.BrainTreePaymentMethodConnectionEdge;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeSearchCustomer;
import com.braintree.model.BraintreeCustomerDetailsModel;
import de.hybris.platform.payment.AdapterException;
import de.hybris.platform.servicelayer.dto.converter.Converter;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import static com.braintree.constants.BraintreeConstants.AFTER_PARAMETER;
import static com.braintree.constants.BraintreeConstants.ID_PARAMETER;
import static com.braintree.constants.BraintreeConstants.INPUT_PARAMETER;
import static com.braintree.constants.BraintreeConstants.RESULT_DATA;
import static com.braintree.constants.BraintreeConstants.RESULT_ERRORS;
import static de.hybris.platform.servicelayer.util.ServicesUtil.validateParameterNotNullStandardMessage;

/**
 * This class extends AbstractCommand, implements BrainTreeFindCustomerCommand and is used in GraphQL API.
 */
public class DefaultBrainTreeGraphQLFindCustomerCommand extends
        AbstractCommand<BrainTreeCustomerRequest, BrainTreeFindCustomerResult> implements
        BrainTreeFindCustomerCommand {

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

    private static final String DEFINITION_FILE_NAME_CUSTOMERS = "searchCustomer";
    private static final String DEFINITION_FILE_NAME_CUSTOMER = "node";
    private static final String MUTATION_NAME = "search";
    private static final String FIND_CUSTOMER_REQUEST_PARAMETER = "Find Customer Request";
    private static final String CUSTOMER_ID_PARAMETER = "customerId";
    private static final String ERROR_DUE_SEARCHING_CUSTOMERS_MESSAGE = "[BT Find Customer] Error due searching customers. ";
    private static final String TRY_TO_FIND_CUSTOMER_ERROR_MESSAGE = "[BT Find Customer] Error during try to find customer: %s";
    private static final String SEARCH_TOO_MANY_RESULTS_LIMIT_MESSAGE =
            "[BT Customer search]Too many results! Limit is %s. Please type customer id or customer email.";
    private static final String ERROR_WHILE_SEARCH_FOR_CUSTOMER_MESSAGE = "Error while search for Customer, message: ";

    private BrainTreeCustomerAccountService brainTreeCustomerAccountService;
    private Converter<BraintreePaymentMethodData, BrainTreePaymentDetailsResult> graphQLPaymentMethodToPaymentDetailsConverter;
    private Converter<BrainTreeCustomerConnection, BrainTreeGraphQLCustomerData> customerSearchResultConverter;

    @Override
    public BrainTreeFindCustomerResult perform(BrainTreeCustomerRequest request) {
        validateParameterNotNullStandardMessage(FIND_CUSTOMER_REQUEST_PARAMETER, request);
        final String customerId = request.getCustomerId();
        validateParameterNotNullStandardMessage(CUSTOMER_ID_PARAMETER, customerId);
        try {
            Map<String, Object> result = makeGraphQlCall(DEFINITION_FILE_NAME_CUSTOMER, createVariablesMap(customerId));
            ArrayList<Map<String, Object>> mapErrors = (ArrayList<Map<String, Object>>) result.get(RESULT_ERRORS);
            if (mapErrors == null) {
                return translateResponse((Map<String, Object>) result.get(RESULT_DATA));
            } else {
                String message = ERROR_DUE_SEARCHING_CUSTOMERS_MESSAGE + getFistErrorMessage(mapErrors);
                LOG.error(message);
                throw new AdapterException(message);
            }

        } catch (final Exception exception) {
            LOG.error(String.format(TRY_TO_FIND_CUSTOMER_ERROR_MESSAGE, customerId), exception);
            throw new IllegalArgumentException(exception.getMessage());
        }
    }

    private BrainTreeFindCustomerResult translateResponse(Map<String, Object> data) {
        BrainTreeCustomer payload = objectMapper
                .convertValue(data.get(DEFINITION_FILE_NAME_CUSTOMER), BrainTreeCustomer.class);
        BrainTreeFindCustomerResult result = new BrainTreeFindCustomerResult(false);

        if (payload != null) {
            result.setCustomerExist(true);
            result.setId(payload.getLegacyId());
            result.setGraphQLId(payload.getId());
            result.setEmail(payload.getEmail());
            result.setFirstName(payload.getFirstName());
            result.setLastName(payload.getLastName());
            if (payload.getPaymentMethods() != null && payload.getPaymentMethods().getEdges() != null
                    && payload.getPaymentMethods().getEdges().size() > 0) {
                List<BrainTreePaymentDetailsResult> paymentMethods = payload.getPaymentMethods()
                        .getEdges()
                        .stream()
                        .map(BrainTreePaymentMethodConnectionEdge::getNode)
                        .map(graphQLPaymentMethodToPaymentDetailsConverter::convert)
                        .collect(Collectors.toList());
                result.setPaymentMethods(paymentMethods);
            }
        }

        return result;
    }

    private Map<String, Object> createVariablesMap(String customerId) {
        Map<String, Object> map = new HashMap<>();
        map.put(ID_PARAMETER, brainTreeCustomerAccountService.getGraphQLIdForCustomer(customerId));
        return map;
    }

    @Override
    public BrainTreeFindCustomersResult process(BrainTreeCustomerRequest request) {
        LOG.info("process, request: " + request);
        int searchLimit = getBrainTreeConfigService().getBackofficeSearchLimit();
        Map<String, Object> variablesMap = createVariablesMap(request);

        List<BraintreeCustomerDetailsModel> customerList = new ArrayList<>();
        try {
            fillInCustomerList(variablesMap, customerList, searchLimit);

            BrainTreeFindCustomersResult customersResult = new BrainTreeFindCustomersResult();

            if (customerList.size() > searchLimit) {
                LOG.warn(String.format(SEARCH_TOO_MANY_RESULTS_LIMIT_MESSAGE, searchLimit));
                customersResult.setExceedLimit(true);
                return customersResult;
            }

            customersResult.setCustomerList(customerList);
            return customersResult;
        } catch (final Exception exception) {
            LOG.error(ERROR_WHILE_SEARCH_FOR_CUSTOMER_MESSAGE + exception.getMessage(), exception);
            throw new IllegalArgumentException(exception.getMessage());
        }
    }

    private void fillInCustomerList(Map<String, Object> variablesMap, List<BraintreeCustomerDetailsModel> customerList, int searchLimit) throws IOException {
        String endCursor = null;
        boolean hasNextPage = false;
        do {
            Optional.ofNullable(endCursor).ifPresent(s -> variablesMap.put(AFTER_PARAMETER, s));
            Map<String, Object> result = makeGraphQlCall(DEFINITION_FILE_NAME_CUSTOMERS, variablesMap);
            ArrayList<Map<String, Object>> mapErrors = (ArrayList<Map<String, Object>>) result.get(RESULT_ERRORS);

            if (mapErrors == null) {
                BrainTreeGraphQLCustomerData customerData = translateCustomersResponse(
                        (Map<String, Object>) result.get(RESULT_DATA));
                if (customerData != null) {
                    customerList.addAll(customerData.getCustomerList());
                    hasNextPage = customerData.getHasNextPage();
                    endCursor = customerData.getEndCursor();
                }
            } else {
                final String errorMessage =
                        ERROR_DUE_SEARCHING_CUSTOMERS_MESSAGE + getFistErrorMessage(mapErrors);
                LOG.error(errorMessage);
                throw new IllegalArgumentException();
            }

        } while (hasNextPage && customerList.size() <= searchLimit);
    }

    private BrainTreeGraphQLCustomerData translateCustomersResponse(Map<String, Object> data) {
        BrainTreeSearchCustomer payload = objectMapper
                .convertValue(data.get(MUTATION_NAME), BrainTreeSearchCustomer.class);
        return customerSearchResultConverter.convert(payload.getCustomers());
    }

    private Map<String, Object> createVariablesMap(BrainTreeCustomerRequest request) {
        final Map<String, Object> map = new HashMap<>();
        boolean isRequestEmpty = true;

        final BrainTreeSearchPaymentCustomerInput input = new BrainTreeSearchPaymentCustomerInput();
        final BrainTreeSearchValueInput customerId = new BrainTreeSearchValueInput();
        final BrainTreeSearchTextInput email = new BrainTreeSearchTextInput();

        if (request != null) {
            if (StringUtils.isNotBlank(request.getCustomerId())) {
                customerId.setIs(brainTreeCustomerAccountService.getGraphQLIdForCustomer(request.getCustomerId()));
                isRequestEmpty = false;
            }

            if (StringUtils.isNotBlank(request.getCustomerEmail())) {
                isRequestEmpty = false;
                switch (BrainTreeSearchTextOptions.valueOf(request.getCustomerEmailOperator())) {
                    case CONTAINS:
                        email.setContains(request.getCustomerEmail());
                        break;

                    case STARTS_WITH:
                        email.setStartsWith(request.getCustomerEmail());
                        break;

                    case ENDS_WITH:
                        email.setEndsWith(request.getCustomerEmail());
                        break;

                    default:
                        email.setIs(request.getCustomerEmail());
                        break;
                }
            }
        }

        input.setEmail(email);
        input.setId(customerId);
        if (isRequestEmpty) {
            map.put(INPUT_PARAMETER, new HashMap<>());
        } else {
            map.put(INPUT_PARAMETER, input);
        }
        return map;
    }

    public BrainTreeCustomerAccountService getBrainTreeCustomerAccountService() {
        return brainTreeCustomerAccountService;
    }

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

    public Converter<BraintreePaymentMethodData, BrainTreePaymentDetailsResult> getGraphQLPaymentMethodToPaymentDetailsConverter() {
        return graphQLPaymentMethodToPaymentDetailsConverter;
    }

    public void setGraphQLPaymentMethodToPaymentDetailsConverter(
            Converter<BraintreePaymentMethodData, BrainTreePaymentDetailsResult> graphQLPaymentMethodToPaymentDetailsConverter) {
        this.graphQLPaymentMethodToPaymentDetailsConverter = graphQLPaymentMethodToPaymentDetailsConverter;
    }

    public Converter<BrainTreeCustomerConnection, BrainTreeGraphQLCustomerData> getCustomerSearchResultConverter() {
        return customerSearchResultConverter;
    }

    public void setCustomerSearchResultConverter(
            Converter<BrainTreeCustomerConnection, BrainTreeGraphQLCustomerData> customerSearchResultConverter) {
        this.customerSearchResultConverter = customerSearchResultConverter;
    }

}
