package com.red.circle.wallet.infra.database.mongo.service.bank.impl;

import com.mongodb.client.model.Filters;
import com.mongodb.client.result.UpdateResult;
import com.red.circle.component.mongodb.config.MongoPrimaryService;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.tuple.PennyAmount;
import com.red.circle.wallet.infra.database.mongo.entity.bank.UserBankBalance;
import com.red.circle.wallet.infra.database.mongo.service.bank.UserBankBalanceService;
import com.red.circle.wallet.inner.model.cmd.UserBankBalanceQryCmd;
import com.red.circle.wallet.inner.model.dto.BankBalanceDTO;
import com.red.circle.wallet.inner.model.dto.UserBankBalanceDTO;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.bson.Document;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;

/**
 * 用户银行卡余额 实现.
 *
 * @author pengliang on 2023/2/2
 */
@Service
@RequiredArgsConstructor
public class UserBankBalanceServiceImpl implements UserBankBalanceService {

  private final MongoTemplate mongoTemplate;
  private final MongoPrimaryService mongoPrimaryService;

  @Override
  public boolean incr(Long userId, String sysOrigin, PennyAmount amount) {

    if (existsPrimary(userId)) {
      UpdateResult updateResult = mongoTemplate.updateFirst(
          Query.query(Criteria.where("id").is(userId))
          , new Update()
              .inc("earnPoints", amount.getPennyAmount())
              .inc("balance", amount.getPennyAmount())
              .set("updateTime", TimestampUtils.now())
          , UserBankBalance.class);
      return updateResult.getModifiedCount() > 0;
    }

    mongoTemplate.save(new UserBankBalance()
        .setId(userId)
        .setSysOrigin(sysOrigin)
        .setEarnPoints(amount.getPennyAmount())
        .setConsumptionPoints(0L)
        .setBalance(amount.getPennyAmount())
        .setCreateTime(TimestampUtils.now())
        .setUpdateTime(TimestampUtils.now())
    );
    return true;
  }

  @Override
  public BankBalanceDTO decr(Long userId, PennyAmount amount) {
    Query query = Query.query(Criteria.where("id").is(userId));
    FindAndModifyOptions options = new FindAndModifyOptions();
    options.returnNew(true);
    UserBankBalance bankBalance = mongoTemplate.findAndModify(
        query
        , new Update()
            .inc("consumptionPoints", amount.getPennyAmount())
            .inc("balance", -amount.getPennyAmount())
            .set("updateTime", TimestampUtils.now())
        , options
        , UserBankBalance.class);
    return Objects.isNull(bankBalance) ? null : BankBalanceDTO.of(bankBalance.getBalance());
  }

  @Override
  public BankBalanceDTO getBalance(Long userId) {
    return BankBalanceDTO.of(mongoPrimaryService.find(UserBankBalance.class,
        Filters.eq("_id", userId),
        documents -> CollectionUtils.isEmpty(documents) ? 0L
            : getLongValue(documents.get(0), "balance")));
  }

  static Long getLongValue(Document document, String key) {
    Object value = document.get(key);
    if (Objects.isNull(value)) {
      return 0L;
    }
    if (value instanceof Number) {
      return ((Number) value).longValue();
    }
    return Long.valueOf(value.toString());
  }

  @Override
  public Map<Long, BankBalanceDTO> mapBalance(Collection<Long> userIds) {
    List<UserBankBalance> userBankBalances = mongoTemplate.find(
        Query.query(Criteria.where("id").in(userIds)),
        UserBankBalance.class);
    if (CollectionUtils.isEmpty(userBankBalances)) {
      return CollectionUtils.newHashMap();
    }
    return userBankBalances.stream()
        .collect(Collectors.toMap(UserBankBalance::getId,
            balance -> BankBalanceDTO.of(balance.getBalance())));
  }

  @Override
  public List<UserBankBalanceDTO> listBalance(UserBankBalanceQryCmd query) {

    Criteria criteria = Criteria.where("sysOrigin").is(query.getSysOrigin());

    if (Objects.nonNull(query.getUserId())) {
      if (Objects.nonNull(query.getLastId())) {
        query.setLastId(null);
        criteria.and("id").lt(query.getLastId());
      } else {
        query.setLastId(null);
        criteria.and("id").is(query.getUserId());
      }
    }

    if (Objects.nonNull(query.getLastId())) {
      criteria.and("id").lt(query.getLastId());
    }

    return mongoTemplate.find(Query.query(criteria)
                .with(Sort.by(Sort.Order.desc("id")))
                .limit(query.getLimit()),
            UserBankBalance.class).stream()
        .map(balance -> new UserBankBalanceDTO()
            .setBalance(BankBalanceDTO.of(
                balance.getBalance()).getAccurateBalance())
            .setSysOrigin(balance.getSysOrigin())
            .setId(balance.getId())
            .setEarnPoints(BankBalanceDTO.of(balance.getEarnPoints()).getAccurateBalance())
            .setConsumptionPoints(
                BankBalanceDTO.of(balance.getConsumptionPoints()).getAccurateBalance())
            .setCreateTime(balance.getCreateTime())
            .setUpdateTime(balance.getUpdateTime())
        )
        .toList();

  }

  @Override
  public List<UserBankBalance> listAll() {
    return mongoTemplate.find(Query.query(new Criteria()),
        UserBankBalance.class);
  }

  @Override
  public boolean existsPrimary(Long userId) {
    return mongoPrimaryService.exists(UserBankBalance.class, Filters.eq("_id", userId));
  }

}
