package com.webchat.service;

import com.webchat.common.bean.APIPageResponseBean;
import com.webchat.common.constants.WebConstant;
import com.webchat.common.enums.RedisKeyEnum;
import com.webchat.common.enums.WalletTransEventEnum;
import com.webchat.common.enums.WalletTransTypeEnum;
import com.webchat.common.exception.BusinessException;
import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
import com.webchat.domain.vo.response.UserWalletDetailResponseVO;
import com.webchat.repository.dao.IUserWalletDAO;
import com.webchat.repository.entity.UserWalletEntity;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author 程序员七七， https://www.coderutil.com网站作者
 * @date 2024/11/9 03:51
 */
@Slf4j
@Service
public class UserWalletService {

    @Autowired
    private IUserWalletDAO userWalletDAO;

    @Autowired
    private RedisService redisService;

    @Autowired
    private UserService userService;

    /**
     * 账户金额增减
     *
     * 注意：对有需要双向进行的操作，上游自己实现，注意保证事务
     *
     * @param event
     * @param userId
     * @param targetUserId
     * @param money
     * @return
     */
    @Transactional
    public BigDecimal doTrans(WalletTransEventEnum event,
                           String userId,
                           String targetUserId,
                           BigDecimal money) {
        // 1. 如果是支出类型的交易需要先校验余额是否充足
        if (WalletTransTypeEnum.EXPENSES == event.getTransType()) {
            BigDecimal balance = this.getUserWalletBalanceFromCache(userId);
            Assert.isTrue(balance.compareTo(money) >= 0 , "钱包余额不足！");
        }
        // 2. 钱包数据流转, 这里如果失败，会回滚操作不会产生脏数据
        this.walletMoneyTransToDatabase(event, userId, targetUserId, money);
        // 3. 刷新用户钱包最新余额缓存
        return this.refreshUserWalletBalanceCache(userId, money.multiply(BigDecimal.valueOf(event.getTransType().getTransType())));
    }

    /**
     * 查询钱包余额
     *
     * @param userId
     * @return
     */
    public BigDecimal getUserWalletBalanceFromCache(String userId) {
        String key = RedisKeyEnum.USER_WALLET_BALANCE_CACHE.getKey(userId);
        String cache = redisService.get(key);
        if (StringUtils.isNotBlank(cache)) {
            return new BigDecimal(cache);
        }
        return this.refreshUserWalletBalanceCache(userId);
    }

    /**
     * 刷新钱包余额缓存，money为整数则表情收入，负数为支出
     *
     * @param userId
     * @param money
     * @return
     */
    public BigDecimal refreshUserWalletBalanceCache(String userId, BigDecimal money) {
        // ========== 下方redis操作非原子性的，需要加锁保证结算的数据一致性 ==========
        String lockKey = RedisKeyEnum.REFRESH_USER_WALLET_BALANCE_CACHE_LOCK.getKey(userId);
        boolean lock = redisService.installLockForMS(lockKey, WebConstant.CACHE_NONE, 1000L);
        if (!lock) {
            // 锁资源抢占失败，直接报错
            throw new BusinessException("结算中，稍后重试");
        }
        // ========== 抢占到锁资源 ==========
        BigDecimal balance = null;
        try {
            String key = RedisKeyEnum.USER_WALLET_BALANCE_CACHE.getKey(userId);
            String cache = redisService.get(key);
            if (StringUtils.isBlank(cache)) {
                // 如果缓存失效了，这里不用刷新，等后续查询余额首次读库会完整缓存初始化
                // 如果在trans中直接读库，反而会造成脏数据：例如事务还没提交，这里并没有拿到最新真实的余额
                return balance;
            }
            balance = new BigDecimal(cache).add(money);
            redisService.set(key, balance.toString(), RedisKeyEnum.USER_WALLET_BALANCE_CACHE.getExpireTime());
        } catch (Exception e) {
            throw e;
        } finally {
            // ========== 释放分布式锁 ==========
            redisService.releaseDistributedLock(lockKey, WebConstant.CACHE_NONE);
        }
        return balance;
    }

    /**
     * 用户钱包余额redis缓存回滚
     *
     * 注意：这里的回滚的money是增加还是扣除需要上游调用方自己把控：比如回滚是增加操作则money为整数，反之为负数（扣除）
     *
     * @param userId
     * @param money
     */
    public void rollbackUserWalletBalanceCache(String userId, BigDecimal money) {
        // 这里bigdecimal redis不支持原子增减，要么使用lua脚本保证原子性操作，要么加锁保证
        // 分布式锁保证操作原子性
        String lockKey = RedisKeyEnum.REFRESH_USER_WALLET_BALANCE_CACHE_LOCK.getKey(userId);
        boolean lock = redisService.installLockForMS(lockKey, WebConstant.CACHE_NONE, 1000L);
        if (!lock) {
            // 钱包余额更新锁抢占失败，直接抛异常
            throw new BusinessException("结算中，稍后重试！");
        }
        try {
            String key = RedisKeyEnum.USER_WALLET_BALANCE_CACHE.getKey(userId);
            String cache = redisService.get(key);
            if (StringUtils.isNotBlank(cache)) {
                // 如果缓存已经失效，则不用回滚，下次查询走数据库重新初始化缓存即可，保证数据库与redis数据一致性
                return;
            }
            // === 回滚redis钱包余额
            // 取钱包缓存余额
            BigDecimal wallet = new BigDecimal(cache);
            wallet = wallet.add(money);
            redisService.set(key, wallet.toString(), RedisKeyEnum.USER_WALLET_BALANCE_CACHE.getExpireTime());
        } catch (Exception e) {
            log.info(" 用户钱包余额redis缓存回滚失败 ======> userId:{}, money:{}", userId, money.toString(), e);
        } finally {
            /**
             * 回滚完成，不论回滚成功还是失败，这里都需要主动释放锁
             */
            redisService.releaseDistributedLock(lockKey, WebConstant.CACHE_NONE);
        }


    }

    /**
     * 刷新钱包余额缓存
     *
     * @param userId
     * @return
     */
    private BigDecimal refreshUserWalletBalanceCache(String userId) {
        BigDecimal balance = userWalletDAO.findBalanceByUserId(userId);
        balance = balance == null ? new BigDecimal("0") : balance;
        String key = RedisKeyEnum.USER_WALLET_BALANCE_CACHE.getKey(userId);
        redisService.set(key, balance.toString(), RedisKeyEnum.USER_WALLET_BALANCE_CACHE.getExpireTime());
        return balance;
    }

    /**
     * 钱包明细流转入库
     *
     * @param event
     * @param userId
     * @param targetUserId
     * @param money
     */
    private void walletMoneyTransToDatabase(WalletTransEventEnum event,
                                            String userId,
                                            String targetUserId,
                                            BigDecimal money) {
        UserWalletEntity userWallet = new UserWalletEntity();
        userWallet.setTransEvent(event.getTransEvent());
        userWallet.setTransType(event.getTransType().getTransType());
        userWallet.setUserId(userId);
        userWallet.setTargetUserId(targetUserId);
        userWallet.setMoney(money);
        userWallet.setTransDate(new Date());
        userWalletDAO.save(userWallet);
    }

    /**
     * 分页查询钱包交易明细
     *
     * @param pageNo
     * @param pageSize
     * @return
     */
    public APIPageResponseBean<List<UserWalletDetailResponseVO>> page(int pageNo, int pageSize) {
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Order.desc("id")));
        Page<UserWalletEntity> userWalletEntityPage = userWalletDAO.findAll(pageable);
        List<UserWalletEntity> userWalletEntities = userWalletEntityPage.getContent();
        Set<String> userIdSet = userWalletEntities.stream().map(UserWalletEntity::getUserId).collect(Collectors.toSet());
        Map<String, UserBaseResponseInfoVO> users = userService.batchGetUserInfoFromCache(new ArrayList<>(userIdSet));
        List<UserWalletDetailResponseVO> userWalletDetailList = new ArrayList<>();;
        for (UserWalletEntity userWallet : userWalletEntityPage) {
            UserWalletDetailResponseVO userWalletDetailResponseVO = new UserWalletDetailResponseVO();
            userWalletDetailResponseVO.setUser(users.get(userWallet.getUserId()));
            userWalletDetailResponseVO.setTime(userWallet.getTransDate().getTime());
            userWalletDetailResponseVO.setMoney(userWallet.getMoney().toString());
            userWalletDetailResponseVO.setEvent(WalletTransEventEnum.getEventName(userWallet.getTransEvent()));
            userWalletDetailResponseVO.setType(userWallet.getTransType());
            userWalletDetailList.add(userWalletDetailResponseVO);
        }
        return APIPageResponseBean.success(pageNo, pageSize, userWalletEntityPage.getTotalElements(), userWalletDetailList);
    }

}
