package com.braintree.graphql.commands.impl;

import com.braintree.command.request.BrainTreeRefundTransactionRequest;
import com.braintree.command.result.BrainTreeRefundTransactionResult;
import com.braintree.commands.BrainTreeRefundCommand;
import com.braintree.commands.impl.AbstractCommand;
import com.braintree.hybris.data.graphql.commands.request.BrainTreeRefundInput;
import com.braintree.hybris.data.graphql.commands.request.BrainTreeRefundTransactionInput;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeErrorDefinition;
import com.braintree.hybris.data.graphql.commands.response.BrainTreePaymentMethodSnapshot;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeRefund;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeRefundDetails;
import com.braintree.hybris.data.graphql.commands.response.BrainTreeRefundTransactionPayload;
import com.braintree.payment.info.service.BraintreePaymentInfoService;
import com.braintree.transaction.service.BrainTreeTransactionService;
import com.braintreegateway.Transaction;
import de.hybris.platform.payment.AdapterException;
import de.hybris.platform.payment.dto.TransactionStatus;
import de.hybris.platform.payment.dto.TransactionStatusDetails;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

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;
import static java.util.Optional.ofNullable;

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

    private static final String DEFINITION_FILE_NAME = "refundTransaction";
    private static final String MUTATION_NAME = "refundTransaction";
    private static final String REFUND_TRANSACTION_REQUEST_PARAMETER = "Refund transaction Request";
    private static final String REFUND_WITH_ERROR_LOG_MESSAGE = "BT transaction id(%s) refund with error: %s %s";

    private BrainTreeTransactionService brainTreeTransactionService;
    private BraintreePaymentInfoService paymentInfoService;

    @Override
    public BrainTreeRefundTransactionResult perform(
            BrainTreeRefundTransactionRequest brainTreeRefundTransactionRequest) {
        validateParameterNotNullStandardMessage(REFUND_TRANSACTION_REQUEST_PARAMETER, brainTreeRefundTransactionRequest);
        try {
            Map<String, Object> result = makeGraphQlCall(DEFINITION_FILE_NAME,
                    createVariablesMap(brainTreeRefundTransactionRequest));
            ArrayList<Map<String, Object>> mapErrors = (ArrayList<Map<String, Object>>) result.get(RESULT_ERRORS);

            BrainTreeRefundTransactionResult braintreeResult;
            if (mapErrors == null) {
                braintreeResult = translateResponse((Map<String, Object>) result.get(RESULT_DATA));
            } else {
                List<BrainTreeErrorDefinition> errors = mapErrors.stream()
                        .map(stringObjectMap -> objectMapper.convertValue(stringObjectMap, BrainTreeErrorDefinition.class))
                        .collect(Collectors.toList());
                braintreeResult = translateErrorResponse(brainTreeRefundTransactionRequest.getTransactionId(), errors);
            }

            braintreeResult.setRequestBody(parseToJson(createVariablesMap(brainTreeRefundTransactionRequest)));
            braintreeResult.setResponseBody(parseToJson(result));
            return braintreeResult;
        } catch (final Exception exception) {
            throw new AdapterException(exception.getMessage(), exception);
        }
    }

    private BrainTreeRefundTransactionResult translateResponse(Map<String, Object> data) {
        final BrainTreeRefundTransactionResult result = new BrainTreeRefundTransactionResult(false);
        BrainTreeRefundTransactionPayload payload = objectMapper
                .convertValue(data.get(MUTATION_NAME), BrainTreeRefundTransactionPayload.class);
        if (payload != null && payload.getRefund() != null) {
            BrainTreeRefund refund = payload.getRefund();
            if (refund.getStatus() != null) {
                if (!refund.getStatus().isSuccess()) {
                    result.setTransactionStatus(TransactionStatus.REJECTED);
                    result.setErrorMessage(Transaction.Status.SETTLEMENT_DECLINED.toString());
                    result.setErrorMessage(getMessageByStatus(refund.getStatusHistory(), refund.getStatus()));
                    result.setErrorCode(getErrorCodeFromStatusHistory(refund.getStatusHistory()));
                } else {
                    result.setSuccess(true);
                    result.setStatus(refund.getStatus().name());
                    result.setAmount(new BigDecimal(refund.getAmount().getValue()));
                    result.setCurrencyIsoCode(refund.getAmount().getCurrencyCode());
                    result.setTransactionGraphQLId(refund.getId());
                    result.setTransactionId(refund.getLegacyId());
                    result.setOrderId(refund.getOrderId());
                    result.setTransactionStatus(TransactionStatus.ACCEPTED);
                    result.setTransactionStatusDetails(TransactionStatusDetails.SUCCESFULL);
                    result.setCreatedAt(refund.getCreatedAt().getTime());

                    BrainTreePaymentMethodSnapshot paymentMethodSnapshot = refund.getPaymentMethodSnapshot();
                    result.setCaptureId(paymentMethodSnapshot.getCaptureId());
                    result.setPaymentInstrumentType(
                            paymentInfoService.getPaymentInstrumentTypeBySnapshotAndOrigin(paymentMethodSnapshot));
                    ofNullable(refund.getDetails())
                            .map(BrainTreeRefundDetails::getRefundId)
                            .ifPresent(result::setRefundId);
                }
            }
        }
        return result;
    }

    private BrainTreeRefundTransactionResult translateErrorResponse(final String transactionId,
                                                                    List<BrainTreeErrorDefinition> errors) {
        final BrainTreeRefundTransactionResult response = new BrainTreeRefundTransactionResult(
                false);

        response.setTransactionStatus(TransactionStatus.REJECTED);

        if (errors != null && errors.size() > 0) {
            BrainTreeErrorDefinition errorDefinition = errors.get(0);
            getLoggingHandler().getLogger().info(
                    String.format(REFUND_WITH_ERROR_LOG_MESSAGE, transactionId,
                            errorDefinition.getExtensions().getLegacyCode(),
                            errorDefinition.getMessage()));

            if (errorDefinition.getExtensions().getLegacyCode() != null) {
                response.setErrorCode(errorDefinition.getExtensions().getLegacyCode());
            }
            response.setErrorMessage(errorDefinition.getMessage());

        }

        return response;
    }

    private Map<String, Object> createVariablesMap(final BrainTreeRefundTransactionRequest request) {
        Map<String, Object> map = new HashMap<>();
        final BrainTreeRefundTransactionInput input = new BrainTreeRefundTransactionInput();
        input.setTransactionId(brainTreeTransactionService.getGraphQLIdFromTransaction(request.getTransactionId()));
        final BrainTreeRefundInput refund = new BrainTreeRefundInput();

        refund.setOrderId(request.getOrderId());
        refund.setAmount(request.getAmount());
        input.setRefund(refund);

        map.put(INPUT_PARAMETER, input);
        return map;
    }

    public BrainTreeTransactionService getBrainTreeTransactionService() {
        return brainTreeTransactionService;
    }

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

    public BraintreePaymentInfoService getPaymentInfoService() {
        return paymentInfoService;
    }

    public void setPaymentInfoService(BraintreePaymentInfoService paymentInfoService) {
        this.paymentInfoService = paymentInfoService;
    }

}
