package com.braintree.graphql.commands.impl;

import com.braintree.command.request.BrainTreeFindTransactionRequest;
import com.braintree.command.result.BrainTreeFindTransactionResult;
import com.braintree.commands.BrainTreeFindTransactionCommand;
import com.braintree.commands.impl.AbstractCommand;
import com.braintree.constants.BrainTreeSearchTextOptions;
import com.braintree.customer.service.BrainTreeCustomerAccountService;
import com.braintree.hybris.data.BrainTreeGraphQLTransactionData;
import com.braintree.hybris.data.BraintreeTransactionData;
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.BrainTreeSearchTimestampInput;
import com.braintree.hybris.data.graphql.commands.request.BrainTreeSearchTransactionStatusInput;
import com.braintree.hybris.data.graphql.commands.request.BrainTreeSearchValueInput;
import com.braintree.hybris.data.graphql.commands.request.BrainTreeTransactionSearchInput;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeSearchTransaction;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeTransactionConnection;
import com.braintree.transaction.service.BrainTreeTransactionService;
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.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.TimeZone;

import static com.braintree.constants.BraintreeConstants.AFTER_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;

/**
 * This class extends AbstractCommand, implements BrainTreeFindTransactionCommand and is used in GraphQL API.
 */
public class DefaultBrainTreeGraphQLFindTransactionCommand extends AbstractCommand implements
        BrainTreeFindTransactionCommand {

    private static final Logger LOG = Logger.getLogger(DefaultBrainTreeGraphQLFindTransactionCommand.class);
    private static final String DEFINITION_FILE_NAME = "searchTransaction";
    private static final String MUTATION_NAME = "search";
    private static final String ERROR_DUE_SEARCHING_TRANSACTIONS_MESSAGE =
            "[BT Find Transaction] Error due searching transactions. ";
    private static final String TOO_MANY_RESULTS_MESSAGE =
            "[BT Transaction search]Too many results! Limit is %s. Please type transaction id or customer email.";
    private static final String UTC = "UTC";
    private static final String DATE_PATTERN = "yyyy-MM-dd'T'HH:mm'Z'";
    private static final String BRAIN_TREE_FIND_TRANSACTION_REQUEST = "brainTreeFindTransactionRequest: ";

    private BrainTreeCustomerAccountService brainTreeCustomerAccountService;
    private BrainTreeTransactionService brainTreeTransactionService;
    private Converter<BrainTreeTransactionConnection, BrainTreeGraphQLTransactionData> transactionGraphQLSearchResultConverter;

    @Override
    public BrainTreeFindTransactionResult perform(BrainTreeFindTransactionRequest brainTreeFindTransactionRequest) {
        try {
            int searchLimit = getBrainTreeConfigService().getBackofficeSearchLimit();
            Map<String, Object> variablesMap = createVariablesMap(brainTreeFindTransactionRequest);
            BraintreeTransactionData transactionDataResult = new BraintreeTransactionData();
            transactionDataResult.setTransactionEntries(new ArrayList<>());

            fillInTransactionEntries(variablesMap, transactionDataResult, searchLimit);

            BrainTreeFindTransactionResult transactionResult = new BrainTreeFindTransactionResult();

            if (transactionDataResult.getTransactionEntries().size() > searchLimit) {
                LOG.warn(String.format(TOO_MANY_RESULTS_MESSAGE, searchLimit));
                transactionResult.setExceedLimit(true);
                return transactionResult;
            }

            transactionResult.setTransactionData(transactionDataResult);
            return transactionResult;
        } catch (final Exception exception) {
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    private void fillInTransactionEntries(Map<String, Object> variablesMap, BraintreeTransactionData transactionDataResult, 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, variablesMap);
            ArrayList<Map<String, Object>> mapErrors = (ArrayList<Map<String, Object>>) result.get(RESULT_ERRORS);

            if (mapErrors == null) {
                BrainTreeGraphQLTransactionData braintreeTransactionData = translateResponse(
                        (Map<String, Object>) result.get(RESULT_DATA));
                if (braintreeTransactionData != null) {
                    transactionDataResult.getTransactionEntries()
                            .addAll(braintreeTransactionData.getTransactionData().getTransactionEntries());
                    hasNextPage = braintreeTransactionData.getHasNextPage();
                    endCursor = braintreeTransactionData.getEndCursor();
                }
            } else {
                final String errorMessage =
                        ERROR_DUE_SEARCHING_TRANSACTIONS_MESSAGE + getFistErrorMessage(mapErrors);
                LOG.error(errorMessage);
                throw new IllegalArgumentException();
            }

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

    private BrainTreeGraphQLTransactionData translateResponse(Map<String, Object> data) {
        BrainTreeSearchTransaction payload = objectMapper
                .convertValue(data.get(MUTATION_NAME), BrainTreeSearchTransaction.class);
        return transactionGraphQLSearchResultConverter.convert(payload.getTransactions());
    }

    private Map<String, Object> createVariablesMap(BrainTreeFindTransactionRequest brainTreeFindTransactionRequest) {

        Map<String, Object> map = new HashMap<>();

        final BrainTreeTransactionSearchInput input = new BrainTreeTransactionSearchInput();
        final BrainTreeSearchValueInput transactionId = new BrainTreeSearchValueInput();
        final BrainTreeSearchTransactionStatusInput transactionStatus = new BrainTreeSearchTransactionStatusInput();
        final BrainTreeSearchTimestampInput createdAt = new BrainTreeSearchTimestampInput();
        final BrainTreeSearchPaymentCustomerInput customer = new BrainTreeSearchPaymentCustomerInput();
        final BrainTreeSearchValueInput customerId = new BrainTreeSearchValueInput();
        final BrainTreeSearchTextInput email = new BrainTreeSearchTextInput();

        if (brainTreeFindTransactionRequest != null) {

            if (StringUtils.isNotBlank(brainTreeFindTransactionRequest.getTransactionId())) {
                transactionId.setIs(brainTreeTransactionService
                        .getGraphQLIdFromTransaction(brainTreeFindTransactionRequest.getTransactionId()));
            }

            if (brainTreeFindTransactionRequest.getTransactionIds() != null) {
                transactionId.setIn(brainTreeFindTransactionRequest.getTransactionIds());
            }

            if (StringUtils.isNotBlank(brainTreeFindTransactionRequest.getTransactionStatus())) {
                transactionStatus.setIs(brainTreeFindTransactionRequest.getTransactionStatus());
            }

            if (startAndEndDateNotNullInTransaction(brainTreeFindTransactionRequest)) {
                createdAt
                        .setGreaterThanOrEqualTo(toISO8601UTC(brainTreeFindTransactionRequest.getStartDate().getTime()));
                createdAt.setLessThanOrEqualTo(toISO8601UTC(brainTreeFindTransactionRequest.getEndDate().getTime()));
            }

            if (StringUtils.isNotBlank(brainTreeFindTransactionRequest.getCustomerId())) {
                customerId.setIs(brainTreeCustomerAccountService
                        .getGraphQLIdForCustomer(brainTreeFindTransactionRequest.getCustomerId()));
            }

            if (StringUtils.isNotBlank(brainTreeFindTransactionRequest.getCustomerEmail())) {
                switch (BrainTreeSearchTextOptions
                        .valueOf(brainTreeFindTransactionRequest.getCustomerEmailOperator())) {
                    case CONTAINS:
                        email.setContains(brainTreeFindTransactionRequest.getCustomerEmail());
                        break;

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

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

                    default:
                        email.setIs(brainTreeFindTransactionRequest.getCustomerEmail());
                        break;
                }
            }
            customer.setId(customerId);
            customer.setEmail(email);

            input.setId(transactionId);
            input.setStatus(transactionStatus);
            input.setCreatedAt(createdAt);
            input.setCustomer(customer);

            map.put(INPUT_PARAMETER, input);
            LOG.info(BRAIN_TREE_FIND_TRANSACTION_REQUEST + map);
        }
        return map;
    }

    private boolean startAndEndDateNotNullInTransaction(BrainTreeFindTransactionRequest
                                                                brainTreeFindTransactionRequest) {
        return brainTreeFindTransactionRequest.getStartDate() != null
                && brainTreeFindTransactionRequest.getEndDate() != null;
    }

    private static String toISO8601UTC(Date date) {
        TimeZone tz = TimeZone.getTimeZone(UTC);
        DateFormat df = new SimpleDateFormat(DATE_PATTERN);
        df.setTimeZone(tz);
        return df.format(date);
    }

    public BrainTreeCustomerAccountService getBrainTreeCustomerAccountService() {
        return brainTreeCustomerAccountService;
    }

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

    public BrainTreeTransactionService getBrainTreeTransactionService() {
        return brainTreeTransactionService;
    }

    public void setBrainTreeTransactionService(
            BrainTreeTransactionService brainTreeTransactionService) {
        this.brainTreeTransactionService = brainTreeTransactionService;
    }

    public Converter<BrainTreeTransactionConnection, BrainTreeGraphQLTransactionData> getTransactionGraphQLSearchResultConverter() {
        return transactionGraphQLSearchResultConverter;
    }

    public void setTransactionGraphQLSearchResultConverter(
            Converter<BrainTreeTransactionConnection, BrainTreeGraphQLTransactionData> transactionGraphQLSearchResultConverter) {
        this.transactionGraphQLSearchResultConverter = transactionGraphQLSearchResultConverter;
    }

}
