package com.rj.service.support;

import com.rj.entity.LbDailyUserTradeReport;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.ComparisonOperator;
import org.apache.poi.ss.usermodel.ConditionalFormattingRule;
import org.apache.poi.ss.usermodel.DataFormat;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.FontFormatting;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.SheetConditionalFormatting;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFColor;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.http.HttpHeaders;

import java.awt.Color;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;

/**
 * 当日用户交易报表 Excel 导出（POI 样式与写入逻辑）。
 */
public final class LbDailyUserTradeReportExportSupport {

    /** 导出工作簿固定 8 个 sheet（顺序与业务模板一致）。 */
    private static final String[] EXPORT_SHEET_NAMES = {
            "总表", "抵扣表", "断货表", "11点表", "支付表", "支付表检查表", "结果表", "结果表检查表"
    };

    private static final int SUMMARY_SHEET_INDEX = 0;
    private static final int DEDUCTION_SHEET_INDEX = 1;
    private static final int OUT_OF_STOCK_SHEET_INDEX = 2;
    private static final int ELEVEN_CLOCK_SHEET_INDEX = 3;
    private static final int PAYMENT_SHEET_INDEX = 4;
    private static final int PAYMENT_CHECK_SHEET_INDEX = 5;
    private static final int RESULT_SHEET_INDEX = 6;
    private static final int RESULT_CHECK_SHEET_INDEX = 7;

    /** 结果表检查表：M/N 先按 K 列、再按 I 列补写 L 合计（M 已有值不覆盖）；O 留白；P 起为买卖检查列。 */
    private static final int RESULT_CHECK_COL_IK_NICKNAME_L_SUM = 12;
    private static final int RESULT_CHECK_COL_CURRENT_NICKNAME = 13;
    private static final int RESULT_CHECK_COL_USER_ID = 15;
    private static final int RESULT_CHECK_COL_NICKNAME = 16;
    private static final int RESULT_CHECK_COL_FINAL_SETTLEMENT = 17;
    private static final int RESULT_CHECK_COL_BUY_DETAIL = 18;
    private static final int RESULT_CHECK_COL_BUY_TOTAL = 19;
    private static final int RESULT_CHECK_COL_BUY_TOTAL_FEE = 20;
    private static final int RESULT_CHECK_COL_SELL_DETAIL = 21;
    private static final int RESULT_CHECK_COL_SELL_TOTAL = 22;
    /** 结果表检查表 Y 列：Z 列订单金额对应昵称；Z 列：S 列多笔订单金额逐行展开。 */
    private static final int RESULT_CHECK_COL_BUY_ORDER_NICKNAME = 24;
    private static final int RESULT_CHECK_COL_BUY_ORDER_AMOUNT = 25;
    /** 结果表检查表 AA 列：按 Y 列昵称汇总 Z 列订单金额。 */
    private static final int RESULT_CHECK_COL_BUY_ORDER_YZ_SUM = 26;
    /** 结果表检查表 AC 列：AD 列卖出订单金额对应昵称；AD 列：V 列多笔卖出金额逐行展开。 */
    private static final int RESULT_CHECK_COL_SELL_ORDER_NICKNAME = 28;
    private static final int RESULT_CHECK_COL_SELL_ORDER_AMOUNT = 29;
    /** 结果表检查表 AE 列：按 AC 列昵称汇总 AD 列卖出订单金额。 */
    private static final int RESULT_CHECK_COL_SELL_ORDER_AD_SUM = 30;
    /** 结果表检查表 AF～AH：总表与检查表买入差异核对。 */
    private static final int RESULT_CHECK_COL_BUY_DIFF_SUMMARY = 31;
    private static final int RESULT_CHECK_COL_BUY_DIFF_CHECK = 32;
    private static final int RESULT_CHECK_COL_BUY_DIFF = 33;
    private static final int RESULT_CHECK_LAST_COL = RESULT_CHECK_COL_BUY_DIFF;
    private static final BigDecimal RESULT_CHECK_BUY_TOTAL_FEE_RATE = new BigDecimal("0.01");

    private static final int PAYMENT_COL_NAME = 0;
    private static final int PAYMENT_COL_ACTUAL = 1;
    /** 支付表：实际收付 < 0 的记录移至 E、F 列 */
    private static final int PAYMENT_NEGATIVE_COL_NAME = 4;
    private static final int PAYMENT_NEGATIVE_COL_ACTUAL = 5;
    private static final int PAYMENT_LAST_COL = PAYMENT_NEGATIVE_COL_ACTUAL;

    /** 结果表：I–L 列为收付款贪心匹配明细（对应 VBA 自动匹配收付款） */
    private static final int RESULT_MATCH_NEG_NAME = 8;
    private static final int RESULT_MATCH_FLAG = 9;
    private static final int RESULT_MATCH_POS_NAME = 10;
    private static final int RESULT_MATCH_AMT = 11;
    private static final int RESULT_LAST_COL = RESULT_MATCH_AMT;
    private static final int PAYMENT_MATCH_MAX_ROWS = 1000;

    private static final int COL_NICKNAME = 0;
    private static final int COL_YESTODAY_BUY = 1;
    private static final int COL_DAILY_SELL = 2;
    private static final int COL_DAILY_BUY = 3;
    private static final int COL_SERVICE = 4;
    private static final int COL_DIFF = 5;
    private static final int COL_ACTUAL = 6;
    private static final int COL_DIKOU = 7;

    private static final int SUMMARY_LAST_COL = COL_DIKOU;
    /** 11点表：最左侧序号列，其余列右移一列 */
    private static final int ELEVEN_CLOCK_LAST_COL = COL_DIKOU + 1;
    private static final int COL_SEQUENCE_LEFT = 0;

    private static final String DATA_TYPE_REPORT_SUM = "report_sum";
    /** 支付表 A/B 列固定追加行姓名 */
    private static final String PAYMENT_AB_SPECIAL_NAME = "刘瑞华";

    private LbDailyUserTradeReportExportSupport() {
    }

    public static void writeExcel(
            LocalDate reportDate,
            List<LbDailyUserTradeReport> rows,
            Map<String, BigDecimal> deductionByUserName,
            Map<String, String> userIdByNickname,
            Map<String, BigDecimal> buyTotalByBuyerId,
            Map<String, String> buyDetailByBuyerId,
            Map<String, BigDecimal> sellTotalBySellerId,
            Map<String, String> sellDetailBySellerId,
            HttpServletResponse response) throws IOException {
        try (Workbook workbook = new XSSFWorkbook()) {
            SheetStyles styles = SheetStyles.create(workbook);
            for (String sheetName : EXPORT_SHEET_NAMES) {
                Sheet sheet = workbook.createSheet(sheetName);
                sheet.setDefaultRowHeightInPoints(sheet.getDefaultRowHeightInPoints() * styles.rowHeightFactor);
            }

            DateHeaderLabels labels = DateHeaderLabels.of(reportDate);
            List<LbDailyUserTradeReport> exportRows = prepareExportRows(rows);
            fillSummarySheet(workbook.getSheetAt(SUMMARY_SHEET_INDEX), labels, exportRows, styles);
            Map<String, BigDecimal> deductionMap = deductionByUserName == null ? Map.of() : deductionByUserName;
            fillDeductionSheet(workbook.getSheetAt(DEDUCTION_SHEET_INDEX), labels, exportRows, styles, deductionMap, false, true, false);
            // 断货表：总表中当日买入金额为 0 的明细行，最左侧序号列
            fillOutOfStockSheet(workbook.getSheetAt(OUT_OF_STOCK_SHEET_INDEX), labels, exportRows, styles);
            // 11点表：与抵扣表相同的数据与计算逻辑，最左侧序号列，不含最后一行汇总，数值四舍五入取整
            fillDeductionSheet(workbook.getSheetAt(ELEVEN_CLOCK_SHEET_INDEX), labels, exportRows, styles, deductionMap, true, false, true);
            // 支付表：A/B 列含刘瑞华，按 B 列倒序；< 0 移至 E/F 列
            Sheet paymentSheet = workbook.getSheetAt(PAYMENT_SHEET_INDEX);
            fillPaymentSheet(paymentSheet, exportRows, styles, deductionMap);
            fillPaymentCheckSheet(workbook.getSheetAt(PAYMENT_CHECK_SHEET_INDEX), paymentSheet, styles);
            Sheet resultSheet = workbook.getSheetAt(RESULT_SHEET_INDEX);
            fillResultSheetFromPayment(resultSheet, paymentSheet, styles);
            fillResultCheckSheet(
                    workbook.getSheetAt(RESULT_CHECK_SHEET_INDEX),
                    resultSheet,
                    workbook.getSheetAt(SUMMARY_SHEET_INDEX),
                    userIdByNickname,
                    buyTotalByBuyerId,
                    buyDetailByBuyerId,
                    sellTotalBySellerId,
                    sellDetailBySellerId,
                    styles);

            String filename = "当日汇总报表" + reportDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + ".xlsx";
            String encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+", "%20");

            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded);

            try (ServletOutputStream os = response.getOutputStream()) {
                workbook.write(os);
                os.flush();
            }
        }
    }

    private static void fillSummarySheet(
            Sheet sheet,
            DateHeaderLabels labels,
            List<LbDailyUserTradeReport> exportRows,
            SheetStyles styles) {
        fillSummaryLikeSheet(sheet, labels, exportRows, styles, false, false);
    }

    /** 断货表：复制总表中当日买入金额（dailyBuyAmt）为 0 的明细行，不含汇总行，最左侧序号列，数值四舍五入取整。 */
    private static void fillOutOfStockSheet(
            Sheet sheet,
            DateHeaderLabels labels,
            List<LbDailyUserTradeReport> exportRows,
            SheetStyles styles) {
        fillSummaryLikeSheet(sheet, labels, filterZeroDailyBuyRows(exportRows), styles, true, true);
    }

    private static List<LbDailyUserTradeReport> filterZeroDailyBuyRows(List<LbDailyUserTradeReport> exportRows) {
        List<LbDailyUserTradeReport> filtered = new ArrayList<>();
        for (LbDailyUserTradeReport row : exportRows) {
            if (DATA_TYPE_REPORT_SUM.equals(row.getDataType())) {
                continue;
            }
            if (defaultZero(row.getDailyBuyAmt()).compareTo(BigDecimal.ZERO) == 0) {
                filtered.add(row);
            }
        }
        return filtered;
    }

    private static void fillSummaryLikeSheet(
            Sheet sheet,
            DateHeaderLabels labels,
            List<LbDailyUserTradeReport> rows,
            SheetStyles styles,
            boolean roundValues,
            boolean sequenceOnLeft) {
        int lastCol = sequenceOnLeft ? ELEVEN_CLOCK_LAST_COL : SUMMARY_LAST_COL;
        int r = writeHeaderRow(sheet, labels, styles, sequenceOnLeft);
        sheet.createFreezePane(0, 1);
        sheet.setAutoFilter(new CellRangeAddress(0, 0, 0, lastCol));

        int sequence = 0;
        for (LbDailyUserTradeReport item : rows) {
            Row row = sheet.createRow(r++);
            applyRowHeight(row, styles);
            RowStylePair rowStyles = rowStyles(item, r, styles, roundValues);
            if (sequenceOnLeft) {
                sequence++;
                createIntCell(row, COL_SEQUENCE_LEFT, sequence, rowStyles.sequenceStyle);
            }
            writeBaseDataCells(row, item, rowStyles, defaultZero(item.getDikouAmt()), sequenceOnLeft, roundValues);
        }

        applyDiffNegativeFormatting(sheet, r, sequenceOnLeft);
        autoSizeColumns(sheet, lastCol);
    }

    /**
     * 支付表：A/B 列含刘瑞华汇总行，与 |F 列总和| 平衡后按 B 列倒序；< 0 写入 E/F 列（升序）。
     */
    private static void fillPaymentSheet(
            Sheet sheet,
            List<LbDailyUserTradeReport> exportRows,
            SheetStyles styles,
            Map<String, BigDecimal> deductionByUserName) {
        boolean roundValues = true;
        int r = 0;
        Row header = sheet.createRow(r++);
        header.setHeightInPoints(20 * styles.rowHeightFactor);
        createHeaderCell(header, PAYMENT_COL_NAME, "姓名", styles.headerStyle);
        createHeaderCell(header, PAYMENT_COL_ACTUAL, "实际收付", styles.headerStyle);
        createHeaderCell(header, PAYMENT_NEGATIVE_COL_NAME, "姓名", styles.headerStyle);
        createHeaderCell(header, PAYMENT_NEGATIVE_COL_ACTUAL, "实际收付", styles.headerStyle);

        sheet.createFreezePane(0, 1);
        sheet.setAutoFilter(new CellRangeAddress(0, 0, 0, PAYMENT_LAST_COL));

        List<PaymentAbEntry> abEntries = new ArrayList<>();
        List<LbDailyUserTradeReport> negativeRows = new ArrayList<>();
        for (LbDailyUserTradeReport item : elevenClockDetailRows(exportRows, deductionByUserName)) {
            BigDecimal actual = computeActualReceiptsForDeduction(item, deductionByUserName, roundValues);
            if (actual.compareTo(BigDecimal.ZERO) >= 0) {
                abEntries.add(PaymentAbEntry.detail(
                        item.getNickname() == null ? "" : item.getNickname(),
                        actual,
                        item));
            } else {
                negativeRows.add(item);
            }
        }
        abEntries.add(PaymentAbEntry.special(
                computeDeductionSheetSummaryActual(exportRows, deductionByUserName)));
        balancePaymentSheetColumnTotals(abEntries, negativeRows, deductionByUserName, roundValues);
        abEntries.sort((a, b) -> b.sortValue().compareTo(a.sortValue()));

        int positiveRow = 1;
        for (PaymentAbEntry entry : abEntries) {
            Row row = sheet.getRow(positiveRow);
            if (row == null) {
                row = sheet.createRow(positiveRow);
                applyRowHeight(row, styles);
            }
            if (entry.isSpecialRow()) {
                createTextCell(row, PAYMENT_COL_NAME, entry.name(), paymentTextStyle(positiveRow, styles));
                createPaymentActualCell(
                        row, PAYMENT_COL_ACTUAL, entry.actualAbs(), paymentActualStyle(positiveRow, styles));
            } else {
                RowStylePair rowStyles = rowStyles(entry.detailRow(), positiveRow, styles, roundValues);
                createTextCell(row, PAYMENT_COL_NAME, entry.name(), rowStyles.textStyle);
                createPaymentActualCell(
                        row, PAYMENT_COL_ACTUAL, entry.actualAbs(), paymentActualStyle(positiveRow, styles));
            }
            positiveRow++;
        }

        negativeRows.sort((a, b) -> computeActualReceiptsForDeduction(a, deductionByUserName, roundValues)
                .compareTo(computeActualReceiptsForDeduction(b, deductionByUserName, roundValues)));

        int negativeRow = 1;
        for (LbDailyUserTradeReport item : negativeRows) {
            Row row = sheet.getRow(negativeRow);
            if (row == null) {
                row = sheet.createRow(negativeRow);
                applyRowHeight(row, styles);
            }
            RowStylePair rowStyles = rowStyles(item, negativeRow, styles, roundValues);
            createTextCell(
                    row,
                    PAYMENT_NEGATIVE_COL_NAME,
                    item.getNickname() == null ? "" : item.getNickname(),
                    rowStyles.textStyle);
            createPaymentActualCell(
                    row,
                    PAYMENT_NEGATIVE_COL_ACTUAL,
                    computeActualReceiptsForDeduction(item, deductionByUserName, roundValues),
                    paymentActualStyle(negativeRow, styles));
            negativeRow++;
        }

        int nextRow = Math.max(positiveRow, negativeRow);
        applyPaymentNegativeActualFormatting(sheet, nextRow);
        autoSizeColumns(sheet, PAYMENT_LAST_COL);
    }

    /**
     * 支付表检查表：完整复制支付表，末尾追加一行「求和」，B、F 列为上方数据行之和。
     */
    private static void fillPaymentCheckSheet(Sheet target, Sheet paymentSource, SheetStyles styles) {
        copyPaymentSheetContent(paymentSource, target);

        BigDecimal sumB = BigDecimal.ZERO;
        BigDecimal sumF = BigDecimal.ZERO;
        int lastDataRow = paymentSource.getLastRowNum();
        for (int rowIdx = 1; rowIdx <= lastDataRow; rowIdx++) {
            Row row = paymentSource.getRow(rowIdx);
            if (row == null) {
                continue;
            }
            sumB = sumB.add(readNumericCellValue(row.getCell(PAYMENT_COL_ACTUAL)));
            sumF = sumF.add(readNumericCellValue(row.getCell(PAYMENT_NEGATIVE_COL_ACTUAL)));
        }

        int sumRowIdx = lastDataRow + 1;
        Row sumRow = target.createRow(sumRowIdx);
        applyRowHeight(sumRow, styles);
        createTextCell(sumRow, PAYMENT_COL_NAME, "求和", styles.summaryTextStyle);
        createPaymentActualCell(sumRow, PAYMENT_COL_ACTUAL, sumB, styles.summaryRoundedAmountStyle);
        createPaymentActualCell(sumRow, PAYMENT_NEGATIVE_COL_ACTUAL, sumF, styles.summaryRoundedAmountStyle);

        applyPaymentNegativeActualFormatting(target, sumRowIdx);
        autoSizeColumns(target, PAYMENT_LAST_COL);
    }

    /**
     * 结果表：从支付表读取 A/B 正值、E/F 负值，排序后贪心匹配，写入 A/B、E/F 副本及 I–L 匹配明细。
     * 逻辑对应 Excel 宏「自动匹配收付款」。
     */
    private static void fillResultSheetFromPayment(Sheet resultSheet, Sheet paymentSheet, SheetStyles styles) {
        Row header = resultSheet.createRow(0);
        header.setHeightInPoints(20 * styles.rowHeightFactor);
        createHeaderCell(header, PAYMENT_COL_NAME, "昵称", styles.headerStyle);
        createHeaderCell(header, PAYMENT_COL_ACTUAL, "实际收付", styles.headerStyle);
        createHeaderCell(header, PAYMENT_NEGATIVE_COL_NAME, "昵称", styles.headerStyle);
        createHeaderCell(header, PAYMENT_NEGATIVE_COL_ACTUAL, "实际收付", styles.headerStyle);
        createHeaderCell(header, RESULT_MATCH_NEG_NAME, "昵称", styles.headerStyle);
        createHeaderCell(header, RESULT_MATCH_FLAG, "F", styles.headerStyle);
        createHeaderCell(header, RESULT_MATCH_POS_NAME, "昵称", styles.headerStyle);
        createHeaderCell(header, RESULT_MATCH_AMT, "金额", styles.headerStyle);

        List<PaymentMatchEntry> positives = collectPaymentMatchEntries(
                paymentSheet, PAYMENT_COL_NAME, PAYMENT_COL_ACTUAL, true);
        List<PaymentMatchEntry> negatives = collectPaymentMatchEntries(
                paymentSheet, PAYMENT_NEGATIVE_COL_NAME, PAYMENT_NEGATIVE_COL_ACTUAL, false);

        positives.sort(Comparator.comparing(PaymentMatchEntry::amount).reversed());
        negatives.sort(Comparator.comparing(PaymentMatchEntry::amount));

        int listRow = 1;
        for (PaymentMatchEntry entry : positives) {
            writeResultListRow(resultSheet, listRow++, PAYMENT_COL_NAME, PAYMENT_COL_ACTUAL, entry, styles);
        }
        listRow = 1;
        for (PaymentMatchEntry entry : negatives) {
            writeResultListRow(
                    resultSheet, listRow++, PAYMENT_NEGATIVE_COL_NAME, PAYMENT_NEGATIVE_COL_ACTUAL, entry, styles);
        }

        if (positives.isEmpty() || negatives.isEmpty()) {
            writeResultMatchColumnLSum(resultSheet, styles);
            autoSizeColumns(resultSheet, RESULT_LAST_COL);
            return;
        }

        int posIdx = 0;
        int negIdx = 0;
        int matchRow = 1;
        while (posIdx < positives.size() && negIdx < negatives.size()) {
            PaymentMatchEntry pos = positives.get(posIdx);
            PaymentMatchEntry neg = negatives.get(negIdx);

            if (pos.amount().compareTo(BigDecimal.ZERO) <= 0) {
                posIdx++;
                continue;
            }
            BigDecimal negAbs = neg.amount().abs();
            if (negAbs.compareTo(BigDecimal.ZERO) <= 0) {
                negIdx++;
                continue;
            }

            BigDecimal matchAmount = pos.amount().min(negAbs);
            Row row = resultSheet.createRow(matchRow++);
            applyRowHeight(row, styles);
            createTextCell(row, RESULT_MATCH_NEG_NAME, neg.name(), styles.textStyle);
            createTextCell(row, RESULT_MATCH_FLAG, "F", styles.textStyle);
            createTextCell(row, RESULT_MATCH_POS_NAME, pos.name(), styles.textStyle);
            createPaymentActualCell(row, RESULT_MATCH_AMT, matchAmount, paymentActualStyle(matchRow, styles));

            pos.setAmount(pos.amount().subtract(matchAmount));
            neg.setAmount(neg.amount().add(matchAmount));
        }

        writeResultMatchColumnLSum(resultSheet, styles);
        autoSizeColumns(resultSheet, RESULT_LAST_COL);
    }

    /** 结果表：L 列（匹配金额）求和，写入最后一行。 */
    private static void writeResultMatchColumnLSum(Sheet sheet, SheetStyles styles) {
        int lastRow = sheet.getLastRowNum();
        if (lastRow < 1) {
            return;
        }

        int sumRowIdx;
        boolean reuseSumRow = false;
        Row trailingRow = sheet.getRow(lastRow);
        if (trailingRow != null && isResultMatchSumLabelRow(trailingRow)) {
            sumRowIdx = lastRow;
            reuseSumRow = true;
        } else {
            sumRowIdx = lastRow + 1;
        }

        int dataEndRow = reuseSumRow ? lastRow - 1 : lastRow;
        BigDecimal sumL = BigDecimal.ZERO;
        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                continue;
            }
            sumL = sumL.add(readNumericCellValue(row.getCell(RESULT_MATCH_AMT)));
        }

        Row sumRow = sheet.getRow(sumRowIdx);
        if (sumRow == null) {
            sumRow = sheet.createRow(sumRowIdx);
            applyRowHeight(sumRow, styles);
        }
        createTextCell(sumRow, RESULT_MATCH_NEG_NAME, "求和", styles.summaryTextStyle);
        createPaymentActualCell(sumRow, RESULT_MATCH_AMT, sumL, styles.summaryRoundedAmountStyle);
    }

    private static boolean isResultMatchSumLabelRow(Row row) {
        return "求和".equals(readPaymentCellText(row.getCell(RESULT_MATCH_NEG_NAME)));
    }

    /**
     * 结果表检查表：I/K 昵称各出现一次；P～V 为用户ID、昵称、最终收付款及买卖数据。
     */
    private static void fillResultCheckSheet(
            Sheet target,
            Sheet resultSource,
            Sheet summarySheet,
            Map<String, String> userIdByNickname,
            Map<String, BigDecimal> buyTotalByBuyerId,
            Map<String, String> buyDetailByBuyerId,
            Map<String, BigDecimal> sellTotalBySellerId,
            Map<String, String> sellDetailBySellerId,
            SheetStyles styles) {
        copyResultSheetContent(resultSource, target, true);
        writeResultCheckExtraHeaders(target, styles);
        Map<String, String> nicknameToUserId = userIdByNickname == null ? Map.of() : userIdByNickname;
        Map<String, BigDecimal> buyTotalMap = buyTotalByBuyerId == null ? Map.of() : buyTotalByBuyerId;
        Map<String, String> buyDetailMap = buyDetailByBuyerId == null ? Map.of() : buyDetailByBuyerId;
        Map<String, BigDecimal> sellTotalMap = sellTotalBySellerId == null ? Map.of() : sellTotalBySellerId;
        Map<String, String> sellDetailMap = sellDetailBySellerId == null ? Map.of() : sellDetailBySellerId;

        int lastRow = target.getLastRowNum();
        Set<Integer> usedSummaryRows = new HashSet<>();
        Set<String> writtenNicknames = new HashSet<>();

        for (int rowIdx = 1; rowIdx <= lastRow; rowIdx++) {
            Row row = target.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                continue;
            }
            String iName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_MATCH_NEG_NAME)));
            String kName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_MATCH_POS_NAME)));
            if (iName.isEmpty() && kName.isEmpty()) {
                continue;
            }

            if (!iName.isEmpty() && !writtenNicknames.contains(iName)) {
                fillResultCheckUserColumns(
                        ensureResultCheckRow(target, rowIdx, styles),
                        iName,
                        nicknameToUserId,
                        buyTotalMap,
                        buyDetailMap,
                        sellTotalMap,
                        sellDetailMap,
                        styles);
                usedSummaryRows.add(rowIdx);
                writtenNicknames.add(iName);
                continue;
            }
            if (!kName.isEmpty() && !writtenNicknames.contains(kName)) {
                fillResultCheckUserColumns(
                        ensureResultCheckRow(target, rowIdx, styles),
                        kName,
                        nicknameToUserId,
                        buyTotalMap,
                        buyDetailMap,
                        sellTotalMap,
                        sellDetailMap,
                        styles);
                usedSummaryRows.add(rowIdx);
                writtenNicknames.add(kName);
            }
        }

        for (int rowIdx = 1; rowIdx <= lastRow; rowIdx++) {
            Row row = target.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                continue;
            }
            String kName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_MATCH_POS_NAME)));
            if (kName.isEmpty() || writtenNicknames.contains(kName)) {
                continue;
            }
            int summaryRow = allocateResultCheckSummaryRow(target, usedSummaryRows, lastRow, styles);
            fillResultCheckUserColumns(
                    ensureResultCheckRow(target, summaryRow, styles),
                    kName,
                    nicknameToUserId,
                    buyTotalMap,
                    buyDetailMap,
                    sellTotalMap,
                    sellDetailMap,
                    styles);
            usedSummaryRows.add(summaryRow);
            writtenNicknames.add(kName);
            lastRow = Math.max(lastRow, summaryRow);
        }

        fillResultCheckColumnsMandNByIandKNickname(target, styles);
        fillResultCheckColumnZFromColumnS(target, styles);
        fillResultCheckColumnAdFromColumnV(target, styles);
        fillResultCheckColumnAaSumByYNickname(target, styles);
        fillResultCheckColumnAeSumByAcNickname(target, styles);
        writeResultCheckColumnLAndMSum(target, styles);
        writeResultCheckBuyAmountDiffRows(target, summarySheet, styles);
        autoSizeColumns(target, RESULT_CHECK_LAST_COL);
        shrinkResultCheckDetailColumnWidths(target);
    }

    /** 结果表检查表 S、V 列（订单/卖出金额明细）宽度缩小为当前 20%。 */
    private static void shrinkResultCheckDetailColumnWidths(Sheet sheet) {
        shrinkColumnWidthToRatio(sheet, RESULT_CHECK_COL_BUY_DETAIL, 0.2d);
        shrinkColumnWidthToRatio(sheet, RESULT_CHECK_COL_SELL_DETAIL, 0.2d);
    }

    private static void shrinkColumnWidthToRatio(Sheet sheet, int col, double ratio) {
        int currentWidth = sheet.getColumnWidth(col);
        int shrunk = Math.max(256, (int) Math.round(currentWidth * ratio));
        sheet.setColumnWidth(col, shrunk);
    }

    /**
     * S 列（订单金额明细）含多笔订单时，按「+」拆分后逐行写入 Z 列，Y 列写入对应昵称；仅 1 笔订单不写 Y/Z。
     */
    private static void fillResultCheckColumnZFromColumnS(Sheet sheet, SheetStyles styles) {
        int dataEndRow = resolveResultCheckDataEndRow(sheet);
        if (dataEndRow < 1) {
            return;
        }

        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                continue;
            }
            String buyDetail = readPaymentCellText(row.getCell(RESULT_CHECK_COL_BUY_DETAIL));
            if (buyDetail.isEmpty()) {
                continue;
            }
            List<BigDecimal> amounts = parseOrderAmountDetailText(buyDetail);
            if (amounts.size() <= 1) {
                continue;
            }
            String nickname = readPaymentCellText(row.getCell(RESULT_CHECK_COL_NICKNAME));
            if (nickname.isEmpty()) {
                nickname = readPaymentCellText(row.getCell(RESULT_CHECK_COL_CURRENT_NICKNAME));
            }

            int writeRow = rowIdx;
            for (BigDecimal amount : amounts) {
                writeRow = findNextEmptyResultCheckColumnZ(sheet, writeRow);
                Row targetRow = ensureResultCheckRow(sheet, writeRow, styles);
                createPaymentActualCell(
                        targetRow, RESULT_CHECK_COL_BUY_ORDER_AMOUNT, amount, paymentActualStyle(writeRow, styles));
                if (!nickname.isEmpty() && isResultCheckColumnYEmpty(targetRow)) {
                    createTextCell(targetRow, RESULT_CHECK_COL_BUY_ORDER_NICKNAME, nickname, styles.textStyle);
                }
                writeRow++;
            }
        }
    }

    /**
     * V 列（卖出金额明细）含多笔订单时，按「+」拆分后逐行写入 AD 列，AC 列写入 Q 列昵称；仅 1 笔订单不写 AC/AD。
     */
    private static void fillResultCheckColumnAdFromColumnV(Sheet sheet, SheetStyles styles) {
        int dataEndRow = resolveResultCheckDataEndRow(sheet);
        if (dataEndRow < 1) {
            return;
        }

        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                continue;
            }
            String sellDetail = readPaymentCellText(row.getCell(RESULT_CHECK_COL_SELL_DETAIL));
            if (sellDetail.isEmpty()) {
                continue;
            }
            List<BigDecimal> amounts = parseOrderAmountDetailText(sellDetail);
            if (amounts.size() <= 1) {
                continue;
            }
            String nickname = readPaymentCellText(row.getCell(RESULT_CHECK_COL_NICKNAME));

            int writeRow = rowIdx;
            for (BigDecimal amount : amounts) {
                writeRow = findNextEmptyResultCheckColumnAd(sheet, writeRow);
                Row targetRow = ensureResultCheckRow(sheet, writeRow, styles);
                createPaymentActualCell(
                        targetRow, RESULT_CHECK_COL_SELL_ORDER_AMOUNT, amount, paymentActualStyle(writeRow, styles));
                if (!nickname.isEmpty() && isResultCheckColumnAcEmpty(targetRow)) {
                    createTextCell(targetRow, RESULT_CHECK_COL_SELL_ORDER_NICKNAME, nickname, styles.textStyle);
                }
                writeRow++;
            }
        }
    }

    /** AE 列：按 AC 列昵称汇总 AD 列卖出订单金额，连续相同昵称仅末行展示。 */
    private static void fillResultCheckColumnAeSumByAcNickname(Sheet sheet, SheetStyles styles) {
        int dataEndRow = resolveResultCheckDataEndRow(sheet);
        if (dataEndRow < 1) {
            return;
        }

        Map<String, BigDecimal> sumByAcNickname = new HashMap<>();
        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                continue;
            }
            String acName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_CHECK_COL_SELL_ORDER_NICKNAME)));
            if (acName.isEmpty() || isResultCheckColumnAdEmpty(row)) {
                continue;
            }
            BigDecimal adValue = readNumericCellValue(row.getCell(RESULT_CHECK_COL_SELL_ORDER_AMOUNT));
            sumByAcNickname.merge(acName, adValue, BigDecimal::add);
        }

        Set<Integer> displayRows = resolveConsecutiveLastRowsByColumnAc(sheet, dataEndRow);

        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                continue;
            }
            String acName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_CHECK_COL_SELL_ORDER_NICKNAME)));
            if (displayRows.contains(rowIdx)) {
                BigDecimal sum = sumByAcNickname.getOrDefault(acName, BigDecimal.ZERO);
                createPaymentActualCell(
                        row, RESULT_CHECK_COL_SELL_ORDER_AD_SUM, sum, paymentActualStyle(rowIdx, styles));
            } else {
                createTextCell(row, RESULT_CHECK_COL_SELL_ORDER_AD_SUM, "", styles.textStyle);
            }
        }
    }

    /** AA 列：按 Y 列昵称汇总 Z 列订单金额，连续相同昵称仅末行展示。 */
    private static void fillResultCheckColumnAaSumByYNickname(Sheet sheet, SheetStyles styles) {
        int dataEndRow = resolveResultCheckDataEndRow(sheet);
        if (dataEndRow < 1) {
            return;
        }

        Map<String, BigDecimal> sumByYNickname = new HashMap<>();
        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                continue;
            }
            String yName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_CHECK_COL_BUY_ORDER_NICKNAME)));
            if (yName.isEmpty() || isResultCheckColumnZEmpty(row)) {
                continue;
            }
            BigDecimal zValue = readNumericCellValue(row.getCell(RESULT_CHECK_COL_BUY_ORDER_AMOUNT));
            sumByYNickname.merge(yName, zValue, BigDecimal::add);
        }

        Set<Integer> displayRows = resolveConsecutiveLastRowsByColumnY(sheet, dataEndRow);

        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                continue;
            }
            String yName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_CHECK_COL_BUY_ORDER_NICKNAME)));
            if (displayRows.contains(rowIdx)) {
                BigDecimal sum = sumByYNickname.getOrDefault(yName, BigDecimal.ZERO);
                createPaymentActualCell(
                        row, RESULT_CHECK_COL_BUY_ORDER_YZ_SUM, sum, paymentActualStyle(rowIdx, styles));
            } else {
                createTextCell(row, RESULT_CHECK_COL_BUY_ORDER_YZ_SUM, "", styles.textStyle);
            }
        }
    }

    private static Set<Integer> resolveConsecutiveLastRowsByColumnY(Sheet sheet, int dataEndRow) {
        Set<Integer> displayRows = new HashSet<>();
        String groupNickname = null;
        int groupLastRow = -1;

        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                flushResultCheckNicknameGroup(displayRows, groupNickname, groupLastRow);
                groupNickname = null;
                groupLastRow = -1;
                continue;
            }
            String yName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_CHECK_COL_BUY_ORDER_NICKNAME)));
            if (yName.isEmpty()) {
                flushResultCheckNicknameGroup(displayRows, groupNickname, groupLastRow);
                groupNickname = null;
                groupLastRow = -1;
                continue;
            }
            if (!yName.equals(groupNickname)) {
                flushResultCheckNicknameGroup(displayRows, groupNickname, groupLastRow);
                groupNickname = yName;
            }
            groupLastRow = rowIdx;
        }
        flushResultCheckNicknameGroup(displayRows, groupNickname, groupLastRow);
        return displayRows;
    }

    private static Set<Integer> resolveConsecutiveLastRowsByColumnAc(Sheet sheet, int dataEndRow) {
        Set<Integer> displayRows = new HashSet<>();
        String groupNickname = null;
        int groupLastRow = -1;

        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                flushResultCheckNicknameGroup(displayRows, groupNickname, groupLastRow);
                groupNickname = null;
                groupLastRow = -1;
                continue;
            }
            String acName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_CHECK_COL_SELL_ORDER_NICKNAME)));
            if (acName.isEmpty()) {
                flushResultCheckNicknameGroup(displayRows, groupNickname, groupLastRow);
                groupNickname = null;
                groupLastRow = -1;
                continue;
            }
            if (!acName.equals(groupNickname)) {
                flushResultCheckNicknameGroup(displayRows, groupNickname, groupLastRow);
                groupNickname = acName;
            }
            groupLastRow = rowIdx;
        }
        flushResultCheckNicknameGroup(displayRows, groupNickname, groupLastRow);
        return displayRows;
    }

    private static List<BigDecimal> parseOrderAmountDetailText(String detail) {
        if (detail == null || detail.isBlank()) {
            return List.of();
        }
        List<BigDecimal> amounts = new ArrayList<>();
        for (String part : detail.split("\\+")) {
            String trimmed = part.trim();
            if (trimmed.isEmpty()) {
                continue;
            }
            try {
                amounts.add(new BigDecimal(trimmed));
            } catch (NumberFormatException ignored) {
                // skip invalid segment
            }
        }
        return amounts;
    }

    private static int findNextEmptyResultCheckColumnZ(Sheet sheet, int startRow) {
        int rowIdx = Math.max(1, startRow);
        while (true) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultCheckColumnZEmpty(row)) {
                return rowIdx;
            }
            rowIdx++;
        }
    }

    private static int findNextEmptyResultCheckColumnAd(Sheet sheet, int startRow) {
        int rowIdx = Math.max(1, startRow);
        while (true) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultCheckColumnAdEmpty(row)) {
                return rowIdx;
            }
            rowIdx++;
        }
    }

    private static boolean isResultCheckColumnYEmpty(Row row) {
        if (row == null) {
            return true;
        }
        Cell cell = row.getCell(RESULT_CHECK_COL_BUY_ORDER_NICKNAME);
        if (cell == null || cell.getCellType() == CellType.BLANK) {
            return true;
        }
        if (cell.getCellType() == CellType.STRING) {
            String text = cell.getStringCellValue();
            return text == null || text.trim().isEmpty();
        }
        return false;
    }

    private static boolean isResultCheckColumnZEmpty(Row row) {
        if (row == null) {
            return true;
        }
        Cell cell = row.getCell(RESULT_CHECK_COL_BUY_ORDER_AMOUNT);
        if (cell == null || cell.getCellType() == CellType.BLANK) {
            return true;
        }
        if (cell.getCellType() == CellType.STRING) {
            String text = cell.getStringCellValue();
            return text == null || text.trim().isEmpty();
        }
        return false;
    }

    private static boolean isResultCheckColumnAcEmpty(Row row) {
        if (row == null) {
            return true;
        }
        Cell cell = row.getCell(RESULT_CHECK_COL_SELL_ORDER_NICKNAME);
        if (cell == null || cell.getCellType() == CellType.BLANK) {
            return true;
        }
        if (cell.getCellType() == CellType.STRING) {
            String text = cell.getStringCellValue();
            return text == null || text.trim().isEmpty();
        }
        return false;
    }

    private static boolean isResultCheckColumnAdEmpty(Row row) {
        if (row == null) {
            return true;
        }
        Cell cell = row.getCell(RESULT_CHECK_COL_SELL_ORDER_AMOUNT);
        if (cell == null || cell.getCellType() == CellType.BLANK) {
            return true;
        }
        if (cell.getCellType() == CellType.STRING) {
            String text = cell.getStringCellValue();
            return text == null || text.trim().isEmpty();
        }
        return false;
    }

    /** 结果表检查表：末行追加 L、M、T、W、Z、AA 列数据合计。 */
    private static void writeResultCheckColumnLAndMSum(Sheet sheet, SheetStyles styles) {
        int lastRow = sheet.getLastRowNum();
        if (lastRow < 1) {
            return;
        }

        int dataEndRow = lastRow;
        Row trailingRow = sheet.getRow(lastRow);
        if (trailingRow != null && isResultMatchSumLabelRow(trailingRow)) {
            dataEndRow = lastRow - 1;
        }
        if (dataEndRow < 1) {
            return;
        }

        BigDecimal sumL = BigDecimal.ZERO;
        BigDecimal sumM = BigDecimal.ZERO;
        BigDecimal sumT = BigDecimal.ZERO;
        BigDecimal sumW = BigDecimal.ZERO;
        BigDecimal sumZ = BigDecimal.ZERO;
        BigDecimal sumAa = BigDecimal.ZERO;
        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                continue;
            }
            sumL = sumL.add(readNumericCellValue(row.getCell(RESULT_MATCH_AMT)));
            if (!isResultCheckColumnMEmpty(row)) {
                sumM = sumM.add(readNumericCellValue(row.getCell(RESULT_CHECK_COL_IK_NICKNAME_L_SUM)));
            }
            if (!isResultCheckColumnTEmpty(row)) {
                sumT = sumT.add(readNumericCellValue(row.getCell(RESULT_CHECK_COL_BUY_TOTAL)));
            }
            if (!isResultCheckColumnWEmpty(row)) {
                sumW = sumW.add(readNumericCellValue(row.getCell(RESULT_CHECK_COL_SELL_TOTAL)));
            }
            if (!isResultCheckColumnZEmpty(row)) {
                sumZ = sumZ.add(readNumericCellValue(row.getCell(RESULT_CHECK_COL_BUY_ORDER_AMOUNT)));
            }
            if (!isResultCheckColumnAaEmpty(row)) {
                sumAa = sumAa.add(readNumericCellValue(row.getCell(RESULT_CHECK_COL_BUY_ORDER_YZ_SUM)));
            }
        }

        int sumRowIdx = dataEndRow + 1;
        Row sumRow = sheet.getRow(sumRowIdx);
        if (sumRow == null) {
            sumRow = sheet.createRow(sumRowIdx);
            applyRowHeight(sumRow, styles);
        }
        createTextCell(sumRow, RESULT_MATCH_NEG_NAME, "求和", styles.summaryTextStyle);
        createPaymentActualCell(sumRow, RESULT_MATCH_AMT, sumL, styles.summaryRoundedAmountStyle);
        createPaymentActualCell(sumRow, RESULT_CHECK_COL_IK_NICKNAME_L_SUM, sumM, styles.summaryRoundedAmountStyle);
        createDecimalCell(sumRow, RESULT_CHECK_COL_BUY_TOTAL, sumT, styles.summaryAmountStyle);
        createDecimalCell(sumRow, RESULT_CHECK_COL_SELL_TOTAL, sumW, styles.summaryAmountStyle);
        createPaymentActualCell(sumRow, RESULT_CHECK_COL_BUY_ORDER_AMOUNT, sumZ, styles.summaryRoundedAmountStyle);
        createPaymentActualCell(sumRow, RESULT_CHECK_COL_BUY_ORDER_YZ_SUM, sumAa, styles.summaryRoundedAmountStyle);
    }

    /**
     * 总表 D 列（当日买入）与检查表 T 列（订单买入）按昵称核对；差异写在求和行之后。
     * 总表 A 列昵称 ↔ 检查表 Q 列昵称。
     */
    private static void writeResultCheckBuyAmountDiffRows(
            Sheet checkSheet, Sheet summarySheet, SheetStyles styles) {
        if (summarySheet == null) {
            return;
        }
        int sumRowIdx = findResultCheckSumRowIndex(checkSheet);
        if (sumRowIdx < 1) {
            return;
        }

        Map<String, BigDecimal> summaryBuyByNickname = readSummaryDailyBuyByNickname(summarySheet);
        Map<String, BigDecimal> checkBuyByNickname = readCheckBuyByNickname(checkSheet, sumRowIdx - 1);
        List<BuyAmountDiffItem> diffItems = buildBuyAmountDiffItems(summaryBuyByNickname, checkBuyByNickname);

        BigDecimal summaryTotal = readSummarySheetDailyBuyTotal(summarySheet);
        Row sumRow = checkSheet.getRow(sumRowIdx);
        BigDecimal checkTotal = sumRow == null
                ? BigDecimal.ZERO
                : readNumericCellValue(sumRow.getCell(RESULT_CHECK_COL_BUY_TOTAL));

        int writeRow = sumRowIdx + 1;
        Row titleRow = ensureResultCheckRow(checkSheet, writeRow++, styles);
        createTextCell(titleRow, RESULT_MATCH_NEG_NAME, "【买入核对差异】", styles.summaryTextStyle);

        if (summaryTotal.compareTo(checkTotal) != 0) {
            Row totalDiffRow = ensureResultCheckRow(checkSheet, writeRow++, styles);
            createTextCell(totalDiffRow, RESULT_MATCH_NEG_NAME, "合计差异", styles.summaryTextStyle);
            createTextCell(totalDiffRow, RESULT_CHECK_COL_NICKNAME, "合计", styles.summaryTextStyle);
            createDecimalCell(totalDiffRow, RESULT_CHECK_COL_BUY_DIFF_SUMMARY, summaryTotal, styles.summaryAmountStyle);
            createDecimalCell(totalDiffRow, RESULT_CHECK_COL_BUY_DIFF_CHECK, checkTotal, styles.summaryAmountStyle);
            createDecimalCell(
                    totalDiffRow,
                    RESULT_CHECK_COL_BUY_DIFF,
                    summaryTotal.subtract(checkTotal),
                    styles.summaryAmountStyle);
        }

        if (diffItems.isEmpty()) {
            Row okRow = ensureResultCheckRow(checkSheet, writeRow, styles);
            createTextCell(okRow, RESULT_MATCH_NEG_NAME, "无昵称级差异", styles.textStyle);
            return;
        }

        for (BuyAmountDiffItem item : diffItems) {
            Row row = ensureResultCheckRow(checkSheet, writeRow++, styles);
            createTextCell(row, RESULT_MATCH_NEG_NAME, item.reason(), styles.textStyle);
            createTextCell(row, RESULT_CHECK_COL_NICKNAME, item.nickname(), styles.textStyle);
            if (item.summaryBuyAmt() != null) {
                createDecimalCell(row, RESULT_CHECK_COL_BUY_DIFF_SUMMARY, item.summaryBuyAmt(), styles.amountStyle);
            }
            if (item.checkBuyAmt() != null) {
                createDecimalCell(row, RESULT_CHECK_COL_BUY_DIFF_CHECK, item.checkBuyAmt(), styles.amountStyle);
            }
            createDecimalCell(row, RESULT_CHECK_COL_BUY_DIFF, item.diffAmt(), styles.amountStyle);
        }
    }

    private static int findResultCheckSumRowIndex(Sheet sheet) {
        for (int rowIdx = sheet.getLastRowNum(); rowIdx >= 1; rowIdx--) {
            Row row = sheet.getRow(rowIdx);
            if (row != null && isResultMatchSumLabelRow(row)) {
                return rowIdx;
            }
        }
        return -1;
    }

    private static Map<String, BigDecimal> readSummaryDailyBuyByNickname(Sheet summarySheet) {
        Map<String, BigDecimal> buyByNickname = new LinkedHashMap<>();
        int lastRow = summarySheet.getLastRowNum();
        for (int rowIdx = 1; rowIdx <= lastRow; rowIdx++) {
            Row row = summarySheet.getRow(rowIdx);
            if (row == null) {
                continue;
            }
            String nickname = normalizeResultCheckNickname(readPaymentCellText(row.getCell(COL_NICKNAME)));
            if (nickname.isEmpty()) {
                continue;
            }
            buyByNickname.put(nickname, readNumericCellValue(row.getCell(COL_DAILY_BUY)));
        }
        return buyByNickname;
    }

    private static BigDecimal readSummarySheetDailyBuyTotal(Sheet summarySheet) {
        int lastRow = summarySheet.getLastRowNum();
        if (lastRow < 1) {
            return BigDecimal.ZERO;
        }
        Row lastDataRow = summarySheet.getRow(lastRow);
        if (lastDataRow == null) {
            return BigDecimal.ZERO;
        }
        return readNumericCellValue(lastDataRow.getCell(COL_DAILY_BUY));
    }

    private static Map<String, BigDecimal> readCheckBuyByNickname(Sheet checkSheet, int dataEndRow) {
        Map<String, BigDecimal> buyByNickname = new LinkedHashMap<>();
        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = checkSheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                continue;
            }
            String nickname = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_CHECK_COL_NICKNAME)));
            if (nickname.isEmpty() || isResultCheckColumnTEmpty(row)) {
                continue;
            }
            buyByNickname.put(nickname, readNumericCellValue(row.getCell(RESULT_CHECK_COL_BUY_TOTAL)));
        }
        return buyByNickname;
    }

    private static List<BuyAmountDiffItem> buildBuyAmountDiffItems(
            Map<String, BigDecimal> summaryBuyByNickname,
            Map<String, BigDecimal> checkBuyByNickname) {
        LinkedHashSet<String> allNicknames = new LinkedHashSet<>();
        allNicknames.addAll(summaryBuyByNickname.keySet());
        allNicknames.addAll(checkBuyByNickname.keySet());

        List<BuyAmountDiffItem> diffItems = new ArrayList<>();
        for (String nickname : allNicknames) {
            boolean inSummary = summaryBuyByNickname.containsKey(nickname);
            boolean inCheck = checkBuyByNickname.containsKey(nickname);
            BigDecimal summaryAmt = summaryBuyByNickname.get(nickname);
            BigDecimal checkAmt = checkBuyByNickname.get(nickname);

            if (inSummary && !inCheck) {
                diffItems.add(new BuyAmountDiffItem(
                        nickname, summaryAmt, null, "仅总表有", summaryAmt));
            } else if (!inSummary && inCheck) {
                diffItems.add(new BuyAmountDiffItem(
                        nickname, null, checkAmt, "仅检查表有", defaultZero(checkAmt).negate()));
            } else if (inSummary && summaryAmt.compareTo(checkAmt) != 0) {
                diffItems.add(new BuyAmountDiffItem(
                        nickname, summaryAmt, checkAmt, "金额不一致", summaryAmt.subtract(checkAmt)));
            }
        }
        return diffItems;
    }

    private record BuyAmountDiffItem(
            String nickname,
            BigDecimal summaryBuyAmt,
            BigDecimal checkBuyAmt,
            String reason,
            BigDecimal diffAmt) {
    }

    /**
     * 结果表检查表：先按 K 列昵称汇总 L 写入 M、N；再按 I 列昵称汇总 L 补写 M、N（M 已有值不覆盖）。
     */
    private static void fillResultCheckColumnsMandNByIandKNickname(Sheet sheet, SheetStyles styles) {
        int dataEndRow = resolveResultCheckDataEndRow(sheet);
        if (dataEndRow < 1) {
            return;
        }
        fillResultCheckColumnsMandNByKNickname(sheet, dataEndRow, styles);
        fillResultCheckColumnsMandNByINicknameIfMEmpty(sheet, dataEndRow, styles);
    }

    private static int resolveResultCheckDataEndRow(Sheet sheet) {
        int lastRow = sheet.getLastRowNum();
        if (lastRow < 1) {
            return -1;
        }
        Row trailingRow = sheet.getRow(lastRow);
        if (trailingRow != null && isResultMatchSumLabelRow(trailingRow)) {
            return lastRow - 1;
        }
        return lastRow;
    }

    /** K 列昵称汇总：M、N 仅在连续相同 K 昵称块的最后一行展示。 */
    private static void fillResultCheckColumnsMandNByKNickname(Sheet sheet, int dataEndRow, SheetStyles styles) {
        Map<String, BigDecimal> sumByKNickname = new HashMap<>();
        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                continue;
            }
            String iName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_MATCH_NEG_NAME)));
            String kName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_MATCH_POS_NAME)));
            if (!includeRowForColumnKSum(iName, kName)) {
                continue;
            }
            BigDecimal lValue = readNumericCellValue(row.getCell(RESULT_MATCH_AMT));
            sumByKNickname.merge(kName, lValue, BigDecimal::add);
        }

        Set<Integer> displayRows = resolveConsecutiveLastRowsByNickname(
                sheet, dataEndRow, LbDailyUserTradeReportExportSupport::includeRowForColumnKSum,
                (iName, kName) -> kName);

        writeResultCheckSummaryColumns(
                sheet, dataEndRow, displayRows, sumByKNickname,
                (iName, kName) -> kName, styles, false);
    }

    /** I 列昵称汇总：仅在 M 列为空时写入，M、N 仅在连续相同 I 昵称块的最后一行展示。 */
    private static void fillResultCheckColumnsMandNByINicknameIfMEmpty(
            Sheet sheet, int dataEndRow, SheetStyles styles) {
        Map<String, BigDecimal> sumByINickname = new HashMap<>();
        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                continue;
            }
            String iName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_MATCH_NEG_NAME)));
            if (iName.isEmpty()) {
                continue;
            }
            BigDecimal lValue = readNumericCellValue(row.getCell(RESULT_MATCH_AMT));
            sumByINickname.merge(iName, lValue, BigDecimal::add);
        }

        Set<Integer> displayRows = resolveConsecutiveLastRowsByNickname(
                sheet, dataEndRow,
                (iName, kName) -> !iName.isEmpty(),
                (iName, kName) -> iName);

        writeResultCheckSummaryColumns(
                sheet, dataEndRow, displayRows, sumByINickname,
                (iName, kName) -> iName, styles, true);
    }

    private static void writeResultCheckSummaryColumns(
            Sheet sheet,
            int dataEndRow,
            Set<Integer> displayRows,
            Map<String, BigDecimal> sumByNickname,
            java.util.function.BiFunction<String, String, String> sumNicknameFn,
            SheetStyles styles,
            boolean skipWhenMHasValue) {
        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                continue;
            }
            String iName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_MATCH_NEG_NAME)));
            String kName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_MATCH_POS_NAME)));
            String sumNickname = sumNicknameFn.apply(iName, kName);
            if (sumNickname == null || sumNickname.isEmpty()) {
                if (!skipWhenMHasValue) {
                    createTextCell(row, RESULT_CHECK_COL_IK_NICKNAME_L_SUM, "", styles.textStyle);
                    createTextCell(row, RESULT_CHECK_COL_CURRENT_NICKNAME, "", styles.textStyle);
                }
                continue;
            }
            if (!displayRows.contains(rowIdx)) {
                if (!skipWhenMHasValue) {
                    createTextCell(row, RESULT_CHECK_COL_IK_NICKNAME_L_SUM, "", styles.textStyle);
                    createTextCell(row, RESULT_CHECK_COL_CURRENT_NICKNAME, "", styles.textStyle);
                }
                continue;
            }
            if (skipWhenMHasValue && !isResultCheckColumnMEmpty(row)) {
                continue;
            }
            BigDecimal sum = sumByNickname.getOrDefault(sumNickname, BigDecimal.ZERO);
            createPaymentActualCell(
                    row, RESULT_CHECK_COL_IK_NICKNAME_L_SUM, sum, paymentActualStyle(rowIdx, styles));
            createTextCell(row, RESULT_CHECK_COL_CURRENT_NICKNAME, sumNickname, styles.textStyle);
        }
    }

    private static boolean isResultCheckColumnMEmpty(Row row) {
        if (row == null) {
            return true;
        }
        Cell cell = row.getCell(RESULT_CHECK_COL_IK_NICKNAME_L_SUM);
        if (cell == null || cell.getCellType() == CellType.BLANK) {
            return true;
        }
        if (cell.getCellType() == CellType.STRING) {
            String text = cell.getStringCellValue();
            return text == null || text.trim().isEmpty();
        }
        return false;
    }

    private static boolean isResultCheckColumnTEmpty(Row row) {
        return isResultCheckNumericColumnEmpty(row, RESULT_CHECK_COL_BUY_TOTAL);
    }

    private static boolean isResultCheckColumnWEmpty(Row row) {
        return isResultCheckNumericColumnEmpty(row, RESULT_CHECK_COL_SELL_TOTAL);
    }

    private static boolean isResultCheckNumericColumnEmpty(Row row, int col) {
        if (row == null) {
            return true;
        }
        Cell cell = row.getCell(col);
        if (cell == null || cell.getCellType() == CellType.BLANK) {
            return true;
        }
        if (cell.getCellType() == CellType.STRING) {
            String text = cell.getStringCellValue();
            return text == null || text.trim().isEmpty();
        }
        return false;
    }

    private static boolean isResultCheckColumnAaEmpty(Row row) {
        if (row == null) {
            return true;
        }
        Cell cell = row.getCell(RESULT_CHECK_COL_BUY_ORDER_YZ_SUM);
        if (cell == null || cell.getCellType() == CellType.BLANK) {
            return true;
        }
        if (cell.getCellType() == CellType.STRING) {
            String text = cell.getStringCellValue();
            return text == null || text.trim().isEmpty();
        }
        return false;
    }

    /** 参与 K 列汇总：I、K 列昵称均非空。 */
    private static boolean includeRowForColumnKSum(String iName, String kName) {
        return !iName.isEmpty() && !kName.isEmpty();
    }

    /**
     * 连续相同汇总昵称块：返回每块最后一行的行号。
     */
    private static Set<Integer> resolveConsecutiveLastRowsByNickname(
            Sheet sheet,
            int dataEndRow,
            java.util.function.BiPredicate<String, String> includeRow,
            java.util.function.BiFunction<String, String, String> sumNicknameFn) {
        Set<Integer> displayRows = new HashSet<>();
        String groupNickname = null;
        int groupLastRow = -1;

        for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) {
            Row row = sheet.getRow(rowIdx);
            if (row == null || isResultMatchSumLabelRow(row)) {
                flushResultCheckNicknameGroup(displayRows, groupNickname, groupLastRow);
                groupNickname = null;
                groupLastRow = -1;
                continue;
            }
            String iName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_MATCH_NEG_NAME)));
            String kName = normalizeResultCheckNickname(
                    readPaymentCellText(row.getCell(RESULT_MATCH_POS_NAME)));
            if (!includeRow.test(iName, kName)) {
                flushResultCheckNicknameGroup(displayRows, groupNickname, groupLastRow);
                groupNickname = null;
                groupLastRow = -1;
                continue;
            }
            String sumNickname = sumNicknameFn.apply(iName, kName);
            if (sumNickname == null || sumNickname.isEmpty()) {
                flushResultCheckNicknameGroup(displayRows, groupNickname, groupLastRow);
                groupNickname = null;
                groupLastRow = -1;
                continue;
            }
            if (!sumNickname.equals(groupNickname)) {
                flushResultCheckNicknameGroup(displayRows, groupNickname, groupLastRow);
                groupNickname = sumNickname;
            }
            groupLastRow = rowIdx;
        }
        flushResultCheckNicknameGroup(displayRows, groupNickname, groupLastRow);
        return displayRows;
    }

    private static void flushResultCheckNicknameGroup(
            Set<Integer> displayRows, String groupNickname, int groupLastRow) {
        if (groupNickname != null && groupLastRow >= 0) {
            displayRows.add(groupLastRow);
        }
    }

    /** 优先使用 1..lastRow 内未占用的行，否则在表尾追加。 */
    private static int allocateResultCheckSummaryRow(
            Sheet sheet, Set<Integer> usedSummaryRows, int lastRow, SheetStyles styles) {
        for (int rowIdx = 1; rowIdx <= lastRow; rowIdx++) {
            if (!usedSummaryRows.contains(rowIdx)) {
                return rowIdx;
            }
        }
        int appendRow = lastRow + 1;
        while (usedSummaryRows.contains(appendRow)) {
            appendRow++;
        }
        ensureResultCheckRow(sheet, appendRow, styles);
        return appendRow;
    }

    private static Row ensureResultCheckRow(Sheet sheet, int rowIdx, SheetStyles styles) {
        Row row = sheet.getRow(rowIdx);
        if (row == null) {
            row = sheet.createRow(rowIdx);
            applyRowHeight(row, styles);
        }
        return row;
    }

    /** P～V：用户ID、昵称、最终收付款、买卖明细与汇总。 */
    private static void fillResultCheckUserColumns(
            Row row,
            String nickname,
            Map<String, String> userIdByNickname,
            Map<String, BigDecimal> buyTotalByBuyerId,
            Map<String, String> buyDetailByBuyerId,
            Map<String, BigDecimal> sellTotalBySellerId,
            Map<String, String> sellDetailBySellerId,
            SheetStyles styles) {
        String trimmed = normalizeResultCheckNickname(nickname);
        if (trimmed.isEmpty()) {
            return;
        }
        String userId = userIdByNickname.getOrDefault(trimmed, "");
        createTextCell(row, RESULT_CHECK_COL_USER_ID, userId, styles.textStyle);
        createTextCell(row, RESULT_CHECK_COL_NICKNAME, trimmed, styles.textStyle);
        if (!userId.isEmpty()) {
            String buyDetail = buyDetailByBuyerId.getOrDefault(userId, "");
            BigDecimal buyTotal = buyTotalByBuyerId.getOrDefault(userId, BigDecimal.ZERO);
            String sellDetail = sellDetailBySellerId.getOrDefault(userId, "");
            BigDecimal sellTotal = sellTotalBySellerId.getOrDefault(userId, BigDecimal.ZERO);
            BigDecimal buyTotalFee = buyTotal
                    .multiply(RESULT_CHECK_BUY_TOTAL_FEE_RATE)
                    .setScale(2, RoundingMode.HALF_UP);
            BigDecimal finalSettlement = sellTotal
                    .subtract(buyTotal)
                    .subtract(buyTotalFee)
                    .setScale(2, RoundingMode.HALF_UP);

            createDecimalCell(row, RESULT_CHECK_COL_FINAL_SETTLEMENT, finalSettlement, styles.amountStyle);
            createTextCell(row, RESULT_CHECK_COL_BUY_DETAIL, buyDetail, styles.textStyle);
            createDecimalCell(row, RESULT_CHECK_COL_BUY_TOTAL, buyTotal, styles.amountStyle);
            createDecimalCell(row, RESULT_CHECK_COL_BUY_TOTAL_FEE, buyTotalFee, styles.amountStyle);
            createTextCell(row, RESULT_CHECK_COL_SELL_DETAIL, sellDetail, styles.textStyle);
            createDecimalCell(row, RESULT_CHECK_COL_SELL_TOTAL, sellTotal, styles.amountStyle);
        }
    }

    private static String normalizeResultCheckNickname(String nickname) {
        return nickname == null ? "" : nickname.trim();
    }

    /** 结果表检查表 M～V 列表头（第 1 行）。 */
    private static void writeResultCheckExtraHeaders(Sheet sheet, SheetStyles styles) {
        Row header = sheet.getRow(0);
        if (header == null) {
            header = sheet.createRow(0);
            header.setHeightInPoints(20 * styles.rowHeightFactor);
        }
        createHeaderCell(header, RESULT_CHECK_COL_IK_NICKNAME_L_SUM, "IK昵称L列合计", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_CURRENT_NICKNAME, "当前人昵称", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_USER_ID, "用户ID", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_NICKNAME, "昵称", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_FINAL_SETTLEMENT, "最终收付款", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_BUY_DETAIL, "订单金额明细", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_BUY_TOTAL, "当日买入金额", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_BUY_TOTAL_FEE, "1%金额", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_SELL_DETAIL, "卖出金额明细", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_SELL_TOTAL, "当日卖出金额", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_BUY_ORDER_NICKNAME, "订单昵称", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_BUY_ORDER_AMOUNT, "订单金额", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_BUY_ORDER_YZ_SUM, "Y昵称Z列合计", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_SELL_ORDER_NICKNAME, "卖出订单昵称", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_SELL_ORDER_AMOUNT, "卖出订单金额", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_SELL_ORDER_AD_SUM, "AC昵称AD列合计", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_BUY_DIFF_SUMMARY, "总表买入", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_BUY_DIFF_CHECK, "检查表买入", styles.headerStyle);
        createHeaderCell(header, RESULT_CHECK_COL_BUY_DIFF, "买入差异", styles.headerStyle);
    }

    private static void copyResultSheetContent(Sheet source, Sheet target) {
        copyResultSheetContent(source, target, false);
    }

    private static void copyResultSheetContent(Sheet source, Sheet target, boolean excludeSumLabelRows) {
        target.setDefaultRowHeight(source.getDefaultRowHeight());
        for (int col = 0; col <= RESULT_LAST_COL; col++) {
            target.setColumnWidth(col, source.getColumnWidth(col));
        }

        int destRowIdx = 0;
        for (int rowIdx = 0; rowIdx <= source.getLastRowNum(); rowIdx++) {
            Row srcRow = source.getRow(rowIdx);
            if (srcRow == null) {
                continue;
            }
            if (excludeSumLabelRows && rowIdx >= 1 && isResultMatchSumLabelRow(srcRow)) {
                continue;
            }
            Row destRow = target.createRow(destRowIdx++);
            destRow.setHeight(srcRow.getHeight());
            for (int col = 0; col <= RESULT_LAST_COL; col++) {
                Cell srcCell = srcRow.getCell(col);
                if (srcCell == null) {
                    continue;
                }
                copyCellValueAndStyle(srcCell, destRow.createCell(col));
            }
        }
    }

    private static List<PaymentMatchEntry> collectPaymentMatchEntries(
            Sheet paymentSheet, int nameCol, int amountCol, boolean positiveSide) {
        List<PaymentMatchEntry> entries = new ArrayList<>();
        for (int rowIdx = 1; rowIdx <= PAYMENT_MATCH_MAX_ROWS; rowIdx++) {
            Row row = paymentSheet.getRow(rowIdx);
            if (row == null) {
                if (positiveSide) {
                    if (isPaymentPairEmpty(paymentSheet, rowIdx, PAYMENT_COL_NAME, PAYMENT_COL_ACTUAL)) {
                        break;
                    }
                } else if (isPaymentPairEmpty(
                        paymentSheet, rowIdx, PAYMENT_NEGATIVE_COL_NAME, PAYMENT_NEGATIVE_COL_ACTUAL)
                        && isPaymentPairEmpty(
                        paymentSheet, rowIdx + 1, PAYMENT_NEGATIVE_COL_NAME, PAYMENT_NEGATIVE_COL_ACTUAL)) {
                    break;
                }
                continue;
            }

            String name = readPaymentCellText(row.getCell(nameCol));
            BigDecimal amount = parsePaymentSheetAmount(row.getCell(amountCol));
            boolean rowEmpty = name.isEmpty() && amount.compareTo(BigDecimal.ZERO) == 0;
            if (rowEmpty) {
                if (positiveSide) {
                    if (isPaymentPairEmpty(paymentSheet, rowIdx, PAYMENT_COL_NAME, PAYMENT_COL_ACTUAL)) {
                        break;
                    }
                } else if (isPaymentPairEmpty(
                        paymentSheet, rowIdx, PAYMENT_NEGATIVE_COL_NAME, PAYMENT_NEGATIVE_COL_ACTUAL)
                        && isPaymentPairEmpty(
                        paymentSheet, rowIdx + 1, PAYMENT_NEGATIVE_COL_NAME, PAYMENT_NEGATIVE_COL_ACTUAL)) {
                    break;
                }
                continue;
            }

            if (name.isEmpty()) {
                continue;
            }
            if (positiveSide) {
                if (amount.compareTo(BigDecimal.ZERO) > 0) {
                    entries.add(new PaymentMatchEntry(name, amount));
                }
            } else if (amount.compareTo(BigDecimal.ZERO) < 0) {
                entries.add(new PaymentMatchEntry(name, amount));
            }
        }
        return entries;
    }

    private static boolean isPaymentPairEmpty(Sheet sheet, int rowIndex, int nameCol, int amountCol) {
        if (rowIndex < 1 || rowIndex > PAYMENT_MATCH_MAX_ROWS) {
            return true;
        }
        Row row = sheet.getRow(rowIndex);
        if (row == null) {
            return true;
        }
        return readPaymentCellText(row.getCell(nameCol)).isEmpty()
                && parsePaymentSheetAmount(row.getCell(amountCol)).compareTo(BigDecimal.ZERO) == 0;
    }

    private static void writeResultListRow(
            Sheet sheet,
            int rowIndex,
            int nameCol,
            int amountCol,
            PaymentMatchEntry entry,
            SheetStyles styles) {
        Row row = sheet.getRow(rowIndex);
        if (row == null) {
            row = sheet.createRow(rowIndex);
            applyRowHeight(row, styles);
        }
        createTextCell(row, nameCol, entry.name(), styles.textStyle);
        createPaymentActualCell(row, amountCol, entry.amount(), paymentActualStyle(rowIndex, styles));
    }

    /** 支付表/结果表读数：去逗号与空格，兼容文本型数字 */
    private static BigDecimal parsePaymentSheetAmount(Cell cell) {
        if (cell == null || cell.getCellType() == CellType.BLANK) {
            return BigDecimal.ZERO;
        }
        if (cell.getCellType() == CellType.NUMERIC
                || (cell.getCellType() == CellType.FORMULA
                && cell.getCachedFormulaResultType() == CellType.NUMERIC)) {
            return BigDecimal.valueOf(cell.getNumericCellValue());
        }
        String text = readPaymentCellText(cell).replace(",", "").replace(" ", "");
        if (text.isEmpty()) {
            return BigDecimal.ZERO;
        }
        try {
            return new BigDecimal(text);
        } catch (NumberFormatException ignored) {
            return BigDecimal.ZERO;
        }
    }

    private static String readPaymentCellText(Cell cell) {
        if (cell == null || cell.getCellType() == CellType.BLANK) {
            return "";
        }
        return switch (cell.getCellType()) {
            case STRING -> {
                String value = cell.getStringCellValue();
                yield value == null ? "" : value.trim();
            }
            case NUMERIC -> BigDecimal.valueOf(cell.getNumericCellValue()).stripTrailingZeros().toPlainString();
            case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
            case FORMULA -> readPaymentCellText(switch (cell.getCachedFormulaResultType()) {
                case STRING, NUMERIC -> cell;
                default -> cell;
            });
            default -> "";
        };
    }

    private static final class PaymentMatchEntry {
        private final String name;
        private BigDecimal amount;

        PaymentMatchEntry(String name, BigDecimal amount) {
            this.name = name;
            this.amount = amount;
        }

        String name() {
            return name;
        }

        BigDecimal amount() {
            return amount;
        }

        void setAmount(BigDecimal amount) {
            this.amount = amount;
        }
    }

    private static void copyPaymentSheetContent(Sheet source, Sheet target) {
        target.setDefaultRowHeight(source.getDefaultRowHeight());
        for (int col = 0; col <= PAYMENT_LAST_COL; col++) {
            target.setColumnWidth(col, source.getColumnWidth(col));
        }

        for (int rowIdx = 0; rowIdx <= source.getLastRowNum(); rowIdx++) {
            Row srcRow = source.getRow(rowIdx);
            if (srcRow == null) {
                continue;
            }
            Row destRow = target.createRow(rowIdx);
            destRow.setHeight(srcRow.getHeight());
            for (int col = 0; col <= PAYMENT_LAST_COL; col++) {
                Cell srcCell = srcRow.getCell(col);
                if (srcCell == null) {
                    continue;
                }
                copyCellValueAndStyle(srcCell, destRow.createCell(col));
            }
        }

        target.createFreezePane(0, 1);
        target.setAutoFilter(new CellRangeAddress(0, 0, 0, PAYMENT_LAST_COL));
    }

    private static void copyCellValueAndStyle(Cell source, Cell target) {
        target.setCellStyle(source.getCellStyle());
        switch (source.getCellType()) {
            case STRING -> target.setCellValue(source.getStringCellValue());
            case NUMERIC -> target.setCellValue(source.getNumericCellValue());
            case BOOLEAN -> target.setCellValue(source.getBooleanCellValue());
            case FORMULA -> target.setCellFormula(source.getCellFormula());
            case BLANK -> target.setBlank();
            default -> {
            }
        }
    }

    private static BigDecimal readNumericCellValue(Cell cell) {
        if (cell == null || cell.getCellType() == CellType.BLANK) {
            return BigDecimal.ZERO;
        }
        if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) {
            return BigDecimal.valueOf(cell.getNumericCellValue());
        }
        if (cell.getCellType() == CellType.STRING) {
            String text = cell.getStringCellValue();
            if (text == null || text.isBlank()) {
                return BigDecimal.ZERO;
            }
            try {
                return new BigDecimal(text.trim());
            } catch (NumberFormatException ignored) {
                return BigDecimal.ZERO;
            }
        }
        return BigDecimal.ZERO;
    }

    /** 抵扣表汇总行「实际收付」= 明细行（抵扣金额 + 原实际收付）之和，与 fillDeductionSheet 口径一致。 */
    private static BigDecimal computeDeductionSheetSummaryActual(
            List<LbDailyUserTradeReport> exportRows,
            Map<String, BigDecimal> deductionByUserName) {
        BigDecimal totalActual = BigDecimal.ZERO;
        for (LbDailyUserTradeReport item : orderRowsForDeductionSheet(exportRows, deductionByUserName, false)) {
            if (DATA_TYPE_REPORT_SUM.equals(item.getDataType())) {
                continue;
            }
            totalActual = totalActual.add(
                    computeActualReceiptsForDeduction(item, deductionByUserName, false));
        }
        return totalActual;
    }

    /** 支付表 B 列展示/排序值：实际收付绝对值四舍五入取整。 */
    private static BigDecimal paymentAbDisplayValue(BigDecimal actual) {
        return roundHalfUp(defaultZero(actual).abs());
    }

    /** 支付表 F 列展示值：实际收付四舍五入取整（保留符号，与写入 F 列口径一致）。 */
    private static BigDecimal paymentFColumnDisplayValue(BigDecimal actual) {
        return roundHalfUp(defaultZero(actual));
    }

    /**
     * 支付表 B/F 列平衡：|F 列总和| 与 B 列总和不一致时，加减刘瑞华 B 列数值使两列总和相等。
     */
    private static void balancePaymentSheetColumnTotals(
            List<PaymentAbEntry> abEntries,
            List<LbDailyUserTradeReport> negativeRows,
            Map<String, BigDecimal> deductionByUserName,
            boolean roundValues) {
        BigDecimal fSum = BigDecimal.ZERO;
        for (LbDailyUserTradeReport item : negativeRows) {
            BigDecimal actual = computeActualReceiptsForDeduction(item, deductionByUserName, roundValues);
            fSum = fSum.add(paymentFColumnDisplayValue(actual));
        }
        BigDecimal fTotalAbs = fSum.abs();

        BigDecimal bSum = BigDecimal.ZERO;
        for (PaymentAbEntry entry : abEntries) {
            bSum = bSum.add(entry.actualAbs());
        }
        if (bSum.compareTo(fTotalAbs) == 0) {
            return;
        }

        BigDecimal diff = fTotalAbs.subtract(bSum);
        for (int i = 0; i < abEntries.size(); i++) {
            PaymentAbEntry entry = abEntries.get(i);
            if (entry.isSpecialRow()) {
                abEntries.set(i, entry.withActualAbs(entry.actualAbs().add(diff)));
                return;
            }
        }
    }

    private record PaymentAbEntry(String name, BigDecimal actualAbs, LbDailyUserTradeReport detailRow) {
        static PaymentAbEntry detail(String name, BigDecimal actual, LbDailyUserTradeReport detailRow) {
            return new PaymentAbEntry(name, paymentAbDisplayValue(actual), detailRow);
        }

        static PaymentAbEntry special(BigDecimal deductionSummaryActual) {
            return new PaymentAbEntry(
                    PAYMENT_AB_SPECIAL_NAME,
                    paymentAbDisplayValue(deductionSummaryActual),
                    null);
        }

        boolean isSpecialRow() {
            return detailRow == null;
        }

        PaymentAbEntry withActualAbs(BigDecimal newActualAbs) {
            return new PaymentAbEntry(name, newActualAbs, detailRow);
        }

        BigDecimal sortValue() {
            return actualAbs;
        }
    }

    private static CellStyle paymentActualDecimalStyle(int rowIndex, SheetStyles styles) {
        return (rowIndex % 2 == 0) ? styles.zebraPaymentActualDecimalStyle : styles.paymentActualDecimalStyle;
    }

    private static CellStyle paymentTextStyle(int rowIndex, SheetStyles styles) {
        return (rowIndex % 2 == 0) ? styles.zebraTextStyle : styles.textStyle;
    }

    /** 11点表 / 支付表共用的明细行：按实际收付倒序，不含汇总行。 */
    private static List<LbDailyUserTradeReport> elevenClockDetailRows(
            List<LbDailyUserTradeReport> exportRows,
            Map<String, BigDecimal> deductionByUserName) {
        List<LbDailyUserTradeReport> rows = new ArrayList<>();
        for (LbDailyUserTradeReport row : orderRowsForDeductionSheet(exportRows, deductionByUserName, true)) {
            if (!DATA_TYPE_REPORT_SUM.equals(row.getDataType())) {
                rows.add(row);
            }
        }
        return rows;
    }

    private static void applyPaymentNegativeActualFormatting(Sheet sheet, int nextRowIndex) {
        if (nextRowIndex > 1) {
            SheetConditionalFormatting scf = sheet.getSheetConditionalFormatting();
            ConditionalFormattingRule negative = scf.createConditionalFormattingRule(ComparisonOperator.LT, "0");
            FontFormatting ff = negative.createFontFormatting();
            ff.setFontColorIndex(IndexedColors.DARK_RED.getIndex());
            ff.setFontStyle(false, false);
            scf.addConditionalFormatting(
                    new CellRangeAddress[]{
                            new CellRangeAddress(
                                    1, nextRowIndex - 1, PAYMENT_NEGATIVE_COL_ACTUAL, PAYMENT_NEGATIVE_COL_ACTUAL)
                    },
                    negative);
        }
    }

    /**
     * 导出行序：明细按差额倒序，汇总行固定在最后。
     * 若库中无 report_sum，则按明细字段聚合生成一条汇总行（与 calculateReportSum 口径一致）。
     */
    private static List<LbDailyUserTradeReport> prepareExportRows(List<LbDailyUserTradeReport> rows) {
        if (rows == null || rows.isEmpty()) {
            return List.of();
        }
        List<LbDailyUserTradeReport> details = new ArrayList<>();
        LbDailyUserTradeReport summaryFromDb = null;
        for (LbDailyUserTradeReport row : rows) {
            if (DATA_TYPE_REPORT_SUM.equals(row.getDataType())) {
                summaryFromDb = row;
            } else {
                details.add(row);
            }
        }
        details.sort((a, b) -> {
            BigDecimal da = a.getDiffAmt() == null ? BigDecimal.ZERO : a.getDiffAmt();
            BigDecimal db = b.getDiffAmt() == null ? BigDecimal.ZERO : b.getDiffAmt();
            return db.compareTo(da);
        });
        LbDailyUserTradeReport summaryRow =
                summaryFromDb != null ? summaryFromDb : (details.isEmpty() ? null : buildSummaryRowFromDetails(details));
        List<LbDailyUserTradeReport> ordered = new ArrayList<>(details.size() + (summaryRow == null ? 0 : 1));
        ordered.addAll(details);
        if (summaryRow != null) {
            ordered.add(summaryRow);
        }
        return ordered;
    }

    private static LbDailyUserTradeReport buildSummaryRowFromDetails(List<LbDailyUserTradeReport> details) {
        BigDecimal yestodayBuyAmt = BigDecimal.ZERO;
        BigDecimal dailySellAmt = BigDecimal.ZERO;
        BigDecimal dailyBuyAmt = BigDecimal.ZERO;
        BigDecimal serviceAmt = BigDecimal.ZERO;
        BigDecimal diffAmt = BigDecimal.ZERO;
        BigDecimal dikouAmt = BigDecimal.ZERO;
        for (LbDailyUserTradeReport row : details) {
            yestodayBuyAmt = yestodayBuyAmt.add(defaultZero(row.getYestodayBuyAmt()));
            dailySellAmt = dailySellAmt.add(defaultZero(row.getDailySellAmt()));
            dailyBuyAmt = dailyBuyAmt.add(defaultZero(row.getDailyBuyAmt()));
            serviceAmt = serviceAmt.add(defaultZero(row.getServiceAmt()));
            diffAmt = diffAmt.add(defaultZero(row.getDiffAmt()));
            dikouAmt = dikouAmt.add(defaultZero(row.getDikouAmt()));
        }
        LbDailyUserTradeReport summary = new LbDailyUserTradeReport();
        summary.setDataType(DATA_TYPE_REPORT_SUM);
        summary.setYestodayBuyAmt(yestodayBuyAmt);
        summary.setDailySellAmt(dailySellAmt);
        summary.setDailyBuyAmt(dailyBuyAmt);
        summary.setServiceAmt(serviceAmt);
        summary.setDiffAmt(diffAmt);
        summary.setDikouAmt(dikouAmt);
        summary.setActualReceiptsPayments(diffAmt.subtract(serviceAmt));
        return summary;
    }

    private static BigDecimal defaultZero(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }

    /**
     * 抵扣表 / 11点表：先按总表写入各行；抵扣金额列改为 lb_deduction_amount 匹配值；
     * 「实际收付」= 抵扣金额 + 原实际收付；明细按实际收付倒序。
     * 抵扣表最后一行汇总行的抵扣金额、实际收付为本列明细之和；11点表不输出汇总行，最左侧为序号列，数值四舍五入取整。
     */
    private static void fillDeductionSheet(
            Sheet sheet,
            DateHeaderLabels labels,
            List<LbDailyUserTradeReport> exportRows,
            SheetStyles styles,
            Map<String, BigDecimal> deductionByUserName,
            boolean sequenceOnLeft,
            boolean includeSummaryRow,
            boolean roundValues) {
        int lastCol = sequenceOnLeft ? ELEVEN_CLOCK_LAST_COL : SUMMARY_LAST_COL;
        int r = writeHeaderRow(sheet, labels, styles, sequenceOnLeft);
        sheet.createFreezePane(0, 1);
        sheet.setAutoFilter(new CellRangeAddress(0, 0, 0, lastCol));

        BigDecimal totalDikou = BigDecimal.ZERO;
        BigDecimal totalActual = BigDecimal.ZERO;
        int sequence = 0;

        for (LbDailyUserTradeReport item : orderRowsForDeductionSheet(exportRows, deductionByUserName, roundValues)) {
            boolean isSummaryRow = DATA_TYPE_REPORT_SUM.equals(item.getDataType());
            if (!includeSummaryRow && isSummaryRow) {
                continue;
            }

            Row row = sheet.createRow(r++);
            applyRowHeight(row, styles);
            RowStylePair rowStyles = rowStyles(item, r, styles, roundValues);

            // 第1步：与总表一致（其它列含汇总行均沿用报表原值）
            writeBaseDataCells(row, item, rowStyles, defaultZero(item.getDikouAmt()), sequenceOnLeft, roundValues);

            if (isSummaryRow) {
                // 汇总行：抵扣金额、实际收付 = 上方明细行之和
                createNumericCell(row, dataCol(COL_DIKOU, sequenceOnLeft), totalDikou, rowStyles, roundValues);
                createNumericCell(row, dataCol(COL_ACTUAL, sequenceOnLeft), totalActual, rowStyles, roundValues);
            } else {
                if (sequenceOnLeft) {
                    sequence++;
                    createIntCell(row, COL_SEQUENCE_LEFT, sequence, rowStyles.sequenceStyle);
                }

                // 第2步：按昵称 + 报表日匹配 lb_deduction_amount，覆盖抵扣金额列
                BigDecimal dikouFromDb = lookupDeductionAmt(item.getNickname(), deductionByUserName);
                createNumericCell(row, dataCol(COL_DIKOU, sequenceOnLeft), dikouFromDb, rowStyles, roundValues);

                // 第3步：抵扣金额 + 原实际收付，覆盖「实际收付」列
                BigDecimal dikouPlusActual = computeActualReceiptsForDeduction(item, deductionByUserName, roundValues);
                createNumericCell(row, dataCol(COL_ACTUAL, sequenceOnLeft), dikouPlusActual, rowStyles, roundValues);

                totalDikou = totalDikou.add(dikouFromDb);
                totalActual = totalActual.add(dikouPlusActual);
            }
        }

        applyDiffNegativeFormatting(sheet, r, sequenceOnLeft);
        autoSizeColumns(sheet, lastCol);
    }

    /** 抵扣表：明细按计算后的实际收付倒序，汇总行保持在最后（exportRows 已保证末尾有汇总行）。 */
    private static List<LbDailyUserTradeReport> orderRowsForDeductionSheet(
            List<LbDailyUserTradeReport> exportRows,
            Map<String, BigDecimal> deductionByUserName,
            boolean roundValues) {
        List<LbDailyUserTradeReport> details = new ArrayList<>();
        LbDailyUserTradeReport summaryRow = null;
        for (LbDailyUserTradeReport row : exportRows) {
            if (DATA_TYPE_REPORT_SUM.equals(row.getDataType())) {
                summaryRow = row;
            } else {
                details.add(row);
            }
        }
        details.sort((a, b) -> computeActualReceiptsForDeduction(b, deductionByUserName, roundValues)
                .compareTo(computeActualReceiptsForDeduction(a, deductionByUserName, roundValues)));
        List<LbDailyUserTradeReport> ordered = new ArrayList<>(details.size() + (summaryRow == null ? 0 : 1));
        ordered.addAll(details);
        if (summaryRow != null) {
            ordered.add(summaryRow);
        }
        return ordered;
    }

    private static BigDecimal computeActualReceiptsForDeduction(
            LbDailyUserTradeReport item,
            Map<String, BigDecimal> deductionByUserName,
            boolean roundValues) {
        BigDecimal dikou = lookupDeductionAmt(item.getNickname(), deductionByUserName);
        BigDecimal actual = defaultZero(item.getActualReceiptsPayments());
        if (roundValues) {
            return roundHalfUp(dikou).add(roundHalfUp(actual));
        }
        return dikou.add(actual);
    }

    private static int dataCol(int col, boolean sequenceOnLeft) {
        return sequenceOnLeft ? col + 1 : col;
    }

    private static int writeHeaderRow(
            Sheet sheet,
            DateHeaderLabels labels,
            SheetStyles styles,
            boolean sequenceOnLeft) {
        int r = 0;
        Row header = sheet.createRow(r++);
        header.setHeightInPoints(20 * styles.rowHeightFactor);
        if (sequenceOnLeft) {
            createHeaderCell(header, COL_SEQUENCE_LEFT, "序号", styles.headerStyle);
        }
        createHeaderCell(header, dataCol(COL_NICKNAME, sequenceOnLeft), "昵称", styles.headerStyle);
        createHeaderCell(header, dataCol(COL_YESTODAY_BUY, sequenceOnLeft), labels.yestodayBuyLabel, styles.headerStyle);
        createHeaderCell(header, dataCol(COL_DAILY_SELL, sequenceOnLeft), labels.dailySellLabel, styles.headerStyle);
        createHeaderCell(header, dataCol(COL_DAILY_BUY, sequenceOnLeft), labels.dailyBuyLabel, styles.headerStyle);
        createHeaderCell(header, dataCol(COL_SERVICE, sequenceOnLeft), "服务费", styles.headerStyle);
        createHeaderCell(header, dataCol(COL_DIFF, sequenceOnLeft), "差额", styles.headerStyle);
        createHeaderCell(header, dataCol(COL_ACTUAL, sequenceOnLeft), "实际收付", styles.headerStyle);
        createHeaderCell(header, dataCol(COL_DIKOU, sequenceOnLeft), "抵扣金额", styles.headerStyle);
        return r;
    }

    private static void writeBaseDataCells(
            Row row,
            LbDailyUserTradeReport item,
            RowStylePair rowStyles,
            BigDecimal dikouAmt,
            boolean sequenceOnLeft,
            boolean roundValues) {
        createTextCell(
                row,
                dataCol(COL_NICKNAME, sequenceOnLeft),
                item.getNickname() == null ? "" : item.getNickname(),
                rowStyles.textStyle);
        createNumericCell(row, dataCol(COL_YESTODAY_BUY, sequenceOnLeft), item.getYestodayBuyAmt(), rowStyles, roundValues);
        createNumericCell(row, dataCol(COL_DAILY_SELL, sequenceOnLeft), item.getDailySellAmt(), rowStyles, roundValues);
        createNumericCell(row, dataCol(COL_DAILY_BUY, sequenceOnLeft), item.getDailyBuyAmt(), rowStyles, roundValues);
        createNumericCell(row, dataCol(COL_SERVICE, sequenceOnLeft), item.getServiceAmt(), rowStyles, roundValues);
        createNumericCell(row, dataCol(COL_DIFF, sequenceOnLeft), item.getDiffAmt(), rowStyles, roundValues);
        createNumericCell(row, dataCol(COL_ACTUAL, sequenceOnLeft), item.getActualReceiptsPayments(), rowStyles, roundValues);
        createNumericCell(row, dataCol(COL_DIKOU, sequenceOnLeft), dikouAmt, rowStyles, roundValues);
    }

    private static RowStylePair rowStyles(LbDailyUserTradeReport item, int rowIndex, SheetStyles styles, boolean roundValues) {
        boolean isSummary = DATA_TYPE_REPORT_SUM.equals(item.getDataType());
        boolean zebra = (rowIndex % 2 == 0);
        CellStyle textStyle = isSummary ? styles.summaryTextStyle : (zebra ? styles.zebraTextStyle : styles.textStyle);
        CellStyle numberStyle;
        if (roundValues) {
            numberStyle = isSummary
                    ? styles.summaryRoundedAmountStyle
                    : (zebra ? styles.zebraRoundedAmountStyle : styles.roundedAmountStyle);
        } else {
            numberStyle = isSummary
                    ? styles.summaryAmountStyle
                    : (zebra ? styles.zebraAmountStyle : styles.amountStyle);
        }
        CellStyle sequenceStyle = isSummary ? styles.summarySequenceStyle : (zebra ? styles.zebraSequenceStyle : styles.sequenceStyle);
        return new RowStylePair(textStyle, numberStyle, sequenceStyle);
    }

    private static BigDecimal lookupDeductionAmt(String nickname, Map<String, BigDecimal> deductionByUserName) {
        if (nickname == null || nickname.trim().isEmpty()) {
            return BigDecimal.ZERO;
        }
        return deductionByUserName.getOrDefault(nickname.trim(), BigDecimal.ZERO);
    }

    private static void applyRowHeight(Row row, SheetStyles styles) {
        row.setHeightInPoints(row.getHeightInPoints() * styles.rowHeightFactor);
    }

    private static void applyDiffNegativeFormatting(Sheet sheet, int nextRowIndex, boolean sequenceOnLeft) {
        if (nextRowIndex > 1) {
            int diffCol = dataCol(COL_DIFF, sequenceOnLeft);
            SheetConditionalFormatting scf = sheet.getSheetConditionalFormatting();
            ConditionalFormattingRule negative = scf.createConditionalFormattingRule(ComparisonOperator.LT, "0");
            FontFormatting ff = negative.createFontFormatting();
            ff.setFontColorIndex(IndexedColors.DARK_RED.getIndex());
            ff.setFontStyle(false, false);
            scf.addConditionalFormatting(
                    new CellRangeAddress[]{new CellRangeAddress(1, nextRowIndex - 1, diffCol, diffCol)}, negative);
        }
    }

    private static void autoSizeColumns(Sheet sheet, int lastCol) {
        for (int i = 0; i <= lastCol; i++) {
            sheet.autoSizeColumn(i);
            int currentWidth = sheet.getColumnWidth(i);
            int widened = (int) Math.min(255 * 256, Math.round(currentWidth * 1.5));
            sheet.setColumnWidth(i, widened);
        }
    }

    public static void writeExportError(HttpServletResponse response, Exception e) {
        try {
            response.reset();
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.setContentType("text/plain;charset=UTF-8");
            response.getWriter().write("导出异常：" + e.getMessage());
        } catch (Exception ignored) {
            // ignore
        }
    }

    private static void createHeaderCell(Row row, int col, String text, CellStyle style) {
        Cell cell = row.createCell(col);
        cell.setCellValue(text);
        cell.setCellStyle(style);
    }

    private static void createTextCell(Row row, int col, String text, CellStyle style) {
        Cell cell = row.createCell(col);
        cell.setCellValue(text);
        cell.setCellStyle(style);
    }

    private static void createIntCell(Row row, int col, long value, CellStyle style) {
        Cell cell = row.createCell(col);
        cell.setCellValue((double) value);
        cell.setCellStyle(style);
    }

    private static void createDecimalCell(Row row, int col, BigDecimal value, CellStyle style) {
        Cell cell = row.createCell(col);
        cell.setCellValue(defaultZero(value).doubleValue());
        cell.setCellStyle(style);
    }

    private static CellStyle paymentActualStyle(int rowIndex, SheetStyles styles) {
        return (rowIndex % 2 == 0) ? styles.zebraPaymentActualStyle : styles.paymentActualStyle;
    }

    private static void createPaymentActualCell(Row row, int col, BigDecimal value, CellStyle style) {
        createIntCell(row, col, roundHalfUp(value).longValue(), style);
    }

    private static void createNumericCell(
            Row row,
            int col,
            BigDecimal value,
            RowStylePair rowStyles,
            boolean roundValues) {
        if (roundValues) {
            createIntCell(row, col, roundHalfUp(value).longValue(), rowStyles.numberStyle);
        } else {
            createDecimalCell(row, col, value, rowStyles.numberStyle);
        }
    }

    private static BigDecimal roundHalfUp(BigDecimal value) {
        return defaultZero(value).setScale(0, RoundingMode.HALF_UP);
    }

    private record DateHeaderLabels(String yestodayBuyLabel, String dailySellLabel, String dailyBuyLabel) {
        static DateHeaderLabels of(LocalDate reportDate) {
            DateTimeFormatter mmdd = DateTimeFormatter.ofPattern("MM-dd");
            String d0 = reportDate.format(mmdd);
            String d1 = reportDate.minusDays(1).format(mmdd);
            return new DateHeaderLabels(d1 + "买货", d0 + "卖货", d0 + "买货");
        }
    }

    private record RowStylePair(CellStyle textStyle, CellStyle numberStyle, CellStyle sequenceStyle) {
    }

    private static final class SheetStyles {
        final float rowHeightFactor = 1.3f;
        final CellStyle headerStyle;
        final CellStyle textStyle;
        final CellStyle zebraTextStyle;
        final CellStyle amountStyle;
        final CellStyle zebraAmountStyle;
        final CellStyle roundedAmountStyle;
        final CellStyle zebraRoundedAmountStyle;
        final CellStyle paymentActualStyle;
        final CellStyle zebraPaymentActualStyle;
        final CellStyle paymentActualDecimalStyle;
        final CellStyle zebraPaymentActualDecimalStyle;
        final CellStyle sequenceStyle;
        final CellStyle zebraSequenceStyle;
        final CellStyle summaryTextStyle;
        final CellStyle summaryAmountStyle;
        final CellStyle summaryRoundedAmountStyle;
        final CellStyle summarySequenceStyle;

        private SheetStyles(
                CellStyle headerStyle,
                CellStyle textStyle,
                CellStyle zebraTextStyle,
                CellStyle amountStyle,
                CellStyle zebraAmountStyle,
                CellStyle roundedAmountStyle,
                CellStyle zebraRoundedAmountStyle,
                CellStyle paymentActualStyle,
                CellStyle zebraPaymentActualStyle,
                CellStyle paymentActualDecimalStyle,
                CellStyle zebraPaymentActualDecimalStyle,
                CellStyle sequenceStyle,
                CellStyle zebraSequenceStyle,
                CellStyle summaryTextStyle,
                CellStyle summaryAmountStyle,
                CellStyle summaryRoundedAmountStyle,
                CellStyle summarySequenceStyle) {
            this.headerStyle = headerStyle;
            this.textStyle = textStyle;
            this.zebraTextStyle = zebraTextStyle;
            this.amountStyle = amountStyle;
            this.zebraAmountStyle = zebraAmountStyle;
            this.roundedAmountStyle = roundedAmountStyle;
            this.zebraRoundedAmountStyle = zebraRoundedAmountStyle;
            this.paymentActualStyle = paymentActualStyle;
            this.zebraPaymentActualStyle = zebraPaymentActualStyle;
            this.paymentActualDecimalStyle = paymentActualDecimalStyle;
            this.zebraPaymentActualDecimalStyle = zebraPaymentActualDecimalStyle;
            this.sequenceStyle = sequenceStyle;
            this.zebraSequenceStyle = zebraSequenceStyle;
            this.summaryTextStyle = summaryTextStyle;
            this.summaryAmountStyle = summaryAmountStyle;
            this.summaryRoundedAmountStyle = summaryRoundedAmountStyle;
            this.summarySequenceStyle = summarySequenceStyle;
        }

        static SheetStyles create(Workbook workbook) {
            DataFormat dataFormat = workbook.createDataFormat();

            CellStyle headerStyle = workbook.createCellStyle();
            Font headerFont = workbook.createFont();
            headerFont.setBold(true);
            headerFont.setColor(IndexedColors.DARK_BLUE.getIndex());
            headerStyle.setFont(headerFont);
            headerStyle.setAlignment(HorizontalAlignment.CENTER);
            headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
            headerStyle.setFillForegroundColor(IndexedColors.PALE_BLUE.getIndex());
            headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            headerStyle.setBorderTop(BorderStyle.THIN);
            headerStyle.setBorderBottom(BorderStyle.THIN);
            headerStyle.setBorderLeft(BorderStyle.THIN);
            headerStyle.setBorderRight(BorderStyle.THIN);

            CellStyle textStyle = workbook.createCellStyle();
            textStyle.setAlignment(HorizontalAlignment.LEFT);
            textStyle.setVerticalAlignment(VerticalAlignment.CENTER);
            textStyle.setBorderTop(BorderStyle.THIN);
            textStyle.setBorderBottom(BorderStyle.THIN);
            textStyle.setBorderLeft(BorderStyle.THIN);
            textStyle.setBorderRight(BorderStyle.THIN);

            CellStyle zebraTextStyle = workbook.createCellStyle();
            zebraTextStyle.cloneStyleFrom(textStyle);
            zebraTextStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

            CellStyle amountStyle = workbook.createCellStyle();
            amountStyle.setAlignment(HorizontalAlignment.RIGHT);
            amountStyle.setVerticalAlignment(VerticalAlignment.CENTER);
            amountStyle.setDataFormat(dataFormat.getFormat("#,##0.##########"));
            amountStyle.setBorderTop(BorderStyle.THIN);
            amountStyle.setBorderBottom(BorderStyle.THIN);
            amountStyle.setBorderLeft(BorderStyle.THIN);
            amountStyle.setBorderRight(BorderStyle.THIN);

            CellStyle zebraAmountStyle = workbook.createCellStyle();
            zebraAmountStyle.cloneStyleFrom(amountStyle);
            zebraAmountStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

            CellStyle roundedAmountStyle = workbook.createCellStyle();
            roundedAmountStyle.cloneStyleFrom(amountStyle);
            roundedAmountStyle.setDataFormat(dataFormat.getFormat("#,##0"));

            CellStyle zebraRoundedAmountStyle = workbook.createCellStyle();
            zebraRoundedAmountStyle.cloneStyleFrom(roundedAmountStyle);
            zebraRoundedAmountStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

            CellStyle paymentActualStyle = workbook.createCellStyle();
            paymentActualStyle.cloneStyleFrom(roundedAmountStyle);
            paymentActualStyle.setDataFormat(dataFormat.getFormat("0"));

            CellStyle zebraPaymentActualStyle = workbook.createCellStyle();
            zebraPaymentActualStyle.cloneStyleFrom(paymentActualStyle);
            zebraPaymentActualStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

            CellStyle paymentActualDecimalStyle = workbook.createCellStyle();
            paymentActualDecimalStyle.cloneStyleFrom(paymentActualStyle);
            paymentActualDecimalStyle.setDataFormat(dataFormat.getFormat("0.##########"));

            CellStyle zebraPaymentActualDecimalStyle = workbook.createCellStyle();
            zebraPaymentActualDecimalStyle.cloneStyleFrom(paymentActualDecimalStyle);
            zebraPaymentActualDecimalStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

            CellStyle sequenceStyle = workbook.createCellStyle();
            sequenceStyle.cloneStyleFrom(amountStyle);
            sequenceStyle.setDataFormat(dataFormat.getFormat("#,##0"));

            CellStyle zebraSequenceStyle = workbook.createCellStyle();
            zebraSequenceStyle.cloneStyleFrom(sequenceStyle);
            zebraSequenceStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

            XSSFColor zebraFill = new XSSFColor(new Color(242, 244, 248), null);
            ((XSSFCellStyle) zebraTextStyle).setFillForegroundColor(zebraFill);
            ((XSSFCellStyle) zebraAmountStyle).setFillForegroundColor(zebraFill);
            ((XSSFCellStyle) zebraRoundedAmountStyle).setFillForegroundColor(zebraFill);
            ((XSSFCellStyle) zebraPaymentActualStyle).setFillForegroundColor(zebraFill);
            ((XSSFCellStyle) zebraPaymentActualDecimalStyle).setFillForegroundColor(zebraFill);
            ((XSSFCellStyle) zebraSequenceStyle).setFillForegroundColor(zebraFill);

            CellStyle summaryTextStyle = workbook.createCellStyle();
            summaryTextStyle.cloneStyleFrom(textStyle);
            summaryTextStyle.setFillForegroundColor(IndexedColors.LEMON_CHIFFON.getIndex());
            summaryTextStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            Font summaryFont = workbook.createFont();
            summaryFont.setBold(true);
            summaryTextStyle.setFont(summaryFont);
            summaryTextStyle.setBorderTop(BorderStyle.THIN);

            CellStyle summaryAmountStyle = workbook.createCellStyle();
            summaryAmountStyle.cloneStyleFrom(amountStyle);
            summaryAmountStyle.setFillForegroundColor(IndexedColors.LEMON_CHIFFON.getIndex());
            summaryAmountStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            summaryAmountStyle.setFont(summaryFont);
            summaryAmountStyle.setBorderTop(BorderStyle.THIN);

            CellStyle summaryRoundedAmountStyle = workbook.createCellStyle();
            summaryRoundedAmountStyle.cloneStyleFrom(roundedAmountStyle);
            summaryRoundedAmountStyle.setFillForegroundColor(IndexedColors.LEMON_CHIFFON.getIndex());
            summaryRoundedAmountStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            summaryRoundedAmountStyle.setFont(summaryFont);
            summaryRoundedAmountStyle.setBorderTop(BorderStyle.THIN);

            CellStyle summarySequenceStyle = workbook.createCellStyle();
            summarySequenceStyle.cloneStyleFrom(sequenceStyle);
            summarySequenceStyle.setFillForegroundColor(IndexedColors.LEMON_CHIFFON.getIndex());
            summarySequenceStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            summarySequenceStyle.setFont(summaryFont);
            summarySequenceStyle.setBorderTop(BorderStyle.THIN);

            return new SheetStyles(
                    headerStyle,
                    textStyle,
                    zebraTextStyle,
                    amountStyle,
                    zebraAmountStyle,
                    roundedAmountStyle,
                    zebraRoundedAmountStyle,
                    paymentActualStyle,
                    zebraPaymentActualStyle,
                    paymentActualDecimalStyle,
                    zebraPaymentActualDecimalStyle,
                    sequenceStyle,
                    zebraSequenceStyle,
                    summaryTextStyle,
                    summaryAmountStyle,
                    summaryRoundedAmountStyle,
                    summarySequenceStyle);
        }
    }
}
