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

import com.red.circle.component.redis.service.RedisService;
import com.red.circle.console.app.dto.clienobject.app.activity.invite.InviteCampaignConfigCO;
import com.red.circle.console.app.dto.clienobject.app.activity.invite.InviteCampaignInviteeCO;
import com.red.circle.console.app.dto.clienobject.app.activity.invite.InviteCampaignInviterCO;
import com.red.circle.console.app.dto.cmd.app.activity.invite.InviteCampaignConfigSaveCmd;
import com.red.circle.console.app.dto.cmd.app.activity.invite.InviteCampaignInviteePageQryCmd;
import com.red.circle.console.app.dto.cmd.app.activity.invite.InviteCampaignInviterPageQryCmd;
import com.red.circle.console.app.service.app.props.PropsActivityRewardGroupService;
import com.red.circle.console.infra.database.rds.dao.app.invite.InviteCampaignQueryDAO;
import com.red.circle.console.infra.database.rds.dto.app.invite.InviteCampaignInviteeQuery;
import com.red.circle.console.infra.database.rds.dto.app.invite.InviteCampaignInviteeRow;
import com.red.circle.console.infra.database.rds.dto.app.invite.InviteCampaignInviterQuery;
import com.red.circle.console.infra.database.rds.dto.app.invite.InviteCampaignInviterRow;
import com.red.circle.console.infra.database.rds.entity.app.invite.InviteCampaignConfig;
import com.red.circle.console.infra.database.rds.entity.app.invite.InviteCampaignRewardRule;
import com.red.circle.console.infra.database.rds.service.app.invite.InviteCampaignConfigService;
import com.red.circle.console.infra.database.rds.service.app.invite.InviteCampaignRewardRuleService;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.CommonErrorCode;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.mybatis.convertor.MybatisConvertor;
import com.red.circle.other.inner.model.dto.material.PropsActivityRewardGroupDTO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.LocalDateTimeUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 常驻邀请活动服务实现.
 */
@Service
@RequiredArgsConstructor
public class ResidentInviteActivityServiceImpl implements ResidentInviteActivityService {

  private static final String DEFAULT_TIMEZONE = "Asia/Riyadh";
  private static final String CONFIG_CACHE_PREFIX = "invite:campaign:config:";
  private static final String RULE_TYPE_INVITER_BIND = "INVITER_BIND";
  private static final String RULE_TYPE_INVITEE_BIND = "INVITEE_BIND";
  private static final String RULE_TYPE_INVITEE_RECHARGE = "INVITEE_RECHARGE";
  private static final String RULE_TYPE_INVITER_VALID_COUNT = "INVITER_VALID_COUNT";
  private static final String RULE_TYPE_INVITER_TOTAL_RECHARGE = "INVITER_TOTAL_RECHARGE";

  private final RedisService redisService;
  private final InviteCampaignQueryDAO inviteCampaignQueryDAO;
  private final InviteCampaignConfigService inviteCampaignConfigService;
  private final InviteCampaignRewardRuleService inviteCampaignRewardRuleService;
  private final PropsActivityRewardGroupService propsActivityRewardGroupService;

  @Override
  public InviteCampaignConfigCO getConfig(String sysOrigin) {
    String normalizedSysOrigin = normalizeSysOrigin(sysOrigin);
    InviteCampaignConfig config = inviteCampaignConfigService.query()
        .eq(InviteCampaignConfig::getSysOrigin, normalizedSysOrigin)
        .getOne();

    InviteCampaignConfigCO result = new InviteCampaignConfigCO()
        .setSysOrigin(normalizedSysOrigin)
        .setEnabled(Boolean.FALSE)
        .setTimezone(DEFAULT_TIMEZONE);

    if (config != null) {
      result.setEnabled(Boolean.TRUE.equals(config.getEnabled()))
          .setDownloadUrl(config.getDownloadUrl())
          .setShareTitle(config.getShareTitle())
          .setShareDesc(config.getShareDesc())
          .setServiceContact(config.getServiceContact())
          .setTimezone(Optional.ofNullable(config.getTimezone()).orElse(DEFAULT_TIMEZONE))
          .setAntiCheat(new InviteCampaignConfigCO.AntiCheatCO()
              .setSameIpOnce(Boolean.TRUE.equals(config.getAntiCheatSameIpOnce()))
              .setSameDeviceOnce(Boolean.TRUE.equals(config.getAntiCheatSameDeviceOnce())));
    }

    List<InviteCampaignRewardRule> rules = inviteCampaignRewardRuleService.query()
        .eq(InviteCampaignRewardRule::getSysOrigin, normalizedSysOrigin)
        .orderByAsc(InviteCampaignRewardRule::getSort)
        .list();

    fillConfigRules(result, rules);
    return result;
  }

  @Override
  @Transactional(rollbackFor = Exception.class)
  public void saveConfig(InviteCampaignConfigSaveCmd cmd) {
    String normalizedSysOrigin = normalizeSysOrigin(cmd.getSysOrigin());
    validateConfig(cmd);
    List<InviteCampaignRewardRule> existingRules = inviteCampaignRewardRuleService.query()
        .eq(InviteCampaignRewardRule::getSysOrigin, normalizedSysOrigin)
        .list();
    validateReusableRuleIds(existingRules, cmd);

    InviteCampaignConfig config = inviteCampaignConfigService.query()
        .eq(InviteCampaignConfig::getSysOrigin, normalizedSysOrigin)
        .getOne();
    if (config == null) {
      config = new InviteCampaignConfig().setId(IdWorkerUtils.getId());
    }

    config.setSysOrigin(normalizedSysOrigin)
        .setEnabled(Boolean.TRUE.equals(cmd.getEnabled()))
        .setDownloadUrl(trimToNull(cmd.getDownloadUrl()))
        .setShareTitle(trimToNull(cmd.getShareTitle()))
        .setShareDesc(trimToNull(cmd.getShareDesc()))
        .setServiceContact(trimToNull(cmd.getServiceContact()))
        .setTimezone(Optional.ofNullable(trimToNull(cmd.getTimezone())).orElse(DEFAULT_TIMEZONE))
        .setAntiCheatSameIpOnce(Boolean.TRUE.equals(cmd.getAntiCheat().getSameIpOnce()))
        .setAntiCheatSameDeviceOnce(Boolean.TRUE.equals(cmd.getAntiCheat().getSameDeviceOnce()));

    if (inviteCampaignConfigService.query().eq(InviteCampaignConfig::getId, config.getId()).count()
        > 0) {
      ResponseAssert.isTrue(CommonErrorCode.UPDATE_FAILURE,
          inviteCampaignConfigService.updateSelectiveById(config));
    } else {
      ResponseAssert.isTrue(CommonErrorCode.SAVE_FAILURE, inviteCampaignConfigService.save(config));
    }

    if (CollectionUtils.isNotEmpty(existingRules)) {
      inviteCampaignRewardRuleService.removeByIds(
          existingRules.stream().map(InviteCampaignRewardRule::getId).collect(Collectors.toSet()));
    }

    List<InviteCampaignRewardRule> rules = buildRules(normalizedSysOrigin, cmd);
    if (CollectionUtils.isNotEmpty(rules)) {
      inviteCampaignRewardRuleService.saveBatch(rules);
    }

    redisService.delete(configCacheKey(normalizedSysOrigin));
  }

  @Override
  public PageResult<InviteCampaignInviterCO> pageInviter(InviteCampaignInviterPageQryCmd cmd) {
    InviteCampaignInviterQuery query = new InviteCampaignInviterQuery()
        .setSysOrigin(normalizeNullable(cmd.getSysOrigin()))
        .setInviterUserId(cmd.getInviterUserId())
        .setInviteeUserId(cmd.getInviteeUserId())
        .setMonthKey(normalizeMonthKey(cmd.getMonthKey()))
        .setStartTime(toLocalDateTime(cmd.getStartTime()))
        .setEndTime(toLocalDateTime(cmd.getEndTime()));

    PageResult<InviteCampaignInviterRow> page = MybatisConvertor.toPageResult(
        inviteCampaignQueryDAO.pageInviters(MybatisConvertor.toPage(cmd.getPageQuery()), query));
    return page.convert(row -> new InviteCampaignInviterCO()
        .setInviterUserId(row.getInviterUserId())
        .setInviterAccount(row.getInviterAccount())
        .setInviterNickname(row.getInviterNickname())
        .setInviterAvatar(row.getInviterAvatar())
        .setInviteCode(row.getInviteCode())
        .setTotalInviteCount(Optional.ofNullable(row.getTotalInviteCount()).orElse(0L))
        .setMonthlyValidUserCount(Optional.ofNullable(row.getMonthlyValidUserCount()).orElse(0L))
        .setTotalInviteeRechargeCoins(
            Optional.ofNullable(row.getTotalInviteeRechargeCoins()).orElse(0L))
        .setRewardGoldTotal(Optional.ofNullable(row.getRewardGoldTotal()).orElse(0L))
        .setLatestInviteTime(row.getLatestInviteTime()));
  }

  @Override
  public PageResult<InviteCampaignInviteeCO> pageInvitee(InviteCampaignInviteePageQryCmd cmd) {
    InviteCampaignInviteeQuery query = new InviteCampaignInviteeQuery()
        .setSysOrigin(normalizeNullable(cmd.getSysOrigin()))
        .setInviterUserId(cmd.getInviterUserId())
        .setMonthKey(normalizeMonthKey(cmd.getMonthKey()));

    PageResult<InviteCampaignInviteeRow> page = MybatisConvertor.toPageResult(
        inviteCampaignQueryDAO.pageInvitees(MybatisConvertor.toPage(cmd.getPageQuery()), query));
    return page.convert(row -> new InviteCampaignInviteeCO()
        .setRelationId(row.getRelationId())
        .setInviterUserId(row.getInviterUserId())
        .setInviteeUserId(row.getInviteeUserId())
        .setInviteeAccount(row.getInviteeAccount())
        .setInviteeNickname(row.getInviteeNickname())
        .setInviteeAvatar(row.getInviteeAvatar())
        .setBindTime(row.getBindTime())
        .setRechargeCoins(Optional.ofNullable(row.getRechargeCoins()).orElse(0L))
        .setValidUser(Boolean.TRUE.equals(row.getValidUser()))
        .setValidTime(row.getValidTime())
        .setRewardedRuleIds(row.getRewardedRuleIds()));
  }

  private void fillConfigRules(InviteCampaignConfigCO result, List<InviteCampaignRewardRule> rules) {
    for (InviteCampaignRewardRule rule : rules) {
      if (Objects.equals(rule.getRuleType(), RULE_TYPE_INVITER_BIND)) {
        result.setInviterBindReward(toRewardItem(rule));
        continue;
      }
      if (Objects.equals(rule.getRuleType(), RULE_TYPE_INVITEE_BIND)) {
        result.setInviteeBindReward(toRewardItem(rule));
        continue;
      }
      InviteCampaignConfigCO.RuleItemCO item = toRuleItem(rule);
      if (Objects.equals(rule.getRuleType(), RULE_TYPE_INVITEE_RECHARGE)) {
        result.getInviteeRechargeRules().add(item);
      } else if (Objects.equals(rule.getRuleType(), RULE_TYPE_INVITER_VALID_COUNT)) {
        result.getInviterValidCountRules().add(item);
      } else if (Objects.equals(rule.getRuleType(), RULE_TYPE_INVITER_TOTAL_RECHARGE)) {
        result.getInviterTotalRechargeRules().add(item);
      }
    }
  }

  private InviteCampaignConfigCO.RewardItemCO toRewardItem(InviteCampaignRewardRule rule) {
    return new InviteCampaignConfigCO.RewardItemCO()
        .setId(rule.getId())
        .setGoldAmount(Optional.ofNullable(rule.getGoldAmount()).orElse(0L))
        .setRewardGroupId(rule.getRewardGroupId())
        .setRewardGroupName(getRewardGroupName(rule.getRewardGroupId()));
  }

  private InviteCampaignConfigCO.RuleItemCO toRuleItem(InviteCampaignRewardRule rule) {
    InviteCampaignConfigCO.RuleItemCO item = new InviteCampaignConfigCO.RuleItemCO();
    item.setId(rule.getId());
    item.setThreshold(Optional.ofNullable(rule.getThresholdValue()).orElse(0L));
    item.setGoldAmount(Optional.ofNullable(rule.getGoldAmount()).orElse(0L));
    item.setRewardGroupId(rule.getRewardGroupId());
    item.setRewardGroupName(getRewardGroupName(rule.getRewardGroupId()));
    item.setEnabled(Boolean.TRUE.equals(rule.getEnabled()));
    item.setSort(Optional.ofNullable(rule.getSort()).orElse(0));
    return item;
  }

  private String getRewardGroupName(Long rewardGroupId) {
    if (rewardGroupId == null) {
      return null;
    }
    try {
      PropsActivityRewardGroupDTO group = propsActivityRewardGroupService.getByGroupId(rewardGroupId);
      return group == null ? null : group.getName();
    } catch (Exception ignore) {
      return null;
    }
  }

  private List<InviteCampaignRewardRule> buildRules(String sysOrigin, InviteCampaignConfigSaveCmd cmd) {
    List<InviteCampaignRewardRule> rules = new ArrayList<>();
    rules.add(buildRewardRule(sysOrigin, RULE_TYPE_INVITER_BIND, 0L, 1, cmd.getInviterBindReward()));
    rules.add(buildRewardRule(sysOrigin, RULE_TYPE_INVITEE_BIND, 0L, 1, cmd.getInviteeBindReward()));
    appendRuleItems(rules, sysOrigin, RULE_TYPE_INVITEE_RECHARGE, cmd.getInviteeRechargeRules());
    appendRuleItems(rules, sysOrigin, RULE_TYPE_INVITER_VALID_COUNT, cmd.getInviterValidCountRules());
    appendRuleItems(rules, sysOrigin, RULE_TYPE_INVITER_TOTAL_RECHARGE,
        cmd.getInviterTotalRechargeRules());
    return rules;
  }

  private void appendRuleItems(List<InviteCampaignRewardRule> rules, String sysOrigin, String ruleType,
      List<InviteCampaignConfigSaveCmd.RuleItemCmd> items) {
    if (CollectionUtils.isEmpty(items)) {
      return;
    }
    List<InviteCampaignConfigSaveCmd.RuleItemCmd> sortedItems = items.stream()
        .sorted(Comparator.comparing(InviteCampaignConfigSaveCmd.RuleItemCmd::getThreshold))
        .collect(Collectors.toList());
    for (int i = 0; i < sortedItems.size(); i++) {
      InviteCampaignConfigSaveCmd.RuleItemCmd item = sortedItems.get(i);
      rules.add(new InviteCampaignRewardRule()
          .setId(resolveRuleId(item))
          .setSysOrigin(sysOrigin)
          .setRuleType(ruleType)
          .setThresholdValue(Optional.ofNullable(item.getThreshold()).orElse(0L))
          .setGoldAmount(Optional.ofNullable(item.getGoldAmount()).orElse(0L))
          .setRewardGroupId(item.getRewardGroupId())
          .setEnabled(Boolean.TRUE.equals(item.getEnabled()))
          .setSort(i + 1));
    }
  }

  private InviteCampaignRewardRule buildRewardRule(String sysOrigin, String ruleType, Long threshold,
      Integer sort, InviteCampaignConfigSaveCmd.RewardItemCmd item) {
    return new InviteCampaignRewardRule()
        .setId(resolveRuleId(item))
        .setSysOrigin(sysOrigin)
        .setRuleType(ruleType)
        .setThresholdValue(threshold)
        .setGoldAmount(Optional.ofNullable(item.getGoldAmount()).orElse(0L))
        .setRewardGroupId(item.getRewardGroupId())
        .setEnabled(Boolean.TRUE)
        .setSort(sort);
  }

  private Long resolveRuleId(InviteCampaignConfigSaveCmd.RewardItemCmd item) {
    return item.getId() == null ? IdWorkerUtils.getId() : item.getId();
  }

  private void validateConfig(InviteCampaignConfigSaveCmd cmd) {
    ResponseAssert.notNull(CommonErrorCode.DATA_ERROR, cmd.getAntiCheat());
    validateRewardItem("inviterBindReward", cmd.getInviterBindReward());
    validateRewardItemOptional("inviteeBindReward", cmd.getInviteeBindReward());
    validateRuleItems(cmd.getInviteeRechargeRules());
    validateRuleItems(cmd.getInviterValidCountRules());
    validateRuleItems(cmd.getInviterTotalRechargeRules());
  }

  private void validateRuleItems(List<InviteCampaignConfigSaveCmd.RuleItemCmd> items) {
    if (CollectionUtils.isEmpty(items)) {
      return;
    }
    Long previous = null;
    Set<Long> thresholdSet = items.stream().map(InviteCampaignConfigSaveCmd.RuleItemCmd::getThreshold)
        .collect(Collectors.toSet());
    ResponseAssert.isTrue(CommonErrorCode.DATA_ERROR, Objects.equals(thresholdSet.size(), items.size()));
    List<InviteCampaignConfigSaveCmd.RuleItemCmd> sortedItems = items.stream()
        .sorted(Comparator.comparing(InviteCampaignConfigSaveCmd.RuleItemCmd::getThreshold))
        .collect(Collectors.toList());
    for (InviteCampaignConfigSaveCmd.RuleItemCmd item : sortedItems) {
      ResponseAssert.notNull(CommonErrorCode.DATA_ERROR, item.getThreshold());
      ResponseAssert.isTrue(CommonErrorCode.DATA_ERROR, item.getThreshold() > 0);
      validateRewardItem("ruleItem", item);
      if (previous != null) {
        ResponseAssert.isTrue(CommonErrorCode.DATA_ERROR, item.getThreshold() > previous);
      }
      previous = item.getThreshold();
    }
  }

  private void validateRewardItem(String fieldName, InviteCampaignConfigSaveCmd.RewardItemCmd item) {
    ResponseAssert.notNull(CommonErrorCode.DATA_ERROR, item);
    long goldAmount = Optional.ofNullable(item.getGoldAmount()).orElse(0L);
    ResponseAssert.isTrue(CommonErrorCode.DATA_ERROR, goldAmount >= 0);
    ResponseAssert.isTrue(CommonErrorCode.DATA_ERROR,
        goldAmount > 0 || item.getRewardGroupId() != null);
  }

  private void validateRewardItemOptional(String fieldName,
      InviteCampaignConfigSaveCmd.RewardItemCmd item) {
    ResponseAssert.notNull(CommonErrorCode.DATA_ERROR, item);
    long goldAmount = Optional.ofNullable(item.getGoldAmount()).orElse(0L);
    ResponseAssert.isTrue(CommonErrorCode.DATA_ERROR, goldAmount >= 0);
  }

  private void validateReusableRuleIds(List<InviteCampaignRewardRule> existingRules,
      InviteCampaignConfigSaveCmd cmd) {
    Map<Long, InviteCampaignRewardRule> existingRuleMap = existingRules.stream()
        .collect(Collectors.toMap(InviteCampaignRewardRule::getId, item -> item));
    Set<Long> submittedIds = new HashSet<>();
    validateReusableRuleId(existingRuleMap, submittedIds, RULE_TYPE_INVITER_BIND,
        cmd.getInviterBindReward());
    validateReusableRuleId(existingRuleMap, submittedIds, RULE_TYPE_INVITEE_BIND,
        cmd.getInviteeBindReward());
    validateReusableRuleIds(existingRuleMap, submittedIds, RULE_TYPE_INVITEE_RECHARGE,
        cmd.getInviteeRechargeRules());
    validateReusableRuleIds(existingRuleMap, submittedIds, RULE_TYPE_INVITER_VALID_COUNT,
        cmd.getInviterValidCountRules());
    validateReusableRuleIds(existingRuleMap, submittedIds, RULE_TYPE_INVITER_TOTAL_RECHARGE,
        cmd.getInviterTotalRechargeRules());
  }

  private void validateReusableRuleIds(Map<Long, InviteCampaignRewardRule> existingRuleMap,
      Set<Long> submittedIds, String ruleType, List<InviteCampaignConfigSaveCmd.RuleItemCmd> items) {
    if (CollectionUtils.isEmpty(items)) {
      return;
    }
    items.forEach(item -> validateReusableRuleId(existingRuleMap, submittedIds, ruleType, item));
  }

  private void validateReusableRuleId(Map<Long, InviteCampaignRewardRule> existingRuleMap,
      Set<Long> submittedIds, String ruleType, InviteCampaignConfigSaveCmd.RewardItemCmd item) {
    if (item == null || item.getId() == null) {
      return;
    }
    ResponseAssert.isTrue(CommonErrorCode.DATA_ERROR, item.getId() > 0);
    ResponseAssert.isTrue(CommonErrorCode.DATA_ERROR, submittedIds.add(item.getId()));
    InviteCampaignRewardRule existingRule = existingRuleMap.get(item.getId());
    ResponseAssert.isTrue(CommonErrorCode.DATA_ERROR,
        existingRule != null && Objects.equals(existingRule.getRuleType(), ruleType));
  }

  private String normalizeSysOrigin(String sysOrigin) {
    String value = trimToNull(sysOrigin);
    ResponseAssert.notNull(CommonErrorCode.DATA_ERROR, value);
    return value;
  }

  private String normalizeNullable(String value) {
    return trimToNull(value);
  }

  private String trimToNull(String value) {
    if (value == null) {
      return null;
    }
    String trimmed = value.trim();
    return trimmed.isEmpty() ? null : trimmed;
  }

  private java.time.LocalDateTime toLocalDateTime(Long timestamp) {
    return timestamp == null ? null : LocalDateTimeUtils.convert(timestamp);
  }

  private String normalizeMonthKey(String monthKey) {
    String value = trimToNull(monthKey);
    if (value != null) {
      return value.replace("-", "");
    }
    return ZonedDateTime.now(ZoneId.of(DEFAULT_TIMEZONE))
        .format(DateTimeFormatter.ofPattern("yyyyMM"));
  }

  private String configCacheKey(String sysOrigin) {
    return CONFIG_CACHE_PREFIX + sysOrigin;
  }
}
