package com.red.circle.console.app.service.gm;

import com.red.circle.console.app.dto.clienobject.gm.GmVipGiftCO;
import com.red.circle.console.app.dto.cmd.gm.GmVipGiftCmd;
import com.red.circle.console.app.dto.cmd.gm.GmVipGiftPageQryCmd;
import com.red.circle.console.infra.database.rds.entity.admin.User;
import com.red.circle.console.infra.database.rds.entity.app.vip.GmVipGiftRecord;
import com.red.circle.console.infra.database.rds.entity.app.vip.RunningWaterUserProps;
import com.red.circle.console.infra.database.rds.entity.app.vip.UserPropsBackpack;
import com.red.circle.console.infra.database.rds.entity.app.vip.UserVipState;
import com.red.circle.console.infra.database.rds.entity.app.vip.VipLevelConfig;
import com.red.circle.console.infra.database.rds.entity.app.vip.VipOrderRecord;
import com.red.circle.console.infra.database.rds.service.admin.UserService;
import com.red.circle.console.infra.database.rds.service.app.vip.GmVipGiftRecordService;
import com.red.circle.console.infra.database.rds.service.app.vip.RunningWaterUserPropsService;
import com.red.circle.console.infra.database.rds.service.app.vip.UserPropsBackpackService;
import com.red.circle.console.infra.database.rds.service.app.vip.UserVipStateService;
import com.red.circle.console.infra.database.rds.service.app.vip.VipLevelConfigService;
import com.red.circle.console.infra.database.rds.service.app.vip.VipOrderRecordService;
import com.red.circle.console.inner.error.ConsoleErrorCode;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.ResponseErrorCode;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.mybatis.constant.PageConstant;
import com.red.circle.other.inner.endpoint.material.props.PropsBackpackClient;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * GM VIP赠送.
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class GmVipGiftServiceImpl implements GmVipGiftService {

  private static final int VIP_MIN_LEVEL = 1;
  private static final int VIP_MAX_LEVEL = 5;
  private static final int DEFAULT_DURATION_DAYS = 30;
  private static final String STATUS_ACTIVE = "ACTIVE";
  private static final String STATUS_SUCCESS = "SUCCESS";
  private static final String ORDER_TYPE_GM_GIFT = "GM_GIFT";
  private static final String WATER_ORIGIN = "VIP_PURCHASE";
  private static final String WATER_ORIGIN_DESC = "VIP purchase";
  private static final Timestamp MYSQL_DATETIME_MAX = Timestamp.valueOf("2038-01-19 00:00:00");

  private final GmVipGiftRecordService giftRecordService;
  private final VipLevelConfigService vipLevelConfigService;
  private final UserVipStateService userVipStateService;
  private final VipOrderRecordService vipOrderRecordService;
  private final UserPropsBackpackService userPropsBackpackService;
  private final RunningWaterUserPropsService runningWaterUserPropsService;
  private final UserProfileClient userProfileClient;
  private final PropsBackpackClient propsBackpackClient;
  private final UserService userService;

  @Override
  @Transactional(rollbackFor = Exception.class)
  public GmVipGiftCO gift(GmVipGiftCmd cmd) {
    validateGift(cmd);

    Timestamp now = now();
    User applicant = getAdminUser(cmd.getReqUserId());
    UserProfileDTO userProfile = getUserProfile(cmd.getUserId());
    VipLevelConfig config = getVipConfig(userProfile.getOriginSys(), cmd.getVipLevel());
    UserVipState currentState = getUserVipState(userProfile.getOriginSys(), userProfile.getId());
    boolean currentActive = isActive(currentState, now);
    int fromLevel = currentActive ? Objects.requireNonNullElse(currentState.getLevel(), 0) : 0;

    ResponseAssert.isFalse(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
        currentActive && fromLevel > config.getLevel());

    Timestamp startAt = currentActive && Objects.nonNull(currentState.getStartAt())
        ? currentState.getStartAt()
        : now;
    Timestamp expireAt = resolveExpireAt(currentState, config, currentActive, now);
    Long orderId = IdWorkerUtils.getId();
    String eventId = "GM-VIP-GIFT-" + orderId;

    VipOrderRecord order = buildOrder(orderId, eventId, userProfile, fromLevel, config, startAt,
        expireAt, now);
    vipOrderRecordService.save(order);

    upsertUserVipState(currentState, userProfile, config, startAt, expireAt, now);
    giftVipResources(userProfile.getId(), config, now, cmd.getReqUserId());

    GmVipGiftRecord record = buildGiftRecord(cmd, applicant, userProfile, fromLevel, config,
        orderId, eventId, startAt, expireAt, now);
    giftRecordService.save(record);

    removeUserCache(userProfile.getId());
    return toCO(record, userProfile);
  }

  @Override
  public PageResult<GmVipGiftCO> page(GmVipGiftPageQryCmd cmd) {
    PageResult<GmVipGiftRecord> page = giftRecordService.query()
        .eq(Objects.nonNull(cmd.getUserId()), GmVipGiftRecord::getUserId, cmd.getUserId())
        .eq(Objects.nonNull(cmd.getApplicantId()), GmVipGiftRecord::getApplicantId,
            cmd.getApplicantId())
        .eq(Objects.nonNull(cmd.getVipLevel()), GmVipGiftRecord::getVipLevel,
            cmd.getVipLevel())
        .eq(Objects.nonNull(cmd.getStatus()), GmVipGiftRecord::getStatus, cmd.getStatus())
        .ge(Objects.nonNull(cmd.getStartTime()), GmVipGiftRecord::getCreateTime,
            toTimestamp(cmd.getStartTime()))
        .le(Objects.nonNull(cmd.getEndTime()), GmVipGiftRecord::getCreateTime,
            toTimestamp(cmd.getEndTime()))
        .orderByDesc(GmVipGiftRecord::getId)
        .page(cmd.getPageQuery());

    Map<Long, UserProfileDTO> userProfileMap = mapUserProfiles(page.getRecords());
    return page.convert(record -> toCO(record, userProfileMap.get(record.getUserId())));
  }

  private void validateGift(GmVipGiftCmd cmd) {
    ResponseAssert.notNull(ResponseErrorCode.REQUEST_PARAMETER_ERROR, cmd.getReqUserId());
    ResponseAssert.notNull(ResponseErrorCode.REQUEST_PARAMETER_ERROR, cmd.getUserId(),
        cmd.getVipLevel());
    ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
        cmd.getVipLevel() >= VIP_MIN_LEVEL && cmd.getVipLevel() <= VIP_MAX_LEVEL);
  }

  private User getAdminUser(Long userId) {
    User user = userService.getById(Math.toIntExact(userId));
    ResponseAssert.notNull(ConsoleErrorCode.USER_NOT_FOUND, user);
    return user;
  }

  private UserProfileDTO getUserProfile(Long userId) {
    UserProfileDTO userProfile = ResponseAssert.requiredSuccess(userProfileClient.getByUserId(userId));
    ResponseAssert.notNull(ConsoleErrorCode.USER_NOT_FOUND, userProfile);
    ResponseAssert.notBlank(ConsoleErrorCode.USER_INFO_NOT_FOUND, userProfile.getOriginSys());
    return userProfile;
  }

  private VipLevelConfig getVipConfig(String sysOrigin, Integer vipLevel) {
    VipLevelConfig config = vipLevelConfigService.query()
        .eq(VipLevelConfig::getSysOrigin, sysOrigin)
        .eq(VipLevelConfig::getLevel, vipLevel)
        .eq(VipLevelConfig::getEnabled, Boolean.TRUE)
        .last(PageConstant.LIMIT_ONE)
        .getOne();
    ResponseAssert.notNull(ConsoleErrorCode.NOT_FOUND_INFO, config);
    if (Objects.isNull(config.getDurationDays()) || config.getDurationDays() <= 0) {
      config.setDurationDays(DEFAULT_DURATION_DAYS);
    }
    return config;
  }

  private UserVipState getUserVipState(String sysOrigin, Long userId) {
    return userVipStateService.query()
        .eq(UserVipState::getSysOrigin, sysOrigin)
        .eq(UserVipState::getUserId, userId)
        .last(PageConstant.LIMIT_ONE)
        .getOne();
  }

  private boolean isActive(UserVipState state, Timestamp now) {
    return Objects.nonNull(state)
        && STATUS_ACTIVE.equals(state.getStatus())
        && Objects.nonNull(state.getExpireAt())
        && state.getExpireAt().after(now)
        && Objects.nonNull(state.getLevel())
        && state.getLevel() > 0;
  }

  private Timestamp resolveExpireAt(UserVipState currentState, VipLevelConfig config,
      boolean currentActive, Timestamp now) {
    Timestamp plusDuration = plusDays(now, config.getDurationDays());
    if (!currentActive) {
      return plusDuration;
    }
    if (Objects.equals(currentState.getLevel(), config.getLevel())) {
      return plusDays(max(currentState.getExpireAt(), now), config.getDurationDays());
    }
    return max(currentState.getExpireAt(), plusDuration);
  }

  private VipOrderRecord buildOrder(Long orderId, String eventId, UserProfileDTO userProfile,
      Integer fromLevel, VipLevelConfig config, Timestamp startAt, Timestamp expireAt,
      Timestamp now) {
    VipOrderRecord order = new VipOrderRecord()
        .setId(orderId)
        .setEventId(eventId)
        .setSysOrigin(userProfile.getOriginSys())
        .setUserId(userProfile.getId())
        .setOrderType(ORDER_TYPE_GM_GIFT)
        .setFromLevel(fromLevel)
        .setToLevel(config.getLevel())
        .setDurationDays(config.getDurationDays())
        .setOriginalPriceGold(Objects.requireNonNullElse(config.getPriceGold(), 0L))
        .setPaidGold(0L)
        .setStatus(STATUS_SUCCESS)
        .setStartAt(startAt)
        .setExpireAt(expireAt)
        .setErrorMessage("");
    order.setCreateTime(now);
    order.setUpdateTime(now);
    return order;
  }

  private void upsertUserVipState(UserVipState currentState, UserProfileDTO userProfile,
      VipLevelConfig config, Timestamp startAt, Timestamp expireAt, Timestamp now) {
    UserVipState state = Objects.nonNull(currentState)
        ? currentState
        : new UserVipState().setId(IdWorkerUtils.getId());
    state.setSysOrigin(userProfile.getOriginSys());
    state.setUserId(userProfile.getId());
    state.setLevel(config.getLevel());
    state.setLevelCode(config.getLevelCode());
    state.setDisplayName(config.getDisplayName());
    state.setStatus(STATUS_ACTIVE);
    state.setStartAt(startAt);
    state.setExpireAt(expireAt);
    copyResources(state, config);
    state.setUpdateTime(now);
    if (Objects.nonNull(currentState)) {
      userVipStateService.updateSelectiveById(state);
      return;
    }
    state.setCreateTime(now);
    userVipStateService.save(state);
  }

  private void copyResources(UserVipState state, VipLevelConfig config) {
    state.setBadgeResourceId(config.getBadgeResourceId());
    state.setBadgeName(config.getBadgeName());
    state.setBadgeCover(config.getBadgeCover());
    state.setBadgeUrl(config.getBadgeUrl());
    state.setAvatarFrameResourceId(config.getAvatarFrameResourceId());
    state.setAvatarFrameName(config.getAvatarFrameName());
    state.setAvatarFrameCover(config.getAvatarFrameCover());
    state.setAvatarFrameUrl(config.getAvatarFrameUrl());
    state.setEntryEffectResourceId(config.getEntryEffectResourceId());
    state.setEntryEffectName(config.getEntryEffectName());
    state.setEntryEffectCover(config.getEntryEffectCover());
    state.setEntryEffectUrl(config.getEntryEffectUrl());
    state.setChatBubbleResourceId(config.getChatBubbleResourceId());
    state.setChatBubbleName(config.getChatBubbleName());
    state.setChatBubbleCover(config.getChatBubbleCover());
    state.setChatBubbleUrl(config.getChatBubbleUrl());
    state.setFloatPictureResourceId(config.getFloatPictureResourceId());
    state.setFloatPictureName(config.getFloatPictureName());
    state.setFloatPictureCover(config.getFloatPictureCover());
    state.setFloatPictureUrl(config.getFloatPictureUrl());
  }

  private void giftVipResources(Long userId, VipLevelConfig config, Timestamp now, Long opUser) {
    giveProp(userId, config.getBadgeResourceId(), "BADGE", config.getDurationDays(), now, opUser);
    giveProp(userId, config.getAvatarFrameResourceId(), "AVATAR_FRAME", config.getDurationDays(),
        now, opUser);
    giveProp(userId, config.getEntryEffectResourceId(), "RIDE", config.getDurationDays(), now,
        opUser);
    giveProp(userId, config.getChatBubbleResourceId(), "CHAT_BUBBLE", config.getDurationDays(),
        now, opUser);
    giveProp(userId, config.getFloatPictureResourceId(), "FLOAT_PICTURE", config.getDurationDays(),
        now, opUser);
  }

  private void giveProp(Long userId, Long propsId, String type, Integer days, Timestamp now,
      Long opUser) {
    if (Objects.isNull(propsId) || propsId <= 0 || Objects.isNull(days) || days <= 0) {
      return;
    }

    UserPropsBackpack oldBackpack = getUserProp(userId, propsId);
    boolean useProps = resolveUseProps(userId, propsId, type, oldBackpack, now);
    Timestamp expireTime = resolvePropExpireTime(oldBackpack, days, now);

    if (Boolean.TRUE.equals(useProps)) {
      userPropsBackpackService.update()
          .set(UserPropsBackpack::getUseProps, Boolean.FALSE)
          .eq(UserPropsBackpack::getUserId, userId)
          .eq(UserPropsBackpack::getType, type)
          .eq(UserPropsBackpack::getUseProps, Boolean.TRUE)
          .execute();
    }

    if (Objects.nonNull(oldBackpack)) {
      oldBackpack.setType(type);
      oldBackpack.setExpireTime(expireTime);
      oldBackpack.setUseProps(useProps);
      oldBackpack.setAllowGive(Boolean.FALSE);
      oldBackpack.setUpdateTime(now);
      oldBackpack.setUpdateUser(opUser);
      userPropsBackpackService.updateSelectiveById(oldBackpack);
    } else {
      UserPropsBackpack backpack = new UserPropsBackpack()
          .setUserId(userId)
          .setPropsId(propsId)
          .setType(type)
          .setExpireTime(expireTime)
          .setUseProps(useProps)
          .setAllowGive(Boolean.FALSE);
      backpack.setCreateTime(now);
      backpack.setUpdateTime(now);
      backpack.setCreateUser(opUser);
      backpack.setUpdateUser(opUser);
      userPropsBackpackService.save(backpack);
    }

    RunningWaterUserProps water = new RunningWaterUserProps()
        .setUserId(userId)
        .setPropsId(propsId)
        .setPropsCandy(BigDecimal.ZERO)
        .setType(type)
        .setOrigin(WATER_ORIGIN)
        .setOriginDesc(WATER_ORIGIN_DESC)
        .setDays(days);
    water.setCreateTime(now);
    water.setUpdateTime(now);
    water.setCreateUser(opUser);
    water.setUpdateUser(0L);
    runningWaterUserPropsService.save(water);
    notifyPropsGift(userId, type, days, propsId);
  }

  private UserPropsBackpack getUserProp(Long userId, Long propsId) {
    return userPropsBackpackService.query()
        .eq(UserPropsBackpack::getUserId, userId)
        .eq(UserPropsBackpack::getPropsId, propsId)
        .last(PageConstant.LIMIT_ONE)
        .getOne();
  }

  private boolean resolveUseProps(Long userId, Long propsId, String type,
      UserPropsBackpack oldBackpack, Timestamp now) {
    if (Objects.nonNull(oldBackpack)) {
      return Boolean.TRUE.equals(oldBackpack.getUseProps());
    }
    List<UserPropsBackpack> notExpiredSameType = userPropsBackpackService.query()
        .eq(UserPropsBackpack::getUserId, userId)
        .eq(UserPropsBackpack::getType, type)
        .gt(UserPropsBackpack::getExpireTime, now)
        .list();
    return notExpiredSameType.stream()
        .noneMatch(item -> Boolean.TRUE.equals(item.getUseProps())
            && !Objects.equals(item.getPropsId(), propsId));
  }

  private Timestamp resolvePropExpireTime(UserPropsBackpack oldBackpack, Integer days,
      Timestamp now) {
    if (Objects.isNull(oldBackpack) || Objects.isNull(oldBackpack.getExpireTime())
        || oldBackpack.getExpireTime().before(now)) {
      return capExpireTime(plusDays(now, days));
    }
    return capExpireTime(plusDays(oldBackpack.getExpireTime(), days));
  }

  private GmVipGiftRecord buildGiftRecord(GmVipGiftCmd cmd, User applicant,
      UserProfileDTO userProfile, Integer fromLevel, VipLevelConfig config, Long orderId,
      String eventId, Timestamp startAt, Timestamp expireAt, Timestamp now) {
    return touchNew(new GmVipGiftRecord()
        .setId(IdWorkerUtils.getId())
        .setUserId(userProfile.getId())
        .setSysOrigin(userProfile.getOriginSys())
        .setFromLevel(fromLevel)
        .setVipLevel(config.getLevel())
        .setLevelCode(config.getLevelCode())
        .setDisplayName(config.getDisplayName())
        .setDurationDays(config.getDurationDays())
        .setPriceGold(Objects.requireNonNullElse(config.getPriceGold(), 0L))
        .setStartAt(startAt)
        .setExpireAt(expireAt)
        .setOrderId(orderId)
        .setEventId(eventId)
        .setStatus(STATUS_SUCCESS)
        .setApplicantId(applicant.getId().longValue())
        .setApplicantName(resolveAdminName(applicant))
        .setRemarks(cmd.getRemarks())
        .setExecutedTime(now), now);
  }

  private String resolveAdminName(User user) {
    if (Objects.isNull(user)) {
      return null;
    }
    if (Objects.nonNull(user.getNickname()) && !user.getNickname().isBlank()) {
      return user.getNickname();
    }
    return user.getLoginName();
  }

  private Map<Long, UserProfileDTO> mapUserProfiles(Collection<GmVipGiftRecord> records) {
    if (CollectionUtils.isEmpty(records)) {
      return Map.of();
    }
    Set<Long> userIds = records.stream()
        .map(GmVipGiftRecord::getUserId)
        .filter(Objects::nonNull)
        .collect(Collectors.toSet());
    if (CollectionUtils.isEmpty(userIds)) {
      return Map.of();
    }
    return ResponseAssert.requiredSuccess(userProfileClient.mapByUserIds(userIds));
  }

  private GmVipGiftCO toCO(GmVipGiftRecord record, UserProfileDTO userProfile) {
    GmVipGiftCO co = new GmVipGiftCO();
    co.setId(record.getId());
    co.setUserId(record.getUserId());
    co.setSysOrigin(record.getSysOrigin());
    co.setFromLevel(record.getFromLevel());
    co.setFromLevelName(levelName(record.getFromLevel()));
    co.setVipLevel(record.getVipLevel());
    co.setLevelCode(record.getLevelCode());
    co.setDisplayName(record.getDisplayName());
    co.setDurationDays(record.getDurationDays());
    co.setPriceGold(record.getPriceGold());
    co.setStartAt(record.getStartAt());
    co.setExpireAt(record.getExpireAt());
    co.setOrderId(record.getOrderId());
    co.setEventId(record.getEventId());
    co.setStatus(record.getStatus());
    co.setStatusName(statusName(record.getStatus()));
    co.setApplicantId(record.getApplicantId());
    co.setApplicantName(record.getApplicantName());
    co.setRemarks(record.getRemarks());
    co.setExecutedTime(record.getExecutedTime());
    co.setCreateTime(record.getCreateTime());
    co.setUpdateTime(record.getUpdateTime());
    co.setUserProfile(userProfile);
    return co;
  }

  private String levelName(Integer level) {
    return Objects.isNull(level) || level <= 0 ? "非VIP" : "VIP" + level;
  }

  private String statusName(String status) {
    return STATUS_SUCCESS.equals(status) ? "已赠送" : status;
  }

  private void notifyPropsGift(Long userId, String type, Integer days, Long propsId) {
    try {
      propsBackpackClient.givePropsNotify(userId, type, days, propsId);
    } catch (Exception exception) {
      log.warn("notify vip gift props failed, userId={}, type={}, propsId={}", userId, type,
          propsId, exception);
    }
  }

  private void removeUserCache(Long userId) {
    try {
      userProfileClient.removeCacheAll(userId);
    } catch (Exception exception) {
      log.warn("remove user cache after vip gift failed, userId={}", userId, exception);
    }
  }

  private <T extends com.red.circle.framework.mybatis.entity.TimestampBaseEntity> T touchNew(T entity,
      Timestamp now) {
    entity.setCreateTime(now);
    entity.setUpdateTime(now);
    return entity;
  }

  private Timestamp max(Timestamp first, Timestamp second) {
    if (Objects.isNull(first)) {
      return second;
    }
    if (Objects.isNull(second)) {
      return first;
    }
    return first.after(second) ? first : second;
  }

  private Timestamp plusDays(Timestamp value, int days) {
    return Timestamp.from(value.toInstant().plus(Duration.ofDays(days)));
  }

  private Timestamp capExpireTime(Timestamp expireTime) {
    if (Objects.isNull(expireTime)) {
      return null;
    }
    return expireTime.after(MYSQL_DATETIME_MAX) ? MYSQL_DATETIME_MAX : expireTime;
  }

  private Timestamp toTimestamp(Long value) {
    return Objects.isNull(value) ? null : new Timestamp(value);
  }

  private Timestamp now() {
    return new Timestamp(System.currentTimeMillis());
  }
}
