package com.braintree.service.impl;

import com.braintree.builders.PaymentTransactionEntryBuilder;
import com.braintree.command.result.BrainTreeAuthorizationResult;
import com.braintree.command.result.BrainTreeTransactionResult;
import com.braintree.configuration.service.BrainTreeConfigService;
import com.braintree.enums.BTFraudToolReviewStatus;
import com.braintree.enums.BrainTreePaymentMethod;
import com.braintree.enums.BraintreePaymentTransactionStatus;
import com.braintree.method.impl.DefaultBrainTreePaymentService;
import com.braintree.model.BrainTreePaymentInfoModel;
import com.braintree.service.BrainTreeFraudCheckToolService;
import com.braintree.transaction.service.BrainTreeTransactionService;
import com.google.gson.Gson;
import de.hybris.platform.core.enums.OrderStatus;
import de.hybris.platform.core.model.c2l.CurrencyModel;
import de.hybris.platform.core.model.order.AbstractOrderModel;
import de.hybris.platform.payment.PaymentService;
import de.hybris.platform.payment.enums.PaymentTransactionType;
import de.hybris.platform.payment.model.PaymentTransactionEntryModel;
import de.hybris.platform.payment.model.PaymentTransactionModel;
import de.hybris.platform.servicelayer.model.ModelService;
import de.hybris.platform.servicelayer.util.ServicesUtil;
import org.apache.log4j.Logger;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static org.apache.commons.lang.BooleanUtils.isFalse;
import static org.apache.commons.lang.StringUtils.isEmpty;
import static org.apache.commons.lang.StringUtils.isNotEmpty;

public class DefaultBrainTreeFraudCheckToolService implements BrainTreeFraudCheckToolService {

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

    @Resource
    private ModelService modelService;

    @Resource(name = "btFraudCheckStatusesToHybrisStatuses")
    private Map<String, BTFraudToolReviewStatus> statusMapper;

    @Resource
    private List<BrainTreePaymentMethod> paymentMethodsApplicableForBTFraudToolReview;

    @Resource
    private DefaultBrainTreePaymentService brainTreePaymentService;
    @Resource
    private PaymentService paymentService;
    @Resource
    private BrainTreeTransactionService brainTreeTransactionService;
    @Resource
    private BrainTreeConfigService brainTreeConfigService;

    @Override
    public void saveBtFraudToolReviewStatus(PaymentTransactionModel transaction, BTFraudToolReviewStatus status) {
        ServicesUtil.validateParameterNotNull(transaction, "transaction cannot be null");
        ServicesUtil.validateParameterNotNull(status, "status cannot be null");

        AbstractOrderModel order = transaction.getOrder();
        order.setBtFraudToolReviewStatus(status);
        transaction.setBtFraudToolReviewStatus(status);

        modelService.saveAll(order, transaction);
        modelService.refresh(order);
        modelService.refresh(transaction);
    }

    @Override
    public BTFraudToolReviewStatus translateStatusFromBtToHybris(String btStatus) {
        ServicesUtil.validateParameterNotNull(btStatus, "btStatus cannot be null");

        return statusMapper.get(btStatus.toUpperCase());
    }

    @Override
    public void updateBtFraudToolReviewStatus(PaymentTransactionModel transaction, BrainTreeAuthorizationResult result) {
        ServicesUtil.validateParameterNotNull(transaction, "transaction cannot be null");
        ServicesUtil.validateParameterNotNull(result, "result cannot be null");

        BrainTreePaymentMethod paymentProvider = BrainTreePaymentMethod.valueOf(((BrainTreePaymentInfoModel)
                transaction.getOrder().getPaymentInfo()).getPaymentProvider());

        if (isNotApplicable(result, paymentProvider)) {
            saveBtFraudToolReviewStatus(transaction, BTFraudToolReviewStatus.NOT_APPLICABLE);
        } else if (isNotEmpty(result.getBtFraudCheckToolDecision())) {
            BTFraudToolReviewStatus decision = translateStatusFromBtToHybris(result.getBtFraudCheckToolDecision());
            saveBtFraudToolReviewStatus(transaction, decision);
        }
    }

    @Override
    public void updateTransactionStatus(PaymentTransactionModel transaction, BTFraudToolReviewStatus decision) {
        ServicesUtil.validateParameterNotNull(transaction, "transaction cannot be null");
        ServicesUtil.validateParameterNotNull(decision, "decision cannot be null");

        String transactionId = transaction.getRequestIdGraphQL();
        BrainTreeTransactionResult transactionResult = brainTreePaymentService.findTransactionById(transactionId);
        LOG.info("Found transaction result by id: " + new Gson().toJson(transactionResult));

        if (isRefundTransactionEntryShouldBeCreated(decision, transactionResult)) {
            LOG.info("[Refund] transaction entry should be created");

            PaymentTransactionEntryModel transactionEntry = createTransactionEntry(transactionResult,
                    PaymentTransactionType.REFUND_PARTIAL, transaction.getCurrency());
            saveRefundTransactionEntry(transaction, transactionEntry);

        } else if (isVoidTransactionEntryShouldBeCreated(decision, transactionResult)) {
            LOG.info("[Void] transaction entry should be created");

            PaymentTransactionEntryModel transactionEntry = createTransactionEntry(transactionResult,
                    PaymentTransactionType.CANCEL, transaction.getOrder().getCurrency());
            saveVoidTransactionEntry(transaction, transactionEntry);
            updateOrderStatus(transaction.getOrder(), OrderStatus.CANCELLED);
        }
    }

    private void saveVoidTransactionEntry(PaymentTransactionModel transaction,
                                          PaymentTransactionEntryModel transactionEntry) {
        String entryCode = paymentService.getNewPaymentTransactionEntryCode(transaction, PaymentTransactionType.CANCEL);
        transactionEntry.setCode(entryCode);

        addTransactionEntryToTheExistingEntries(transaction, transactionEntry);

        modelService.saveAll(transaction, transactionEntry);
        modelService.refresh(transaction);
        modelService.refresh(transactionEntry);
        brainTreeTransactionService.saveBraintreeTransactionStatus(
                BraintreePaymentTransactionStatus.VOIDED.getCode(), transaction);
    }

    private void saveRefundTransactionEntry(PaymentTransactionModel transaction,
                                            PaymentTransactionEntryModel transactionEntry) {
        String entryCode = paymentService.getNewPaymentTransactionEntryCode(transaction,
                PaymentTransactionType.REFUND_PARTIAL);
        transactionEntry.setCode(entryCode);

        addTransactionEntryToTheExistingEntries(transaction, transactionEntry);

        modelService.saveAll(transaction, transactionEntry);
        modelService.refresh(transaction);
        modelService.refresh(transactionEntry);
        brainTreeTransactionService.saveBraintreeTransactionStatus(
                BraintreePaymentTransactionStatus.REFUNDED.getCode(), transaction);
    }

    private void updateOrderStatus(AbstractOrderModel order, OrderStatus status) {
        order.setStatus(status);
        modelService.save(order);
        modelService.refresh(order);
    }

    private void addTransactionEntryToTheExistingEntries(PaymentTransactionModel transaction,
                                                         PaymentTransactionEntryModel transactionEntry) {
        ArrayList<PaymentTransactionEntryModel> newEntriesList = new ArrayList<>(transaction.getEntries());
        newEntriesList.add(transactionEntry);
        transaction.setEntries(newEntriesList);
    }

    private boolean isVoidTransactionEntryShouldBeCreated(BTFraudToolReviewStatus decision,
                                                          BrainTreeTransactionResult transactionResult) {
        return transactionResult.getStatus().equals(BraintreePaymentTransactionStatus.VOIDED.getCode())
                && decision.equals(BTFraudToolReviewStatus.REJECTED);
    }

    private boolean isRefundTransactionEntryShouldBeCreated(BTFraudToolReviewStatus decision,
                                                            BrainTreeTransactionResult transactionResult) {
        return (transactionResult.getStatus().equals(BraintreePaymentTransactionStatus.SETTLED.getCode())
                || transactionResult.getStatus().equals(BraintreePaymentTransactionStatus.SETTLING.getCode()))
                && decision.equals(BTFraudToolReviewStatus.REJECTED) && isFalse(transactionResult.getRefunds().isEmpty());
    }

    private PaymentTransactionEntryModel createTransactionEntry(BrainTreeTransactionResult transactionResult,
                                                                PaymentTransactionType type, CurrencyModel currency) {
        PaymentTransactionEntryBuilder transactionEntryBuilder = new PaymentTransactionEntryBuilder(type);
        transactionEntryBuilder.setCurrency(currency);
        if (transactionResult.getRefunds().isEmpty()) {
            transactionEntryBuilder.setRequestId(transactionResult.getId());
            transactionEntryBuilder.setRequestIdGraphQL(transactionResult.getGraphQLId());
        } else {
            // looking for the full refund (from the fraud tool)
            transactionResult.getRefunds().forEach(refund -> {
                if (refund.getAmount().equals(transactionResult.getTotalAmount())) {
                    transactionEntryBuilder.setRequestId(refund.getTransactionId());
                    transactionEntryBuilder.setRequestIdGraphQL(refund.getTransactionGraphQLId());
                }
            });
        }
        transactionEntryBuilder.setAmount(transactionResult.getTotalAmount());

        return transactionEntryBuilder.getResult();
    }

    private boolean isNotApplicable(BrainTreeAuthorizationResult result, BrainTreePaymentMethod paymentProvider) {
        return isEmpty(result.getBtFraudCheckToolDecision()) &&
                isFalse(paymentMethodsApplicableForBTFraudToolReview.contains(paymentProvider));
    }

}
