/*
 *  Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
 *  <p>
 *  Licensed under the Apache License, Version 2.0.
 */
package com.easyagents.core.file2text.extractor.impl;

import com.easyagents.core.file2text.DocumentReadErrorCode;
import com.easyagents.core.file2text.DocumentReadException;
import com.easyagents.core.file2text.DocumentReadSupport;
import com.easyagents.core.file2text.LightweightDocumentReadRequest;
import com.easyagents.core.file2text.LightweightDocumentReadResult;
import com.easyagents.core.file2text.extractor.FileExtractor;
import com.easyagents.core.file2text.source.DocumentSource;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.util.XMLHelper;
import org.apache.poi.xssf.eventusermodel.ReadOnlySharedStringsTable;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;
import org.apache.poi.xssf.model.SharedStrings;
import org.apache.poi.xssf.model.Styles;
import org.apache.poi.xssf.usermodel.XSSFComment;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/**
 * 基于 XSSF SAX 的 XLSX 文档提取器。
 */
public class XlsxExtractor implements FileExtractor {

    private static final Set<String> MIME_TYPES = Set.of(
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            "application/vnd.openxmlformats-officedocument.spreadsheetml.template");

    /**
     * 判断是否支持 XLSX。
     *
     * @param source 文档来源
     * @return 是否支持
     */
    @Override
    public boolean supports(DocumentSource source) {
        if (source.getMimeType() != null && MIME_TYPES.contains(source.getMimeType().toLowerCase(Locale.ROOT))) {
            return true;
        }
        String name = source.getFileName();
        return name != null && (name.toLowerCase(Locale.ROOT).endsWith(".xlsx")
                || name.toLowerCase(Locale.ROOT).endsWith(".xltx"));
    }

    /**
     * 提取兼容纯文本。
     *
     * @param source 文档来源
     * @return 文本
     * @throws IOException 文档 I/O 失败
     */
    @Override
    public String extractText(DocumentSource source) throws IOException {
        return read(new LightweightDocumentReadRequest(source)).getText();
    }

    /**
     * 使用 SAX 按工作表和行读取 XLSX。
     *
     * @param request 读取请求
     * @return 表格结构化结果
     * @throws IOException 文档 I/O 失败
     */
    @Override
    public LightweightDocumentReadResult read(LightweightDocumentReadRequest request) throws IOException {
        DocumentSource source = request.getSource();
        try (InputStream input = source.openStream();
             OPCPackage opcPackage = OPCPackage.open(input)) {
            XSSFReader reader = new XSSFReader(opcPackage);
            Styles styles = reader.getStylesTable();
            SharedStrings sharedStrings = new ReadOnlySharedStringsTable(opcPackage);
            XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator) reader.getSheetsData();
            List<SpreadsheetReadSupport.SheetRows> resultSheets = new ArrayList<>();
            int[] nonEmptyCells = {0};
            int sheetIndex = 0;
            while (sheets.hasNext()) {
                request.checkCancelled();
                sheetIndex++;
                if (sheetIndex > request.getMaxSheets()) {
                    throw limit("Sheet count exceeds " + request.getMaxSheets());
                }
                try (InputStream sheetInput = sheets.next()) {
                    String sheetName = sheets.getSheetName();
                    SheetHandler handler = new SheetHandler(request, nonEmptyCells);
                    XMLReader parser = XMLHelper.newXMLReader();
                    parser.setContentHandler(new XSSFSheetXMLHandler(
                            styles, null, sharedStrings, handler, new DataFormatter(), false));
                    parser.parse(new InputSource(sheetInput));
                    resultSheets.add(new SpreadsheetReadSupport.SheetRows(
                            sheetIndex, sheetName, handler.rows()));
                }
            }
            return DocumentReadSupport.result(source,
                    SpreadsheetReadSupport.toSegments(resultSheets), request);
        } catch (StructureLimitRuntimeException error) {
            throw limit(error.getMessage());
        } catch (DocumentReadException error) {
            throw error;
        } catch (Exception error) {
            throw new DocumentReadException(DocumentReadErrorCode.DOCUMENT_CORRUPTED,
                    "Failed to extract XLSX", error);
        }
    }

    /**
     * 获取读取器优先级。
     *
     * @return 优先级
     */
    @Override
    public int getOrder() {
        return 10;
    }

    private DocumentReadException limit(String message) {
        return new DocumentReadException(DocumentReadErrorCode.DOCUMENT_STRUCTURE_LIMIT_EXCEEDED, message);
    }

    /**
     * SAX 工作表内容处理器。
     */
    private static final class SheetHandler implements XSSFSheetXMLHandler.SheetContentsHandler {

        private final LightweightDocumentReadRequest request;
        private final int[] nonEmptyCells;
        private final List<SpreadsheetReadSupport.RowText> rows = new ArrayList<>();
        private final Map<String, String> currentCells = new LinkedHashMap<>();
        private int currentRow;

        private SheetHandler(LightweightDocumentReadRequest request, int[] nonEmptyCells) {
            this.request = request;
            this.nonEmptyCells = nonEmptyCells;
        }

        /**
         * 开始读取一行。
         *
         * @param rowNum 零基行号
         */
        @Override
        public void startRow(int rowNum) {
            currentRow = rowNum + 1;
            currentCells.clear();
        }

        /**
         * 完成一行并保存有效单元格。
         *
         * @param rowNum 零基行号
         */
        @Override
        public void endRow(int rowNum) {
            if (currentCells.isEmpty()) {
                return;
            }
            String text = currentCells.entrySet().stream()
                    .map(item -> item.getKey() + "=" + item.getValue())
                    .reduce((left, right) -> left + " | " + right)
                    .orElse("");
            rows.add(new SpreadsheetReadSupport.RowText(currentRow, text));
        }

        /**
         * 接收一个格式化单元格值。
         *
         * @param cellReference 单元格引用
         * @param formattedValue 格式化显示值
         * @param comment 批注
         */
        @Override
        public void cell(String cellReference, String formattedValue, XSSFComment comment) {
            if (formattedValue == null || formattedValue.isBlank()) {
                return;
            }
            request.checkCancelled();
            nonEmptyCells[0]++;
            if (nonEmptyCells[0] > request.getMaxNonEmptyCells()) {
                throw new StructureLimitRuntimeException(
                        "Non-empty cell count exceeds " + request.getMaxNonEmptyCells());
            }
            String column = cellReference == null ? "?" : CellReference.convertNumToColString(
                    new CellReference(cellReference).getCol());
            currentCells.put(column + currentRow, formattedValue.trim());
        }

        /**
         * 接收页眉页脚；轻量读取不纳入正文。
         *
         * @param text 文本
         * @param isHeader 是否页眉
         * @param tagName 标签名
         */
        @Override
        public void headerFooter(String text, boolean isHeader, String tagName) {
        }

        private List<SpreadsheetReadSupport.RowText> rows() {
            return rows;
        }
    }

    /**
     * SAX 回调跨层传递结构上限异常。
     */
    private static final class StructureLimitRuntimeException extends RuntimeException {

        private StructureLimitRuntimeException(String message) {
            super(message);
        }
    }
}
