/*
 *  Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
 *  <p>
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *  <p>
 *  http://www.apache.org/licenses/LICENSE-2.0
 *  <p>
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
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.DocumentTextSegment;
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 java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 纯文本文件提取器（支持 UTF-8、GBK、GB2312 编码自动检测）
 * 支持 .txt, .md, .log, .csv, .json, .xml 等文本格式
 */
public class PlainTextExtractor implements FileExtractor {

    private static final int LINES_PER_SEGMENT = 40;
    private static final Charset GB18030 = Charset.forName("GB18030");
    private static final Pattern MARKDOWN_HEADING = Pattern.compile("^(#{1,6})\\s+(.+?)\\s*$");
    private static final Set<String> SUPPORTED_MIME_TYPES;
    private static final Set<String> SUPPORTED_EXTENSIONS;

    static {
        Set<String> mimeTypes = new HashSet<>();
        mimeTypes.add("text/plain");
        mimeTypes.add("text/markdown");
        mimeTypes.add("text/csv");
        mimeTypes.add("application/json");
        mimeTypes.add("application/xml");
        SUPPORTED_MIME_TYPES = Collections.unmodifiableSet(mimeTypes);

        Set<String> extensions = new HashSet<>();
        extensions.add("txt");
        extensions.add("text");
        extensions.add("md");
        extensions.add("markdown");
        extensions.add("log");
        extensions.add("csv");
        extensions.add("json");
        extensions.add("xml");
        extensions.add("yml");
        extensions.add("yaml");
        extensions.add("properties");
        extensions.add("conf");
        SUPPORTED_EXTENSIONS = Collections.unmodifiableSet(extensions);
    }

    @Override
    public boolean supports(DocumentSource source) {
        String mimeType = source.getMimeType();
        String fileName = source.getFileName();

        if (mimeType != null && (mimeType.startsWith("text/") || SUPPORTED_MIME_TYPES.contains(mimeType))) {
            return true;
        }

        if (fileName != null) {
            String ext = getExtension(fileName);
            return ext != null && SUPPORTED_EXTENSIONS.contains(ext.toLowerCase());
        }

        return false;
    }

    @Override
    public String extractText(DocumentSource source) throws IOException {
        return read(new LightweightDocumentReadRequest(source)).getText();
    }

    /**
     * 按文本行区间读取 TXT 或 Markdown。
     *
     * @param request 读取请求
     * @return 行区间结构化结果
     * @throws IOException 文本 I/O 失败
     */
    @Override
    public LightweightDocumentReadResult read(LightweightDocumentReadRequest request) throws IOException {
        DocumentSource source = request.getSource();
        CharsetDetection detection = detectCharset(source);
        try {
            return readWithCharset(request, detection.charset(), detection.bomLength());
        } catch (CharacterCodingException error) {
            if (!StandardCharsets.UTF_8.equals(detection.charset()) || detection.bomLength() > 0) {
                throw new DocumentReadException(DocumentReadErrorCode.DOCUMENT_CORRUPTED,
                        "Text encoding is invalid", error);
            }
            // 严格 UTF-8 解码失败时仅回退到受控 GB18030。
            return readWithCharset(request, GB18030, 0);
        } catch (DocumentReadException error) {
            throw error;
        } catch (Exception error) {
            throw new DocumentReadException(DocumentReadErrorCode.DOCUMENT_READ_FAILED,
                    "Failed to read text document", error);
        }
    }

    /**
     * 使用指定字符集流式读取文本。
     *
     * @param request 读取请求
     * @param charset 字符集
     * @param bomLength BOM 长度
     * @return 结构化结果
     * @throws Exception 打开或读取失败
     */
    private LightweightDocumentReadResult readWithCharset(LightweightDocumentReadRequest request,
                                                          Charset charset,
                                                          int bomLength) throws IOException {
        DocumentSource source = request.getSource();
        List<DocumentTextSegment> segments = new ArrayList<>();
        List<String> headingPath = new ArrayList<>();
        boolean markdown = isMarkdown(source.getFileName(), source.getMimeType());
        try (InputStream input = openStream(source)) {
            input.skipNBytes(bomLength);
            InputStreamReader streamReader = new InputStreamReader(input,
                    charset.newDecoder()
                            .onMalformedInput(CodingErrorAction.REPORT)
                            .onUnmappableCharacter(CodingErrorAction.REPORT));
            try (BufferedReader reader = new BufferedReader(streamReader, 8192)) {
                StringBuilder block = new StringBuilder();
                int line = 0;
                int blockStart = 1;
                int offset = 0;
                String value;
                while ((value = reader.readLine()) != null) {
                    request.checkCancelled();
                    line++;
                    if (markdown) {
                        updateHeadingPath(headingPath, value);
                    }
                    if (!block.isEmpty()) {
                        block.append('\n');
                    }
                    block.append(value);
                    if (line - blockStart + 1 >= LINES_PER_SEGMENT) {
                        DocumentTextSegment segment = addLineSegment(
                                segments, block, blockStart, line, offset, headingPath);
                        offset = segment == null ? offset : segment.getEndIndex() + 1;
                        block.setLength(0);
                        blockStart = line + 1;
                    }
                    if ((long) offset + block.length() > request.getMaxExpandedChars()) {
                        throw new DocumentReadException(DocumentReadErrorCode.DOCUMENT_STRUCTURE_LIMIT_EXCEEDED,
                                "Expanded text exceeds the configured limit");
                    }
                }
                if (!block.isEmpty()) {
                    addLineSegment(segments, block, blockStart, line, offset, headingPath);
                }
            }
        }
        return DocumentReadSupport.result(source, segments, request);
    }

    private InputStream openStream(DocumentSource source) throws IOException {
        try {
            return source.openStream();
        } catch (IOException error) {
            throw error;
        } catch (Exception error) {
            throw new IOException("Failed to open text document", error);
        }
    }

    private DocumentTextSegment addLineSegment(List<DocumentTextSegment> segments,
                                               StringBuilder block,
                                               int startLine,
                                               int endLine,
                                               int offset,
                                               List<String> headingPath) {
        if (block.toString().isBlank()) {
            return null;
        }
        DocumentTextSegment segment = DocumentReadSupport.segment(
                "lines-" + startLine + "-" + endLine, block.toString(), "LINE_RANGE",
                "第 " + startLine + "-" + endLine + " 行", offset, headingPath);
        segments.add(segment);
        return segment;
    }

    private void updateHeadingPath(List<String> headingPath, String line) {
        Matcher matcher = MARKDOWN_HEADING.matcher(line);
        if (!matcher.matches()) {
            return;
        }
        int level = matcher.group(1).length();
        while (headingPath.size() >= level) {
            headingPath.remove(headingPath.size() - 1);
        }
        headingPath.add(matcher.group(2).trim());
    }

    private CharsetDetection detectCharset(DocumentSource source) throws IOException {
        try (InputStream input = source.openStream()) {
            byte[] prefix = input.readNBytes(3);
            if (prefix.length >= 3 && (prefix[0] & 0xff) == 0xef
                    && (prefix[1] & 0xff) == 0xbb && (prefix[2] & 0xff) == 0xbf) {
                return new CharsetDetection(StandardCharsets.UTF_8, 3);
            }
            if (prefix.length >= 2 && (prefix[0] & 0xff) == 0xff && (prefix[1] & 0xff) == 0xfe) {
                return new CharsetDetection(StandardCharsets.UTF_16LE, 2);
            }
            if (prefix.length >= 2 && (prefix[0] & 0xff) == 0xfe && (prefix[1] & 0xff) == 0xff) {
                return new CharsetDetection(StandardCharsets.UTF_16BE, 2);
            }
            return new CharsetDetection(StandardCharsets.UTF_8, 0);
        } catch (IOException error) {
            throw error;
        } catch (Exception error) {
            throw new IOException("Failed to inspect text encoding", error);
        }
    }

    private boolean isMarkdown(String fileName, String mimeType) {
        return "text/markdown".equalsIgnoreCase(mimeType)
                || (fileName != null && (fileName.toLowerCase().endsWith(".md")
                || fileName.toLowerCase().endsWith(".markdown")));
    }


    @Override
    public int getOrder() {
        return 5; // 高优先级
    }

    private String getExtension(String fileName) {
        if (fileName == null || !fileName.contains(".")) return null;
        int lastDot = fileName.lastIndexOf('.');
        return fileName.substring(lastDot + 1).toLowerCase();
    }

    private record CharsetDetection(Charset charset, int bomLength) {
    }
}
