package com.sonic.lion.service.impl;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.sonic.common.exception.BizException;
import com.sonic.common.exception.SysException;
import com.sonic.lion.dao.*;
import com.sonic.lion.domain.entity.*;
import com.sonic.lion.domain.enums.*;
import com.sonic.lion.domain.input.*;
import com.sonic.lion.domain.output.*;
import com.sonic.lion.enums.BizType;
import com.sonic.lion.enums.PayChannel;
import com.sonic.lion.enums.PayGenrtatorCodeType;
import com.sonic.lion.enums.ToastResultCode;
import com.sonic.lion.service.PayCallChannelService;
import com.sonic.lion.service.PayChannelRouterService;
import com.sonic.lion.service.PayChannelService;
import com.sonic.lion.utils.KeyGenerator;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @author: code
 * @date: 2025/05/13
 * @Description:
 * @version: 1.0.0
 */
@Slf4j
@Service
public class PayCallChannelServiceImpl implements PayCallChannelService {

    @Value("${stripe.withdrawFeeRate}")
    private BigDecimal withdrawFeeRate;

    @Autowired
    private PayCallChannelRecordDao payCallChannelRecordDao;

    @Autowired
    private PayChannelBillDao payChannelBillDao;

    @Autowired
    private PayChannelRouterService payChannelRouterService;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private PayTradeDao payTradeDao;

    @Autowired
    private ProcessingChargeDao processingChargeDao;

    @Autowired
    private ProcessingWithdrawDao processingWithdrawDao;


    private static final long CHECK_RECORD_DAY = 30L;

    private static final String PAYOUT_LOCK_PREFIX = "pay:channel:payout:tradeNo:";


    @Override
    public PayCallChannelRecord get(Long id) {
        if (id == null) {
            return null;
        }
        return payCallChannelRecordDao.selectById(id);
    }

    @Override
    public PayCallChannelRecord getByTradeNoAndBizTypeLast(String tradeNo, BizType bizType) {
        if (StringUtils.isBlank(tradeNo) || bizType == null) {
            return null;
        }
        return payCallChannelRecordDao.selectByTradeNoAndBizTypeLast(tradeNo, bizType);
    }

    @Override
    public List<PayCallChannelRecord> waitCheckRecord() {
        LocalDateTime startCreateTime = LocalDateTime.now().minusDays(CHECK_RECORD_DAY);
        return payCallChannelRecordDao.selectWillCheckRecord(startCreateTime);
    }


    public static void main(String[] args) {
        LocalDateTime startCreateTime = LocalDateTime.now().minusDays(CHECK_RECORD_DAY);
        System.out.println(startCreateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }

    /**
     * 防止调用记录被回滚掉，此方法不能加事务
     *
     * @param input
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ChannelPayoutOutput callPayout(@NonNull ChannelPayoutInput input) {
        ChannelPayoutOutput result = ChannelPayoutOutput.builder().build();
        //加锁
        RLock rLock = redissonClient.getLock(PAYOUT_LOCK_PREFIX + input.getTradeNo());
        try {
            if (!rLock.tryLock(0L, 60L, TimeUnit.SECONDS)) {
                result.setStatus(CallChannelStatus.CANCEL);
                log.info("获得锁失败取消 , tradeNo: {}, time: {}", rLock.getName(), System.currentTimeMillis());
                return result;
            }
        } catch (InterruptedException e) {
            result.setStatus(CallChannelStatus.CANCEL);
            log.info("阻塞异常取消 , tradeNo: {}, time: {}", rLock.getName(), System.currentTimeMillis());
            return result;
        }

        log.info("获得锁, tradeNo: {}, time: {}", rLock.getName(), System.currentTimeMillis());
        try {
            PayCallChannelRecord lastRecord = getByTradeNoAndBizTypeLast(input.getTradeNo(), BizType.WITHDRAW);
            //已经调用过一次渠道，不发起请求
            if (lastRecord != null) {
                result.setStatus(CallChannelStatus.CANCEL);
                log.info("重复发起 取消, tradeNo: {}, time: {}", rLock.getName(), System.currentTimeMillis());
                return result;
            }

            PayCallChannelRecord record = PayCallChannelRecord.builder()
                    .tradeNo(input.getTradeNo())
                    .bizType(input.getBizType())
                    .channel(input.getPayChannel())
                    .status(CallChannelStatus.PROCESSING)
                    .amount(input.getAmount())
                    .checkNum(0)
                    .nextCheckTime(LocalDateTime.now().plusMinutes(5))
                    .id(IdWorker.getId())
                    .build();

            this.save(record);

            if(BizType.WITHDRAW.equals(input.getPayTrade().getBizType())){
                //创建渠道调用记录的时候同时创建一条待 处理记录
                ProcessingWithdraw  processingWithdraw   = ProcessingWithdraw.builder()
                        .tradeNo(input.getPayTrade().getTradeNo())
                        .callChannelRecordId(record.getId())
                        .payChannel(input.getPayTrade().getPayChannel())
                        .createTime(LocalDateTime.now())
                        .handCount(0)
                        .nextHandTime(LocalDateTime.now())
                        .build();
                processingWithdrawDao.insert(processingWithdraw);
            }

            input.setSubmitId(record.getId().toString());
            PayChannelService payChannelService = payChannelRouterService.getPayChannelService(input.getPayChannel());
            ChannelPayoutOutput payoutOutput = payChannelService.payout(input);
            log.info("提现调用渠道结果,  {}", payoutOutput);

            //更新状态
            PayCallChannelRecord updater = PayCallChannelRecord.builder()
                    .id(record.getId())
                    .status(payoutOutput.getStatus())
                    .batchId(payoutOutput.getBatchId())
                    .transactionId(payoutOutput.getTransactionId())
                    .result(payoutOutput.getResult())
                    .build();
            payCallChannelRecordDao.updateById(updater);
            return payoutOutput;
        } catch (Exception e) {
            //因为提现重试风险较高，所以捕获异常后只做log记录，留待人工排查，不做重试
            log.error("call payout error", e);
            result.setStatus(CallChannelStatus.CANCEL);
            return result;
        } finally {
            try {
                log.info("释放锁, tradeNo: {}, time: {}", rLock.getName(), System.currentTimeMillis());
                rLock.unlock();
            }catch (Exception e){

            }
        }
    }


    @Override
    public ChannelPaymentOutput callPayment(@NonNull ChannelPaymentInput input) {
        PayTrade payTrade = input.getPayTrade();
        PayCallChannelRecord lastRecord = getByTradeNoAndBizTypeLast(payTrade.getTradeNo(), payTrade.getBizType());
        //该笔支付没有调用第三方的记录,直接调用并记录
        if (lastRecord == null || (lastRecord.getStatus() == CallChannelStatus.INIT && lastRecord.getBatchId() == null)) {
            return callChannelAndRecord(input);
        }
        ChannelPaymentOutput output = new ChannelPaymentOutput();
        output.setSubmitId(lastRecord.getId().toString());

        //有调用记录
        if (lastRecord.getStatus() == CallChannelStatus.SUCC) {
            //调用记录显示已成功支付
            throw new BizException("", "Payment success");
        } else if (lastRecord.getStatus() == CallChannelStatus.PROCESSING && Objects.equals(payTrade.getPayChannel(), lastRecord.getChannel())) {
            output.setBatchId(lastRecord.getBatchId());
            output.setStatus(lastRecord.getStatus());
            output.setPaymentUrl(lastRecord.getPaymentUrl());
            return output;
        } else {
            //调用记录显示未完成
            PayChannelService payChannelService = payChannelRouterService.getPayChannelService(lastRecord.getChannel());

            ChannelCheckPaymentInput checkPaymentInput = ChannelCheckPaymentInput.builder()
                    .batchId(lastRecord.getBatchId())
                    .transactionId(lastRecord.getTransactionId())
                    .tradeNo(payTrade.getTradeNo())
                    .build();
            ChannelCheckOutput checkOutput = payChannelService.checkPayment(checkPaymentInput);
            if (checkOutput.getStatus() == CallChannelStatus.SUCC) {
                //支付渠道返回成功
                updateCheckOutput(lastRecord, checkOutput);
                output.setBatchId(lastRecord.getBatchId());
                output.setStatus(lastRecord.getStatus());
                output.setPaymentUrl(lastRecord.getPaymentUrl());
                return output;
            } else if (checkOutput.getStatus() == CallChannelStatus.FAIL) {
                //支付渠道返回失败
                updateCheckOutput(lastRecord, checkOutput);

                //重新开启一笔支付
                return callChannelAndRecord(input);
            } else {
                //都不是,取消上一笔 由于这里不退款 先不更新状态。
                payChannelService.cancel(lastRecord.getBatchId());
                updateCheckOutput(lastRecord, checkOutput);
                return callChannelAndRecord(input);
            }
        }
    }


    @Override
    public void updateStatus(@NonNull Long id, @NonNull CallChannelStatus status, @NonNull CallChannelStatus expectStatus) {
        UpdateWrapper<PayCallChannelRecord> updateWrapper = new UpdateWrapper<>();
        LambdaUpdateWrapper<PayCallChannelRecord> lambdaUpdateWrapper = updateWrapper.lambda()
                .eq(PayCallChannelRecord::getId, id)
                .eq(PayCallChannelRecord::getStatus, expectStatus);
        PayCallChannelRecord record = PayCallChannelRecord.builder()
                .status(status)
                .build();
        int n = payCallChannelRecordDao.update(record, lambdaUpdateWrapper);
        ToastResultCode.DATA_NOT_EXITS.check(n != 1);
    }

    @Override
    public Long calculateFee(@NonNull Long amount, @NonNull PayChannel payChannel, @NonNull BizType bizType) {
        PayChannelService payChannelService = payChannelRouterService.getPayChannelService(payChannel);
        if (bizType == BizType.CHARGE) {
            return payChannelService.calculatePaymentFee(amount);
        } else if (bizType == BizType.WITHDRAW) {
            return payChannelService.calculateWithdrawFee(amount);
        } else {
            throw new SysException("", "Types of transactions that cannot calculate commissions");
        }
    }

    @Override
    public Long calculatePlatformFee(Long amount, BizType bizType) {
       if (bizType == BizType.WITHDRAW) {
            //提现收20%手续费
            return new BigDecimal(amount).multiply(withdrawFeeRate).longValue();
        } else if (bizType == BizType.CHARGE) {
           return 0L;
       } else {
           //其他任何业务场景下都收10%的手续费
           return new BigDecimal(amount).multiply(new BigDecimal(0.1)).longValue();
        }
    }

    @Override
    public ChannelCreateCustomerOutput callCreateCustomer(@NonNull ChannelCreateCustomerInput input) {
        PayChannelService payChannelService = payChannelRouterService.getPayChannelService(input.getPayChannel());
        return payChannelService.createCustomer(input);
    }

    @Override
    public ChannelCheckOutput checkRecord(PayCallChannelRecord record) {
        //查询支付渠道
        BizType bizType = record.getBizType();
        ChannelCheckOutput checkOutput;
        PayChannelService payChannelService = payChannelRouterService.getPayChannelService(record.getChannel());
        if (bizType == BizType.GAME || bizType == BizType.CHARGE) {
            ChannelCheckPaymentInput input = ChannelCheckPaymentInput.builder()
                    .batchId(record.getBatchId())
                    .transactionId(record.getTransactionId())
                    .tradeNo(record.getTradeNo())
                    .build();
            //从渠道侧 查询充值结果
            checkOutput = payChannelService.checkPayment(input);
            checkOutput.setChannelEvent(TradeEvent.PAYMENT);
        } else if (bizType == BizType.WITHDRAW) {
            //从渠道侧 查询提现结果
            checkOutput = payChannelService.checkPayout(record.getBatchId());
            checkOutput.setChannelEvent(TradeEvent.WITHDRAW);
        } else if (bizType == BizType.REFUND) {
            ChannelCheckRefundInput input = ChannelCheckRefundInput.builder()
                    .batchId(record.getBatchId())
                    .build();
            //从渠道侧 查询退款结果
            checkOutput = payChannelService.checkRefund(input);
            checkOutput.setChannelEvent(TradeEvent.REFUND);
        } else {
            throw new BizException("", "Types of transactions that cannot be checked");
        }
        //更新
        updateCheckOutput(record, checkOutput);
        return checkOutput;
    }


    @Override
    public ChannelBindBankCardOutput callBindBankCard(ChannelBindBankCardInput input) {
        PayChannelService payChannelService = payChannelRouterService.getPayChannelService(input.getPayChannel());
        return payChannelService.bindBankCard(input);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public void save(PayCallChannelRecord payCallChannelRecord) {
        payCallChannelRecordDao.insert(payCallChannelRecord);
    }

    /**
     * 更新渠道调用记录
     * @param record
     * @param checkOutput
     */
    private void updateCheckOutput(PayCallChannelRecord record, ChannelCheckOutput checkOutput) {
        log.info("channel message,tradeNo {} ,transactionId {},channel status {} ", record.getTradeNo(), checkOutput.getTransactionId(), checkOutput.getStatus());
        LocalDateTime now = LocalDateTime.now();
        record.setTransactionId(checkOutput.getTransactionId());
        record.setStatus(checkOutput.getStatus());
        record.setLastCheckTime(now);
        record.setNextCheckTime(now.plusMinutes(5 * (record.getCheckNum() + 1)));
        record.setCheckNum(record.getCheckNum() + 1);
        record.setResult(checkOutput.getResult());

        String payerId = "";
        String payerName = "";
        String payerEmail = "";
        try {
            payerId = checkOutput.getExtendInfo().get("payerId").toString();
            payerName = checkOutput.getExtendInfo().get("payerName").toString();
            payerEmail = checkOutput.getExtendInfo().get("payerEmail").toString();
        } catch (Exception e) {
            //啥也不做
        }

        record.setPayerId(payerId);
        record.setPayerName(payerName);
        record.setPayerEmail(payerEmail);

        //多渠道支付 只显示了一个渠道的 bill 问题修复。
        if(CallChannelStatus.SUCC.equals(checkOutput.getStatus() )){
            payTradeDao.updateChannelByTradeNo(record.getChannel().name(), record.getTradeNo());
        }
        //记录不为最终状态时更新
        LambdaUpdateWrapper<PayCallChannelRecord> updateWrapper = Wrappers.<PayCallChannelRecord>lambdaUpdate()
                .eq(PayCallChannelRecord::getId, record.getId())
                .notIn(PayCallChannelRecord::getStatus, BillStatus.SUCC, BillStatus.FAIL, BillStatus.CANCEL);
        int n = payCallChannelRecordDao.update(record, updateWrapper);

        ToastResultCode.IN_PROCESSING.check(n != 1);
        if (checkOutput.getStatus() == CallChannelStatus.SUCC) {
            //记录渠道账单
            saveChannelBill(record);
        }
    }

    @Override
    public void saveChannelBill(PayCallChannelRecord record) {
        InOrOut inOrOut = null;
        if (record.getBizType() == BizType.GAME || record.getBizType() == BizType.CHARGE) {
            inOrOut = InOrOut.IN;
        } else if (record.getBizType() == BizType.WITHDRAW || record.getBizType() == BizType.REFUND) {
            inOrOut = InOrOut.OUT;
        }

        PayChannelBill payChannelBill = PayChannelBill.builder()
                .channelBillId(KeyGenerator.instance().generatorUniqueKey(PayGenrtatorCodeType.CHANNEL_BILL))
                .accountId(SystemUser.SYSTEM_USER_1.getValue())
                .accountName("")
                .bizType(record.getBizType())
                .bizNum(record.getTradeNo())
                .status(BillStatus.SUCC.getValue())
                .payChannel(record.getChannel())
                .channelName("")
                .channelSn(record.getTransactionId())
                .submitId(record.getId().toString())
                .desAccountNo("")
                .desAccountName("")
                .inOrOut(inOrOut)
                .amount(record.getAmount())
                .build();
        payChannelBillDao.insert(payChannelBill);
    }

    private ChannelPaymentOutput callChannelAndRecord(ChannelPaymentInput input) {
        LocalDateTime callChannelRecordCreateTime = LocalDateTime.now();
        //默认过期时间30分钟
        LocalDateTime expTime = callChannelRecordCreateTime.plusMinutes(30);
        PayCallChannelRecord record = PayCallChannelRecord.builder()
                .tradeNo(input.getPayTrade().getTradeNo())
                .bizType(input.getPayTrade().getBizType())
                .channel(input.getPayTrade().getPayChannel())
                .status(CallChannelStatus.INIT)
                .amount(input.getPayTrade().getOccurAmount())
                .exchangeAmount(input.getPayTrade().getExchangeOccurAmount())
                .exchangeCurrency(input.getPayTrade().getExchangeOccurCurrency())
                .nextCheckTime(callChannelRecordCreateTime)
                .createTime(callChannelRecordCreateTime)
                //根据渠道设置过期时间
                .expTime(expTime)
                .id(IdWorker.getId())
                .build();
        payCallChannelRecordDao.insert(record);

        if(BizType.CHARGE.equals(input.getPayTrade().getBizType())){
            //创建渠道调用记录的时候同时创建一条待 处理记录
            try {
                //默认的下次执行时间为当前时间
                LocalDateTime nextHandTime = callChannelRecordCreateTime;
                ProcessingCharge processingCharge   = ProcessingCharge.builder()
                        .tradeNo(input.getPayTrade().getTradeNo())
                        .callChannelRecordId(record.getId())
                        .payChannel(input.getPayTrade().getPayChannel())
                        .createTime(callChannelRecordCreateTime)
                        //根据渠道来判断设置下次的执行时间
                        .nextHandTime(nextHandTime)
                        .handCount(0)
                        .build();
                processingChargeDao.insert(processingCharge);
            } catch (DuplicateKeyException e) {
                //当触发唯一索引时不做任何处理，以保证单个交易号在不同渠道中永远只会存在一条数据
                ToastResultCode.PARAM_ERROR.check(true, "", "The transaction exists. DO NOT make duplicate payments");
            }
        }


        input.setSubmitId(String.valueOf(record.getId()));
        PayChannelService payChannelService = payChannelRouterService.getPayChannelService(input.getPayTrade().getPayChannel());
        ChannelPaymentOutput output = payChannelService.payment(input);
        //将submitId返回给前端
        output.setSubmitId(input.getSubmitId());

        String payerId = "";
        String payerName = "";
        String payerEmail = "";
        if (output.getExtendInfo() != null) {
            payerId = output.getExtendInfo().get("payerId") == null ? "" : output.getExtendInfo().get("payerId").toString();
            payerName = output.getExtendInfo().get("payerName") == null ? "" : output.getExtendInfo().get("payerName").toString();
            payerEmail = output.getExtendInfo().get("payerEmail") == null ? "" : output.getExtendInfo().get("payerEmail").toString();
        }
        PayCallChannelRecord updater = PayCallChannelRecord.builder()
                .id(record.getId())
                .status(output.getStatus())
                .batchId(output.getBatchId())
                .transactionId(output.getTransactionId())
                .paymentUrl(output.getPaymentUrl())
                .payerId(payerId)
                .payerEmail(payerEmail)
                .payerName(payerName)
                .result(output.getResult())
                .build();
        payCallChannelRecordDao.updateById(updater);
        return output;
    }
}
