package com.liquidnet.service.account.wallet.service.processor;

import com.liquidnet.commons.lang.util.JsonUtils;
import com.liquidnet.service.account.common.ErrorConstants;
import com.liquidnet.service.account.common.FinConstants;
import com.liquidnet.service.account.exception.FinaccountException;
import com.liquidnet.service.account.wallet.dto.base.WalletCallResult;
import com.liquidnet.service.account.wallet.dto.base.WalletContextParam;
import com.liquidnet.service.account.wallet.entity.FinBizTrans;
import com.liquidnet.service.account.wallet.entity.FinBizTransExtra;
import com.liquidnet.service.account.wallet.service.FinBizTransExtraService;
import com.liquidnet.service.account.wallet.service.FinBizTransService;
import com.liquidnet.service.account.wallet.service.WalletProcessorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.stream.Collectors;

/**
 * WalletCostTransProcessService.class
 *
 * @author zhanggb
 * Created by IntelliJ IDEA at 2020/11/2
 */
@Service
public class WalletTransCalBalanceProcessor extends WalletProcessorService {
    private static final Logger log = LoggerFactory.getLogger(WalletTransCalBalanceProcessor.class);
    @Autowired
    private FinBizTransService bizTransService;
    @Autowired
    private FinBizTransExtraService bizTransExtraService;

    @Override
    public WalletCallResult<?> checkInputParams(WalletContextParam context) {
        if (null == context.getLimitedSize()
                || (Math.max(context.getLimitedSize(), 100) != Math.min(context.getLimitedSize(), 10000))) {
            return new WalletCallResult<>(ErrorConstants.PARAMS_VALUE_ERROR_CODE, "Invalid limitedSize, the range should be 100~10000");
        }
        return new WalletCallResult<>();
    }

    @Override
    public WalletCallResult<?> checkBussinessLogic(WalletContextParam context) {
        return new WalletCallResult<>();
    }

    @Override
    public WalletCallResult<?> doBussiness(WalletContextParam context) {
        List<FinBizTrans> transToBeCalculatedList;
        Integer limitedSize = context.getLimitedSize();
        int threadPool = 5, calCount = (null == limitedSize || 0 == limitedSize) ? 1000 : limitedSize;
        try {
//            while (true) {
//                if (CollectionUtils.isEmpty(
//                        transToBeCalculatedList = bizTransExtraService.queryTransToBeCalculated(calCount)
//                )) {
//                    Thread.sleep(20 * 1000);
//                    continue;
//                }
            if (CollectionUtils.isEmpty(
                    transToBeCalculatedList = bizTransService.queryTransToBeCalculated(calCount)
            )) {
                return new WalletCallResult<>(String.format("No transaction to be calculated:[calCount=%s,pending.size=0]", calCount));
            }

            Map<String, List<FinBizTrans>> transForWalletMap = transToBeCalculatedList.parallelStream()
                    .collect(Collectors.groupingBy(FinBizTrans::getWalletNo));
            log.info("[threadPool={},calCount={},transToBeCalculatedList.size={},transForWalletMap.size={}]",
                    threadPool, calCount, transToBeCalculatedList.size(), transForWalletMap.size());

            ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(threadPool);
            final CountDownLatch latch = new CountDownLatch(transForWalletMap.size());
            final Semaphore semaphore = new Semaphore(threadPool);
            for (Map.Entry<String, List<FinBizTrans>> entries : transForWalletMap.entrySet()) {
                semaphore.acquire();
                threadPoolExecutor.execute(() -> {
                    try {
                        String walletNo = entries.getKey();
                        List<FinBizTrans> subTransToBeCalculatedList = entries.getValue();
                        subTransToBeCalculatedList.sort(Comparator.comparing(FinBizTrans::getTransNo));
                        List<FinBizTransExtra> addTransExtraList = new ArrayList<>(),
                                lastTransExtraList = bizTransExtraService.queryLastTransBalance(walletNo);
                        FinBizTransExtra lastTransExtra;
                        if (CollectionUtils.isEmpty(lastTransExtraList)) {
                            lastTransExtra = FinBizTransExtra.getNew();
                            lastTransExtra.setTransNo(walletNo);
                            lastTransExtra.setWalletNo(walletNo);
                            lastTransExtra.setBalanceAvailable(BigDecimal.ZERO);
                            lastTransExtra.setBalancePending(BigDecimal.ZERO);
                            lastTransExtra.setBalanceFrozen(BigDecimal.ZERO);
//                            lastTransExtra.setReceived(1);

                            addTransExtraList.add(lastTransExtra.copy());
                        } else {
                            lastTransExtra = lastTransExtraList.get(0);
                        }
                        lastTransExtra.setCalTime(LocalDateTime.now());

                        int size = subTransToBeCalculatedList.size();
                        log.info("calBegin:[walletNo={},pending.size={},lastTransExtra={}]", walletNo, size, JsonUtils.toJson(lastTransExtra));

                        for (int i = 0; i < size; i++) {
                            FinBizTrans pendingTrans = subTransToBeCalculatedList.get(i);
                            Integer transStatus = pendingTrans.getTransStatus();
                            if (FinConstants.TxStatus.PROCESSING.getVal() == transStatus) {
                                i = size;
                                // If there is processing, stop calculation
                                log.warn("There is a transaction in process, please process it in time[transNo=[{}]]", pendingTrans.getTransNo());
                                // TODO: 2020/11/3 Send warning message


                                continue;
                            }
                            if (FinConstants.TxStatus.SUCCESS.getVal() == transStatus) {
                                switch (FinConstants.TransType.valueOf(pendingTrans.getTransType()).getVal()) {
                                    case "C":
                                        lastTransExtra = this.creditProcessing(pendingTrans, lastTransExtra);
                                        break;
                                    case "D":
                                        lastTransExtra = this.debitProcessing(pendingTrans, lastTransExtra);
                                        break;
                                    default:
                                        log.warn("calWarn:Unknown type handling[transNo={},transType={}]",
                                                pendingTrans.getTransNo(), pendingTrans.getTransType());
                                        continue;
                                }
                            }

                            log.info("calProcessedBo:[{},{},{},{},avl={},apd={},afz={}]",
                                    pendingTrans.getTransNo(),
                                    pendingTrans.getAmount(),
                                    pendingTrans.getTransType(),
                                    pendingTrans.getTransStatus(),
                                    lastTransExtra.getBalanceAvailable(),
                                    lastTransExtra.getBalancePending(),
                                    lastTransExtra.getBalanceFrozen());

                            lastTransExtra.setTransNo(pendingTrans.getTransNo());
                            addTransExtraList.add(lastTransExtra.copy());
                        }
                        log.info("calEnd:[walletNo={},pending.size={},processed.size={},lastTransExtra={}]",
                                walletNo, size, addTransExtraList.size(), JsonUtils.toJson(lastTransExtra));

                        if (!addTransExtraList.isEmpty()) {
                            if (bizTransExtraService.addByBatch(addTransExtraList) <= 0) {
                                log.warn("Add data failed:trans_extra.calculated:[walletNo={},calInsert.size={}]", walletNo, addTransExtraList.size());

                                // TODO: 2020/11/3 Send warning message
                            }

                            if (BigDecimal.ZERO.compareTo(lastTransExtra.getBalanceAvailable()) > 0
                                    || BigDecimal.ZERO.compareTo(lastTransExtra.getBalancePending()) > 0
                                    || BigDecimal.ZERO.compareTo(lastTransExtra.getBalanceFrozen()) > 0) {
                                log.warn("Transaction balance warning:[walletNo={},lastTransExtra={}]", walletNo, JsonUtils.toJson(lastTransExtra));

                                // TODO: 2020/11/3 Send warning message
                            }
                        }
                    } catch (Exception e) {
                        log.error("calException:[calculate.walletNo={}]", entries.getKey(), e);

                        // TODO: 2020/11/3 Send warning message
                    }
                    semaphore.release();
                    latch.countDown();
                });
            }
            latch.await();
            threadPoolExecutor.shutdown();
//            transToBeCalculatedList.clear();
//            }
        } catch (Exception e) {
            log.error("calException:Calculate transaction balance", e);

            // TODO: 2020/11/3 Send warning message

            return new WalletCallResult<>(ErrorConstants.SYSTEM_ERROR_CODE, ErrorConstants.SYSTEM_ERROR_DESC);
        }
        return new WalletCallResult<>(String.format("Number of transactions calculated:[calCount=%s,pending.size=%s]",
                calCount, transToBeCalculatedList.size()));
    }

    private FinBizTransExtra creditProcessing(FinBizTrans pendingTrans, FinBizTransExtra baseTransExtra) {
        BigDecimal transAmount = pendingTrans.getAmount(),
                lastBalanceAvailable = baseTransExtra.getBalanceAvailable(),
                lastBalancePending = baseTransExtra.getBalancePending(),
                lastBalanceFrozen = baseTransExtra.getBalanceFrozen();
        switch (FinConstants.TransType.valueOf(pendingTrans.getTransType())) {
            case CREDIT_ONWAY:
                lastBalancePending = lastBalancePending.add(transAmount);
                break;
            case CREDIT_CONFIRM:
                lastBalancePending = lastBalancePending.subtract(transAmount);
                lastBalanceAvailable = lastBalanceAvailable.add(transAmount);
                break;
            case CREDIT_CANCEL:
                lastBalancePending = lastBalancePending.subtract(transAmount);
                break;
            default:
                throw new FinaccountException(ErrorConstants.BUSINESS_ERROR_CODE, String.format("Unknown transType[%s]", pendingTrans.getTransType()));
        }
        baseTransExtra.setBalanceAvailable(lastBalanceAvailable);
        baseTransExtra.setBalancePending(lastBalancePending);
        baseTransExtra.setBalanceFrozen(lastBalanceFrozen);
        return baseTransExtra;
    }

    private FinBizTransExtra debitProcessing(FinBizTrans pendingBo, FinBizTransExtra baseTransExtra) {
        BigDecimal transAmount = pendingBo.getAmount(),
                lastBalanceAvailable = baseTransExtra.getBalanceAvailable(),
                lastBalancePending = baseTransExtra.getBalancePending(),
                lastBalanceFrozen = baseTransExtra.getBalanceFrozen();
        switch (FinConstants.TransType.valueOf(pendingBo.getTransType())) {
            case DEBIT_ONWAY:
                lastBalanceAvailable = lastBalanceAvailable.subtract(transAmount);
                lastBalancePending = lastBalancePending.add(transAmount);
                break;
            case DEBIT_CONFIRM:
                lastBalancePending = lastBalancePending.subtract(transAmount);
                break;
            case DEBIT_CANCEL:
                lastBalanceAvailable = lastBalanceAvailable.add(transAmount);
                lastBalancePending = lastBalancePending.subtract(transAmount);
                break;
            default:
                throw new FinaccountException(ErrorConstants.BUSINESS_ERROR_CODE, String.format("Unknown transType[%s]", pendingBo.getTransType()));
        }
        baseTransExtra.setBalanceAvailable(lastBalanceAvailable);
        baseTransExtra.setBalancePending(lastBalancePending);
        baseTransExtra.setBalanceFrozen(lastBalanceFrozen);
        return baseTransExtra;
    }
}
