package com.red.circle.wallet.infra.database.cache.service.impl;

import com.fasterxml.jackson.core.type.TypeReference;
import com.red.circle.common.business.core.date.AsiaShanghaiDateUtils;
import com.red.circle.common.business.core.enums.ReceiptType;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.component.redis.service.ZsetTypedTuple;
import com.red.circle.framework.mybatis.constant.PageConstant;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.DateUtils;
import com.red.circle.tool.core.json.JacksonUtils;
import com.red.circle.tool.core.parse.DataTypeUtils;
import com.red.circle.tool.core.text.StringUtils;
import com.red.circle.tool.core.tuple.PennyAmount;
import com.red.circle.wallet.infra.database.cache.key.WalletGoldKey;
import com.red.circle.wallet.infra.database.cache.service.WalletGoldCacheService;
import com.red.circle.wallet.infra.database.rds.entity.WalletExceptionLog;
import com.red.circle.wallet.infra.database.rds.entity.WalletGoldAssetRecord;
import com.red.circle.wallet.infra.database.rds.entity.WalletGoldCount;
import com.red.circle.wallet.infra.database.rds.service.WalletGoldAssetRecordService;
import com.red.circle.wallet.infra.database.rds.service.WalletGoldBalanceService;
import com.red.circle.wallet.infra.database.rds.service.WalletGoldCountService;
import com.red.circle.wallet.infra.tool.WalletGoldCountTool;
import com.red.circle.wallet.inner.model.enums.GoldOrigin;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

/**
 * 金币钱包缓存 实现.
 *
 * @author pengliang on 2023/7/2
 */
@Slf4j
@Service
public class WalletGoldCacheServiceImpl implements WalletGoldCacheService {

  private final Resource scriptLocation;
  private final RedisService redisService;
  private final WalletGoldCountTool walletGoldCountTool;
  private final WalletGoldCountService walletGoldCountService;
  private final WalletGoldBalanceService walletGoldBalanceService;
  private final WalletGoldAssetRecordService walletGoldAssetRecordService;

  public WalletGoldCacheServiceImpl(RedisService redisService,
      WalletGoldCountService walletGoldCountService,
      WalletGoldCountTool walletGoldCountTool,
      WalletGoldBalanceService walletGoldBalanceService,
      WalletGoldAssetRecordService walletGoldAssetRecordService) {
    this.redisService = redisService;
    this.walletGoldCountService = walletGoldCountService;
    this.walletGoldCountTool = walletGoldCountTool;
    this.walletGoldBalanceService = walletGoldBalanceService;
    this.walletGoldAssetRecordService = walletGoldAssetRecordService;
    this.scriptLocation = new ClassPathResource("wallet_gold.lua");
  }

  @Override
  public Long getBalance(Long userId) {

    String balance = redisService.getString(WalletGoldKey.BALANCE.getKey(userId));
    if (StringUtils.isBlank(balance)) {
      Long dbBalance = Optional.ofNullable(walletGoldBalanceService.getBalance(userId)).orElse(0L);

      // 余额出现负数
      if (dbBalance < 0) {
        return 0L;
      }

      setBalance(userId, dbBalance);
      return dbBalance;
    }
    return Optional.ofNullable(DataTypeUtils.toLong(balance))
        .map(amount -> {
          if (amount < 0) {
            log.error("缓存余额异常: {},{}", userId, amount);
            return 0L;
          }
          return amount;
        })
        .orElse(0L);
  }

  @Override
  public void setBalance(Long userId, Long balance) {
    redisService.setString(WalletGoldKey.BALANCE.getKey(userId), balance < 0 ? 0L : balance);
  }

  @Override
  public void removeBalance(Long userId) {
    redisService.delete(WalletGoldKey.BALANCE.getKey(userId));
  }

  @Override
  public Long changeBalance(ReceiptType type, Long userId, Long amount) {
    return redisService.execute(scriptLocation,
        Long.class,
        List.of(WalletGoldKey.BALANCE.getKey(userId)),
        type.getType(), amount
    );
  }

  @Override
  public void pushErrorLog(WalletExceptionLog log) {
    redisService.listPush(WalletGoldKey.ERROR_LOG.getKey(), JacksonUtils.toJson(log));
  }

  @Override
  public List<WalletExceptionLog> popErrorLog(long size) {
    List<Object> errorLogs = redisService.listPop(WalletGoldKey.ERROR_LOG.getKey(), size);
    if (CollectionUtils.isEmpty(errorLogs)) {
      return CollectionUtils.newArrayList();
    }

    return errorLogs.stream()
        .filter(Objects::nonNull)
        .map(DataTypeUtils::toString)
        .map(obj -> JacksonUtils.readValue(obj, WalletExceptionLog.class))
        .toList();
  }

  @Override
  public Map<Long, Long> mapUserBalance(List<Long> userIds) {

    if (CollectionUtils.isEmpty(userIds)) {
      return CollectionUtils.newHashMap();
    }

    List<Long> qryUserIds = userIds.stream().distinct().toList();

    List<Long> values = redisService.multiGet(qryUserIds.stream().map(
        WalletGoldKey.BALANCE::getKey).toList(), DataTypeUtils::toLong);

    if (!Objects.equals(qryUserIds.size(), values.size())) {
      throw new IllegalArgumentException(
          String.format("Query balance error: Data not match:(%s,%s).",
              qryUserIds.size(), values.size()));
    }

    Map<Long, Long> res = CollectionUtils.newHashMap();
    int userIdSize = qryUserIds.size();
    for (int index = 0; index < userIdSize; index++) {
      Long balance = Optional.ofNullable(values.get(index))
          .orElse(0L);
      res.put(qryUserIds.get(index), PennyAmount.ofPenny(balance).dollarAmountLongValue());
    }
    return res;
  }

  @Override
  public void pushEvent(Long id, String eventId) {
    long score = DateUtils.now().getTime();

    List<ZsetTypedTuple<Object>> values = CollectionUtils.newArrayList();
    if (Objects.nonNull(id)) {
      values.add(ZsetTypedTuple.of(id, (double) score));
    }
    if (StringUtils.isNotBlank(eventId)) {
      values.add(ZsetTypedTuple.of(eventId, (double) score));
    }

    if (CollectionUtils.isEmpty(values)) {
      log.warn("param is error.");
      return;
    }

    redisService.zsetAdd(WalletGoldKey.EVENT.getKey(), values);
  }

  @Override
  public void removeEvent(Long id, String eventId) {
    redisService.zsetRemove(WalletGoldKey.EVENT.getKey(), Objects.toString(id), eventId);
  }

  @Override
  public void removeEvent(int hour) {
    redisService.zsetRemoveByScore(WalletGoldKey.EVENT.getKey(),
        DateUtils.nowMinusDays(30).getTime(),
        DateUtils.nowMinusHours(hour).getTime());
  }

  @Override
  public boolean existsId(Long id) {
    return redisService.zsetGetScore(WalletGoldKey.EVENT.getKey(), Objects.toString(id)) > 0;
  }

  @Override
  public boolean existsEventId(String eventId) {
    return redisService.zsetGetScore(WalletGoldKey.EVENT.getKey(), eventId) > 0;
  }

  @Override
  public void countConsumeDaily(String sysOrigin, String eventType, Integer type,
      double quantity) {
    String key = WalletGoldKey.COUNT_CONSUME.getKey(sysOrigin,
        AsiaShanghaiDateUtils.nowDateToInt());
    redisService.zsetIncrementScore(key, type + "#" + eventType, quantity);
    redisService.expire(key, 3, TimeUnit.DAYS);
  }

  @Override
  public List<WalletGoldCount> getCacheCountConsumeAll(String sysOrigin, Integer groupNum) {
    Set<ZsetTypedTuple<String>> typedTupleSet = redisService.zsetGetWithScores(
        WalletGoldKey.COUNT_CONSUME.getKey(sysOrigin, groupNum), 0, -1,
        val -> ZsetTypedTuple.of(Objects.toString(val.getValue()), val.getScore()));

    if (CollectionUtils.isEmpty(typedTupleSet)) {
      return CollectionUtils.newArrayList();
    }

    List<WalletGoldCount> walletGoldCounts = typedTupleSet.stream().map(typedTuple -> {
      String[] values = typedTuple.getValue().split("#");
      String eventType = values[1];
      WalletGoldCount walletGoldCount = new WalletGoldCount()
          .setType(DataTypeUtils.toInteger(values[0]))
          .setSysOrigin(sysOrigin)
          .setGroupNum(groupNum)
          .setEventType(eventType)
          .setEventDesc(GoldOrigin.matchSpecialDescByEventType(eventType, getEventDesc(eventType)))
          .setAmount(DataTypeUtils.toLong(typedTuple.getScore()));
      walletGoldCount.setCreateTime(DateUtils.now());
      walletGoldCount.setUpdateTime(DateUtils.now());
      return walletGoldCount;
    }).toList();
    return walletGoldCountTool.peekPercentage(walletGoldCounts);
  }

  private String getEventDesc(String eventType) {
    return Optional.ofNullable(GoldOrigin.toEnum(eventType))
        .map(GoldOrigin::getDesc)
        .orElse(eventType);
  }

  @Override
  public List<WalletGoldCount> listCountConsume(String sysOrigin, Integer groupNum) {

    List<WalletGoldCount> walletGoldCounts = listWalletGoldCounts(sysOrigin, groupNum);

    if (CollectionUtils.isEmpty(walletGoldCounts)) {
      return walletGoldCounts;
    }

    return walletGoldCounts.stream()
        .sorted(Comparator.comparing(WalletGoldCount::getAmount, Comparator.reverseOrder()))
        .toList();
  }

  @Override
  public List<WalletGoldAssetRecord> listFirstLatestGoldAssetRecord(String sysOrigin, Long userId,
      Integer type, Integer size) {

    String key = WalletGoldKey.FIRST_WATER.getKey(sysOrigin, userId);
    List<WalletGoldAssetRecord> cacheRecord = redisService.getStringToObject(
        key, new TypeReference<>() {
        });

    if (CollectionUtils.isNotEmpty(cacheRecord)) {
      return cacheRecord;
    }

    List<WalletGoldAssetRecord> walletGoldAssetRecords = listFirstAssetRecordRunningWater(sysOrigin,
        userId, type, size);

    redisService.setString(key,
        Objects.isNull(walletGoldAssetRecords)
            ? CollectionUtils.newArrayList()
            : walletGoldAssetRecords);

    return walletGoldAssetRecords;
  }

  @Override
  public void removeFirstLatestGoldAssetRecord(String sysOrigin, Long userId) {
    redisService.delete(WalletGoldKey.FIRST_WATER.getKey(sysOrigin, userId));
  }

  private List<WalletGoldCount> listWalletGoldCounts(String sysOrigin, Integer groupNum) {
    List<WalletGoldCount> walletGoldCounts = getCacheCountConsumeAll(sysOrigin, groupNum);

    if (CollectionUtils.isNotEmpty(walletGoldCounts)) {
      return walletGoldCounts.stream().peek(count -> {

      }).toList();
    }

    return walletGoldCountService.list(sysOrigin, groupNum);
  }

  /**
   * 第一页面，最新资产.
   */
  private List<WalletGoldAssetRecord> listFirstAssetRecordRunningWater(
      String sysOrigin, Long userId, Integer type, Integer size) {
    // 本月
    List<WalletGoldAssetRecord> waters = walletGoldAssetRecordService.listThisMonth(
        sysOrigin, userId, type, null, size);

    if (CollectionUtils.isNotEmpty(waters)) {
      return waters;
    }

    // 上个月近7天
    return walletGoldAssetRecordService.listLastMonthEnd(sysOrigin,
        userId, PageConstant.DEFAULT_LIMIT_SIZE, 7);
  }

}
