package tech.easyflow.ai.documentimport.task;

import com.easyagents.rag.core.BgeM3ChunkSafety;
import com.easyagents.rag.core.RagDefaults;
import com.mybatisflex.core.keygen.impl.FlexIDKeyGenerator;
import tech.easyflow.ai.documentimport.DocumentImportKeys;
import tech.easyflow.ai.entity.DocumentChunk;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 表格行窗口分块构建器。
 *
 * <p>CSV 与 XLSX 统一复用“表头 + 连续数据行”的语义，并对超长内容做有界续片，
 * 保持单个分块满足下游模型的 Token 硬限制。</p>
 *
 * @author Codex
 * @since 2026-08-04
 */
public class TabularRowWindowChunkBuilder {

    /**
     * 单个分块允许的最大 Java 字符数。
     */
    public static final int MAX_CHUNK_CONTENT_CHARS = 60_000;

    private final FlexIDKeyGenerator keyGenerator = new FlexIDKeyGenerator();

    /**
     * 构建一个表格行窗口对应的分块。
     *
     * @param documentId 文档 ID
     * @param collectionId 知识库 ID
     * @param sourceLabel 表格或 Sheet 名称
     * @param headers 规范化表头
     * @param rows 窗口数据行
     * @param startingSorting 起始排序号
     * @param chunkType 分块类型
     * @return 一个或多个有界分块
     */
    public List<DocumentChunk> build(
        BigInteger documentId,
        BigInteger collectionId,
        String sourceLabel,
        List<String> headers,
        List<TabularRow> rows,
        int startingSorting,
        String chunkType) {
        if (headers == null || headers.isEmpty()) {
            throw new IllegalArgumentException("表格分块缺少表头");
        }
        List<TabularRow> safeRows = rows == null
            ? Collections.<TabularRow>emptyList()
            : rows;
        List<RenderedPart> parts =
            renderParts(sourceLabel, headers, safeRows);
        List<DocumentChunk> chunks =
            new ArrayList<DocumentChunk>(parts.size());
        for (int index = 0; index < parts.size(); index++) {
            RenderedPart part = parts.get(index);
            String content = part.content;
            DocumentChunk chunk = new DocumentChunk();
            chunk.setId(generateId(chunk));
            chunk.setDocumentId(documentId);
            chunk.setDocumentCollectionId(collectionId);
            chunk.setSorting(startingSorting + index);
            chunk.setContent(content);

            Map<String, Object> options = new LinkedHashMap<String, Object>();
            options.put("chunkType", chunkType);
            options.put(
                "sourceLabel",
                sourceLabel + " · " + part.rowStart + "-" + part.rowEnd + " 行");
            options.put("headingPath", Collections.singletonList(sourceLabel));
            options.put("charCount", content.length());
            options.put("tokenEstimate", Math.max(
                1, BgeM3ChunkSafety.estimateContentTokens(content)));
            options.put("partNo", index + 1);
            options.put("partTotal", parts.size());
            options.put("warnings", new ArrayList<String>());
            options.put(DocumentImportKeys.KEY_DOCUMENT_RENDER_MARKDOWN, content);
            options.put(DocumentImportKeys.KEY_DOCUMENT_SHEET_NAME, sourceLabel);
            options.put(
                DocumentImportKeys.KEY_DOCUMENT_ROW_START, part.rowStart);
            options.put(
                DocumentImportKeys.KEY_DOCUMENT_ROW_END, part.rowEnd);
            chunk.setOptions(options);
            chunks.add(chunk);
        }
        return chunks;
    }

    /**
     * 估算表格标题和表头渲染后的字符数。
     *
     * @param sourceLabel 表格名称
     * @param headers 表头
     * @return 渲染字符数
     */
    public int estimateHeaderChars(
        String sourceLabel,
        List<String> headers) {
        validateHeaders(headers);
        long length = 6L + safeText(sourceLabel).length();
        length += estimateCellsChars(headers);
        length += 4L + 6L * headers.size();
        return boundedInt(length);
    }

    /**
     * 估算表格标题和表头渲染后的 Token 数。
     *
     * @param sourceLabel 表格名称
     * @param headers 表头
     * @return 保守 Token 数
     */
    public int estimateHeaderTokens(
        String sourceLabel,
        List<String> headers) {
        validateHeaders(headers);
        long tokens = 6L
            + BgeM3ChunkSafety.estimateContentTokens(safeText(sourceLabel));
        tokens += estimateCellsTokens(headers);
        tokens += 4L + 6L * headers.size();
        return boundedInt(tokens);
    }

    /**
     * 估算单行追加到 Markdown 表格后的字符数。
     *
     * @param headers 表头
     * @param row 表格数据行
     * @return 渲染字符数
     */
    public int estimateRowChars(
        List<String> headers,
        TabularRow row) {
        validateRow(headers, row);
        List<String> values =
            normalizeValues(row.getValues(), headers.size());
        return boundedInt(5L + estimateCellsChars(values));
    }

    /**
     * 估算单行追加到 Markdown 表格后的 Token 数。
     *
     * @param headers 表头
     * @param row 表格数据行
     * @return 保守 Token 数
     */
    public int estimateRowTokens(
        List<String> headers,
        TabularRow row) {
        validateRow(headers, row);
        List<String> values =
            normalizeValues(row.getValues(), headers.size());
        return boundedInt(5L + estimateCellsTokens(values));
    }

    /**
     * 按表格语义生成有界 Markdown 续片。
     *
     * @param sourceLabel 表格名称
     * @param headers 表头
     * @param rows 数据行
     * @return 有界续片
     */
    private List<RenderedPart> renderParts(
        String sourceLabel,
        List<String> headers,
        List<TabularRow> rows) {
        int headerChars = estimateHeaderChars(sourceLabel, headers);
        int headerTokens = estimateHeaderTokens(sourceLabel, headers);
        if (!fitsLimits(headerChars, headerTokens)) {
            throw new IllegalArgumentException("表格标题或表头超过分块上限");
        }
        if (rows.isEmpty()) {
            return Collections.singletonList(new RenderedPart(
                renderMarkdown(sourceLabel, headers, rows), 1, 1));
        }

        List<RenderedPart> parts = new ArrayList<RenderedPart>();
        List<TabularRow> currentRows = new ArrayList<TabularRow>();
        long currentChars = headerChars;
        long currentTokens = headerTokens;
        for (TabularRow row : rows) {
            int rowChars = estimateRowChars(headers, row);
            int rowTokens = estimateRowTokens(headers, row);
            if (!fitsLimits(
                (long) headerChars + rowChars,
                (long) headerTokens + rowTokens)) {
                flushRows(parts, sourceLabel, headers, currentRows);
                currentRows.clear();
                currentChars = headerChars;
                currentTokens = headerTokens;
                parts.addAll(renderOversizedRow(
                    sourceLabel, headers, row));
                continue;
            }
            if (!currentRows.isEmpty()
                && !fitsLimits(
                    currentChars + rowChars,
                    currentTokens + rowTokens)) {
                flushRows(parts, sourceLabel, headers, currentRows);
                currentRows.clear();
                currentChars = headerChars;
                currentTokens = headerTokens;
            }
            currentRows.add(row);
            currentChars += rowChars;
            currentTokens += rowTokens;
        }
        flushRows(parts, sourceLabel, headers, currentRows);
        return parts;
    }

    /**
     * 输出一个完整行集合，续片始终包含标题和表头。
     *
     * @param parts 输出续片
     * @param sourceLabel 表格名称
     * @param headers 表头
     * @param rows 数据行
     */
    private void flushRows(
        List<RenderedPart> parts,
        String sourceLabel,
        List<String> headers,
        List<TabularRow> rows) {
        if (rows.isEmpty()) {
            return;
        }
        String content = renderMarkdown(sourceLabel, headers, rows);
        assertWithinLimits(content);
        parts.add(new RenderedPart(
            content,
            rows.get(0).getRowNumber(),
            rows.get(rows.size() - 1).getRowNumber()));
    }

    /**
     * 将单条超长记录按列边界生成连续续片。
     *
     * @param sourceLabel 表格名称
     * @param headers 表头
     * @param row 超长数据行
     * @return 语义完整的续片
     */
    private List<RenderedPart> renderOversizedRow(
        String sourceLabel,
        List<String> headers,
        TabularRow row) {
        List<String> values =
            normalizeValues(row.getValues(), headers.size());
        List<RenderedPart> parts = new ArrayList<RenderedPart>();
        List<String> partHeaders = new ArrayList<String>();
        List<String> partValues = new ArrayList<String>();
        for (int index = 0; index < headers.size(); index++) {
            String header = headers.get(index);
            String value = values.get(index);
            if (!fitsSingleRow(
                sourceLabel,
                Collections.singletonList(header),
                Collections.singletonList(value),
                row.getRowNumber())) {
                flushColumnGroup(
                    parts, sourceLabel, partHeaders, partValues,
                    row.getRowNumber());
                partHeaders.clear();
                partValues.clear();
                parts.addAll(renderOversizedCell(
                    sourceLabel, header, value, row.getRowNumber()));
                continue;
            }

            List<String> candidateHeaders =
                new ArrayList<String>(partHeaders);
            candidateHeaders.add(header);
            List<String> candidateValues =
                new ArrayList<String>(partValues);
            candidateValues.add(value);
            if (!partHeaders.isEmpty()
                && !fitsLimits(renderSingleRow(
                    sourceLabel,
                    candidateHeaders,
                    candidateValues,
                    row.getRowNumber()))) {
                flushColumnGroup(
                    parts, sourceLabel, partHeaders, partValues,
                    row.getRowNumber());
                partHeaders.clear();
                partValues.clear();
            }
            partHeaders.add(header);
            partValues.add(value);
        }
        flushColumnGroup(
            parts, sourceLabel, partHeaders, partValues,
            row.getRowNumber());
        return parts;
    }

    /**
     * 通过有界估算判断单行表格是否满足字符与 Token 上限。
     *
     * @param sourceLabel 表格名称
     * @param headers 表头
     * @param values 行值
     * @param rowNumber 逻辑行号
     * @return 是否满足
     */
    private boolean fitsSingleRow(
        String sourceLabel,
        List<String> headers,
        List<String> values,
        int rowNumber) {
        TabularRow row = new TabularRow(rowNumber, values);
        return fitsLimits(
            (long) estimateHeaderChars(sourceLabel, headers)
                + estimateRowChars(headers, row),
            (long) estimateHeaderTokens(sourceLabel, headers)
                + estimateRowTokens(headers, row));
    }

    /**
     * 输出同一记录的一组完整列。
     *
     * @param parts 输出续片
     * @param sourceLabel 表格名称
     * @param headers 当前列头
     * @param values 当前列值
     * @param rowNumber 逻辑行号
     */
    private void flushColumnGroup(
        List<RenderedPart> parts,
        String sourceLabel,
        List<String> headers,
        List<String> values,
        int rowNumber) {
        if (headers.isEmpty()) {
            return;
        }
        String content = renderSingleRow(
            sourceLabel, headers, values, rowNumber);
        assertWithinLimits(content);
        parts.add(new RenderedPart(content, rowNumber, rowNumber));
    }

    /**
     * 对单个超长单元格做安全续片，每个续片重复当前列名。
     *
     * @param sourceLabel 表格名称
     * @param header 当前列名
     * @param value 单元格值
     * @param rowNumber 逻辑行号
     * @return 单列续片
     */
    private List<RenderedPart> renderOversizedCell(
        String sourceLabel,
        String header,
        String value,
        int rowNumber) {
        String normalizedValue = normalizeCellNewlines(value);
        String emptyContent = renderSingleRow(
            sourceLabel,
            Collections.singletonList(header),
            Collections.singletonList(""),
            rowNumber);
        if (!fitsLimits(emptyContent)) {
            throw new IllegalArgumentException("表格列名超过分块上限");
        }

        int fixedChars = emptyContent.length();
        int fixedTokens =
            BgeM3ChunkSafety.estimateContentTokens(emptyContent);
        int maxCandidateCodePoints = Math.max(
            1,
            Math.min(
                MAX_CHUNK_CONTENT_CHARS - fixedChars,
                RagDefaults.BGE_M3_HARD_CHUNK_TOKEN_LIMIT - fixedTokens));
        List<RenderedPart> parts = new ArrayList<RenderedPart>();
        int offset = 0;
        while (offset < normalizedValue.length()) {
            int remainingCodePoints =
                normalizedValue.codePointCount(offset, normalizedValue.length());
            int low = 1;
            int high = Math.min(remainingCodePoints, maxCandidateCodePoints);
            int acceptedEnd = -1;
            String acceptedContent = null;
            while (low <= high) {
                int middle = low + (high - low) / 2;
                int end = normalizedValue.offsetByCodePoints(offset, middle);
                String fragment = normalizedValue.substring(offset, end);
                String content = renderSingleRow(
                    sourceLabel,
                    Collections.singletonList(header),
                    Collections.singletonList(fragment),
                    rowNumber);
                if (fitsLimits(content)) {
                    acceptedEnd = end;
                    acceptedContent = content;
                    low = middle + 1;
                } else {
                    high = middle - 1;
                }
            }
            if (acceptedEnd <= offset || acceptedContent == null) {
                throw new IllegalArgumentException(
                    "表格单元格无法在分块预算内安全续片");
            }
            parts.add(new RenderedPart(
                acceptedContent, rowNumber, rowNumber));
            offset = acceptedEnd;
        }
        return parts;
    }

    /**
     * 统一单元格换行，避免续片边界拆开 CRLF。
     *
     * @param value 原始单元格值
     * @return 统一换行后的值
     */
    private String normalizeCellNewlines(String value) {
        return safeText(value)
            .replace("\r\n", "\n")
            .replace('\r', '\n');
    }

    /**
     * 渲染单行 Markdown 表格。
     *
     * @param sourceLabel 表格名称
     * @param headers 表头
     * @param values 行值
     * @param rowNumber 逻辑行号
     * @return Markdown 内容
     */
    private String renderSingleRow(
        String sourceLabel,
        List<String> headers,
        List<String> values,
        int rowNumber) {
        return renderMarkdown(
            sourceLabel,
            headers,
            Collections.singletonList(new TabularRow(rowNumber, values)));
    }

    /**
     * 将表格渲染为 Markdown。
     *
     * @param sourceLabel 表格名称
     * @param headers 表头
     * @param rows 数据行
     * @return Markdown 内容
     */
    private String renderMarkdown(
        String sourceLabel,
        List<String> headers,
        List<TabularRow> rows) {
        StringBuilder builder = new StringBuilder();
        builder.append("# ").append(safeText(sourceLabel)).append("\n\n| ");
        appendCells(builder, headers);
        builder.append(" |\n|");
        for (int index = 0; index < headers.size(); index++) {
            builder.append(" --- |");
        }
        for (TabularRow row : rows) {
            builder.append("\n| ");
            appendCells(builder, normalizeValues(row.getValues(), headers.size()));
            builder.append(" |");
        }
        return builder.toString();
    }

    /**
     * 追加经过 Markdown 转义的单元格。
     *
     * @param builder 输出缓冲区
     * @param cells 单元格值
     */
    private void appendCells(StringBuilder builder, List<String> cells) {
        for (int index = 0; index < cells.size(); index++) {
            if (index > 0) {
                builder.append(" | ");
            }
            appendEscapedCell(builder, cells.get(index));
        }
    }

    /**
     * 将一行补齐到固定列数。
     *
     * @param values 原始值
     * @param columnCount 列数
     * @return 补齐后的值
     */
    private List<String> normalizeValues(List<String> values, int columnCount) {
        List<String> normalized = new ArrayList<String>(columnCount);
        for (int index = 0; index < columnCount; index++) {
            normalized.add(values != null && index < values.size() && values.get(index) != null
                ? values.get(index)
                : "");
        }
        return normalized;
    }

    /**
     * 转义 Markdown 表格单元格。
     *
     * @param value 原始值
     * @return 转义后的值
     */
    private void appendEscapedCell(
        StringBuilder builder,
        String value) {
        String safeValue = safeText(value);
        for (int index = 0; index < safeValue.length();) {
            char current = safeValue.charAt(index);
            if (current == '\r') {
                builder.append("<br/>");
                index += index + 1 < safeValue.length()
                    && safeValue.charAt(index + 1) == '\n' ? 2 : 1;
                continue;
            }
            if (current == '\n') {
                builder.append("<br/>");
                index++;
                continue;
            }
            if (current == '\\' || current == '|') {
                builder.append('\\');
            }
            int rawChars = rawUnitChars(safeValue, index);
            builder.append(safeValue, index, index + rawChars);
            index += rawChars;
        }
    }

    /**
     * 估算多个单元格完成 Markdown 转义后的字符数。
     *
     * @param cells 单元格
     * @return 字符数
     */
    private long estimateCellsChars(List<String> cells) {
        long length = Math.max(0, cells.size() - 1) * 3L;
        for (String cell : cells) {
            String value = safeText(cell);
            for (int index = 0; index < value.length();) {
                char current = value.charAt(index);
                if (current == '\r') {
                    length += 5L;
                    index += index + 1 < value.length()
                        && value.charAt(index + 1) == '\n' ? 2 : 1;
                    continue;
                }
                if (current == '\n') {
                    length += 5L;
                    index++;
                    continue;
                }
                length += current == '\\' || current == '|'
                    ? 2L
                    : rawUnitChars(value, index);
                index += rawUnitChars(value, index);
                if (length >= Integer.MAX_VALUE) {
                    return Integer.MAX_VALUE;
                }
            }
        }
        return length;
    }

    /**
     * 估算多个单元格完成 Markdown 转义后的 Token 数。
     *
     * @param cells 单元格
     * @return 保守 Token 数
     */
    private long estimateCellsTokens(List<String> cells) {
        long tokens = Math.max(0, cells.size() - 1) * 3L;
        for (String cell : cells) {
            String value = safeText(cell);
            tokens += BgeM3ChunkSafety.estimateContentTokens(value);
            for (int index = 0; index < value.length();) {
                char current = value.charAt(index);
                if (current == '\r') {
                    boolean crlf = index + 1 < value.length()
                        && value.charAt(index + 1) == '\n';
                    tokens += crlf ? 3L : 4L;
                    index += crlf ? 2 : 1;
                    continue;
                }
                if (current == '\n') {
                    tokens += 4L;
                } else if (current == '\\' || current == '|') {
                    tokens++;
                }
                index += rawUnitChars(value, index);
            }
            if (tokens >= Integer.MAX_VALUE) {
                return Integer.MAX_VALUE;
            }
        }
        return tokens;
    }

    /**
     * 计算当前 Unicode 单元占用的 Java 字符数。
     *
     * @param value 原始值
     * @param index 当前偏移
     * @return Java 字符数
     */
    private int rawUnitChars(String value, int index) {
        char current = value.charAt(index);
        if (Character.isHighSurrogate(current)
            && index + 1 < value.length()
            && Character.isLowSurrogate(value.charAt(index + 1))) {
            return 2;
        }
        return 1;
    }

    /**
     * 校验表头。
     *
     * @param headers 表头
     */
    private void validateHeaders(List<String> headers) {
        if (headers == null || headers.isEmpty()) {
            throw new IllegalArgumentException("表格分块缺少表头");
        }
    }

    /**
     * 校验数据行。
     *
     * @param headers 表头
     * @param row 数据行
     */
    private void validateRow(
        List<String> headers,
        TabularRow row) {
        validateHeaders(headers);
        if (row == null) {
            throw new IllegalArgumentException("表格分块缺少数据行");
        }
    }

    /**
     * 判断估算值是否同时满足字符与 Token 上限。
     *
     * @param chars 字符数
     * @param tokens Token 数
     * @return 是否满足
     */
    private boolean fitsLimits(long chars, long tokens) {
        return chars <= MAX_CHUNK_CONTENT_CHARS
            && tokens <= RagDefaults.BGE_M3_HARD_CHUNK_TOKEN_LIMIT;
    }

    /**
     * 判断实际内容是否同时满足字符与 Token 上限。
     *
     * @param content 内容
     * @return 是否满足
     */
    private boolean fitsLimits(String content) {
        return content.length() <= MAX_CHUNK_CONTENT_CHARS
            && BgeM3ChunkSafety.isWithinHardLimit(content);
    }

    /**
     * 断言实际内容满足字符与 Token 上限。
     *
     * @param content 内容
     */
    private void assertWithinLimits(String content) {
        if (!fitsLimits(content)) {
            throw new IllegalStateException("表格续片超过分块上限");
        }
    }

    /**
     * 将长度安全压缩到整数范围。
     *
     * @param value 长度
     * @return 整数长度
     */
    private int boundedInt(long value) {
        return value >= Integer.MAX_VALUE
            ? Integer.MAX_VALUE
            : (int) value;
    }

    /**
     * 返回非空文本。
     *
     * @param value 原始文本
     * @return 非空文本
     */
    private String safeText(String value) {
        return value == null ? "" : value;
    }

    /**
     * 生成分块主键。
     *
     * @param entity 分块实体
     * @return 主键
     */
    private BigInteger generateId(Object entity) {
        return new BigInteger(String.valueOf(keyGenerator.generate(entity, null)));
    }

    /**
     * 已完成渲染的语义续片。
     */
    private static final class RenderedPart {

        private final String content;
        private final int rowStart;
        private final int rowEnd;

        /**
         * 创建语义续片。
         *
         * @param content Markdown 内容
         * @param rowStart 起始行
         * @param rowEnd 结束行
         */
        private RenderedPart(
            String content,
            int rowStart,
            int rowEnd) {
            this.content = content;
            this.rowStart = rowStart;
            this.rowEnd = rowEnd;
        }
    }

    /**
     * 表格逻辑数据行。
     */
    public static class TabularRow {

        private final int rowNumber;
        private final List<String> values;

        /**
         * 创建表格数据行。
         *
         * @param rowNumber 一基逻辑行号
         * @param values 单元格值
         */
        public TabularRow(int rowNumber, List<String> values) {
            this.rowNumber = rowNumber;
            this.values = values == null
                ? Collections.<String>emptyList()
                : new ArrayList<String>(values);
        }

        /**
         * 返回逻辑行号。
         *
         * @return 一基逻辑行号
         */
        public int getRowNumber() {
            return rowNumber;
        }

        /**
         * 返回单元格值。
         *
         * @return 不可变语义的单元格值副本
         */
        public List<String> getValues() {
            return new ArrayList<String>(values);
        }
    }
}
