package cn.iocoder.yudao.module.system.api.order;

import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.string.StringUtil;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.system.api.order.DTO.OrderItemRespDTO;
import cn.iocoder.yudao.module.system.api.order.VO.OrderItemRespVO;
import cn.iocoder.yudao.module.system.api.order.VO.OrderItemTotalRespVO;
import cn.iocoder.yudao.module.system.dal.dataobject.biz.*;
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
import cn.iocoder.yudao.module.system.dal.mysql.biz.*;
import cn.iocoder.yudao.module.system.dal.mysql.tenant.TenantMapper;
import cn.iocoder.yudao.module.system.enums.change.BusinessConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;

import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;

/**
 * @Author: jinyd
 * @CreateTime: 2024/11/27  8:53
 * @Description:
 * @Version: 4.0
 */

@Service
@Slf4j
public class OrderApiImpl implements OrderApi {

    // 进程级共享短期缓存：跨请求/线程复用 getReceivableListByMonth 结果，避免重复 DB 查询
    // 注意：仅做短期加速，TTL 到期后自然失效；数据一致性以数据库为准
    private static class CacheEntry {
        final List<OrderItemRespVO> data;
        final long expireAt;
        CacheEntry(List<OrderItemRespVO> data, long expireAt) {
            this.data = data;
            this.expireAt = expireAt;
        }
    }
    private final ConcurrentHashMap<String, CacheEntry> receivableCache = new ConcurrentHashMap<>();
    private static final long RECEIVABLE_CACHE_TTL_MS = 1 * 60 * 1000L; // 1 分钟

    private String buildReceivableCacheKey(Integer orgType, Long tenantId, String normalizedMonth) {
        return orgType + "|" + tenantId + "|" + normalizedMonth;
    }

    private String normalizeMonth(String queryFlag, String billingMonth) {
        if (StringUtil.isEmptyORNull(billingMonth)) return null;
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(FORMAT_YEAR_MONTH_DAY);
        LocalDate parse = LocalDate.parse(billingMonth + "-01", formatter);
        int queryInt = Integer.parseInt(queryFlag);
        if (queryInt > parse.getMonthValue()) {
            return null;
        }
        String month = String.format("%02d", queryInt);
        return parse.getYear() + "-" + month;
    }

    // 统计 getListByMonth 实际触发次数，辅助评估 DB 查询次数（每次大致包含 4 次 SQL）
    private final ThreadLocal<Integer> listByMonthCallCount = ThreadLocal.withInitial(() -> 0);

    @Autowired
    private ExpenseOrderMapper orderMapper;

    @Resource
    private ExpenseOrderItemMapper orderItemMapper;

    @Resource
    private ExpenseItemMapper itemMapper;

    @Resource
    private DailyExpensesMapper dailyExpensesMapper;

    @Resource
    private TenantMapper tenantMapper;

    @Resource
    private ElderlyInfoMapper elderlyInfoMapper;
    @Resource
    private ElderlyAskLeaveMapper elderlyAskLeaveMapper;

    @Resource
    private OutboundRefundConfigMapper outboundRefundConfigMapper;

    @Resource
    private RefundSettlementOrderMapper refundSettlementOrderMapper;

    @Resource
    private RefundSettlementOrderItemMapper refundSettlementOrderItemMapper;

    @Override
    @Cacheable(cacheNames = "order_api_list_by_month#30s",
            key = "T(String).valueOf(#orgType) + ':' + #tenantId + ':' + #billMonth + ':' + #inStatus + ':' + #payStatus",
            unless = "#result == null || #result.isEmpty()")
    public List<OrderItemRespVO> getListByMonth(Integer orgType, Long tenantId, String billMonth, Integer inStatus,Integer payStatus) {
        // 委托至重构后的实现，便于维护与单测
        return getListByMonthRefactored(orgType, tenantId, billMonth, inStatus, payStatus);
    }

    /**
     * 重构后的主流程：
     * 1) 查询账单头列表
     * 2) 一次性批量拉取账单项、月度费用、日常费用（避免 N+1）
     * 3) 按长者循环：先算标准应收，再按外出/变更调整实际应收，计算合计与欠费，输出备注
     */
    private List<OrderItemRespVO> getListByMonthRefactored(Integer orgType, Long tenantId, String billMonth, Integer inStatus,Integer payStatus) {
        // 统计调用次数，用于估算数据库查询数
        listByMonthCallCount.set(listByMonthCallCount.get() + 1);

        // 1. 查询订单列表
        List<OrderItemRespDTO> list = orderMapper.selectListByMonth(orgType, tenantId, billMonth, inStatus,payStatus);
        if (CollectionUtils.isEmpty(list)) {
            return new ArrayList<>();
        }

        // 2. 批量查询所有订单项，避免 N+1 查询
        List<Long> orderIds = list.stream()
                .map(OrderItemRespDTO::getOrderId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(orderIds)) {
            return new ArrayList<>();
        }

        List<ExpenseOrderItemDO> allOrderItems = orderItemMapper.selectList(
                new LambdaQueryWrapperX<ExpenseOrderItemDO>()
                        .in(ExpenseOrderItemDO::getExpenseOrderId, orderIds));
        Map<Long, List<ExpenseOrderItemDO>> orderItemsMap = groupOrderItems(allOrderItems);
        Map<Long, ExpenseItemDO> expenseItemMap = buildExpenseItemMap(allOrderItems);
        Map<Long, DailyExpensesDO> dailyExpenseMap = buildDailyExpenseMap(allOrderItems);

        Map<Long, Map<Integer, BigDecimal>> retreatDeductByElderAndType = buildRetreatDeductByElderAndType(tenantId, list);

        // 3. 处理数据，填充报表所需的所有字段
        List<OrderItemRespVO> result = new ArrayList<>();
        for (OrderItemRespDTO dto : list) {
            StringBuilder remarkBuilder = initRemarkBuilder(dto);
            initAmounts(dto);
            normalizeInStatusByBillMonth(dto, billMonth);

            List<ExpenseOrderItemDO> expenseOrderItemDOS = orderItemsMap.getOrDefault(dto.getOrderId(), new ArrayList<>());
            if (CollectionUtils.isEmpty(expenseOrderItemDOS)) {
                result.add(buildOrderItemRespVO(dto, billMonth));
                continue;
            }

            // 月度费用类（expenseSource=EXPENSE_ITEM）：按账单月区间计算“标准应收”和“实际应收”
            applyMonthlyItems(dto, expenseOrderItemDOS, expenseItemMap, billMonth);

            // 3) 日常费用的外出/变更等调整：叠加到“实际应收”
            applyDailyAdjustments(dto, expenseOrderItemDOS, dailyExpenseMap, remarkBuilder);
            computeTotals(dto);
            applyRefundSettlementDeductionsIfRetreat(dto, retreatDeductByElderAndType);
            computeArrears(dto, expenseOrderItemDOS, dailyExpenseMap);

            if (remarkBuilder.length() > 0) {
                dto.setRemark(remarkBuilder + (dto.getRemark() == null ? "" : dto.getRemark()));
            }

            result.add(buildOrderItemRespVO(dto, billMonth));
        }

        return result;
    }

    /** 按订单ID分组账单项，避免重复遍历 */
    private Map<Long, List<ExpenseOrderItemDO>> groupOrderItems(List<ExpenseOrderItemDO> allOrderItems) {
        return allOrderItems.stream().collect(Collectors.groupingBy(ExpenseOrderItemDO::getExpenseOrderId));
    }

    /** 构建月度费用项映射（主键 -> 费用项DO），用于快速查类型与名称 */
    private Map<Long, ExpenseItemDO> buildExpenseItemMap(List<ExpenseOrderItemDO> allOrderItems) {
        Set<Long> expenseItemIds = allOrderItems.stream()
                .filter(item -> BusinessConstants.EXPENSE_ITEM.equals(item.getExpenseSource()))
                .map(ExpenseOrderItemDO::getSourceExpenseItemId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (CollectionUtils.isEmpty(expenseItemIds)) {
            return new HashMap<>();
        }
        List<ExpenseItemDO> expenseItems = itemMapper.selectBatchIds(expenseItemIds);
        if (CollectionUtils.isEmpty(expenseItems)) {
            return new HashMap<>();
        }
        return expenseItems.stream().collect(Collectors.toMap(ExpenseItemDO::getId, item -> item, (v1, v2) -> v1));
    }

    /** 构建日常费用映射（主键 -> 日常费用DO），用于外出/变更和增值服务处理 */
    private Map<Long, DailyExpensesDO> buildDailyExpenseMap(List<ExpenseOrderItemDO> allOrderItems) {
        Set<Long> dailyExpenseIds = allOrderItems.stream()
                .filter(item -> BusinessConstants.DAILY_EXPENSES.equals(item.getExpenseSource()))
                .map(ExpenseOrderItemDO::getSourceExpenseItemId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (CollectionUtils.isEmpty(dailyExpenseIds)) {
            return new HashMap<>();
        }
            List<DailyExpensesDO> dailyExpenses = dailyExpensesMapper.selectBatchIds(dailyExpenseIds);
        if (CollectionUtils.isEmpty(dailyExpenses)) {
            return new HashMap<>();
        }
        return dailyExpenses.stream().collect(Collectors.toMap(DailyExpensesDO::getId, item -> item, (v1, v2) -> v1));
        }

    /** 统一初始化金额，保证后续累加安全 */
    private void initAmounts(OrderItemRespDTO dto) {
        dto.setBedAmount(BigDecimal.ZERO);
        dto.setMealAmount(BigDecimal.ZERO);
        dto.setNurseAmount(BigDecimal.ZERO);
        dto.setServiceAmount(BigDecimal.ZERO);
        dto.setBedActualAmount(BigDecimal.ZERO);
        dto.setMealActualAmount(BigDecimal.ZERO);
        dto.setNurseActualAmount(BigDecimal.ZERO);
        dto.setServiceActualAmount(BigDecimal.ZERO);
        dto.setExtraServiceAmount(BigDecimal.ZERO);
        dto.setOrderWaterElectricityAmount(BigDecimal.ZERO);
        dto.setTotalAmount(BigDecimal.ZERO);
        dto.setTotalActualAmount(BigDecimal.ZERO);
        dto.setAdjustAmount(BigDecimal.ZERO);
        dto.setArrearsAmount(BigDecimal.ZERO);
    }

    /**
     * 计算月度费用类（expenseSource=EXPENSE_ITEM）的标准应收与实际应收（一次遍历完成）：
     *
     * - 标准金额：取月度费用明细(elderly_expense_item)的实际金额 ExpenseItemDO.actualAmount
     * - 实际金额：取账单项金额 ExpenseOrderItemDO.totalAmount
     * - 费用分类：按 ExpenseItemDO.type 划分床/膳/护/服务
     */
    private void applyMonthlyItems(OrderItemRespDTO dto, List<ExpenseOrderItemDO> items,
                                   Map<Long, ExpenseItemDO> expenseItemMap,
                                   String billMonth) {
        YearMonth billYm = null;
        if (StringUtil.isNotEmptyORNull(billMonth)) {
            try {
                billYm = YearMonth.parse(billMonth);
            } catch (Exception ignore) {
                billYm = null;
            }
        }
        LocalDate billStart = billYm == null ? null : billYm.atDay(1);
        LocalDate billEnd = billYm == null ? null : billYm.atEndOfMonth();

        for (ExpenseOrderItemDO expenseOrderItemDO : items) {
            if (!BusinessConstants.EXPENSE_ITEM.equals(expenseOrderItemDO.getExpenseSource())) {
                continue;
            }
            ExpenseItemDO expenseItemDO = expenseItemMap.get(expenseOrderItemDO.getSourceExpenseItemId());
            if (expenseItemDO == null) {
                continue;
            }

            BigDecimal standardAmount = expenseItemDO.getActualAmount() == null
                    ? BigDecimal.ZERO : expenseItemDO.getActualAmount();
            BigDecimal actualAmount = expenseOrderItemDO.getTotalAmount() == null
                    ? BigDecimal.ZERO : expenseOrderItemDO.getTotalAmount();

            BigDecimal billedStandardAmount = standardAmount;

            if (billYm != null) {
                int daysInBillMonth = billYm.lengthOfMonth();
                LocalDate changeStartDate = expenseItemDO.getChangeStartDate();
                LocalDate changeEndDate = expenseItemDO.getChangeEndDate();

                // 费用生效区间与账单月做交集：
                // - 无开始日：视为本月月初
                // - 无结束日：视为本月月末
                LocalDate periodStart = changeStartDate == null ? billStart : changeStartDate;
                LocalDate periodEnd = changeEndDate == null ? billEnd : changeEndDate;

                LocalDate overlapStart = periodStart.isAfter(billStart) ? periodStart : billStart;
                LocalDate overlapEnd = periodEnd.isBefore(billEnd) ? periodEnd : billEnd;

                if (overlapEnd.isBefore(overlapStart)) {
                    // 该费用在账单月无覆盖，跳过
                    continue;
                }

                long actualDays = ChronoUnit.DAYS.between(overlapStart, overlapEnd) + 1;
                if (daysInBillMonth > 0 && actualDays > 0 && actualDays != daysInBillMonth) {
                    BigDecimal dayStandard = standardAmount
                            .divide(BigDecimal.valueOf(daysInBillMonth), 8, RoundingMode.HALF_UP);
                    billedStandardAmount = dayStandard
                            .multiply(BigDecimal.valueOf(actualDays))
                            .setScale(2, RoundingMode.HALF_UP);
                }
            }

            Integer type = expenseItemDO.getType();
            if ((type != null  && 1 == type) || expenseItemDO.getItemName().contains("床位费")) {
                dto.setBedItemName(expenseItemDO.getItemName());
                dto.setBedAmount(dto.getBedAmount().add(billedStandardAmount));
                dto.setBedActualAmount(dto.getBedActualAmount().add(actualAmount));
            } else if ((type != null  && 2 == type) || expenseItemDO.getItemName().contains("护理费")) {
                dto.setNurseAmount(dto.getNurseAmount().add(billedStandardAmount));
                dto.setNurseActualAmount(dto.getNurseActualAmount().add(actualAmount));
            } else if ((type != null  && 3 == type) || expenseItemDO.getItemName().contains("餐饮费")) {
                dto.setMealAmount(dto.getMealAmount().add(billedStandardAmount));
                dto.setMealActualAmount(dto.getMealActualAmount().add(actualAmount));
            } else if ((type != null  && 7 == type) ||  expenseItemDO.getItemName().contains("服务费")) {
                dto.setServiceAmount(dto.getServiceAmount().add(billedStandardAmount));
                dto.setServiceActualAmount(dto.getServiceActualAmount().add(actualAmount));
            }
        }
    }

    /**
     * 处理日常费用：
     * - type=2 变更 / type=3 外出：对实际应收做加减，并写入备注（金额为0跳过备注）
     * - 其他类型：增值服务费、点餐、护理、服务等累加到对应字段
     */
    private void applyDailyAdjustments(OrderItemRespDTO dto, List<ExpenseOrderItemDO> items,
                                       Map<Long, DailyExpensesDO> dailyExpenseMap, StringBuilder remarkBuilder) {
        for (ExpenseOrderItemDO expenseOrderItemDO : items) {
            if(expenseOrderItemDO.getTotalAmount().compareTo(BigDecimal.ZERO) == 0){
                continue;
            }
            if (!BusinessConstants.DAILY_EXPENSES.equals(expenseOrderItemDO.getExpenseSource())) {
                continue;
            }
            DailyExpensesDO dailyExpensesDO = dailyExpenseMap.get(expenseOrderItemDO.getSourceExpenseItemId());
            if (dailyExpensesDO == null) {
                continue;
            }

            Integer dailyExpenseType = dailyExpensesDO.getType();
            // type=2: 变更, type=3: 外出
            if (dailyExpenseType == null || (dailyExpenseType != 2 && dailyExpenseType != 3)) {
                handleOtherDailyExpenses(dto, dailyExpensesDO);
                continue;
            }

            BigDecimal adjustAmount = dailyExpensesDO.getAmount() == null ? BigDecimal.ZERO : dailyExpensesDO.getAmount();
                    String itemName = dailyExpensesDO.getItemName();
            String itemCategory = dailyExpensesDO.getItemCategory();

            boolean isBedFee = false, isMealFee = false, isNurseFee = false, isServiceFee = false;
                    if (itemName != null) {
                if (itemName.contains("床位") || itemName.contains("房费")) {
                    isBedFee = true;
                } else if (itemName.contains("膳食") || itemName.contains("餐费") || itemName.contains("伙食")) {
                    isMealFee = true;
                } else if (itemName.contains("护理")) {
                    isNurseFee = true;
                } else if (itemName.contains("服务")) {
                    isServiceFee = true;
                }
            }
            if (itemCategory != null) {
                if (itemCategory.contains("床位") || itemCategory.contains("房费")) {
                    isBedFee = true;
                } else if (itemCategory.contains("膳食") || itemCategory.contains("餐费") || itemCategory.contains("伙食")) {
                    isMealFee = true;
                } else if (itemCategory.contains("护理")) {
                    isNurseFee = true;
                } else if (itemCategory.contains("服务") && !itemCategory.contains("增值服务费")) {
                    isServiceFee = true;
                }
            }

            if (isBedFee) {
                dto.setBedActualAmount(dto.getBedActualAmount().add(adjustAmount));
                dto.setAdjustAmount(dto.getAdjustAmount().add(adjustAmount));
            } else if (isMealFee) {
                dto.setMealActualAmount(dto.getMealActualAmount().add(adjustAmount));
                dto.setAdjustAmount(dto.getAdjustAmount().add(adjustAmount));
            } else if (isNurseFee) {
                dto.setNurseActualAmount(dto.getNurseActualAmount().add(adjustAmount));
                dto.setAdjustAmount(dto.getAdjustAmount().add(adjustAmount));
            } else if (isServiceFee) {
                dto.setServiceActualAmount(dto.getServiceActualAmount().add(adjustAmount));
                dto.setAdjustAmount(dto.getAdjustAmount().add(adjustAmount));
            } else {
                dto.setAdjustAmount(dto.getAdjustAmount().add(adjustAmount));
            }

            // 记录调整说明到备注（调整金额为 0 时不追加）
            if (adjustAmount.compareTo(BigDecimal.ZERO) != 0) {
                String typeLabel = dailyExpenseType == 2 ? "变更" : "外出";
                String feeLabel = "其他";
                if (isBedFee) {
                    feeLabel = "床位费";
                } else if (isMealFee) {
                    feeLabel = "膳食费";
                } else if (isNurseFee) {
                    feeLabel = "护理费";
                } else if (isServiceFee) {
                    feeLabel = "服务费";
                }
                remarkBuilder.append(typeLabel)
                        .append("-")
                        .append(feeLabel)
                        .append(":")
                        .append(adjustAmount.toPlainString())
                        .append(";");
            }
        }
    }

    /** 处理非外出/变更的日常费用（增值、点餐、护理、服务等） */
    private void handleOtherDailyExpenses(OrderItemRespDTO dto, DailyExpensesDO dailyExpensesDO) {
        String itemName = dailyExpensesDO.getItemName();
        if (itemName != null) {
            BigDecimal roundAmount = dailyExpensesDO.getAmount() == null ? BigDecimal.ZERO : dailyExpensesDO.getAmount();
            if ("点餐".equals(itemName)) {
                BigDecimal currentAmount = dto.getOrderFoodAmount() == null ? BigDecimal.ZERO : dto.getOrderFoodAmount();
                dto.setOrderFoodAmount(currentAmount.add(roundAmount));
            }
            if (itemName.contains("生命体征") || itemName.contains("医疗护理")) {
                BigDecimal currentAmount = dto.getOrderNursingAmount() == null ? BigDecimal.ZERO : dto.getOrderNursingAmount();
                dto.setOrderNursingAmount(currentAmount.add(roundAmount));
            }
            if (itemName.contains("身体护理")) {
                BigDecimal currentAmount = dto.getOrderServiceAmount() == null ? BigDecimal.ZERO : dto.getOrderServiceAmount();
                dto.setOrderServiceAmount(currentAmount.add(roundAmount));
            }
        }
        BigDecimal extraAmount = dailyExpensesDO.getAmount() == null ? BigDecimal.ZERO : dailyExpensesDO.getAmount();
        if (isWaterElectricityItem(itemName)) {
            BigDecimal currentAmount = dto.getOrderWaterElectricityAmount() == null ? BigDecimal.ZERO : dto.getOrderWaterElectricityAmount();
            dto.setOrderWaterElectricityAmount(currentAmount.add(extraAmount));
        } else {
            dto.setExtraServiceAmount(dto.getExtraServiceAmount().add(extraAmount));
        }
        dto.setAdjustAmount(dto.getAdjustAmount().add(extraAmount));
        dto.setRemark((dto.getRemark() == null ? "" : dto.getRemark()) + dailyExpensesDO.getRemarks() + itemName + ":" + dailyExpensesDO.getAmount());
    }

    /** 判断是否为水电费相关项目 */
    private boolean isWaterElectricityItem(String itemName) {
        return itemName != null
                && (itemName.contains("水费") || itemName.contains("电费") || itemName.contains("水电费"));
    }

    /** 汇总标准/实际合计及占比 */
    private void computeTotals(OrderItemRespDTO dto) {
        BigDecimal standardTotal = dto.getBedAmount()
                .add(dto.getMealAmount())
                .add(dto.getServiceAmount())
                .add(dto.getNurseAmount());
        dto.setTotalAmount(standardTotal);

        BigDecimal actualTotal = dto.getBedActualAmount()
                .add(dto.getMealActualAmount().setScale(2, RoundingMode.HALF_UP))
                .add(dto.getServiceActualAmount().setScale(2, RoundingMode.HALF_UP))
                .add(dto.getNurseActualAmount().setScale(2, RoundingMode.HALF_UP))
                .add(dto.getExtraServiceAmount() == null ? BigDecimal.ZERO : dto.getExtraServiceAmount().setScale(2, RoundingMode.HALF_UP));
        dto.setTotalActualAmount(actualTotal);

        String actualProportion = "0%";
        if (standardTotal.compareTo(BigDecimal.ZERO) > 0) {
            BigDecimal proportion = actualTotal
                    .divide(standardTotal, 4, RoundingMode.HALF_UP)
                    .multiply(new BigDecimal("100"));
            proportion = proportion.setScale(2, RoundingMode.HALF_UP);
            actualProportion = proportion + "%";
        }
        dto.setActualProportion(actualProportion);
    }

    private Map<Long, Map<Integer, BigDecimal>> buildRetreatDeductByElderAndType(Long tenantId, List<OrderItemRespDTO> list) {
        if (tenantId == null || CollectionUtils.isEmpty(list)) {
            return Collections.emptyMap();
        }

        List<Long> elderIds = list.stream()
                .filter(Objects::nonNull)
                .filter(dto -> dto.getInStatus() != null && dto.getInStatus() == 2)
                .map(OrderItemRespDTO::getId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(elderIds)) {
            return Collections.emptyMap();
        }

        List<Map<String, Object>> rows = refundSettlementOrderItemMapper.selectRetreatRefundDeductGrouped(tenantId, elderIds);
        if (CollectionUtils.isEmpty(rows)) {
            return Collections.emptyMap();
        }

        Map<Long, Map<Integer, BigDecimal>> result = new HashMap<>();
        for (Map<String, Object> row : rows) {
            if (row == null) {
                continue;
            }
            Object elderIdObj = row.get("elderId");
            Object typeObj = row.get("expenseItemType");
            Object deductObj = row.get("totalDeduct");
            if (!(elderIdObj instanceof Number) || !(typeObj instanceof Number) || deductObj == null) {
                continue;
            }
            Long elderId = ((Number) elderIdObj).longValue();
            Integer type = ((Number) typeObj).intValue();
            BigDecimal deduct;
            if (deductObj instanceof BigDecimal) {
                deduct = (BigDecimal) deductObj;
            } else if (deductObj instanceof Number) {
                deduct = new BigDecimal(deductObj.toString());
            } else {
                try {
                    deduct = new BigDecimal(deductObj.toString());
                } catch (Exception e) {
                    continue;
                }
            }

            result.computeIfAbsent(elderId, k -> new HashMap<>())
                    .merge(type, deduct, BigDecimal::add);
        }
        return result;
    }

    private void applyRefundSettlementDeductionsIfRetreat(OrderItemRespDTO dto,
                                                         Map<Long, Map<Integer, BigDecimal>> retreatDeductByElderAndType) {
        if (dto == null || dto.getInStatus() == null || dto.getInStatus() != 2) {
            return;
        }
        if (dto.getId() == null) {
            return;
        }
        if (retreatDeductByElderAndType == null) {
            return;
        }

        Map<Integer, BigDecimal> typeMap = retreatDeductByElderAndType.get(dto.getId());
        if (typeMap == null || typeMap.isEmpty()) {
            return;
        }

        BigDecimal bedDeduct = nz(typeMap.get(1));
        BigDecimal nurseDeduct = nz(typeMap.get(2));
        BigDecimal mealDeduct = nz(typeMap.get(3));
        BigDecimal serviceDeduct = nz(typeMap.get(7));

        dto.setBedActualAmount(nz(dto.getBedActualAmount()).add(bedDeduct));
        dto.setMealActualAmount(nz(dto.getMealActualAmount()).add(mealDeduct));
        dto.setNurseActualAmount(nz(dto.getNurseActualAmount()).add(nurseDeduct));
        dto.setServiceActualAmount(nz(dto.getServiceActualAmount()).add(serviceDeduct));

        if (dto.getBedActualAmount().compareTo(BigDecimal.ZERO) < 0) {
            dto.setBedActualAmount(BigDecimal.ZERO);
        }
        if (dto.getMealActualAmount().compareTo(BigDecimal.ZERO) < 0) {
            dto.setMealActualAmount(BigDecimal.ZERO);
        }
        if (dto.getNurseActualAmount().compareTo(BigDecimal.ZERO) < 0) {
            dto.setNurseActualAmount(BigDecimal.ZERO);
        }
        if (dto.getServiceActualAmount().compareTo(BigDecimal.ZERO) < 0) {
            dto.setServiceActualAmount(BigDecimal.ZERO);
        }

        BigDecimal actualTotal = dto.getBedActualAmount()
                .add(dto.getMealActualAmount())
                .add(dto.getServiceActualAmount())
                .add(dto.getNurseActualAmount())
                .add(dto.getExtraServiceAmount() == null ? BigDecimal.ZERO : dto.getExtraServiceAmount());
        dto.setTotalActualAmount(actualTotal);

        BigDecimal standardTotal = dto.getTotalAmount() == null ? BigDecimal.ZERO : dto.getTotalAmount();
        if (standardTotal.compareTo(BigDecimal.ZERO) > 0) {
            BigDecimal proportion = actualTotal
                    .divide(standardTotal, 4, RoundingMode.HALF_UP)
                    .multiply(new BigDecimal("100"))
                    .setScale(2, RoundingMode.HALF_UP);
            dto.setActualProportion(proportion + "%");
        } else {
            dto.setActualProportion("0%");
        }
    }

    /** 计算欠费：只累加未缴费项的标准或日常应收金额 */
    private void computeArrears(OrderItemRespDTO dto, List<ExpenseOrderItemDO> items,
                               Map<Long, DailyExpensesDO> dailyExpenseMap) {
        for (ExpenseOrderItemDO expenseOrderItemDO : items) {
            if (expenseOrderItemDO.getPayStatus() == null || expenseOrderItemDO.getPayStatus() != 0) {
                continue;
            }
            BigDecimal unpaidAmount = BigDecimal.ZERO;
            if (BusinessConstants.EXPENSE_ITEM.equals(expenseOrderItemDO.getExpenseSource())) {
                unpaidAmount = expenseOrderItemDO.getTotalAmount() == null
                        ? BigDecimal.ZERO : expenseOrderItemDO.getTotalAmount();
            } else if (BusinessConstants.DAILY_EXPENSES.equals(expenseOrderItemDO.getExpenseSource())) {
                DailyExpensesDO dailyExpensesDO = dailyExpenseMap.get(expenseOrderItemDO.getSourceExpenseItemId());
                if (dailyExpensesDO != null) {
                    unpaidAmount = dailyExpensesDO.getAmount() == null
                            ? BigDecimal.ZERO : dailyExpensesDO.getAmount();
                }
            }
            dto.setArrearsAmount(dto.getArrearsAmount().add(unpaidAmount));
        }
    }

    private StringBuilder initRemarkBuilder(OrderItemRespDTO dto) {
        return new StringBuilder(StringUtil.isEmptyORNull(dto.getRemark()) ? "" : dto.getRemark());
    }

    /**
     * 按查询月份修正在住/退住口径：
     * - elderly_info.in_status=2 代表当前是“退住”
     * - 但历史月份查询时，如果退住日期在查询月份之后，则该月份仍应视为“在住”
     */
    private void normalizeInStatusByBillMonth(OrderItemRespDTO dto, String billMonth) {
        if (dto == null || StringUtil.isEmptyORNull(billMonth)) {
            return;
        }

        // billMonth: yyyy-MM
        YearMonth queryYm = YearMonth.parse(billMonth);
        LocalDate queryMonthStart = queryYm.atDay(1);
        LocalDate queryMonthEnd = queryYm.atEndOfMonth();

        // 1) 退住按月份修正：当前状态为退住，但退住日期在查询月份之后 => 该月仍视为在住
        if (dto.getInStatus() != null && dto.getInStatus() == 2) {
            if (dto.getCheckOutTime() == null) {
                // 没有退住时间时保持原样，避免误判
                return;
            }

            LocalDate checkOutDate = dto.getCheckOutTime().toInstant()
                    .atZone(java.time.ZoneId.systemDefault())
                    .toLocalDate();

            if (checkOutDate.isAfter(queryMonthEnd)) {
                dto.setInStatus(1);
            }else {
                return;
            }
        }

        // 2) 新入住：入住时间落在查询月份内 => 状态显示“新入住”
        if (dto.getCheckInTime() != null) {
            LocalDate checkInDate = dto.getCheckInTime().toInstant()
                    .atZone(java.time.ZoneId.systemDefault())
                    .toLocalDate();
            if (!checkInDate.isBefore(queryMonthStart) && !checkInDate.isAfter(queryMonthEnd)) {
                // 用 inStatus=3 表示“新入住”（仅用于展示，不回写数据库）
                dto.setInStatus(3);
            }
        }

    }

    /**
     * 构建 OrderItemRespVO 对象，填充所有报表所需字段
     */
    private OrderItemRespVO buildOrderItemRespVO(OrderItemRespDTO orderItemRespDTO, String billMonth) {
            OrderItemRespVO orderItemRespVO = BeanUtils.toBean(orderItemRespDTO, OrderItemRespVO.class);
        
        // 月份
            orderItemRespVO.setBillingMonth(billMonth);
        
        // 合同编号
        orderItemRespVO.setContractNumber(orderItemRespDTO.getContractNumber() == null ? "" : orderItemRespDTO.getContractNumber());
        
        // 姓名
        orderItemRespVO.setElderName(orderItemRespDTO.getElderName() == null ? "" : orderItemRespDTO.getElderName());
        
        // 性别转换：1-男，2-女
        String elderSexStr = "";
        if (orderItemRespDTO.getElderSex() != null) {
            elderSexStr = (orderItemRespDTO.getElderSex() == 1) ? "男" : "女";
        }
        orderItemRespVO.setElderSex(elderSexStr);
        
        // 年龄
        orderItemRespVO.setElderAge(orderItemRespDTO.getElderAge() == null ? 0 : orderItemRespDTO.getElderAge());
        
        // 楼层
        orderItemRespVO.setBuildName(orderItemRespDTO.getBuildName() == null ? "" : orderItemRespDTO.getBuildName());
        
        // 房号
        orderItemRespVO.setBedName(orderItemRespDTO.getBedName() == null ? "" : orderItemRespDTO.getBedName());
        
        // 房间类型
        orderItemRespVO.setBedItemName(orderItemRespDTO.getBedItemName() == null ? "" : orderItemRespDTO.getBedItemName());
        
        // 护理等级
        orderItemRespVO.setNurseLevelName(orderItemRespDTO.getNurseLevelName() == null ? "" : orderItemRespDTO.getNurseLevelName());
        
        // 照护类型，1：供养，2：寄养，3：托养
        String careTypeStr = "";
        if (orderItemRespDTO.getCareType() != null) {
            switch (orderItemRespDTO.getCareType()) {
                case 1: careTypeStr = "供养"; break;
                case 2: careTypeStr = "寄养"; break;
                case 3: careTypeStr = "托养"; break;
                default: careTypeStr = ""; break;
            }
        }
        orderItemRespVO.setCareType(careTypeStr);
        
        // 合同开始日
            orderItemRespVO.setBegintime(orderItemRespDTO.getBegintime() == null 
                    ? "" : orderItemRespDTO.getBegintime().toString());
        
        // 合同终止日
            orderItemRespVO.setExpireTime(orderItemRespDTO.getExpireTime() == null 
                    ? "" : orderItemRespDTO.getExpireTime().toString());
        
        // 合同期限（月份转年限）
        orderItemRespVO.setContractTerm(getYearStr(orderItemRespDTO.getContractTerm()));
        
        // 标准应收：确保所有字段都有默认值（按标准价格和标准天数计算）
        orderItemRespVO.setBedAmount(orderItemRespDTO.getBedAmount() == null ? BigDecimal.ZERO : orderItemRespDTO.getBedAmount());
        orderItemRespVO.setMealAmount(orderItemRespDTO.getMealAmount() == null ? BigDecimal.ZERO : orderItemRespDTO.getMealAmount());
        orderItemRespVO.setServiceAmount(orderItemRespDTO.getServiceAmount() == null ? BigDecimal.ZERO : orderItemRespDTO.getServiceAmount());
        orderItemRespVO.setNurseAmount(orderItemRespDTO.getNurseAmount() == null ? BigDecimal.ZERO : orderItemRespDTO.getNurseAmount());
        orderItemRespVO.setTotalAmount(orderItemRespDTO.getTotalAmount() == null ? BigDecimal.ZERO : orderItemRespDTO.getTotalAmount());
        
        // 实际应收：确保所有字段都有默认值（考虑外出、变更等情况后的实际应收金额，不是已缴费金额）
        orderItemRespVO.setBedActualAmount(orderItemRespDTO.getBedActualAmount() == null ? BigDecimal.ZERO : orderItemRespDTO.getBedActualAmount());
        orderItemRespVO.setMealActualAmount(orderItemRespDTO.getMealActualAmount() == null ? BigDecimal.ZERO : orderItemRespDTO.getMealActualAmount());
        orderItemRespVO.setServiceActualAmount(orderItemRespDTO.getServiceActualAmount() == null ? BigDecimal.ZERO : orderItemRespDTO.getServiceActualAmount());
        orderItemRespVO.setNurseActualAmount(orderItemRespDTO.getNurseActualAmount() == null ? BigDecimal.ZERO : orderItemRespDTO.getNurseActualAmount());
        
        // 增值服务费（11）
        orderItemRespVO.setExtraServiceAmount(orderItemRespDTO.getExtraServiceAmount() == null ? BigDecimal.ZERO : orderItemRespDTO.getExtraServiceAmount());
        
        // 实际应收合计（12）
        orderItemRespVO.setTotalActualAmount(orderItemRespDTO.getTotalActualAmount() == null ? BigDecimal.ZERO : orderItemRespDTO.getTotalActualAmount());
        
        // 实际与标准占比（13）
        orderItemRespVO.setActualProportion(orderItemRespDTO.getActualProportion() == null ? "0%" : orderItemRespDTO.getActualProportion());
        
        // 长者情况：1-在住，2-退住，3-新入住
        String inStatusStr = "";
        if (orderItemRespDTO.getInStatus() != null) {
            if (1 == orderItemRespDTO.getInStatus()) {
                inStatusStr = "在住";
            } else if (2 == orderItemRespDTO.getInStatus()) {
                inStatusStr = "退住";
            } else if (3 == orderItemRespDTO.getInStatus()) {
                inStatusStr = "本月新入住";
            }
        }
        orderItemRespVO.setInStatus(inStatusStr);

        // 账单类型
        orderItemRespVO.setOrderType(orderItemRespDTO.getOrderType());
        
        // 长者所属街道
        orderItemRespVO.setAddress(orderItemRespDTO.getAddress() == null ? "" : orderItemRespDTO.getAddress());
        
        // 收费情况：0-未缴费，1-已缴费，2-部分缴费
        String payStatusStr = "";
        if (orderItemRespDTO.getPayStatus() != null) {
            if (0 == orderItemRespDTO.getPayStatus()) {
                payStatusStr = "未缴费";
            } else if (1 == orderItemRespDTO.getPayStatus()) {
                payStatusStr = "已缴费";
            } else if (2 == orderItemRespDTO.getPayStatus()) {
                payStatusStr = "部分缴费";
            }
            }
        orderItemRespVO.setPayStatus(payStatusStr);
        
        // 欠费金额
        orderItemRespVO.setArrearsAmount(orderItemRespDTO.getArrearsAmount() == null ? BigDecimal.ZERO : orderItemRespDTO.getArrearsAmount());
        
        // 虚拟账户余额
        orderItemRespVO.setVirtualBalance(orderItemRespDTO.getVirtualBalance() == null ? BigDecimal.ZERO : orderItemRespDTO.getVirtualBalance());
        
        // 长护险账户余额
        orderItemRespVO.setExpenseSubsidyBalance(orderItemRespDTO.getExpenseSubsidyBalance() == null ? BigDecimal.ZERO : orderItemRespDTO.getExpenseSubsidyBalance());
        
        // 实际收费情况备注
        orderItemRespVO.setRemark(orderItemRespDTO.getRemark() == null ? "" : orderItemRespDTO.getRemark());
        
        return orderItemRespVO;
    }

    @Override
    public OrderItemTotalRespVO getTotalByMonth(Integer orgType, Long tenantId, String billingMonth,Integer payStatus) {

        // 使用与三明细接口一致的取数口：不按 in_status 过滤，避免分类差异导致的口径不一致
        List<OrderItemRespVO> all = getListByMonth(orgType, tenantId, billingMonth, null, payStatus);
        OrderItemTotalRespVO orderItemTotalRespVO = new OrderItemTotalRespVO();
        if (StringUtil.isEmptyORNull(billingMonth)) {
            return orderItemTotalRespVO;
        }
        if (CollectionUtils.isNotEmpty(all)) {
            for (OrderItemRespVO vo : all) {
                BigDecimal bedAmount = vo.getBedAmount() == null ? BigDecimal.ZERO : vo.getBedAmount();
                BigDecimal mealAmount = vo.getMealAmount() == null ? BigDecimal.ZERO : vo.getMealAmount();
                BigDecimal nurseAmount = vo.getNurseAmount() == null ? BigDecimal.ZERO : vo.getNurseAmount();
                BigDecimal serviceAmount = vo.getServiceAmount() == null ? BigDecimal.ZERO : vo.getServiceAmount();
                BigDecimal totalAmount = vo.getTotalAmount() == null ? BigDecimal.ZERO : vo.getTotalAmount();
                BigDecimal adjustTotalAmount = vo.getAdjustAmount() == null ? BigDecimal.ZERO : vo.getAdjustAmount();
                BigDecimal bedActualTotalAmount = vo.getBedActualAmount() == null ? BigDecimal.ZERO : vo.getBedActualAmount();
                BigDecimal mealActualTotalAmount = vo.getMealActualAmount() == null ? BigDecimal.ZERO : vo.getMealActualAmount();
                BigDecimal nurseActualTotalAmount = vo.getNurseActualAmount() == null ? BigDecimal.ZERO : vo.getNurseActualAmount();
                BigDecimal serviceActualTotalAmount = vo.getServiceActualAmount() == null ? BigDecimal.ZERO : vo.getServiceActualAmount();
                BigDecimal totalActualAmount = vo.getTotalActualAmount() == null ? BigDecimal.ZERO : vo.getTotalActualAmount();
                BigDecimal extraServiceAmount = vo.getExtraServiceAmount() == null ? BigDecimal.ZERO : vo.getExtraServiceAmount();

                orderItemTotalRespVO.setBedTotalAmount(orderItemTotalRespVO.getBedTotalAmount().add(bedAmount));
                orderItemTotalRespVO.setMealTotalAmount(orderItemTotalRespVO.getMealTotalAmount().add(mealAmount));
                orderItemTotalRespVO.setNurseTotalAmount(orderItemTotalRespVO.getNurseTotalAmount().add(nurseAmount));
                orderItemTotalRespVO.setServiceTotalAmount(orderItemTotalRespVO.getServiceTotalAmount().add(serviceAmount));
                orderItemTotalRespVO.setTotalAmount(orderItemTotalRespVO.getTotalAmount().add(totalAmount));
                orderItemTotalRespVO.setAdjustTotalAmount(orderItemTotalRespVO.getAdjustTotalAmount().add(adjustTotalAmount));
                orderItemTotalRespVO.setBedActualTotalAmount(orderItemTotalRespVO.getBedActualTotalAmount().add(bedActualTotalAmount));
                orderItemTotalRespVO.setMealActualTotalAmount(orderItemTotalRespVO.getMealActualTotalAmount().add(mealActualTotalAmount));
                orderItemTotalRespVO.setNurseActualTotalAmount(orderItemTotalRespVO.getNurseActualTotalAmount().add(nurseActualTotalAmount));
                orderItemTotalRespVO.setServiceActualTotalAmount(orderItemTotalRespVO.getServiceActualTotalAmount().add(serviceActualTotalAmount));
                orderItemTotalRespVO.setTotalActualAmount(orderItemTotalRespVO.getTotalActualAmount().add(totalActualAmount));
                orderItemTotalRespVO.setExtraServiceAmount(orderItemTotalRespVO.getExtraServiceAmount().add(extraServiceAmount));
            }
        }

        String actualTotalProportion = "";
        if (orderItemTotalRespVO.getTotalAmount().compareTo(BigDecimal.ZERO) > 0) {
            BigDecimal multiply = orderItemTotalRespVO.getTotalActualAmount()
                    .divide(orderItemTotalRespVO.getTotalAmount(), 2, BigDecimal.ROUND_HALF_UP)
                    .multiply(new BigDecimal("100"));
            multiply = multiply.setScale(0, BigDecimal.ROUND_HALF_UP);
            actualTotalProportion = multiply.toString() + "%";
        } else {
            actualTotalProportion = "0";
        }
        orderItemTotalRespVO.setActualTotalProportion(actualTotalProportion);
        orderItemTotalRespVO.setBillingMonth(billingMonth);
        TenantDO tenantDO = tenantMapper.selectById(tenantId);
        orderItemTotalRespVO.setTenantName(tenantDO.getName());
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(FORMAT_YEAR_MONTH_DAY);
        LocalDate parse = LocalDate.parse(billingMonth + "-01", formatter);
        // 上个月
        LocalDate lastMonth = parse.minusMonths(1);
        // 获取年月
        DateTimeFormatter lastFormatter = DateTimeFormatter.ofPattern("yyyy-MM");
        String lastMonthStr = lastMonth.format(lastFormatter);

        // 账单口径统计人数：基于当月账单明细去重后的长者集合
        int add = 0;
        int out = 0;
        Set<Long> uniqueElders = new HashSet<>();
        Set<Long> uniqueAdd = new HashSet<>();
        Set<Long> uniqueOut = new HashSet<>();
        for (OrderItemRespVO vo : all) {
            if (vo == null || vo.getId() == null) {
                continue;
            }
            uniqueElders.add(vo.getId());
            if (vo.getOrderType() != null && (vo.getOrderType() == 1 || vo.getOrderType() == 3)) {
                uniqueAdd.add(vo.getId());
            }
            if ("退住".equals(vo.getInStatus())) {
                uniqueOut.add(vo.getId());
            }
        }
        add = uniqueAdd.size();
        out = uniqueOut.size();

        // 上月月底在住：同样用账单口径（上月账单中在住的人数）
        int lastIn = 0;
        try {
            Integer dbLastIn = orderMapper.countLastInByMonth(orgType, tenantId, lastMonthStr, payStatus);
            lastIn = dbLastIn == null ? 0 : dbLastIn;
        } catch (Exception ignore) {
            // 保底：异常时按 0 处理，避免影响金额汇总
        }

        int currentIn = all.size() - out;
        if (currentIn < 0) {
            currentIn = 0;
        }

        orderItemTotalRespVO.setLastElderlyInNumber(lastIn);
        orderItemTotalRespVO.setCurrentElderlyAddNumber(add);
        orderItemTotalRespVO.setCurrentElderlyOutNumber(out);
        orderItemTotalRespVO.setCurrentElderlyInNumber(currentIn);
        return orderItemTotalRespVO;
    }

    @Override
    public List<OrderItemRespVO> getReceivableListByMonth(Integer orgType, Long tenantId, String queryFLag, String billingMonth,Integer payStatus) {
        if (StringUtil.isEmptyORNull(billingMonth)) return null;

        // 规范化出查询月份（yyyy-MM），并校验月份合法性
        String normalizedMonth = normalizeMonth(queryFLag, billingMonth);
        if (normalizedMonth == null) return null;

        // 进程级短期缓存，复用刚刚查询过的月份数据（满足“必定先调且一致”的前置条件）
        String cacheKey = buildReceivableCacheKey(orgType, tenantId, normalizedMonth);
        CacheEntry entry = receivableCache.get(cacheKey);
        long now = System.currentTimeMillis();
        if (entry != null && entry.expireAt > now) {
            log.debug("getReceivableListByMonth cache hit: key={}", cacheKey);
            return entry.data;
        }

        // 查询数据库（内部会合并N+1），并写入短期缓存
        List<OrderItemRespVO> listByMonth = getListByMonth(orgType, tenantId, normalizedMonth, null,payStatus);
        receivableCache.put(cacheKey, new CacheEntry(listByMonth, now + RECEIVABLE_CACHE_TTL_MS));
        return listByMonth;
    }

    @Override
    public OrderItemTotalRespVO getOrderReceivableTotal(Integer orgType, Long tenantId, String billingMonth,Integer payStatus) {
        OrderItemTotalRespVO total = new OrderItemTotalRespVO();
        TenantDO tenantDO = tenantMapper.selectById(tenantId);
        total.setTenantName(tenantDO.getName());
        total.setBillingMonth(billingMonth);
        List<OrderItemRespVO> list = new ArrayList<>();
        for (int i = 1; i <= Integer.parseInt(billingMonth.split("-")[1]); i++) {
            list.addAll(getReceivableListByMonth(orgType, tenantId, String.valueOf(i), billingMonth, payStatus));
        }
        // 汇总当月数据：直接复用 getListByMonth（避免重复 DB 查询与计算）
        if (CollectionUtils.isEmpty(list)) {
            return total;
        }

        for (OrderItemRespVO vo : list) {
            BigDecimal bedAmount = nz(vo.getBedAmount());
            BigDecimal mealAmount = nz(vo.getMealAmount());
            BigDecimal nurseAmount = nz(vo.getNurseAmount());
            BigDecimal serviceAmount = nz(vo.getServiceAmount());
            BigDecimal totalAmount = nz(vo.getTotalAmount());
            BigDecimal adjustAmount = nz(vo.getAdjustAmount());
            BigDecimal bedActual = nz(vo.getBedActualAmount());
            BigDecimal mealActual = nz(vo.getMealActualAmount());
            BigDecimal nurseActual = nz(vo.getNurseActualAmount());
            BigDecimal serviceActual = nz(vo.getServiceActualAmount());
            BigDecimal totalActual = nz(vo.getTotalActualAmount());
            BigDecimal extraService = nz(vo.getExtraServiceAmount());

            total.setBedTotalAmount(total.getBedTotalAmount().add(bedAmount));
            total.setMealTotalAmount(total.getMealTotalAmount().add(mealAmount));
            total.setNurseTotalAmount(total.getNurseTotalAmount().add(nurseAmount));
            total.setServiceTotalAmount(total.getServiceTotalAmount().add(serviceAmount));
            total.setTotalAmount(total.getTotalAmount().add(totalAmount));
            total.setAdjustTotalAmount(total.getAdjustTotalAmount().add(adjustAmount));
            total.setBedActualTotalAmount(total.getBedActualTotalAmount().add(bedActual));
            total.setMealActualTotalAmount(total.getMealActualTotalAmount().add(mealActual));
            total.setNurseActualTotalAmount(total.getNurseActualTotalAmount().add(nurseActual));
            total.setServiceActualTotalAmount(total.getServiceActualTotalAmount().add(serviceActual));
            total.setTotalActualAmount(total.getTotalActualAmount().add(totalActual));
            total.setExtraServiceAmount(total.getExtraServiceAmount().add(extraService));
                }

        String proportion = "0";
        if (total.getTotalAmount().compareTo(BigDecimal.ZERO) > 0) {
            BigDecimal p = total.getTotalActualAmount()
                    .divide(total.getTotalAmount(), 2, BigDecimal.ROUND_HALF_UP)
                    .multiply(new BigDecimal("100"))
                    .setScale(0, BigDecimal.ROUND_HALF_UP);
            proportion = p.toString() + "%";
        }
        total.setActualTotalProportion(proportion);

        if (StringUtil.isNotEmptyORNull(billingMonth)) {
            String[] split = billingMonth.split("-");
            total.setYear(split[0]);
            total.setBillingMonth(split[0] + "年" + split[1] + "月");
        }
        return total;
    }

    /** BigDecimal null 安全处理 */
    private BigDecimal nz(BigDecimal val) {
        return val == null ? BigDecimal.ZERO : val;
    }

    private String getYearStr(Integer contractTerm) {
        if (null == contractTerm || 0 == contractTerm) return "";
        if (contractTerm < 12) {
            if (6 == contractTerm) {
                return "半年";
            }
            return contractTerm + "月";
        }
        Integer year = contractTerm / 12;
        return year + "年";

    }
    /**
     * 查询长者本月是否整月外出，且外出配置名称为"离住"
     * 支持分段外出场景：如果同一个月内有多次外出记录，合并后覆盖整个月也算整月外出
     *
     * @param elderId 长者ID
     * @param month 月份，格式：yyyy-MM
     * @return 如果满足条件（整月外出且配置为"离住"），返回外出记录信息；否则返回null
     */
    public ElderlyAskLeaveDO checkElderlyWholeMonthOutbound(Long elderId, String month) {
        if (elderId == null || StringUtil.isEmptyORNull(month)) {
            return null;
        }

        try {
            // 1. 查询名称为"离住"的外出退费配置
            OutboundRefundConfigDO outboundConfig = outboundRefundConfigMapper.selectOne(
                    new LambdaQueryWrapperX<OutboundRefundConfigDO>()
                            .eq(OutboundRefundConfigDO::getName, "离住")
                            .last("LIMIT 1"));

            if (outboundConfig == null) {
                log.debug("未找到名称为'离住'的外出退费配置");
                return null;
            }

            // 2. 解析月份，获取月初和月末日期
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(FORMAT_YEAR_MONTH_DAY);
            LocalDate monthStart = LocalDate.parse(month + "-01", formatter);
            LocalDate monthEnd = monthStart.withDayOfMonth(monthStart.lengthOfMonth());

            // 转换为Date类型用于查询
            Date monthStartDate = java.sql.Date.valueOf(monthStart);
            Date monthEndDate = java.sql.Date.valueOf(monthEnd);

            // 3. 查询该长者的所有外出记录，条件：
            //    - 外出配置ID匹配"离住"配置
            //    - 外出时间在月份范围内或之前，且可能与月份有交集
            List<ElderlyAskLeaveDO> askLeaveList = elderlyAskLeaveMapper.selectList(
                    new LambdaQueryWrapperX<ElderlyAskLeaveDO>()
                            .eq(ElderlyAskLeaveDO::getElderId, elderId)
                            .eq(ElderlyAskLeaveDO::getOutboundRefundConfigId, outboundConfig.getId())
                            .eq(ElderlyAskLeaveDO::getStatus, 0) // 0表示正常状态
                            .le(ElderlyAskLeaveDO::getOutDate, monthEndDate) // 外出时间不能晚于月末
                            .and(wrapper -> wrapper
                                    // 返回时间>=月初 或 返回时间为空（还在外出中）
                                    .ge(ElderlyAskLeaveDO::getUpdateDate, monthStartDate)
                                    .or()
                                    .isNull(ElderlyAskLeaveDO::getUpdateDate))
                            .orderByAsc(ElderlyAskLeaveDO::getOutDate)); // 按外出时间排序

            if (CollectionUtils.isEmpty(askLeaveList)) {
                return null;
            }

            // 4. 先检查是否有单次整月外出的记录（快速路径）
            for (ElderlyAskLeaveDO askLeave : askLeaveList) {
                if (isWholeMonthOutbound(askLeave, monthStart, monthEnd)) {
                    return askLeave;
                }
            }

            // 5. 如果没有单次整月外出，检查分段外出是否覆盖整月
            if (isWholeMonthByMultipleOutbounds(askLeaveList, monthStart, monthEnd)) {
                // 返回最早的外出记录作为代表
                return askLeaveList.get(0);
            }

            return null;
        } catch (Exception e) {
            log.error("查询长者整月外出信息失败，elderId={}, month={}", elderId, month, e);
            return null;
        }
    }

    public List<ElderlyAskLeaveDO> listLeaveRecordsByMonth(Long elderId, String month) {
        if (elderId == null || StringUtil.isEmptyORNull(month)) {
            return Collections.emptyList();
        }
        // 仅查询外出配置名称为“离住”，且与当月有时间交集的记录
        OutboundRefundConfigDO outboundConfig = outboundRefundConfigMapper.selectOne(
                new LambdaQueryWrapperX<OutboundRefundConfigDO>()
                        .eq(OutboundRefundConfigDO::getName, "离住")
                        .last("LIMIT 1"));
        if (outboundConfig == null) {
            return Collections.emptyList();
        }

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(FORMAT_YEAR_MONTH_DAY);
        LocalDate monthStart = LocalDate.parse(month + "-01", formatter);
        LocalDate monthEnd = monthStart.withDayOfMonth(monthStart.lengthOfMonth());
        Date monthStartDate = java.sql.Date.valueOf(monthStart);
        Date monthEndDate = java.sql.Date.valueOf(monthEnd);

        return elderlyAskLeaveMapper.selectList(
                new LambdaQueryWrapperX<ElderlyAskLeaveDO>()
                        .eq(ElderlyAskLeaveDO::getElderId, elderId)
                        .eq(ElderlyAskLeaveDO::getOutboundRefundConfigId, outboundConfig.getId())
                        .eq(ElderlyAskLeaveDO::getStatus, 0)
                        // 有时间交集：外出开始<=月底，返回时间>=月初或为空
                        .le(ElderlyAskLeaveDO::getOutDate, monthEndDate)
                        .and(w -> w.ge(ElderlyAskLeaveDO::getUpdateDate, monthStartDate).or().isNull(ElderlyAskLeaveDO::getUpdateDate))
                        .orderByAsc(ElderlyAskLeaveDO::getOutDate));
    }

    /**
     * 判断外出记录是否覆盖整月（单次外出）
     * 整月外出的条件：
     * 1. 外出时间 <= 月初（包括等于月初）
     * 2. 返回时间 >= 月末（或没有返回时间，表示还在外出中）
     *
     * @param askLeave 外出记录
     * @param monthStart 月初日期
     * @param monthEnd 月末日期
     * @return true表示整月外出，false表示非整月外出
     */
    private boolean isWholeMonthOutbound(ElderlyAskLeaveDO askLeave, LocalDate monthStart, LocalDate monthEnd) {
        if (askLeave.getOutDate() == null) {
            return false;
        }

        // 将Date转换为LocalDate
        LocalDate outDate = askLeave.getOutDate().toInstant()
                .atZone(java.time.ZoneId.systemDefault())
                .toLocalDate();

        // 外出时间必须 <= 月初（包括等于月初），才能算整月外出
        if (outDate.isAfter(monthStart)) {
            return false;
        }

        // 如果有返回时间，判断是否>=月末
        if (askLeave.getUpdateDate() != null) {
            LocalDate updateDate = askLeave.getUpdateDate().toInstant()
                    .atZone(java.time.ZoneId.systemDefault())
                    .toLocalDate();
            // 返回时间必须>=月末，才算整月外出
            return !updateDate.isBefore(monthEnd);
        } else {
            // 如果没有返回时间，且外出时间<=月初，视为整月外出（可能还在外出中）
            return true;
        }
    }

    /**
     * 判断多次分段外出是否覆盖整月
     * 例如：2025-11-01外出，2025-11-15回来，2025-11-15又外出，2025-11-30回来，这样也算整月外出
     *
     * @param askLeaveList 外出记录列表（已按外出时间排序）
     * @param monthStart 月初日期
     * @param monthEnd 月末日期
     * @return true表示分段外出覆盖整月，false表示未覆盖整月
     */
    private boolean isWholeMonthByMultipleOutbounds(List<ElderlyAskLeaveDO> askLeaveList,
                                                    LocalDate monthStart, LocalDate monthEnd) {
        if (CollectionUtils.isEmpty(askLeaveList)) {
            return false;
        }

        // 将外出记录转换为时间段列表
        List<TimeRange> timeRanges = new ArrayList<>();
        for (ElderlyAskLeaveDO askLeave : askLeaveList) {
            if (askLeave.getOutDate() == null) {
                continue;
            }

            LocalDate outDate = askLeave.getOutDate().toInstant()
                    .atZone(java.time.ZoneId.systemDefault())
                    .toLocalDate();

            // 外出时间必须在该月或之前
            if (outDate.isAfter(monthEnd)) {
                continue;
            }

            LocalDate endDate;
            if (askLeave.getUpdateDate() != null) {
                endDate = askLeave.getUpdateDate().toInstant()
                        .atZone(java.time.ZoneId.systemDefault())
                        .toLocalDate();
                // 返回时间必须在该月或之后
                if (endDate.isBefore(monthStart)) {
                    continue;
                }
            } else {
                // 没有返回时间，视为还在外出中，结束时间设为月末
                endDate = monthEnd;
            }

            // 只考虑与月份有交集的时间段
            LocalDate rangeStart = outDate.isBefore(monthStart) ? monthStart : outDate;
            LocalDate rangeEnd = endDate.isAfter(monthEnd) ? monthEnd : endDate;

            if (!rangeStart.isAfter(rangeEnd)) {
                timeRanges.add(new TimeRange(rangeStart, rangeEnd));
            }
        }

        if (timeRanges.isEmpty()) {
            return false;
        }

        // 合并重叠或连续的时间段
        List<TimeRange> mergedRanges = mergeTimeRanges(timeRanges);

        // 检查合并后的时间段是否覆盖整个月
        // 必须满足：最早开始时间 <= 月初，最晚结束时间 >= 月末，且中间没有空隙
        if (mergedRanges.isEmpty()) {
            return false;
        }

        TimeRange firstRange = mergedRanges.get(0);
        TimeRange lastRange = mergedRanges.get(mergedRanges.size() - 1);

        // 最早开始时间必须 <= 月初
        if (firstRange.start.isAfter(monthStart)) {
            return false;
        }

        // 最晚结束时间必须 >= 月末
        if (lastRange.end.isBefore(monthEnd)) {
            return false;
        }

        // 如果只有一个时间段，且满足上述条件，则覆盖整月
        if (mergedRanges.size() == 1) {
            return true;
        }

        // 多个时间段时，检查时间段之间是否有空隙（不能有未覆盖的日期）
        for (int i = 0; i < mergedRanges.size() - 1; i++) {
            TimeRange current = mergedRanges.get(i);
            TimeRange next = mergedRanges.get(i + 1);

            // 如果当前结束时间和下一个开始时间之间有间隔（超过1天），说明有空隙
            if (current.end.plusDays(1).isBefore(next.start)) {
                return false;
            }
        }

        return true;
    }

    /**
     * 合并重叠或连续的时间段
     *
     * @param timeRanges 时间段列表（已排序）
     * @return 合并后的时间段列表
     */
    private List<TimeRange> mergeTimeRanges(List<TimeRange> timeRanges) {
        if (CollectionUtils.isEmpty(timeRanges)) {
            return new ArrayList<>();
        }

        // 按开始时间排序
        timeRanges.sort((a, b) -> a.start.compareTo(b.start));

        List<TimeRange> merged = new ArrayList<>();
        TimeRange current = timeRanges.get(0);

        for (int i = 1; i < timeRanges.size(); i++) {
            TimeRange next = timeRanges.get(i);

            // 如果当前时间段和下一个时间段重叠或连续（间隔<=1天），则合并
            if (!current.end.isBefore(next.start.minusDays(1))) {
                // 合并：取较晚的结束时间
                if (next.end.isAfter(current.end)) {
                    current = new TimeRange(current.start, next.end);
                }
            } else {
                // 不重叠，保存当前时间段，开始处理下一个
                merged.add(current);
                current = next;
            }
        }

        // 添加最后一个时间段
        merged.add(current);

        return merged;
    }

    /**
     * 时间段内部类
     */
    private static class TimeRange {
        LocalDate start;
        LocalDate end;

        TimeRange(LocalDate start, LocalDate end) {
            this.start = start;
            this.end = end;
        }
    }

    @Override
    public List<OrderItemRespVO> getOrderSubListForReport(Integer orgType, Long tenantId, String billingMonth, Integer payStatus) {
        List<OrderItemRespVO> listByMonth = getListByMonth(orgType, tenantId, billingMonth, null, payStatus);
        if (CollectionUtils.isEmpty(listByMonth)) {
            return listByMonth;
        }

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
        LocalDate monthStart = YearMonth.parse(billingMonth, formatter).atDay(1);
        LocalDate monthEnd = monthStart.withDayOfMonth(monthStart.lengthOfMonth());

        // 批量查询离住记录，避免逐条查询导致性能问题
        Set<Long> elderIds = listByMonth.stream()
                .map(OrderItemRespVO::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, List<ElderlyAskLeaveDO>> leaveMap = fetchLeaveRecordsByElders(elderIds, monthStart, monthEnd,tenantId);

        for (OrderItemRespVO vo : listByMonth) {
            Long elderId = vo.getId();
            if (elderId == null) {
                continue;
            }

            List<ElderlyAskLeaveDO> leaveList = leaveMap.get(elderId);
            if (CollectionUtils.isEmpty(leaveList)) {
                continue;
            }

            // 1) 整月离住，分组为“离住”
            boolean whole = leaveList.stream().anyMatch(r -> isWholeMonthOutbound(r, monthStart, monthEnd))
                    || isWholeMonthByMultipleOutbounds(leaveList, monthStart, monthEnd);
            if (whole) {
                vo.setInStatus("离住");
                continue;
            }

            boolean hasEndInMonth = false;
            boolean hasStartInMonthAndNotEnd = false;

            for (ElderlyAskLeaveDO leave : leaveList) {
                LocalDate outDate = leave.getOutDate() == null ? null :
                        leave.getOutDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
                LocalDate backDate = leave.getUpdateDate() == null ? null :
                        leave.getUpdateDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();

                if (backDate != null && !backDate.isBefore(monthStart) && !backDate.isAfter(monthEnd)) {
                    hasEndInMonth = true;
                }
                if (outDate != null && !outDate.isBefore(monthStart) && !outDate.isAfter(monthEnd)) {
                    if (backDate == null || backDate.isAfter(monthEnd)) {
                        hasStartInMonthAndNotEnd = true;
                    }
                }
            }

            String remark = vo.getRemark() == null ? "" : vo.getRemark();

            // 退住 + 当月有离住记录 => 离住转退住（保持退住）
            if ("退住".equals(vo.getInStatus())) {
                vo.setRemark(appendRemark(remark, "离住转退住"));
                continue;
            }

            // 在住场景：当月开始未结束 => 在住转离住；当月结束 => 离住转在住
            if (hasStartInMonthAndNotEnd) {
                vo.setRemark(appendRemark(remark, "在住转离住"));
                vo.setInStatus("在住");
            } else if (hasEndInMonth) {
                vo.setRemark(appendRemark(remark, "离住转在住"));
                vo.setInStatus("在住");
            }
        }
        return listByMonth;
    }

    /**
     * 批量查询指定长者在某月的“离住”外出记录（有交集的记录），并按 elderId 分组
     */
    private Map<Long, List<ElderlyAskLeaveDO>> fetchLeaveRecordsByElders(Set<Long> elderIds, LocalDate monthStart, LocalDate monthEnd,Long tenantId) {
        if (CollectionUtils.isEmpty(elderIds)) {
            return Collections.emptyMap();
        }

        OutboundRefundConfigDO outboundConfig = outboundRefundConfigMapper.selectOne(
                new LambdaQueryWrapperX<OutboundRefundConfigDO>()
                        .eq(OutboundRefundConfigDO::getName, "离住")
                        .eq(OutboundRefundConfigDO::getTenantId,tenantId)
                        .last("LIMIT 1"));
        if (outboundConfig == null) {
            return Collections.emptyMap();
        }

        Date monthStartDate = java.sql.Date.valueOf(monthStart);
        Date monthEndDate = java.sql.Date.valueOf(monthEnd);

        List<ElderlyAskLeaveDO> records = elderlyAskLeaveMapper.selectList(
                new LambdaQueryWrapperX<ElderlyAskLeaveDO>()
                        .in(ElderlyAskLeaveDO::getElderId, elderIds)
                        .eq(ElderlyAskLeaveDO::getOutboundRefundConfigId, outboundConfig.getId())
                        .eq(ElderlyAskLeaveDO::getStatus, 0)
                        .le(ElderlyAskLeaveDO::getOutDate, monthEndDate)
                        .and(w -> w.ge(ElderlyAskLeaveDO::getUpdateDate, monthStartDate).or().isNull(ElderlyAskLeaveDO::getUpdateDate))
                        .orderByAsc(ElderlyAskLeaveDO::getElderId, ElderlyAskLeaveDO::getOutDate)
        );

        if (CollectionUtils.isEmpty(records)) {
            return Collections.emptyMap();
        }

        return records.stream().collect(Collectors.groupingBy(ElderlyAskLeaveDO::getElderId));
    }

    private String appendRemark(String base, String addition) {
        if (StringUtil.isEmptyORNull(base)) {
            return addition;
        }
        if (base.endsWith(";")) {
            return base + addition;
        }
        return base + ";" + addition;
    }



}
