package com.yc.ship.module.trade.service.report.impl;

import cn.hutool.core.collection.CollUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.converters.longconverter.LongStringConverter;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yc.ship.framework.common.pojo.PageResult;
import com.yc.ship.framework.excel.core.util.ExcelUtils;
import com.yc.ship.module.resource.helper.DateHelper;
import com.yc.ship.module.resource.helper.MathHelper;
import com.yc.ship.module.trade.controller.admin.report.vo.CruiseOpsDailyReqVO;
import com.yc.ship.module.trade.controller.admin.report.vo.CruiseOpsDailyRespVO;
import com.yc.ship.module.trade.controller.admin.report.vo.YangtzePassengerSummaryPageReqVO;
import com.yc.ship.module.trade.controller.admin.report.vo.YangtzePassengerSummaryRespVO;
import com.yc.ship.module.trade.dal.mysql.report.OpsDailyMapper;
import com.yc.ship.module.trade.dal.mysql.report.YangtzePassengerSummaryMapper;
import com.yc.ship.module.trade.service.report.OpsDailyService;
import com.yc.ship.module.trade.utils.excel.CruiseOpsDailyExportStyleHandler;
import com.yc.ship.module.trade.utils.excel.YangtzePassengerSummaryExportStyleHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 省际度假游轮经营情况日报 Service 实现类
 * <p>
 * 数据结构：日常数据行 + 月度小计(数据+同比) + 累计行(数据+同比)
 *
 * @author system
 */
@Service
@Slf4j
public class OpsDailyServiceImpl implements OpsDailyService {

    @Resource
    private OpsDailyMapper cruiseOpsDailyMapper;

    @Override
    public List<CruiseOpsDailyRespVO> getCruiseOpsDailyList(CruiseOpsDailyReqVO reqVO) {
        // 1. 查询本期原始数据
        List<CruiseOpsDailyRespVO> currentDataList = cruiseOpsDailyMapper.selectCruiseOpsDailyList(reqVO);
        if (CollUtil.isEmpty(currentDataList)) {
            return new ArrayList<>();
        }

        // 2. 计算本期数据的派生字段（载客率、用房率、收入合计）
        calculateDerivedFields(currentDataList);

        // 3. 计算去年同期范围并查询去年数据
        String startDate = reqVO.getStartDate();
        String endDate = reqVO.getEndDate();
        CruiseOpsDailyReqVO lastYearReq = new CruiseOpsDailyReqVO();
        try {
            Date startD = DateHelper.parseDate(startDate);
            Date endD = endDate != null ? DateHelper.parseDate(endDate) : null;
            // 去年同期
            lastYearReq.setStartDate(DateHelper.getAppointYearStartDate(startD));
            if (endD != null) {
                // 计算去年的结束日期（保持相同的相对天数差）
                Calendar cal = Calendar.getInstance();
                cal.setTime(startD);
                int daysDiff = (int) ((endD.getTime() - startD.getTime()) / (1000 * 60 * 60 * 24));
                cal.add(Calendar.YEAR, -1);
                cal.add(Calendar.DATE, daysDiff);
                lastYearReq.setEndDate(DateHelper.format(cal.getTime(), "yyyy-MM-dd"));
            } else {
                // 如果没有结束日期，查到去年同期当天
                lastYearReq.setEndDate(DateHelper.format(new Date(), "yyyy-MM-dd"));
            }
        } catch (Exception e) {
            log.warn("[getCruiseOpsDailyList] 解析日期失败, 跳过同比计算: {}", e.getMessage());
        }

        List<CruiseOpsDailyRespVO> lastYearDataList = new ArrayList<>();
        if (lastYearReq.getStartDate() != null && lastYearReq.getEndDate() != null) {
            lastYearDataList = cruiseOpsDailyMapper.selectCruiseOpsDailyList(lastYearReq);
            calculateDerivedFields(lastYearDataList);
        }

        // 4. 构建完整报表（含小计、累计、同比）
        return buildReportWithYoy(currentDataList, lastYearDataList);
    }

    @Override
    public void exportCruiseOpsDailyExcel(CruiseOpsDailyReqVO reqVO, HttpServletResponse response) throws IOException {
        List<CruiseOpsDailyRespVO> dataList = getCruiseOpsDailyList(reqVO);
        if (CollUtil.isEmpty(dataList)) {
            ExcelUtils.exportEmpty(response, "省际度假游轮经营情况日报表");
            return;
        }

        // 1. 构建表头（与前端页面15列一致）
        List<List<String>> head = buildExportHeaders();

        // 2. 转换数据：同比行的同比值写入对应数据列
        List<List<Object>> exportData = transformExportData(dataList);

        // 3. 使用 EasyExcel 写出，注册自定义样式处理器
        CruiseOpsDailyExportStyleHandler styleHandler = new CruiseOpsDailyExportStyleHandler(dataList);
        EasyExcel.write(response.getOutputStream())
                .head(head)
                .autoCloseStream(false)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .registerWriteHandler(styleHandler)
                .registerConverter(new LongStringConverter())
                .sheet("省际度假游轮经营情况日报")
                .doWrite(exportData);

        response.addHeader("Content-Disposition",
                "attachment;filename=" + URLEncoder.encode("省际度假游轮经营情况日报表.xls", StandardCharsets.UTF_8.name()));
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
    }

    // ==================== 导出相关私有方法 ====================

    /**
     * 构建导出表头（与前端页面15列一一对应）
     */
    private List<List<String>> buildExportHeaders() {
        String[] headers = {"序号", "月份", "日期", "船舶", "航线", "航次号",
                "客容量", "载客量", "载客率", "房总数", "用房数", "用房率",
                "船票收入", "二消收入", "收入合计"};
        List<List<String>> head = new ArrayList<>(headers.length);
        for (String h : headers) {
            head.add(Collections.singletonList(h));
        }
        return head;
    }

    /**
     * 将报表数据转为 EasyExcel 导出格式（List<List<Object>>）
     * 同比行：同比值写入对应数据列位置（与前端页面展示一致）
     * 小计/累计数据行：日期/船舶/航线列留空（合并到月份列）
     */
    private List<List<Object>> transformExportData(List<CruiseOpsDailyRespVO> dataList) {
        List<List<Object>> result = new ArrayList<>(dataList.size());
        for (CruiseOpsDailyRespVO row : dataList) {
            List<Object> rowData = new ArrayList<>(15);
            boolean isYoy = "同比".equals(row.getVoyageNo());

            if (isYoy) {
                // 同比行：序号~航线列留空（合并到上一行），航次号显示"同比"
                rowData.add("");    // 序号 - 合并
                rowData.add("");    // 月份 - 合并
                rowData.add("");    // 日期 - 合并
                rowData.add("");    // 船舶 - 合并
                rowData.add("");    // 航线 - 合并
                rowData.add("同比");
                rowData.add(formatYoyForExport(row.getPassengerCapacityYoy()));
                rowData.add(formatYoyForExport(row.getPassengerCountYoy()));
                rowData.add(formatYoyForExport(row.getPassengerRateYoy()));
                rowData.add(formatYoyForExport(row.getTotalRoomsYoy()));
                rowData.add(formatYoyForExport(row.getUsedRoomsYoy()));
                rowData.add(formatYoyForExport(row.getRoomRateYoy()));
                rowData.add(formatYoyForExport(row.getTicketIncomeYoy()));
                rowData.add(formatYoyForExport(row.getSecondIncomeYoy()));
                rowData.add(formatYoyForExport(row.getTotalIncomeYoy()));
            } else {
                // 普通行 / 小计数据行 / 累计数据行
                rowData.add(row.getIndex() != null ? String.valueOf(row.getIndex()) : "");
                rowData.add(row.getMonth() != null ? row.getMonth() : "");
                rowData.add(row.getDate() != null ? row.getDate() : "");
                rowData.add(row.getShip() != null ? row.getShip() : "");
                rowData.add(row.getRoute() != null ? row.getRoute() : "");
                rowData.add(row.getVoyageNo() != null ? row.getVoyageNo() : "");
                rowData.add(formatNumber(row.getPassengerCapacity()));
                rowData.add(formatNumber(row.getPassengerCount()));
                rowData.add(row.getPassengerRate() != null ? row.getPassengerRate() : "");
                rowData.add(formatNumber(row.getTotalRooms()));
                rowData.add(row.getUsedRooms() != null ? row.getUsedRooms() : "");
                rowData.add(row.getRoomRate() != null ? row.getRoomRate() : "");
                rowData.add(formatMoneyForExport(row.getTicketIncome()));
                rowData.add(formatMoneyForExport(row.getSecondIncome()));
                rowData.add(formatMoneyForExport(row.getTotalIncome()));
            }

            result.add(rowData);
        }
        return result;
    }

    /**
     * 格式化同比值为导出显示格式（带箭头标识）
     * 正值: ↑ +15.5%   负值: ↓ -12.3%   持平: — 0.0%   无数据: -
     */
    private String formatYoyForExport(String yoyValue) {
        if (yoyValue == null || "-".equals(yoyValue)) {
            return "-";
        }
        String numStr = yoyValue.replace("%", "");
        try {
            double num = Double.parseDouble(numStr);
            if (num > 0) {
                String prefix = yoyValue.startsWith("+") ? "↑ " : "↑ +";
                return prefix + yoyValue;
            } else if (num < 0) {
                return "↓ " + yoyValue;
            } else {
                return "— " + yoyValue;
            }
        } catch (NumberFormatException e) {
            return yoyValue;
        }
    }

    /**
     * 格式化金额为导出显示（与前端 formatMoney 一致）
     */
    private String formatMoneyForExport(BigDecimal amount) {
        if (amount == null) {
            return "-";
        }
        return "¥" + amount.setScale(2, RoundingMode.HALF_UP).toPlainString();
    }

    /**
     * 格式化整数为字符串
     */
    private String formatNumber(Integer val) {
        return val != null ? String.valueOf(val) : "";
    }

    // ==================== 私有方法 ====================

    /**
     * 计算派生字段（载客率、用房率、收入合计）
     */
    private void calculateDerivedFields(List<CruiseOpsDailyRespVO> dataList) {
        for (CruiseOpsDailyRespVO item : dataList) {
            // 载客率 = 载客量 / 客容量 * 100
            if (item.getPassengerCapacity() != null && item.getPassengerCapacity() > 0
                    && item.getPassengerCount() != null) {
                double rate = (double) item.getPassengerCount() / item.getPassengerCapacity() * 100;
                item.setPassengerRate(String.format("%.1f%%", rate));
            }
            // 用房率 = 用房数 / 房总数 * 100
            if (item.getTotalRooms() != null && item.getTotalRooms() > 0
                    && item.getUsedRooms() != null) {
                double usedRoomsVal = safeParseInt(item.getUsedRooms());
                double rate = usedRoomsVal / item.getTotalRooms() * 100;
                item.setRoomRate(String.format("%.1f%%", rate));
            }
            // 收入合计 = 船票收入 + 二消收入
            BigDecimal ticket = item.getTicketIncome() != null ? item.getTicketIncome() : BigDecimal.ZERO;
            BigDecimal second = item.getSecondIncome() != null ? item.getSecondIncome() : BigDecimal.ZERO;
            item.setTotalIncome(ticket.add(second));
        }
    }

    /**
     * 构建完整报表数据结构：
     * - 日常数据行（带序号）
     * - 月度小计数据行 + 月度小计同比行（小计同比=当月小计 vs 去年同月小计）
     * - 累计数据行 + 累计同比行（累计同比=累计本期 vs 累计去年同期，从第二个月起）
     *
     * 去年同期月份通过年份减1来匹配（如 2026-01 → 2025-01），
     * 不再使用位置索引匹配，保证数据对应准确。
     */
    private List<CruiseOpsDailyRespVO> buildReportWithYoy(
            List<CruiseOpsDailyRespVO> currentDataList,
            List<CruiseOpsDailyRespVO> lastYearDataList) {

        List<CruiseOpsDailyRespVO> result = new ArrayList<>();

        // 1. 本期按月份分组（TreeMap保证有序）
        Map<String, List<CruiseOpsDailyRespVO>> currentMonthMap = currentDataList.stream()
                .collect(Collectors.groupingBy(
                        item -> item.getMonth() != null ? item.getMonth() : "",
                        TreeMap::new, Collectors.toList()));

        // 2. 去年同期按月份分组（同样用TreeMap保持顺序）
        Map<String, List<CruiseOpsDailyRespVO>> lastYearMonthMap = lastYearDataList.stream()
                .collect(Collectors.groupingBy(
                        item -> item.getMonth() != null ? item.getMonth() : "",
                        TreeMap::new, Collectors.toList()));

        // 3. 全局序号和累计容器
        int globalIdx = 1;
        List<CruiseOpsDailyRespVO> allDailyDataForCumulative = new ArrayList<>();
        List<CruiseOpsDailyRespVO> allLastYearDailyForCumulative = new ArrayList<>();

        // 4. 遍历每个月份（通过年份-1匹配去年同期月份）
        int monthIndex = 0;
        for (Map.Entry<String, List<CruiseOpsDailyRespVO>> entry : currentMonthMap.entrySet()) {
            String month = entry.getKey();
            List<CruiseOpsDailyRespVO> dailyItems = entry.getValue();

            // --- 4.1 设置序号 ---
            for (CruiseOpsDailyRespVO item : dailyItems) {
                item.setIndex(globalIdx++);
            }

            // --- 4.2 加入结果和累计容器 ---
            result.addAll(dailyItems);
            allDailyDataForCumulative.addAll(dailyItems);

            // --- 4.3 月度小计数据行 ---
            CruiseOpsDailyRespVO subtotalData = buildSubtotalRow(dailyItems, month);
            result.add(subtotalData);

            // --- 4.4 月度小计同比行（通过月份key转换匹配去年同期）---
            String lastYearMonth = getMonthMinusOneYear(month);
            List<CruiseOpsDailyRespVO> lastYearMonthData = lastYearMonthMap.getOrDefault(
                    lastYearMonth, new ArrayList<>());
            CruiseOpsDailyRespVO lastYearSubtotal = buildSubtotalRow(lastYearMonthData, "");
            CruiseOpsDailyRespVO subtotalYoy = buildYoyRow(subtotalData, lastYearSubtotal);
            result.add(subtotalYoy);

            // 累加去年同期月度数据（用于累计同比计算）
            allLastYearDailyForCumulative.addAll(lastYearMonthData);

            // --- 4.5 累计数据行 + 累计同比行（从第二个月起，累计同比=累计本期 vs 累计去年同期） ---
            if (monthIndex > 0) {
                // 累计数据行
                CruiseOpsDailyRespVO cumData = buildSubtotalRow(allDailyDataForCumulative, "累计");
                result.add(cumData);

                // 累计同比行：用累计本期 vs 累计去年同期 重新计算同比
                CruiseOpsDailyRespVO lastYearCumData = buildSubtotalRow(allLastYearDailyForCumulative, "");
                CruiseOpsDailyRespVO cumYoy = buildYoyRow(cumData, lastYearCumData);
                result.add(cumYoy);
            }

            monthIndex++;
        }

        return result;
    }

    /**
     * 将月份字符串减去一年（如 2026-01 → 2025-01）
     */
    private String getMonthMinusOneYear(String month) {
        if (month == null || month.length() < 7) {
            return "";
        }
        try {
            int year = Integer.parseInt(month.substring(0, 4));
            String rest = month.substring(4);
            return (year - 1) + rest;
        } catch (NumberFormatException e) {
            return "";
        }
    }

    /**
     * 构建小计/汇总行（汇总一组数据的各指标）
     */
    private CruiseOpsDailyRespVO buildSubtotalRow(List<CruiseOpsDailyRespVO> items, String monthLabel) {
        CruiseOpsDailyRespVO row = new CruiseOpsDailyRespVO();

        if ("累计".equals(monthLabel)) {
            row.setMonth("累计");
        } else if (!"累计".equals(monthLabel) && monthLabel.contains("小计")) {
            row.setMonth(monthLabel);
        } else {
            // 正常月度小计
            row.setMonth(monthLabel + "小计");
        }
        row.setDate("");
        row.setShip("");
        row.setRoute("");
        row.setVoyageNo("数据");
        row.setIsSubtotal(!"累计".equals(row.getMonth()));
        row.setIsCumulative("累计".equals(row.getMonth()));

        if (CollUtil.isEmpty(items)) {
            return row;
        }

        // 数值汇总
        int passengerCapacitySum = 0;
        int passengerCountSum = 0;
        int totalRoomsSum = 0;
        int usedRoomsSum = 0;
        BigDecimal ticketIncomeSum = BigDecimal.ZERO;
        BigDecimal secondIncomeSum = BigDecimal.ZERO;

        for (CruiseOpsDailyRespVO item : items) {
            passengerCapacitySum += safeInt(item.getPassengerCapacity());
            passengerCountSum += safeInt(item.getPassengerCount());
            totalRoomsSum += safeInt(item.getTotalRooms());
            usedRoomsSum += safeParseInt(item.getUsedRooms());
            ticketIncomeSum = ticketIncomeSum.add(safeBigDecimal(item.getTicketIncome()));
            secondIncomeSum = secondIncomeSum.add(safeBigDecimal(item.getSecondIncome()));
        }

        row.setPassengerCapacity(passengerCapacitySum);
        row.setPassengerCount(passengerCountSum);
        row.setTotalRooms(totalRoomsSum);
        row.setUsedRooms(String.valueOf(usedRoomsSum));
        row.setTicketIncome(ticketIncomeSum);
        row.setSecondIncome(secondIncomeSum);

        // 派生字段重新计算
        if (passengerCapacitySum > 0) {
            double rate = (double) passengerCountSum / passengerCapacitySum * 100;
            row.setPassengerRate(String.format("%.1f%%", rate));
        }
        if (totalRoomsSum > 0) {
            double rate = (double) usedRoomsSum / totalRoomsSum * 100;
            row.setRoomRate(String.format("%.1f%%", rate));
        }
        row.setTotalIncome(ticketIncomeSum.add(secondIncomeSum));

        return row;
    }

    /**
     * 构建同比行（对比本期汇总 vs 往期汇总）
     * 仅保留同比率数据，原始数值字段不填充（前端同比行只展示同比率）
     */
    private CruiseOpsDailyRespVO buildYoyRow(CruiseOpsDailyRespVO currentRow, CruiseOpsDailyRespVO lastYearRow) {
        CruiseOpsDailyRespVO yoyRow = new CruiseOpsDailyRespVO();
        yoyRow.setIndex(null);
        yoyRow.setMonth("");   // 与上一行(小计/累计)的月份合并
        yoyRow.setDate("");
        yoyRow.setShip("");
        yoyRow.setRoute("");
        yoyRow.setVoyageNo("同比");
        yoyRow.setIsSubtotal(currentRow.getIsSubtotal() != null && currentRow.getIsSubtotal());
        yoyRow.setIsCumulative(currentRow.getIsCumulative() != null && currentRow.getIsCumulative());

        // ===== 计算各指标的同比率 =====
        yoyRow.setPassengerCapacityYoy(calcYoyPercent(
                safeInt(currentRow.getPassengerCapacity()), safeInt(lastYearRow.getPassengerCapacity())));
        yoyRow.setPassengerCountYoy(calcYoyPercent(
                safeInt(currentRow.getPassengerCount()), safeInt(lastYearRow.getPassengerCount())));

        // 载客率同比（百分点差异）
        yoyRow.setPassengerRateYoy(calcRateDiff(currentRow.getPassengerRate(), lastYearRow.getPassengerRate()));

        yoyRow.setTotalRoomsYoy(calcYoyPercent(
                safeInt(currentRow.getTotalRooms()), safeInt(lastYearRow.getTotalRooms())));
        yoyRow.setUsedRoomsYoy(calcYoyPercent(
                safeParseInt(currentRow.getUsedRooms()), safeParseInt(lastYearRow.getUsedRooms())));

        // 用房率同比（百分点差异）
        yoyRow.setRoomRateYoy(calcRateDiff(currentRow.getRoomRate(), lastYearRow.getRoomRate()));

        // 收入同比率
        yoyRow.setTicketIncomeYoy(calcYoyPercent(
                safeBigDecimal(currentRow.getTicketIncome()), safeBigDecimal(lastYearRow.getTicketIncome())));
        yoyRow.setSecondIncomeYoy(calcYoyPercent(
                safeBigDecimal(currentRow.getSecondIncome()), safeBigDecimal(lastYearRow.getSecondIncome())));
        yoyRow.setTotalIncomeYoy(calcYoyPercent(
                safeBigDecimal(currentRow.getTotalIncome()), safeBigDecimal(lastYearRow.getTotalIncome())));

        return yoyRow;
    }

    /**
     * 计算同比百分比 (本期-往期)/往期*100
     */
    private String calcYoyPercent(Number current, Number last) {
        if (current == null || last == null || last.doubleValue() == 0) {
            return "-";
        }
        try {
            return MathHelper.getNumberYOY(current, last, 1) + "%";
        } catch (Exception e) {
            log.debug("calcYoyPercent error: {},{}", current, last);
            return "-";
        }
    }

    /**
     * 计算比率百分点差异（如 84.0% - 82.0% = +2.0）
     */
    private String calcRateDiff(String currentRate, String lastRate) {
        if (currentRate == null || lastRate == null || !currentRate.contains("%") || !lastRate.contains("%")) {
            return "-";
        }
        try {
            double currentVal = Double.parseDouble(currentRate.replace("%", ""));
            double lastVal = Double.parseDouble(lastRate.replace("%", ""));
            double diff = currentVal - lastVal;
            return (diff >= 0 ? "+" : "") + String.format("%.1f", diff);
        } catch (NumberFormatException e) {
            return "-";
        }
    }

    private int safeInt(Integer val) {
        return val != null ? val : 0;
    }

    private int safeParseInt(String val) {
        if (val == null || val.isEmpty()) {
            return 0;
        }
        try {
            return Integer.parseInt(val);
        } catch (NumberFormatException e) {
            try {
                return (int) Double.parseDouble(val);
            } catch (NumberFormatException e2) {
                return 0;
            }
        }
    }

    private BigDecimal safeBigDecimal(BigDecimal val) {
        return val != null ? val : BigDecimal.ZERO;
    }


    @Resource
    private YangtzePassengerSummaryMapper yangtzePassengerSummaryMapper;

    @Override
    public PageResult<YangtzePassengerSummaryRespVO> getYangtzePassengerSummaryPage(YangtzePassengerSummaryPageReqVO pageReqVO) {
        IPage<YangtzePassengerSummaryRespVO> page = yangtzePassengerSummaryMapper.selectYangtzePassengerSummaryPage(
                new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()), pageReqVO);
        List<YangtzePassengerSummaryRespVO> records = page.getRecords();
        // 设置序号
        int startIdx = (pageReqVO.getPageNo() - 1) * pageReqVO.getPageSize() + 1;
        for (int i = 0; i < records.size(); i++) {
            records.get(i).setIndex(startIdx + i);
        }
        // 计算派生字段
        calculateDerivedFields2(records);
        return new PageResult<>(records, page.getTotal());
    }

    @Override
    public void exportYangtzePassengerSummaryExcel(YangtzePassengerSummaryPageReqVO pageReqVO,
                                                   HttpServletResponse response) throws IOException {
        // 查询全部数据（不分页）
        List<YangtzePassengerSummaryRespVO> dataList = yangtzePassengerSummaryMapper.selectYangtzePassengerSummaryExportList(pageReqVO);
        if (CollUtil.isEmpty(dataList)) {
            ExcelUtils.exportEmpty(response, "长江行游轮游客数据及营收统计一览表");
            return;
        }
        calculateDerivedFields2(dataList);

        // 设置序号
        int idx = 1;
        for (YangtzePassengerSummaryRespVO row : dataList) {
            row.setIndex(idx++);
        }

        // 构建多级表头
        List<List<String>> head = buildExportHeader();

        // 转换数据
        List<List<Object>> exportData = transformExportData2(dataList);

        // 先设置响应头
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
        response.addHeader("Content-Disposition",
                "attachment;filename=" + URLEncoder.encode("长江行游轮游客数据及营收统计一览表.xlsx", StandardCharsets.UTF_8.name()));

        // 使用 EasyExcel 写出
        YangtzePassengerSummaryExportStyleHandler styleHandler = new YangtzePassengerSummaryExportStyleHandler();
        EasyExcel.write(response.getOutputStream())
                .head(head)
                .autoCloseStream(false)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .registerWriteHandler(styleHandler)
                .registerConverter(new LongStringConverter())
                .sheet("长江行游轮游客数据及营收统计")
                .doWrite(exportData);
    }

    // ==================== 私有方法 ====================

    /**
     * 计算派生字段（床位载客率、团散比、境内外比、均价）
     */
    private void calculateDerivedFields2(List<YangtzePassengerSummaryRespVO> dataList) {
        for (YangtzePassengerSummaryRespVO row : dataList) {
            // 床位载客率 = 实收人数总数 / 客容量 * 100
            if (row.getCapacity() != null && row.getCapacity() > 0 && row.getTotalPassengers() != null) {
                double rate = (double) row.getTotalPassengers() / row.getCapacity() * 100;
                row.setBedOccupancyRate(String.format("%.1f%%", rate));
            } else {
                row.setBedOccupancyRate("-");
            }

            // 团散比
            int group = row.getGroupCount() != null ? row.getGroupCount() : 0;
            int individual = row.getIndividualCount() != null ? row.getIndividualCount() : 0;
            row.setGroupIndividualRatio(calcRatio(group, individual));

            // 境内外比
            int domestic = row.getDomesticCount() != null ? row.getDomesticCount() : 0;
            int overseas = row.getOverseasCount() != null ? row.getOverseasCount() : 0;
            row.setDomesticOverseasRatio(calcRatio(domestic, overseas));

            // 均价 = 应收 / 购票人数
            if (row.getReceivableAmount() != null && row.getTicketedPassengers() != null && row.getTicketedPassengers() > 0) {
                row.setAvgPrice(row.getReceivableAmount().divide(
                        BigDecimal.valueOf(row.getTicketedPassengers()), 2, RoundingMode.HALF_UP));
            } else {
                row.setAvgPrice(BigDecimal.ZERO);
            }

            // 未收 = 应收 - 实收
            BigDecimal receivable = row.getReceivableAmount() != null ? row.getReceivableAmount() : BigDecimal.ZERO;
            BigDecimal received = row.getReceivedAmount() != null ? row.getReceivedAmount() : BigDecimal.ZERO;
            row.setUnreceivedAmount(receivable.subtract(received));
        }
    }

    /**
     * 计算比率字符串（如 2:1）
     */
    private String calcRatio(int a, int b) {
        if (b == 0 && a == 0) return "-";
        if (b == 0) return a + ":0";
        if (a == 0) return "0:" + b;
        int gcd = gcd(a, b);
        return (a / gcd) + ":" + (b / gcd);
    }

    private int gcd(int a, int b) {
        a = Math.abs(a);
        b = Math.abs(b);
        while (b != 0) {
            int temp = b;
            b = a % b;
            a = temp;
        }
        return a;
    }

    /**
     * 构建导出多级表头
     * 与图片的列结构保持一致
     */
    private List<List<String>> buildExportHeader() {
        List<List<String>> head = new ArrayList<>();

        // 1. 序号
        head.add(new ArrayList<>(Collections.singletonList("序号")));
        // 2. 船舶
        head.add(new ArrayList<>(Collections.singletonList("船舶")));
        // 3. 航次
        head.add(new ArrayList<>(Collections.singletonList("航次")));
        // 4. 倒计时(天)
        head.add(new ArrayList<>(Collections.singletonList("倒计时(天)")));
        // 5-7. 预订间数
        head.add(new ArrayList<>(Arrays.asList("预订间数", "总数")));
        head.add(new ArrayList<>(Arrays.asList("预订间数", "实收房间数")));
        head.add(new ArrayList<>(Arrays.asList("预订间数", "占位房间数")));
        // 8-10. 实收人数
        head.add(new ArrayList<>(Arrays.asList("实收人数", "总数")));
        head.add(new ArrayList<>(Arrays.asList("实收人数", "购票人数")));
        head.add(new ArrayList<>(Arrays.asList("实收人数", "免票人数")));
        // 11. 预计收客人数
        head.add(new ArrayList<>(Collections.singletonList("预计收客人数")));
        // 12. 备注
        head.add(new ArrayList<>(Collections.singletonList("备注")));
        // 13-16. 财务情况
        head.add(new ArrayList<>(Arrays.asList("财务情况", "应收")));
        head.add(new ArrayList<>(Arrays.asList("财务情况", "实收")));
        head.add(new ArrayList<>(Arrays.asList("财务情况", "未收")));
        head.add(new ArrayList<>(Arrays.asList("财务情况", "均价")));
        // 16-17. 床位载客率
        head.add(new ArrayList<>(Arrays.asList("床位载客率(人/航次)", "客容量")));
        head.add(new ArrayList<>(Arrays.asList("床位载客率(人/航次)", "载客率")));
        // 18-21. 实收详情
        head.add(new ArrayList<>(Arrays.asList("实收详情", "成人")));
        head.add(new ArrayList<>(Arrays.asList("实收详情", "儿童")));
        head.add(new ArrayList<>(Arrays.asList("实收详情", "婴儿")));
        head.add(new ArrayList<>(Arrays.asList("实收详情", "陪同+领队")));
        // 22-27. 团队情况
        head.add(new ArrayList<>(Arrays.asList("团队情况", "团队人次(>10人)")));
        head.add(new ArrayList<>(Arrays.asList("团队情况", "散客人次(<10人)")));
        head.add(new ArrayList<>(Arrays.asList("团队情况", "团散比")));
        head.add(new ArrayList<>(Arrays.asList("团队情况", "境内")));
        head.add(new ArrayList<>(Arrays.asList("团队情况", "境外")));
        head.add(new ArrayList<>(Arrays.asList("团队情况", "境内外比")));

        return head;
    }

    /**
     * 将报表数据转为 EasyExcel 导出格式
     */
    private List<List<Object>> transformExportData2(List<YangtzePassengerSummaryRespVO> dataList) {
        List<List<Object>> result = new ArrayList<>(dataList.size());
        for (YangtzePassengerSummaryRespVO row : dataList) {
            List<Object> rowData = new ArrayList<>(28);
            rowData.add(row.getIndex() != null ? String.valueOf(row.getIndex()) : "");
            rowData.add(row.getShipName() != null ? row.getShipName() : "");
            rowData.add(row.getVoyageInfo() != null ? row.getVoyageInfo() : "");
            rowData.add(row.getCountdown() != null ? String.valueOf(row.getCountdown()) : "");
            rowData.add(formatNumber2(row.getTotalRooms()));
            rowData.add(formatNumber2(row.getPaidRooms()));
            rowData.add(formatNumber2(row.getReservedRooms()));
            rowData.add(formatNumber2(row.getTotalPassengers()));
            rowData.add(formatNumber2(row.getTicketedPassengers()));
            rowData.add(formatNumber2(row.getFreePassengers()));
            rowData.add(formatNumber2(row.getEstimatedPassengers()));
            rowData.add(row.getRemark() != null ? row.getRemark() : "");
            rowData.add(formatMoney(row.getReceivableAmount()));
            rowData.add(formatMoney(row.getReceivedAmount()));
            rowData.add(formatMoney(row.getUnreceivedAmount()));
            rowData.add(formatMoney(row.getAvgPrice()));
            rowData.add(formatNumber2(row.getCapacity()));
            rowData.add(row.getBedOccupancyRate() != null ? row.getBedOccupancyRate() : "");
            rowData.add(formatNumber2(row.getAdultCount()));
            rowData.add(formatNumber2(row.getChildCount()));
            rowData.add(formatNumber2(row.getInfantCount()));
            rowData.add(formatNumber2(row.getCompanionLeaderCount()));
            rowData.add(formatNumber2(row.getGroupCount()));
            rowData.add(formatNumber2(row.getIndividualCount()));
            rowData.add(row.getGroupIndividualRatio() != null ? row.getGroupIndividualRatio() : "");
            rowData.add(formatNumber2(row.getDomesticCount()));
            rowData.add(formatNumber2(row.getOverseasCount()));
            rowData.add(row.getDomesticOverseasRatio() != null ? row.getDomesticOverseasRatio() : "");
            result.add(rowData);
        }
        return result;
    }

    private String formatNumber2(Integer val) {
        return val != null ? String.valueOf(val) : "0";
    }

    private String formatMoney(BigDecimal amount) {
        if (amount == null) return "0.00";
        return amount.setScale(2, RoundingMode.HALF_UP).toPlainString();
    }


}
