package com.izouma.nineth.service;

import com.alibaba.fastjson.JSONObject;
import com.izouma.nineth.annotations.RedisLock;
import com.izouma.nineth.config.Constants;
import com.izouma.nineth.domain.WithdrawApply;
import com.izouma.nineth.dto.PageQuery;
import com.izouma.nineth.dto.UserBankCard;
import com.izouma.nineth.enums.BalanceType;
import com.izouma.nineth.enums.WithdrawStatus;
import com.izouma.nineth.exception.BusinessException;
import com.izouma.nineth.repo.BalanceRecordRepo;
import com.izouma.nineth.repo.UserBalanceRepo;
import com.izouma.nineth.repo.UserBankCardRepo;
import com.izouma.nineth.repo.WithdrawApplyRepo;
import com.izouma.nineth.utils.JpaUtils;
import com.izouma.nineth.utils.SnowflakeIdWorker;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RedissonClient;
import org.springframework.core.env.Environment;
import org.springframework.data.domain.Page;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

@Service
@AllArgsConstructor
@Slf4j
public class WithdrawApplyService {

    private WithdrawApplyRepo  withdrawApplyRepo;
    private UserBalanceService userBalanceService;
    private UserBalanceRepo    userBalanceRepo;
    private BalanceRecordRepo  balanceRecordRepo;
    private SysConfigService   sysConfigService;
    private SandPayService     sandPayService;
    private UserBankCardRepo   userBankCardRepo;
    private SnowflakeIdWorker  snowflakeIdWorker;
    private Environment        env;
    private RedissonClient     redissonClient;

    public Page<WithdrawApply> all(PageQuery pageQuery) {
        return withdrawApplyRepo.findAll(JpaUtils.toSpecification(pageQuery, WithdrawApply.class), JpaUtils.toPageRequest(pageQuery));
    }

    @RedisLock("'withdrawApply'+#userId")
    public WithdrawApply apply(Long userId, BigDecimal amount) {
        if (!sysConfigService.getBoolean("enable_withdraw")) {
            throw new BusinessException("提现功能暂时关闭");
        }
        int limit = sysConfigService.getInt("daily_withdraw_limit");
        if (withdrawApplyRepo.countByUserIdAndCreatedAtBetween(userId,
                LocalDate.now().atStartOfDay(), LocalDate.now().atTime(LocalTime.MAX)) >= limit) {
            throw new BusinessException("每天只能申请提现" + limit + "次");
        }
        userBalanceService.preWithdraw(userId, amount);
        WithdrawApply withdrawApply = WithdrawApply.builder()
                .userId(userId)
                .amount(amount)
                .status(WithdrawStatus.PENDING)
                .build();
        return withdrawApplyRepo.save(withdrawApply);
    }

    @RedisLock("'finishWithdrawApply'+#id")
    public WithdrawApply finishWithdrawApply(Long id, boolean approve, String reason) {
        WithdrawApply apply = withdrawApplyRepo.findById(id).orElseThrow(new BusinessException("提现申请不存在"));
        if (apply.getStatus() != WithdrawStatus.PENDING) {
            throw new BusinessException("提现申请已处理");
        }
        if (approve) {
            UserBankCard bankCard = userBankCardRepo.findByUserId(apply.getUserId())
                    .stream().findFirst()
                    .orElse(null);

            if (bankCard == null) {
                apply.setStatus(WithdrawStatus.FAIL);
                apply.setFinishTime(LocalDateTime.now());
                apply.setReason(Optional.ofNullable(reason).orElse("用户未绑卡"));

                userBalanceService.modifyBalance(apply.getUserId(), apply.getAmount(), BalanceType.DENY,
                        "用户未绑卡", false, null);
            } else {
                BigDecimal chargeAmount;
                String withdrawCharge = sysConfigService.getString("withdraw_charge");
                if (StringUtils.isNotEmpty(withdrawCharge) && Pattern.matches("^(\\d+|\\d+.\\d+),\\d+$", withdrawCharge)) {
                    String[] arr = withdrawCharge.split(",");
                    chargeAmount = apply.getAmount().multiply(new BigDecimal(arr[0]))
                            .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
                    BigDecimal minChargeAmount = new BigDecimal(arr[1]);
                    if (chargeAmount.compareTo(minChargeAmount) < 0) {
                        chargeAmount = minChargeAmount;
                    }
                } else {
                    chargeAmount = BigDecimal.ZERO;
                }
                String withdrawId = snowflakeIdWorker.nextId() + "";
                try {
                    String msg = "";
                    boolean success = false;

                    if (Arrays.asList(env.getActiveProfiles()).contains("prod")) {
                        try {
                            JSONObject res = sandPayService.transfer(withdrawId, bankCard.getRealName(), bankCard.getBankNo(),
                                    apply.getAmount().subtract(chargeAmount));
                            if ("0000".equals(res.getString("respCode"))) {
                                success = true;
                            } else {
                                msg = res.getString("respDesc");
                            }
                        } catch (Exception e) {
                            msg = e.getMessage();
                        }
                        if (!success) {
                            throw new BusinessException(msg);
                        }

                        apply.setStatus(WithdrawStatus.SUCCESS);
                        apply.setFinishTime(LocalDateTime.now());
                        apply.setWithdrawId(withdrawId);
                        apply.setChannel(Constants.PayChannel.SAND);
                    } else {
                        if (Math.random() > 0.5) {
                            success = true;
                            apply.setStatus(WithdrawStatus.SUCCESS);
                            apply.setFinishTime(LocalDateTime.now());
                            apply.setWithdrawId(withdrawId);
                            apply.setChannel(Constants.PayChannel.SAND);
                        } else {
                            throw new BusinessException("测试服随机失败");
                        }
                    }
                } catch (Exception e) {
                    apply.setStatus(WithdrawStatus.FAIL);
                    apply.setFinishTime(LocalDateTime.now());
                    apply.setReason(e.getMessage());

                    boolean lock = e.getMessage() == null || !e.getMessage().contains("系统繁忙");
                    userBalanceService.modifyBalance(apply.getUserId(), apply.getAmount(), BalanceType.RETURN,
                            e.getMessage(), lock, withdrawId);
                }
            }

        } else {
            apply.setStatus(WithdrawStatus.FAIL);
            apply.setFinishTime(LocalDateTime.now());
            apply.setReason(Optional.ofNullable(reason).orElse("审核不通过"));

            userBalanceService.modifyBalance(apply.getUserId(), apply.getAmount(), BalanceType.DENY,
                    apply.getReason(), false, null);
        }
        return withdrawApplyRepo.save(apply);
    }

    @Async
    @RedisLock(value = "'approveAll'", expire = 1, unit = TimeUnit.HOURS)
    public void approveAllAsync() throws ExecutionException, InterruptedException {
        approveAll();
    }

    @RedisLock(value = "'approveAll'", expire = 1, unit = TimeUnit.HOURS)
    public void approveAll() throws ExecutionException, InterruptedException {
        new ForkJoinPool(5).submit(() -> {
            withdrawApplyRepo.findByCreatedAtBeforeAndStatus(LocalDate.now().atStartOfDay(),
                            WithdrawStatus.PENDING)
                    .parallelStream().forEach(withdrawApply -> {
                        try {
                            finishWithdrawApply(withdrawApply.getId(), true, null);
                        } catch (Exception ignored) {
                        }
                    });
        }).get();
    }

    @Async
    public void fixCharge() throws ExecutionException, InterruptedException {
        new ForkJoinPool(5).submit(() -> {
            withdrawApplyRepo.findByStatusAndAmountLessThanEqual(WithdrawStatus.SUCCESS, new BigDecimal("1000"))
                    .parallelStream().forEach(apply -> {
                        try {
                            UserBankCard bankCard = userBankCardRepo.findByUserId(apply.getUserId())
                                    .stream().findFirst()
                                    .orElseThrow(new BusinessException("未绑卡"));
                            JSONObject res = sandPayService.transfer(snowflakeIdWorker.nextId() + "", bankCard.getRealName(),
                                    bankCard.getBankNo(), new BigDecimal("6"));
                        } catch (Exception e) {
                            log.error("fixCharge", e);
                        }
                    });
        }).get();

    }

    @Async
    @RedisLock(value = "'applyAll'", expire = 1, unit = TimeUnit.HOURS)
    public void applyAll() throws ExecutionException, InterruptedException {
        new ForkJoinPool(5).submit(() -> {
            userBalanceRepo.findAll().parallelStream().forEach(userBalance -> {
                LocalDateTime time = LocalDateTime.now().minusDays(14);
                if (userBalance.getCreatedAt() != null && userBalance.getCreatedAt().isBefore(time)
                        && userBalance.getBalance().compareTo(new BigDecimal("100")) >= 0
                        && withdrawApplyRepo.countByUserIdAndCreatedAtAfter(userBalance.getUserId(), time) <= 0) {
                    log.info("apply withdraw for user {}", userBalance.getUserId());
                    try {
                        apply(userBalance.getUserId(), userBalance.getBalance());
                    } catch (Exception e) {
                        log.error("apply withdraw for user {}", userBalance.getUserId(), e);
                    }
                }
            });
        }).get();
    }
}
