package com.red.circle.other.app.inner.service.team.impl;

import com.red.circle.common.business.core.enums.ReceiptType;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.CommonErrorCode;
import com.red.circle.framework.core.response.ResponseErrorCode;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.app.inner.service.team.TeamManualSalaryPaymentClientService;
import com.red.circle.other.infra.database.mongo.dto.team.TeamBillMemberTarget;
import com.red.circle.other.infra.database.mongo.dto.team.TeamPolicy;
import com.red.circle.other.infra.database.mongo.dto.team.TeamSalaryPaymentDetails;
import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMember;
import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMemberTarget;
import com.red.circle.other.infra.database.mongo.entity.team.team.TeamPolicyManager;
import com.red.circle.other.infra.database.mongo.entity.team.team.TeamProfile;
import com.red.circle.other.infra.database.mongo.entity.team.team.TeamSalaryPayment;
import com.red.circle.other.infra.database.mongo.service.team.TeamBillCycleUtils;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberTargetService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamPolicyManagerService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamProfileService;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamSalaryPaymentService;
import com.red.circle.other.inner.enums.team.TeamMemberRole;
import com.red.circle.other.inner.enums.team.TeamPolicyTypeEnum;
import com.red.circle.other.inner.model.cmd.team.TeamManualSalaryPaymentCmd;
import com.red.circle.other.inner.model.cmd.team.TeamManualSalaryPaymentQryCmd;
import com.red.circle.other.inner.model.dto.agency.agency.TeamManualSalaryPaymentDTO;
import com.red.circle.other.inner.model.dto.agency.agency.TeamManualSalaryPaymentResultDTO;
import com.red.circle.other.inner.model.dto.agency.agency.TeamSalaryPaymentRemark;
import com.red.circle.other.inner.model.dto.agency.agency.TeamStatus;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils;
import com.red.circle.tool.core.text.StringUtils;
import com.red.circle.tool.core.tuple.PennyAmount;
import com.red.circle.wallet.inner.endpoint.bank.UserBankBalanceClient;
import com.red.circle.wallet.inner.endpoint.bank.UserBankRunningWaterClient;
import com.red.circle.wallet.inner.model.cmd.UserBankBalanceIncrCmd;
import com.red.circle.wallet.inner.model.dto.BankBalanceDTO;
import com.red.circle.wallet.inner.model.dto.UserBankRunningWaterDTO;
import com.red.circle.wallet.inner.model.enums.UserBankWaterEvent;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Timestamp;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * 后台手动发放主播工资.
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class TeamManualSalaryPaymentClientServiceImpl implements
    TeamManualSalaryPaymentClientService {

  private static final String MANUAL_SALARY_LOCK_PREFIX = "TeamManualSalaryPayment:";
  private static final int LOCK_SECONDS = 300;
  private static final String SORT_FIELD_CURRENT_SALARY_BALANCE = "currentSalaryBalance";
  private static final String SORT_FIELD_PAYABLE_SALARY = "payableSalary";
  private static final String SORT_FIELD_TOTAL_ISSUED_SALARY = "totalIssuedSalary";
  private static final String SORT_ORDER_ASCEND = "ascend";
  private static final int LEGACY_HALF_MONTH_COMPAT_BILL_BELONG = 202605;

  private final RedisService redisService;
  private final TeamMemberService teamMemberService;
  private final TeamProfileService teamProfileService;
  private final UserBankBalanceClient userBankBalanceClient;
  private final TeamMemberTargetService teamMemberTargetService;
  private final TeamSalaryPaymentService teamSalaryPaymentService;
  private final TeamPolicyManagerService teamPolicyManagerService;
  private final UserBankRunningWaterClient userBankRunningWaterClient;

  @Override
  public PageResult<TeamManualSalaryPaymentDTO> page(TeamManualSalaryPaymentQryCmd cmd) {
    validatePage(cmd);

    Integer billBelong = resolveBillBelong(cmd.getBillBelong());
    List<ManualSalaryRow> rows = buildRows(cmd.getSysOrigin(), cmd.getCountryCode(),
        singletonUserId(cmd.getUserId()), billBelong);
    rows = sortRows(rows, cmd.getSortField(), cmd.getSortOrder());

    int cursor = 1;
    int limit = 20;
    if (Objects.nonNull(cmd.getPageQuery())) {
      cursor = Math.max(1, Objects.isNull(cmd.getPageQuery().getCursor()) ? 1
          : cmd.getPageQuery().getCursor());
      limit = Math.max(1, Objects.isNull(cmd.getPageQuery().getLimit()) ? 20
          : cmd.getPageQuery().getLimit());
    }
    int fromIndex = Math.min((cursor - 1) * limit, rows.size());
    int toIndex = Math.min(fromIndex + limit, rows.size());

    PageResult<TeamManualSalaryPaymentDTO> pageResult = new PageResult<>();
    pageResult.setCurrent(cursor);
    pageResult.setSize(limit);
    pageResult.setTotal((long) rows.size());
    pageResult.setRecords(rows.subList(fromIndex, toIndex).stream()
        .map(ManualSalaryRow::getDto)
        .collect(Collectors.toList()));
    return pageResult;
  }

  @Override
  public List<TeamManualSalaryPaymentResultDTO> pay(TeamManualSalaryPaymentCmd cmd) {
    validatePay(cmd);

    Integer billBelong = resolveBillBelong(cmd.getBillBelong());
    String lockKey = MANUAL_SALARY_LOCK_PREFIX + cmd.getSysOrigin() + ":" + cmd.getCountryCode()
        + ":" + billBelong;
    ResponseAssert.isTrue(CommonErrorCode.OPERATION_CONFLICT,
        redisService.lock(lockKey, LOCK_SECONDS));

    try {
      List<ManualSalaryRow> rows = buildRows(cmd.getSysOrigin(), cmd.getCountryCode(),
          normalizeUserIds(cmd.getUserIds()), billBelong);
      List<TeamManualSalaryPaymentResultDTO> results = new ArrayList<>();

      for (ManualSalaryRow row : rows) {
        if (CollectionUtils.isEmpty(row.getPayments())) {
          continue;
        }

        for (PolicyPayment policyPayment : row.getPayments()) {
          Long receiverUserId = getPaymentReceiverUserId(row, policyPayment);
          if (Objects.isNull(receiverUserId)) {
            continue;
          }
          TeamSalaryPayment payment = teamSalaryPaymentService.createIfAbsent(receiverUserId,
              billBelong, row.getSysOrigin());
          if (policyPayment.getSalary().compareTo(BigDecimal.ZERO) <= 0
              && Boolean.TRUE.equals(teamSalaryPaymentService.checkRepeatPayment(payment.getId(),
              row.getUserId(), row.getRegion(), policyPayment.getLevel(),
              policyPayment.getAnchor()))) {
            continue;
          }

          BigDecimal issueSalary = policyPayment.getSalary().compareTo(BigDecimal.ZERO) > 0
              ? policyPayment.getSalary()
              : BigDecimal.ZERO.setScale(2, RoundingMode.DOWN);
          BigDecimal beforeBalance = getAccurateBalance(receiverUserId);
          String detailsId = IdWorkerUtils.getIdStr();

          if (issueSalary.compareTo(BigDecimal.ZERO) > 0) {
            Boolean incrSuccess = ResponseAssert.requiredSuccess(userBankBalanceClient.incr(new UserBankBalanceIncrCmd()
                .setUserId(receiverUserId)
                .setSysOrigin(row.getSysOrigin())
                .setAmount(PennyAmount.ofDollar(issueSalary))));
            ResponseAssert.isTrue(CommonErrorCode.OPERATION_CONFLICT, Boolean.TRUE.equals(incrSuccess));
          }

          BigDecimal afterBalance = getAccurateBalance(receiverUserId);
          String salaryRoleName = policyPayment.isAnchor() ? "主播" : "代理";
          if (issueSalary.compareTo(BigDecimal.ZERO) > 0) {
            addRunningWater(detailsId, receiverUserId, row.getSysOrigin(),
                issueSalary, afterBalance, cmd.getOperationUserId(), salaryRoleName);
          }
          String settlementRemark = issueSalary.compareTo(BigDecimal.ZERO) > 0
              ? "后台手动发放" + salaryRoleName + "工资"
              : "后台手动结算" + salaryRoleName + "工资，实收已大于等于应发，本次发放0";

          teamSalaryPaymentService.paymentSalary(payment.getId(), new TeamSalaryPaymentDetails()
              .setId(detailsId)
              .setSysOrigin(row.getSysOrigin())
              .setTeamId(row.getTeamId())
              .setMemberTargetId(row.getMemberTargetId())
              .setTeamOwnId(row.getTeamOwnId())
              .setTeamMemberId(row.getUserId())
              .setTeamBillId(Long.valueOf(billBelong))
              .setTeamRegionId(row.getRegion())
              .setRemarks(List.of(new TeamSalaryPaymentRemark()
                  .setRemark(settlementRemark)
                  .setCreateTime(TimestampUtils.now())))
              .setAnchor(policyPayment.getAnchor())
              .setHostSalaryToAgent(Boolean.FALSE)
              .setTarget(row.getCurrentTarget())
              .setSalary(issueSalary)
              .setPolicyLevel(policyPayment.getLevel())
              .setPolicy(policyPayment.getPolicy()));

          updatePaymentPolicyMap(payment.getId(), row.getRegion(), row.getPolicyManager());

          results.add(new TeamManualSalaryPaymentResultDTO()
              .setId(detailsId)
              .setSysOrigin(row.getSysOrigin())
              .setCountryCode(row.getCountryCode())
              .setRegion(row.getRegion())
              .setTeamId(row.getTeamId())
              .setTeamOwnId(row.getTeamOwnId())
              .setUserId(receiverUserId)
              .setBillBelong(billBelong)
              .setBeforeSalaryBalance(beforeBalance)
              .setAfterSalaryBalance(afterBalance)
              .setIssuedSalary(issueSalary)
              .setCurrentTarget(row.getCurrentTarget())
              .setPolicyLevel(policyPayment.getLevel()));
        }
      }

      return results;
    } finally {
      redisService.unlock(lockKey);
    }
  }

  private List<ManualSalaryRow> buildRows(String sysOrigin, String countryCode, Set<Long> userIds,
      Integer billBelong) {
    List<TeamMemberTarget> targets = teamMemberTargetService.listManualSalaryTargets(sysOrigin,
        countryCode, userIds, billBelong);
    if (CollectionUtils.isEmpty(targets)) {
      return List.of();
    }

    targets = targets.stream()
        .filter(target -> CollectionUtils.isNotEmpty(target.getDailyTargets()))
        .filter(target -> target.sumAcceptGiftValue() > 0)
        // 手动工资只按当前有效目标结算；旧账期查询会带出历史目标，而计算工具会在历史目标上返回空结果。
        .filter(target -> !Boolean.TRUE.equals(target.getHistory()))
        .collect(Collectors.toList());
    if (CollectionUtils.isEmpty(targets)) {
      return List.of();
    }

    Set<Long> memberIds = targets.stream()
        .map(TeamMemberTarget::getUserId)
        .filter(Objects::nonNull)
        .collect(Collectors.toSet());
    Set<Long> teamIds = targets.stream()
        .map(TeamMemberTarget::getTeamId)
        .filter(Objects::nonNull)
        .collect(Collectors.toSet());
    Map<Long, TeamMember> memberMap = teamMemberService.mapByMemberIds(memberIds);
    Map<Long, TeamProfile> teamProfileMap = teamProfileService.mapProfileByIds(teamIds);
    Set<Integer> salaryPaymentBillBelongs = getMergedSalaryPaymentBillBelongs(billBelong);
    Set<Long> paymentRelatedUserIds = new HashSet<>(memberIds);
    teamProfileMap.values().stream()
        .filter(Objects::nonNull)
        .map(TeamProfile::getOwnUserId)
        .filter(Objects::nonNull)
        .forEach(paymentRelatedUserIds::add);
    List<TeamSalaryPayment> salaryPayments = teamSalaryPaymentService
        .listByPaymentOrRecipientUserIdsAndDateNumbers(sysOrigin, paymentRelatedUserIds,
            salaryPaymentBillBelongs);
    Map<String, TeamSalaryPayment> salaryPaymentMap = toSalaryPaymentMap(salaryPayments);

    Map<String, TeamPolicyManager> policyCache = CollectionUtils.newHashMap();
    return targets.stream()
        .map(target -> buildRow(target, memberMap, teamProfileMap, policyCache, billBelong,
            countryCode, salaryPaymentMap))
        .filter(Objects::nonNull)
        .sorted(Comparator.comparing(ManualSalaryRow::getCurrentTarget).reversed()
        .thenComparing(ManualSalaryRow::getUserId))
        .collect(Collectors.toList());
  }

  private Map<String, TeamSalaryPayment> toSalaryPaymentMap(List<TeamSalaryPayment> payments) {
    if (CollectionUtils.isEmpty(payments)) {
      return CollectionUtils.newHashMap();
    }
    return payments.stream()
        .filter(Objects::nonNull)
        .filter(payment -> StringUtils.isNotBlank(payment.getId()))
        .collect(Collectors.toMap(TeamSalaryPayment::getId, payment -> payment,
            (first, second) -> first));
  }

  private List<ManualSalaryRow> sortRows(List<ManualSalaryRow> rows, String sortField,
      String sortOrder) {
    if (CollectionUtils.isEmpty(rows) || StringUtils.isBlank(sortField)
        || StringUtils.isBlank(sortOrder)) {
      return rows;
    }
    Comparator<ManualSalaryRow> comparator = getSalarySortComparator(sortField);
    if (Objects.isNull(comparator)) {
      return rows;
    }
    if (!SORT_ORDER_ASCEND.equalsIgnoreCase(sortOrder)) {
      comparator = comparator.reversed();
    }
    return rows.stream()
        .sorted(comparator.thenComparing(ManualSalaryRow::getUserId))
        .collect(Collectors.toList());
  }

  private Comparator<ManualSalaryRow> getSalarySortComparator(String sortField) {
    if (SORT_FIELD_PAYABLE_SALARY.equals(sortField)) {
      return Comparator.comparing(row -> decimalValue(row.getPayableSalary()));
    }
    if (SORT_FIELD_TOTAL_ISSUED_SALARY.equals(sortField)) {
      return Comparator.comparing(row -> decimalValue(row.getTotalIssuedSalary()));
    }
    if (SORT_FIELD_CURRENT_SALARY_BALANCE.equals(sortField)) {
      return Comparator.comparing(row -> decimalValue(row.getCurrentSalaryBalance()));
    }
    return null;
  }

  private BigDecimal decimalValue(BigDecimal value) {
    return Objects.isNull(value) ? BigDecimal.ZERO : value;
  }

  private Set<Long> singletonUserId(Long userId) {
    if (Objects.isNull(userId)) {
      return Set.of();
    }
    return Set.of(userId);
  }

  private Set<Long> normalizeUserIds(List<Long> userIds) {
    if (CollectionUtils.isEmpty(userIds)) {
      return Set.of();
    }
    return userIds.stream()
        .filter(Objects::nonNull)
        .collect(Collectors.toSet());
  }

  private Set<Integer> getMergedSalaryPaymentBillBelongs(Integer billBelong) {
    if (Objects.isNull(billBelong)) {
      return Set.of();
    }
    Set<Integer> billBelongs = new HashSet<>();
    billBelongs.add(billBelong);
    Integer month = TeamBillCycleUtils.extractMonth(billBelong);
    if (Objects.equals(month, LEGACY_HALF_MONTH_COMPAT_BILL_BELONG)) {
      billBelongs.add(month);
      billBelongs.add(month * 100 + 1);
      billBelongs.add(month * 100 + 16);
    }
    return billBelongs;
  }

  private List<TeamSalaryPayment> listCyclePayments(
      Map<String, TeamSalaryPayment> salaryPaymentMap, Long userId, Integer billBelong) {
    if (Objects.isNull(userId) || CollectionUtils.isEmpty(salaryPaymentMap)) {
      return List.of();
    }
    Set<Integer> mergedBillBelongs = getMergedSalaryPaymentBillBelongs(billBelong);
    return salaryPaymentMap.values().stream()
        .filter(Objects::nonNull)
        .filter(payment -> mergedBillBelongs.contains(payment.getDateNumber()))
        .filter(payment -> Objects.equals(payment.getUserId(), userId)
            || listValidSalaryDetails(List.of(payment)).stream()
            .anyMatch(details -> Objects.equals(details.getTeamMemberId(), userId)))
        .collect(Collectors.toList());
  }

  private ManualSalaryRow buildRow(TeamMemberTarget target, Map<Long, TeamMember> memberMap,
      Map<Long, TeamProfile> teamProfileMap, Map<String, TeamPolicyManager> policyCache,
      Integer billBelong, String requestedCountryCode,
      Map<String, TeamSalaryPayment> salaryPaymentMap) {
    TeamMember member = memberMap.get(target.getUserId());
    TeamProfile teamProfile = teamProfileMap.get(target.getTeamId());
    if (Objects.isNull(member) || Objects.isNull(teamProfile)
        || !Objects.equals(member.getTeamId(), target.getTeamId())
        || TeamStatus.CLOSE.equals(teamProfile.getStatus())) {
      return null;
    }

    String region = StringUtils.isNotBlank(target.getRegion()) ? target.getRegion()
        : teamProfile.getRegion();
    String countryCode = StringUtils.isNotBlank(target.getCountryCode()) ? target.getCountryCode()
        : Optional.ofNullable(teamProfile.getCountry()).map(c -> c.getCountryCode()).orElse(null);
    if (StringUtils.isBlank(countryCode)) {
      countryCode = requestedCountryCode;
    }
    TeamPolicyManager policyManager = getPolicyManager(target.getSysOrigin(), region, countryCode,
        policyCache);
    TeamBillMemberTarget billTarget = calculateBillTarget(target, policyManager);
    boolean agentMember = isAgentMember(member, teamProfile, target.getUserId());

    List<TeamSalaryPayment> cyclePayments = listCyclePayments(salaryPaymentMap,
        target.getUserId(), billBelong);
    BigDecimal expectedMemberSalary = salaryValue(billTarget.getMemberSalary());
    BigDecimal expectedAgentSalary = Objects.nonNull(teamProfile.getOwnUserId())
        ? salaryValue(billTarget.getOwnSalary())
        : BigDecimal.ZERO.setScale(2, RoundingMode.DOWN);
    BigDecimal expectedSalary = expectedMemberSalary.add(expectedAgentSalary)
        .setScale(2, RoundingMode.DOWN);
    PaymentSummary receivedPaymentSummary = summarizeTargetEntitlementPayments(cyclePayments,
        target.getUserId(), expectedMemberSalary, expectedAgentSalary);
    BigDecimal payableMemberSalary = expectedMemberSalary
        .subtract(receivedPaymentSummary.getMemberIssuedSalary())
        .setScale(2, RoundingMode.DOWN);
    BigDecimal payableAgentSalary = expectedAgentSalary
        .subtract(receivedPaymentSummary.getAgentIssuedSalary())
        .setScale(2, RoundingMode.DOWN);
    BigDecimal payableSalary = payableAmount(payableMemberSalary).add(
            payableAmount(payableAgentSalary))
        .setScale(2, RoundingMode.DOWN);
    boolean currentLevelSettled = areCurrentRolesSettled(cyclePayments, target.getUserId(), region,
        billTarget, receivedPaymentSummary, expectedMemberSalary, expectedAgentSalary);
    List<PolicyPayment> policyPayments = buildSettlementPayments(policyManager, billTarget,
        cyclePayments, target.getUserId(), region, receivedPaymentSummary, currentLevelSettled);

    TeamManualSalaryPaymentDTO dto = new TeamManualSalaryPaymentDTO()
        .setId(target.getId())
        .setSysOrigin(target.getSysOrigin())
        .setCountryCode(countryCode)
        .setRegion(region)
        .setTeamId(target.getTeamId())
        .setTeamOwnId(teamProfile.getOwnUserId())
        .setUserId(target.getUserId())
        .setBillBelong(billBelong)
        .setLastSettlementTarget(getLastSettlementTarget(cyclePayments, target.getUserId()))
        .setCurrentTarget(target.sumAcceptGiftValue())
        .setPolicyLevel(billTarget.getLevel())
        .setSatisfyLevel(billTarget.getSatisfyLevel())
        .setAgentMember(agentMember)
        .setExpectedSalary(expectedSalary)
        .setExpectedMemberSalary(expectedMemberSalary)
        .setExpectedAgentSalary(expectedAgentSalary)
        .setPayableSalary(payableSalary)
        .setPayableMemberSalary(payableMemberSalary)
        .setPayableAgentSalary(payableAgentSalary)
        .setTotalIssuedSalary(receivedPaymentSummary.getTotalIssuedSalary())
        .setMemberIssuedSalary(receivedPaymentSummary.getMemberIssuedSalary())
        .setAgentIssuedSalary(receivedPaymentSummary.getAgentIssuedSalary())
        .setOverIssuedSalary(receivedPaymentSummary.getOverIssuedSalary())
        .setMemberOverIssuedSalary(receivedPaymentSummary.getMemberOverIssuedSalary())
        .setAgentOverIssuedSalary(receivedPaymentSummary.getAgentOverIssuedSalary())
        .setCurrentSalaryBalance(getAccurateBalance(target.getUserId()))
        .setPaid(currentLevelSettled);

    return new ManualSalaryRow(dto, target, teamProfile, policyManager, billTarget, policyPayments);
  }

  private boolean isAgentMember(TeamMember member, TeamProfile teamProfile, Long userId) {
    if (Objects.isNull(userId)) {
      return false;
    }
    Long teamOwnUserId = Optional.ofNullable(teamProfile)
        .map(TeamProfile::getOwnUserId)
        .orElse(null);
    TeamMemberRole role = Optional.ofNullable(member)
        .map(TeamMember::getRole)
        .orElse(null);
    return Objects.equals(teamOwnUserId, userId) || TeamMemberRole.OWN.eq(role);
  }

  private TeamPolicyManager getPolicyManager(String sysOrigin, String region, String countryCode,
      Map<String, TeamPolicyManager> policyCache) {
    if (StringUtils.isBlank(sysOrigin) || StringUtils.isBlank(region)) {
      return null;
    }

    String key = sysOrigin + ":" + region + ":" + countryCode;
    if (policyCache.containsKey(key)) {
      return policyCache.get(key);
    }

    TeamPolicyManager policyManager = teamPolicyManagerService.getReleaseByRegionAndType(sysOrigin,
        region, null, countryCode);
    if (Objects.isNull(policyManager)) {
      policyManager = teamPolicyManagerService.listReleaseTeamPolicy(sysOrigin, region).stream()
          .filter(this::moneyPolicy)
          .filter(policy -> StringUtils.isBlank(policy.getCountryCode()))
          .findFirst()
          .orElse(null);
    }
    policyCache.put(key, policyManager);
    return policyManager;
  }

  private boolean moneyPolicy(TeamPolicyManager policyManager) {
    return Objects.nonNull(policyManager)
        && (StringUtils.isBlank(policyManager.getPolicyType())
        || Objects.equals(TeamPolicyTypeEnum.MONEY.name(), policyManager.getPolicyType()));
  }

  private TeamBillMemberTarget calculateBillTarget(TeamMemberTarget target,
      TeamPolicyManager policyManager) {
    if (Objects.nonNull(policyManager) && CollectionUtils.isNotEmpty(policyManager.getPolicy())) {
      return TeamBillCycleUtils.calculatorPolicyTarget(target, policyManager);
    }

    return new TeamBillMemberTarget()
        .setTargetId(target.getTimeId())
        .setSysOrigin(target.getSysOrigin())
        .setUserId(target.getUserId())
        .setAcceptGiftValue(target.sumAcceptGiftValue())
        .setGiveGiftValue(target.sumGiveGiftValue())
        .setRoomValue(target.sumRoomValue())
        .setOnlineTime(target.sumAllTargetTime())
        .setMemberSalary(BigDecimal.ZERO)
        .setOwnSalary(BigDecimal.ZERO)
        .setTotalSalary(BigDecimal.ZERO)
        .setSatisfyLevel(Boolean.FALSE);
  }

  private BigDecimal salaryValue(BigDecimal value) {
    return Optional.ofNullable(value)
        .orElse(BigDecimal.ZERO)
        .setScale(2, RoundingMode.DOWN);
  }

  private BigDecimal payableAmount(BigDecimal value) {
    BigDecimal amount = salaryValue(value);
    return amount.compareTo(BigDecimal.ZERO) > 0 ? amount : BigDecimal.ZERO.setScale(2,
        RoundingMode.DOWN);
  }

  private List<PolicyPayment> buildSettlementPayments(TeamPolicyManager policyManager,
      TeamBillMemberTarget billTarget, List<TeamSalaryPayment> cyclePayments, Long userId,
      String region, PaymentSummary paymentSummary, boolean currentLevelSettled) {
    if (currentLevelSettled || Objects.isNull(billTarget)
        || Objects.isNull(billTarget.getLevel())) {
      return List.of();
    }

    TeamPolicy policy = findPolicy(policyManager, billTarget.getLevel());
    BigDecimal memberExpected = salaryValue(billTarget.getMemberSalary());
    BigDecimal agentExpected = salaryValue(billTarget.getOwnSalary());
    BigDecimal memberMissing = memberExpected.subtract(paymentSummary.getMemberIssuedSalary())
        .setScale(2, RoundingMode.DOWN);
    BigDecimal agentMissing = agentExpected.subtract(paymentSummary.getAgentIssuedSalary())
        .setScale(2, RoundingMode.DOWN);
    boolean memberHasSettlementRecord = hasPaidPolicy(cyclePayments, userId, region,
        billTarget.getLevel(), Boolean.TRUE);
    boolean agentHasSettlementRecord = hasPaidPolicy(cyclePayments, userId, region,
        billTarget.getLevel(), Boolean.FALSE);

    List<PolicyPayment> payments = new ArrayList<>();
    if (memberMissing.compareTo(BigDecimal.ZERO) > 0) {
      payments.add(new PolicyPayment(policy, billTarget.getLevel(), memberMissing, Boolean.TRUE));
    } else if (!memberHasSettlementRecord && memberExpected.compareTo(BigDecimal.ZERO) > 0) {
      payments.add(new PolicyPayment(policy, billTarget.getLevel(),
          BigDecimal.ZERO.setScale(2, RoundingMode.DOWN), Boolean.TRUE));
    }
    if (agentMissing.compareTo(BigDecimal.ZERO) > 0) {
      payments.add(new PolicyPayment(policy, billTarget.getLevel(), agentMissing, Boolean.FALSE));
    } else if (!agentHasSettlementRecord && agentExpected.compareTo(BigDecimal.ZERO) > 0) {
      payments.add(new PolicyPayment(policy, billTarget.getLevel(),
          BigDecimal.ZERO.setScale(2, RoundingMode.DOWN), Boolean.FALSE));
    }
    return payments;
  }

  private TeamPolicy findPolicy(TeamPolicyManager policyManager, Integer level) {
    if (Objects.isNull(policyManager) || CollectionUtils.isEmpty(policyManager.getPolicy())
        || Objects.isNull(level)) {
      return null;
    }
    return policyManager.getPolicy().stream()
        .filter(policy -> Objects.equals(policy.getLevel(), level))
        .findFirst()
        .orElse(null);
  }

  private boolean areCurrentRolesSettled(List<TeamSalaryPayment> cyclePayments, Long userId,
      String region, TeamBillMemberTarget billTarget, PaymentSummary paymentSummary,
      BigDecimal expectedMemberSalary, BigDecimal expectedAgentSalary) {
    if (Objects.isNull(billTarget) || Objects.isNull(billTarget.getLevel())
        || Objects.isNull(paymentSummary)) {
      return false;
    }
    return isRoleSettled(cyclePayments, userId, region, billTarget.getLevel(), Boolean.TRUE,
        expectedMemberSalary, paymentSummary.getMemberIssuedSalary())
        && isRoleSettled(cyclePayments, userId, region, billTarget.getLevel(), Boolean.FALSE,
        expectedAgentSalary, paymentSummary.getAgentIssuedSalary());
  }

  private boolean isRoleSettled(List<TeamSalaryPayment> cyclePayments, Long userId, String region,
      Integer policyLevel, Boolean anchor, BigDecimal expectedSalary, BigDecimal issuedSalary) {
    if (expectedSalary.compareTo(BigDecimal.ZERO) <= 0) {
      return true;
    }
    return Optional.ofNullable(issuedSalary).orElse(BigDecimal.ZERO).compareTo(expectedSalary) >= 0
        && hasPaidPolicy(cyclePayments, userId, region, policyLevel, anchor);
  }

  private boolean hasPaidPolicy(List<TeamSalaryPayment> cyclePayments, Long userId, String region,
      Integer policyLevel, Boolean anchor) {
    return listValidSalaryDetails(cyclePayments).stream()
        .anyMatch(details -> Objects.equals(details.getTeamMemberId(), userId)
            && Objects.equals(details.getTeamRegionId(), region)
            && Objects.equals(details.getPolicyLevel(), policyLevel)
            && Objects.equals(details.getAnchor(), anchor));
  }

  private Long getLastSettlementTarget(List<TeamSalaryPayment> cyclePayments, Long targetUserId) {
    return listValidSalaryDetails(cyclePayments).stream()
        .filter(details -> Objects.equals(details.getTeamMemberId(), targetUserId))
        .max(Comparator.comparing(details -> Optional.ofNullable(details.getCreateTime())
            .orElse(new Timestamp(0))))
        .map(TeamSalaryPaymentDetails::getTarget)
        .orElse(0L);
  }

  private PaymentSummary summarizeTargetEntitlementPayments(List<TeamSalaryPayment> cyclePayments,
      Long targetUserId, BigDecimal expectedMemberSalary, BigDecimal expectedAgentSalary) {
    if (Objects.isNull(targetUserId)) {
      return emptyPaymentSummary();
    }
    List<TeamSalaryPaymentDetails> details = listValidSalaryDetails(cyclePayments).stream()
        .filter(detail -> Objects.equals(targetUserId, detail.getTeamMemberId()))
        .collect(Collectors.toList());
    return summarizeDetails(details, expectedMemberSalary, expectedAgentSalary);
  }

  private PaymentSummary summarizeDetails(List<TeamSalaryPaymentDetails> details,
      BigDecimal expectedMemberSalary, BigDecimal expectedAgentSalary) {
    BigDecimal memberIssuedSalary = sumDetailsSalary(details, Boolean.TRUE);
    BigDecimal agentIssuedSalary = sumDetailsSalary(details, Boolean.FALSE);
    BigDecimal totalIssuedSalary = memberIssuedSalary.add(agentIssuedSalary)
        .setScale(2, RoundingMode.DOWN);
    BigDecimal memberOverIssuedSalary = memberIssuedSalary.subtract(
            salaryValue(expectedMemberSalary))
        .max(BigDecimal.ZERO)
        .setScale(2, RoundingMode.DOWN);
    BigDecimal agentOverIssuedSalary = agentIssuedSalary.subtract(salaryValue(expectedAgentSalary))
        .max(BigDecimal.ZERO)
        .setScale(2, RoundingMode.DOWN);
    BigDecimal overIssuedSalary = memberOverIssuedSalary.add(agentOverIssuedSalary)
        .setScale(2, RoundingMode.DOWN);
    return new PaymentSummary(totalIssuedSalary, memberIssuedSalary, agentIssuedSalary,
        overIssuedSalary, memberOverIssuedSalary, agentOverIssuedSalary);
  }

  private PaymentSummary emptyPaymentSummary() {
    BigDecimal zero = BigDecimal.ZERO.setScale(2, RoundingMode.DOWN);
    return new PaymentSummary(zero, zero, zero, zero, zero, zero);
  }

  private BigDecimal sumDetailsSalary(List<TeamSalaryPaymentDetails> details, Boolean anchor) {
    return details.stream()
        .filter(detail -> Objects.equals(detail.getAnchor(), anchor))
        .map(detail -> Optional.ofNullable(detail.getSalary()).orElse(BigDecimal.ZERO))
        .reduce(BigDecimal.ZERO, BigDecimal::add)
        .setScale(2, RoundingMode.DOWN);
  }

  private List<TeamSalaryPaymentDetails> listValidSalaryDetails(
      List<TeamSalaryPayment> cyclePayments) {
    if (CollectionUtils.isEmpty(cyclePayments)) {
      return List.of();
    }
    return cyclePayments.stream()
        .filter(Objects::nonNull)
        .flatMap(payment -> Optional.ofNullable(payment.getSalarySendDetails())
            .stream()
            .flatMap(List::stream))
        .filter(this::validSalaryDetails)
        .collect(Collectors.toList());
  }

  private boolean validSalaryDetails(TeamSalaryPaymentDetails details) {
    return Objects.nonNull(details)
        && Objects.nonNull(details.getAnchor())
        && !Boolean.TRUE.equals(details.getRecycled());
  }

  private BigDecimal getAccurateBalance(Long userId) {
    BankBalanceDTO balance = ResponseAssert.requiredSuccess(userBankBalanceClient.getBalance(userId));
    return Optional.ofNullable(balance)
        .map(BankBalanceDTO::getAccurateBalance)
        .orElse(BigDecimal.ZERO)
        .setScale(2, RoundingMode.DOWN);
  }

  private void addRunningWater(String trackId, Long userId, String sysOrigin, BigDecimal salary,
      BigDecimal afterBalance, Long operationUserId, String salaryRoleName) {
    try {
      userBankRunningWaterClient.add(new UserBankRunningWaterDTO()
          .setId(IdWorkerUtils.getId())
          .setUserId(userId)
          .setSysOrigin(sysOrigin)
          .setType(ReceiptType.INCOME.getType())
          .setAmount(salary)
          .setEvent(UserBankWaterEvent.SYSTEM_SETTLEMENT_WAGES.name())
          .setEventDescribe(UserBankWaterEvent.SYSTEM_SETTLEMENT_WAGES.getDescribe())
          .setTrackId(trackId)
          .setRemark("后台手动发放" + salaryRoleName + "工资")
          .setBalance(afterBalance)
          .setCreateTime(TimestampUtils.now())
          .setCreateUser(Optional.ofNullable(operationUserId).orElse(0L))
          .setCreateUserOrigin(1));
    } catch (Exception e) {
      log.error("[后台手动发放{}工资] 插入用户银行流水失败, userId:{}, trackId:{}",
          salaryRoleName, userId, trackId, e);
    }
  }

  private void updatePaymentPolicyMap(String paymentId, String region,
      TeamPolicyManager policyManager) {
    if (StringUtils.isBlank(paymentId) || StringUtils.isBlank(region)
        || Objects.isNull(policyManager)) {
      return;
    }
    Map<String, TeamPolicyManager> policyMap = teamSalaryPaymentService.mapPolicy(paymentId);
    policyMap.put(region, policyManager);
    teamSalaryPaymentService.updatePolicyMap(paymentId, policyMap);
  }

  private Long getPaymentReceiverUserId(ManualSalaryRow row, PolicyPayment payment) {
    if (Objects.isNull(row) || Objects.isNull(payment)) {
      return null;
    }
    return payment.isAnchor() ? row.getUserId() : row.getTeamOwnId();
  }

  private void validatePage(TeamManualSalaryPaymentQryCmd cmd) {
    ResponseAssert.notNull(ResponseErrorCode.REQUEST_PARAMETER_ERROR, cmd);
    ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR, cmd.getSysOrigin());
    ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR, cmd.getCountryCode());
  }

  private void validatePay(TeamManualSalaryPaymentCmd cmd) {
    ResponseAssert.notNull(ResponseErrorCode.REQUEST_PARAMETER_ERROR, cmd);
    ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR, cmd.getSysOrigin());
    ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR, cmd.getCountryCode());
  }

  private Integer resolveBillBelong(Integer billBelong) {
    if (Objects.isNull(billBelong)) {
      return TeamBillCycleUtils.getCalcBillBelong();
    }
    int month = billBelong % 100;
    ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
        billBelong >= 200001 && billBelong <= 209912 && month >= 1 && month <= 12);
    return billBelong;
  }

  private static class PaymentSummary {

    private final BigDecimal totalIssuedSalary;
    private final BigDecimal memberIssuedSalary;
    private final BigDecimal agentIssuedSalary;
    private final BigDecimal overIssuedSalary;
    private final BigDecimal memberOverIssuedSalary;
    private final BigDecimal agentOverIssuedSalary;

    private PaymentSummary(BigDecimal totalIssuedSalary, BigDecimal memberIssuedSalary,
        BigDecimal agentIssuedSalary, BigDecimal overIssuedSalary,
        BigDecimal memberOverIssuedSalary, BigDecimal agentOverIssuedSalary) {
      this.totalIssuedSalary = totalIssuedSalary;
      this.memberIssuedSalary = memberIssuedSalary;
      this.agentIssuedSalary = agentIssuedSalary;
      this.overIssuedSalary = overIssuedSalary;
      this.memberOverIssuedSalary = memberOverIssuedSalary;
      this.agentOverIssuedSalary = agentOverIssuedSalary;
    }

    private BigDecimal getTotalIssuedSalary() {
      return totalIssuedSalary;
    }

    private BigDecimal getMemberIssuedSalary() {
      return memberIssuedSalary;
    }

    private BigDecimal getAgentIssuedSalary() {
      return agentIssuedSalary;
    }

    private BigDecimal getOverIssuedSalary() {
      return overIssuedSalary;
    }

    private BigDecimal getMemberOverIssuedSalary() {
      return memberOverIssuedSalary;
    }

    private BigDecimal getAgentOverIssuedSalary() {
      return agentOverIssuedSalary;
    }
  }

  private static class ManualSalaryRow {

    private final TeamManualSalaryPaymentDTO dto;
    private final TeamMemberTarget target;
    private final TeamProfile teamProfile;
    private final TeamPolicyManager policyManager;
    private final TeamBillMemberTarget billTarget;
    private final List<PolicyPayment> payments;

    private ManualSalaryRow(TeamManualSalaryPaymentDTO dto, TeamMemberTarget target,
        TeamProfile teamProfile, TeamPolicyManager policyManager, TeamBillMemberTarget billTarget,
        List<PolicyPayment> payments) {
      this.dto = dto;
      this.target = target;
      this.teamProfile = teamProfile;
      this.policyManager = policyManager;
      this.billTarget = billTarget;
      this.payments = payments;
    }

    private TeamManualSalaryPaymentDTO getDto() {
      return dto;
    }

    private String getMemberTargetId() {
      return target.getId();
    }

    private String getSysOrigin() {
      return target.getSysOrigin();
    }

    private String getCountryCode() {
      return dto.getCountryCode();
    }

    private String getRegion() {
      return dto.getRegion();
    }

    private Long getTeamId() {
      return target.getTeamId();
    }

    private Long getTeamOwnId() {
      return teamProfile.getOwnUserId();
    }

    private Long getUserId() {
      return target.getUserId();
    }

    private Long getCurrentTarget() {
      return dto.getCurrentTarget();
    }

    private BigDecimal getPayableSalary() {
      return dto.getPayableSalary();
    }

    private BigDecimal getTotalIssuedSalary() {
      return dto.getTotalIssuedSalary();
    }

    private BigDecimal getCurrentSalaryBalance() {
      return dto.getCurrentSalaryBalance();
    }

    private TeamPolicyManager getPolicyManager() {
      return policyManager;
    }

    private List<PolicyPayment> getPayments() {
      return payments;
    }
  }

  private static class PolicyPayment {

    private final TeamPolicy policy;
    private final Integer level;
    private final BigDecimal salary;
    private final Boolean anchor;

    private PolicyPayment(TeamPolicy policy, Integer level, BigDecimal salary, Boolean anchor) {
      this.policy = policy;
      this.level = level;
      this.salary = salary;
      this.anchor = anchor;
    }

    private TeamPolicy getPolicy() {
      return policy;
    }

    private Integer getLevel() {
      return level;
    }

    private BigDecimal getSalary() {
      return salary;
    }

    private Boolean getAnchor() {
      return anchor;
    }

    private boolean isAnchor() {
      return Boolean.TRUE.equals(anchor);
    }
  }
}
