package com.braintree.order.adjust.authorization.impl;

import com.braintree.command.request.BrainTreeAdjustAuthorizationRequest;
import com.braintree.command.result.BrainTreeAbstractTransactionResult;
import com.braintree.command.result.BrainTreeAdjustedAuthorizationResult;
import com.braintree.constants.BraintreeConstants;
import com.braintree.enums.BrainTreeCardType;
import com.braintree.enums.BraintreePaymentTransactionStatus;
import com.braintree.exceptions.BraintreeErrorException;
import com.braintree.method.BrainTreePaymentService;
import com.braintree.model.BrainTreePaymentInfoModel;
import com.braintree.model.PaymentTransactionLogEntryModel;
import com.braintree.order.adjust.authorization.BraintreeAdjustAuthorizationService;
import com.braintree.transaction.service.BrainTreeTransactionService;
import de.hybris.platform.core.model.order.OrderModel;
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 java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Comparator;

import static com.braintree.constants.BraintreeConstants.BRAINTREE_SUCCESS;
import static de.hybris.platform.util.localization.Localization.getLocalizedString;

public class DefaultBraintreeAdjustAuthorizationService implements BraintreeAdjustAuthorizationService {

    private static final String ADJUST_AUTHORIZATION_HARD_DECLINE_ERROR_95602 = "95602";
    private static final String ADJUST_AUTHORIZATION_HARD_DECLINE_ERROR_95607 = "95607";

    private ModelService modelService;
    private BrainTreePaymentService brainTreePaymentService;
    private BrainTreeTransactionService brainTreeTransactionService;

    @Override
    public BrainTreeAdjustedAuthorizationResult adjustAuthorization(OrderModel orderModel, String transactionId,
                                                                    BigDecimal amount) throws BraintreeErrorException {
        final PaymentTransactionEntryModel transactionEntry = getTransactionEntryById(orderModel, transactionId);
        final PaymentTransactionModel paymentTransaction = transactionEntry.getPaymentTransaction();
        final BrainTreeAdjustAuthorizationRequest request = new BrainTreeAdjustAuthorizationRequest(transactionId, amount);

        if (isAdjustAuthorizationPossible(orderModel, paymentTransaction)) {
            final BrainTreeAdjustedAuthorizationResult adjustedAuthorizationResult = brainTreePaymentService.adjustAuthorization(request);

            if (adjustedAuthorizationResult.isSuccess()) {
                processSuccessResult(paymentTransaction, transactionEntry, adjustedAuthorizationResult.getAmount());
            } else {
                processErrorResult(adjustedAuthorizationResult, transactionEntry);
            }

            createTransactionLogEntry(adjustedAuthorizationResult, paymentTransaction, transactionEntry, amount);

            return adjustedAuthorizationResult;
        } else {
            throw new BraintreeErrorException(getLocalizedMessage(BraintreeConstants.PropertyConfiguration.TRANSACTION_AUTHORIZATION_NOT_ADJUSTABLE),
                    paymentTransaction.getRequestId());
        }
    }

    private void createTransactionLogEntry(BrainTreeAdjustedAuthorizationResult adjustedAuthorizationResult,
                                           PaymentTransactionModel paymentTransaction,
                                           PaymentTransactionEntryModel transactionEntry, BigDecimal newAmount) {
        PaymentTransactionLogEntryModel logEntry =
                brainTreeTransactionService.createTransactionLogEntry(adjustedAuthorizationResult, PaymentTransactionType.ADJUST_AUTHORIZATION.name());
        setTransactionLogEntryOldAmount(paymentTransaction, transactionEntry, logEntry);
        logEntry.setNewAmount(newAmount.doubleValue());
        logEntry.setPaymentTransaction(paymentTransaction);
        modelService.save(logEntry);
    }

    private void setTransactionLogEntryOldAmount(PaymentTransactionModel paymentTransaction,
                                                 PaymentTransactionEntryModel transactionEntry,
                                                 PaymentTransactionLogEntryModel logEntryModel) {
        paymentTransaction.getEntriesLogs()
                .stream()
                .filter(logEntry -> PaymentTransactionType.ADJUST_AUTHORIZATION.name().equals(logEntry.getInteractionType())
                        && BRAINTREE_SUCCESS.equals(logEntry.getStatus()))
                .max(Comparator.comparing(PaymentTransactionLogEntryModel::getCreationtime))
                .ifPresentOrElse(logEntry -> logEntryModel.setOldAmount(logEntry.getNewAmount()),
                        () -> logEntryModel.setOldAmount(transactionEntry.getOriginalAmount().doubleValue()));
    }

    private void processSuccessResult(PaymentTransactionModel paymentTransaction,
                                      PaymentTransactionEntryModel transactionEntry, BigDecimal updatedAmount) {
        paymentTransaction.setPlannedAmount(updatedAmount);
        transactionEntry.setAmount(updatedAmount);
        modelService.saveAll(paymentTransaction, transactionEntry);
    }

    private void processErrorResult(BrainTreeAbstractTransactionResult transactionResult,
                                    PaymentTransactionEntryModel transactionEntry) {
        checkAndProcessHardDeclineError(transactionResult, transactionEntry);
    }

    @Override
    public void checkAndProcessHardDeclineError(BrainTreeAbstractTransactionResult transactionResult,
                                                PaymentTransactionEntryModel transactionEntry) {
        String errorCode = transactionResult.getErrorCode();
        if (isHardDeclineError(errorCode)) {
            transactionEntry.setErrorCode(errorCode);
            transactionResult.setErrorMessage(getLocalizedMessage(BraintreeConstants.PropertyConfiguration.ADJUST_TRANSACTION_AUTHORIZATION_HARD_DECLINE_ERROR));
            modelService.save(transactionEntry);
        }
    }

    @Override
    public boolean isAdjustAuthorizationPossible(OrderModel order, PaymentTransactionModel transaction) {
        final BrainTreeCardType cardType = ((BrainTreePaymentInfoModel) order.getPaymentInfo()).getCardType();
        return isCardTypeApplicable(cardType)
                && BraintreePaymentTransactionStatus.AUTHORIZED == transaction.getBraintreePaymentTransactionStatus()
                && !isTransactionContainsHardDeclineError(transaction);
    }

    private boolean isCardTypeApplicable(BrainTreeCardType cardType) {
        return BrainTreeCardType.MASTERCARD.equals(cardType) || BrainTreeCardType.VISA.equals(cardType);
    }

    private boolean isTransactionContainsHardDeclineError(PaymentTransactionModel transaction) {
        return transaction.getEntries().stream()
                .map(PaymentTransactionEntryModel::getErrorCode)
                .anyMatch(this::isHardDeclineError);
    }

    private boolean isHardDeclineError(String errorCode) {
        return ADJUST_AUTHORIZATION_HARD_DECLINE_ERROR_95602.equals(errorCode)
                || ADJUST_AUTHORIZATION_HARD_DECLINE_ERROR_95607.equals(errorCode);
    }

    @Override
    public PaymentTransactionEntryModel getTransactionEntryById(OrderModel orderModel, String transactionId) {
        return orderModel.getPaymentTransactions().stream()
                .flatMap(transaction -> transaction.getEntries().stream())
                .filter(entry -> entry.getRequestId().equals(transactionId))
                .findFirst()
                .orElseThrow();
    }

    protected String getLocalizedMessage(String message) {
        return getLocalizedString(message);
    }

    public void setModelService(ModelService modelService) {
        this.modelService = modelService;
    }

    public void setBrainTreePaymentService(BrainTreePaymentService brainTreePaymentService) {
        this.brainTreePaymentService = brainTreePaymentService;
    }

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

}
