package tech.easyflow.agent.runtime.document;

import com.easyagents.core.file2text.DocumentReadErrorCode;
import com.easyagents.core.file2text.DocumentReadException;
import com.easyagents.core.file2text.File2TextService;
import com.easyagents.core.file2text.LightweightDocumentReadRequest;
import com.easyagents.core.file2text.LightweightDocumentReadResult;
import com.easyagents.core.file2text.source.FileDocumentSource;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.core.query.QueryWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import tech.easyflow.agent.config.AgentDocumentProperties;
import tech.easyflow.agent.entity.AgentDocumentAttachment;
import tech.easyflow.agent.entity.AgentDocumentSnapshot;
import tech.easyflow.agent.mapper.AgentDocumentAttachmentMapper;
import tech.easyflow.agent.mapper.AgentDocumentSnapshotMapper;
import tech.easyflow.agent.runtime.media.AgentMediaObjectStorage;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Date;
import java.util.HexFormat;
import java.util.Set;
import java.util.UUID;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * Agent 文档本地轻量读取与不可变快照服务。
 */
@Service
public class AgentDocumentReaderService {

    private static final Logger LOG = LoggerFactory.getLogger(AgentDocumentReaderService.class);
    private static final Set<String> EXCEL_EXTENSIONS = Set.of("xls", "xlsx");
    private static final Set<String> TEXT_EXTENSIONS = Set.of("txt", "md");

    private final AgentDocumentAttachmentMapper attachmentMapper;
    private final AgentDocumentSnapshotMapper snapshotMapper;
    private final AgentMediaObjectStorage objectStorage;
    private final AgentDocumentProperties properties;
    private final ObjectMapper objectMapper;
    private final File2TextService file2TextService = new File2TextService();

    /**
     * 创建文档读取服务。
     *
     * @param attachmentMapper 附件 Mapper
     * @param snapshotMapper 快照 Mapper
     * @param objectStorage 私有对象存储
     * @param properties 文档配置
     * @param objectMapper JSON 映射器
     */
    public AgentDocumentReaderService(AgentDocumentAttachmentMapper attachmentMapper,
                                      AgentDocumentSnapshotMapper snapshotMapper,
                                      AgentMediaObjectStorage objectStorage,
                                      AgentDocumentProperties properties,
                                      ObjectMapper objectMapper) {
        this.attachmentMapper = attachmentMapper;
        this.snapshotMapper = snapshotMapper;
        this.objectStorage = objectStorage;
        this.properties = properties;
        this.objectMapper = objectMapper;
    }

    /**
     * 幂等处理一个附件读取任务。
     *
     * @param attachmentId 稳定附件 ID
     */
    public void process(String attachmentId) {
        AgentDocumentAttachment attachment = findAttachment(attachmentId);
        if (attachment == null) {
            LOG.warn("Agent 文档读取任务附件不存在: attachmentId={}", attachmentId);
            return;
        }
        if (AgentDocumentStatus.READY.name().equals(attachment.getStatus())
                || AgentDocumentStatus.BINDING.name().equals(attachment.getStatus())
                || AgentDocumentStatus.BOUND.name().equals(attachment.getStatus())
                || AgentDocumentStatus.DELETE_PENDING.name().equals(attachment.getStatus())
                || AgentDocumentStatus.DELETED.name().equals(attachment.getStatus())) {
            return;
        }
        if (!claim(attachment)) {
            return;
        }
        Path tempDirectory = null;
        Path sourceFile = null;
        Path snapshotFile = null;
        boolean snapshotStored = false;
        try {
            tempDirectory = createTempDirectory();
            sourceFile = tempDirectory.resolve("source." + attachment.getExtension());
            long maxBytes = maxBytes(attachment.getExtension());
            long downloaded = objectStorage.downloadTo(attachment.getObjectKey(), sourceFile, maxBytes);
            if (attachment.getFileSize() != null && downloaded != attachment.getFileSize()) {
                throw new DocumentReadException(DocumentReadErrorCode.DOCUMENT_CORRUPTED,
                        "Stored document size does not match upload metadata");
            }
            LightweightDocumentReadRequest request = new LightweightDocumentReadRequest(
                    new FileDocumentSource(sourceFile.toFile(), attachment.getMimeType()));
            request.setMaxPdfPages(properties.getLimits().getMaxPdfPages());
            request.setMaxSlides(properties.getLimits().getMaxSlides());
            request.setMaxSheets(properties.getLimits().getMaxSheets());
            request.setMaxNonEmptyCells(properties.getLimits().getMaxNonEmptyCells());
            request.setMaxExpandedChars(properties.getLimits().getMaxExpandedBytes().toBytes());
            LightweightDocumentReadResult result = file2TextService.readFromSource(request);

            AgentDocumentSnapshot existing = findVersionSnapshot(attachment);
            if (existing != null && AgentDocumentSnapshotStatus.READY.name().equals(existing.getStatus())
                    && objectStorage.exists(existing.getSnapshotObjectKey())) {
                completeAttachment(attachment, existing.getReadSnapshotId());
                return;
            }
            AgentDocumentSnapshot snapshot = existing == null
                    ? createSnapshotMetadata(attachment, result) : existing;
            AgentDocumentReadSnapshot payload = snapshotPayload(attachment, snapshot, result);
            snapshotFile = tempDirectory.resolve(snapshot.getReadSnapshotId() + ".json.gz");
            writeSnapshot(snapshotFile, payload);
            try (InputStream input = Files.newInputStream(snapshotFile)) {
                objectStorage.put(snapshot.getSnapshotObjectKey(), input, Files.size(snapshotFile),
                        "application/gzip");
            }
            snapshotStored = true;
            markSnapshotReady(snapshot);
            completeAttachment(attachment, snapshot.getReadSnapshotId());
            LOG.info("Agent 文档读取完成: attachmentId={}, snapshotId={}, chars={}, segments={}",
                    attachmentId, snapshot.getReadSnapshotId(), result.getCharCount(), result.getSegments().size());
        } catch (DocumentReadException error) {
            markReadFailed(attachment, error.getErrorCode().name(), userMessage(error));
            LOG.warn("Agent 文档读取被拒绝: attachmentId={}, errorCode={}",
                    attachmentId, error.getErrorCode(), error);
        } catch (RuntimeException | IOException error) {
            // 快照对象写入后不覆盖为失败，补偿任务将完成或清理这次部分成功。
            if (!snapshotStored) {
                markReadFailed(attachment, "DOCUMENT_READ_FAILED", "文档读取失败，请重试");
            }
            LOG.error("Agent 文档读取失败: attachmentId={}", attachmentId, error);
            throw error instanceof RuntimeException runtime
                    ? runtime : new IllegalStateException("Agent document read failed", error);
        } finally {
            deleteQuietly(snapshotFile);
            deleteQuietly(sourceFile);
            deleteQuietly(tempDirectory);
        }
    }

    /**
     * 读取一个 READY 快照。
     *
     * @param readSnapshotId 快照 ID
     * @return 快照正文与结构片段
     */
    public AgentDocumentReadSnapshot readSnapshot(String readSnapshotId) {
        AgentDocumentSnapshot snapshot = snapshotMapper.selectOneByQuery(QueryWrapper.create()
                .eq("read_snapshot_id", readSnapshotId)
                .eq("status", AgentDocumentSnapshotStatus.READY.name())
                .limit(1));
        if (snapshot == null) {
            throw new IllegalStateException("Document snapshot is not available: " + readSnapshotId);
        }
        try (InputStream input = objectStorage.openStream(snapshot.getSnapshotObjectKey());
             GZIPInputStream gzip = new GZIPInputStream(input, 64 * 1024)) {
            return objectMapper.readValue(gzip, AgentDocumentReadSnapshot.class);
        } catch (IOException error) {
            throw new IllegalStateException("Document snapshot read failed: " + readSnapshotId, error);
        }
    }

    /**
     * 队列已满时把尚未领取的任务标记为可重试失败。
     *
     * @param attachmentId 附件 ID
     */
    public void markBusy(String attachmentId) {
        AgentDocumentAttachment update = new AgentDocumentAttachment();
        update.setStatus(AgentDocumentStatus.READ_FAILED.name());
        update.setErrorCode("DOCUMENT_READ_BUSY");
        update.setErrorMessage("当前文档读取繁忙，请稍后重试");
        update.setModified(new Date());
        attachmentMapper.updateByQuery(update, QueryWrapper.create()
                .eq("attachment_id", attachmentId)
                .eq("status", AgentDocumentStatus.UPLOADED.name()));
    }

    /**
     * 超时后阻止旧读取任务继续提交 READY。
     *
     * @param attachmentId 附件 ID
     */
    public void markTimeout(String attachmentId) {
        AgentDocumentAttachment update = new AgentDocumentAttachment();
        update.setStatus(AgentDocumentStatus.READ_FAILED.name());
        update.setErrorCode("DOCUMENT_READ_TIMEOUT");
        update.setErrorMessage("文档读取超时，请重试");
        update.setModified(new Date());
        attachmentMapper.updateByQuery(update, QueryWrapper.create()
                .eq("attachment_id", attachmentId)
                .eq("status", AgentDocumentStatus.READING.name()));
    }

    /**
     * 修复对象已存在但仍停留在 UPLOADING 的状态记录。
     *
     * @param attachment 附件记录
     * @return 是否已修复
     */
    public boolean repairUploading(AgentDocumentAttachment attachment) {
        if (attachment == null || !AgentDocumentStatus.UPLOADING.name().equals(attachment.getStatus())
                || !objectStorage.exists(attachment.getObjectKey())) {
            return false;
        }
        try (InputStream raw = objectStorage.openStream(attachment.getObjectKey())) {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            try (DigestInputStream input = new DigestInputStream(raw, digest)) {
                input.transferTo(OutputStream.nullOutputStream());
            }
            AgentDocumentAttachment update = new AgentDocumentAttachment();
            update.setFileSha256(HexFormat.of().formatHex(digest.digest()));
            update.setStatus(AgentDocumentStatus.UPLOADED.name());
            update.setVersion((attachment.getVersion() == null ? 0L : attachment.getVersion()) + 1L);
            update.setModified(new Date());
            return attachmentMapper.updateByQuery(update, QueryWrapper.create()
                    .eq("id", attachment.getId())
                    .eq("status", AgentDocumentStatus.UPLOADING.name())) == 1;
        } catch (Exception error) {
            LOG.error("Agent 文档上传状态修复失败: attachmentId={}", attachment.getAttachmentId(), error);
            return false;
        }
    }

    /**
     * 完成快照对象已存在但元数据仍为 WRITING 的部分成功。
     *
     * @param snapshot 快照元数据
     * @return 是否完成
     */
    public boolean repairWritingSnapshot(AgentDocumentSnapshot snapshot) {
        if (snapshot == null || !AgentDocumentSnapshotStatus.WRITING.name().equals(snapshot.getStatus())
                || !objectStorage.exists(snapshot.getSnapshotObjectKey())) {
            return false;
        }
        AgentDocumentSnapshot update = new AgentDocumentSnapshot();
        update.setStatus(AgentDocumentSnapshotStatus.READY.name());
        update.setModified(new Date());
        int updated = snapshotMapper.updateByQuery(update, QueryWrapper.create()
                .eq("id", snapshot.getId())
                .eq("status", AgentDocumentSnapshotStatus.WRITING.name()));
        if (updated != 1) {
            return false;
        }
        AgentDocumentAttachment attachment = findAttachment(snapshot.getAttachmentId());
        if (attachment != null && AgentDocumentStatus.READING.name().equals(attachment.getStatus())) {
            completeAttachment(attachment, snapshot.getReadSnapshotId());
        }
        return true;
    }

    private boolean claim(AgentDocumentAttachment attachment) {
        AgentDocumentAttachment update = new AgentDocumentAttachment();
        update.setStatus(AgentDocumentStatus.READING.name());
        update.setVersion((attachment.getVersion() == null ? 0L : attachment.getVersion()) + 1L);
        update.setErrorCode(null);
        update.setErrorMessage(null);
        update.setModified(new Date());
        int updated = attachmentMapper.updateByQuery(update, QueryWrapper.create()
                .eq("id", attachment.getId())
                .eq("status", AgentDocumentStatus.UPLOADED.name())
                .eq("version", attachment.getVersion()));
        if (updated != 1) {
            return false;
        }
        attachment.setStatus(update.getStatus());
        attachment.setVersion(update.getVersion());
        return true;
    }

    private AgentDocumentSnapshot createSnapshotMetadata(AgentDocumentAttachment attachment,
                                                         LightweightDocumentReadResult result) {
        String snapshotId = opaqueId();
        AgentDocumentSnapshot snapshot = new AgentDocumentSnapshot();
        snapshot.setReadSnapshotId(snapshotId);
        snapshot.setTenantId(attachment.getTenantId());
        snapshot.setAttachmentId(attachment.getAttachmentId());
        snapshot.setFileSha256(attachment.getFileSha256());
        snapshot.setReaderVersion(result.getReaderVersion());
        snapshot.setReadPolicyVersion(result.getReadPolicyVersion());
        snapshot.setSnapshotObjectKey("documents/snapshots/%s/%s/%s/%s.json.gz".formatted(
                attachment.getTenantId(), attachment.getUserId(), attachment.getAttachmentId(), snapshotId));
        snapshot.setCharCount(result.getCharCount());
        snapshot.setTokenEstimate(result.getTokenEstimate());
        snapshot.setSegmentCount(result.getSegments().size());
        snapshot.setStatus(AgentDocumentSnapshotStatus.WRITING.name());
        snapshot.setCreated(new Date());
        snapshot.setCreatedBy(attachment.getUserId());
        snapshot.setModified(new Date());
        snapshot.setModifiedBy(attachment.getUserId());
        snapshotMapper.insert(snapshot);
        return snapshot;
    }

    private AgentDocumentReadSnapshot snapshotPayload(AgentDocumentAttachment attachment,
                                                      AgentDocumentSnapshot snapshot,
                                                      LightweightDocumentReadResult result) {
        AgentDocumentReadSnapshot payload = new AgentDocumentReadSnapshot();
        payload.setReadSnapshotId(snapshot.getReadSnapshotId());
        payload.setAttachmentId(attachment.getAttachmentId());
        payload.setFileSha256(attachment.getFileSha256());
        payload.setReaderVersion(result.getReaderVersion());
        payload.setReadPolicyVersion(result.getReadPolicyVersion());
        payload.setCharCount(result.getCharCount());
        payload.setTokenEstimate(result.getTokenEstimate());
        payload.setSegments(result.getSegments());
        return payload;
    }

    private void writeSnapshot(Path target, AgentDocumentReadSnapshot snapshot) throws IOException {
        try (OutputStream fileOutput = Files.newOutputStream(target);
             GZIPOutputStream gzip = new GZIPOutputStream(fileOutput, 64 * 1024)) {
            objectMapper.writeValue(gzip, snapshot);
        }
    }

    private void markSnapshotReady(AgentDocumentSnapshot snapshot) {
        AgentDocumentSnapshot update = new AgentDocumentSnapshot();
        update.setStatus(AgentDocumentSnapshotStatus.READY.name());
        update.setModified(new Date());
        int updated = snapshotMapper.updateByQuery(update, QueryWrapper.create()
                .eq("id", snapshot.getId())
                .eq("status", AgentDocumentSnapshotStatus.WRITING.name()));
        if (updated != 1) {
            throw new IllegalStateException("Document snapshot metadata commit failed");
        }
        snapshot.setStatus(update.getStatus());
    }

    private void completeAttachment(AgentDocumentAttachment attachment, String snapshotId) {
        AgentDocumentAttachment update = new AgentDocumentAttachment();
        update.setStatus(AgentDocumentStatus.READY.name());
        update.setCurrentSnapshotId(snapshotId);
        update.setVersion(attachment.getVersion() + 1L);
        update.setErrorCode(null);
        update.setErrorMessage(null);
        update.setModified(new Date());
        int updated = attachmentMapper.updateByQuery(update, QueryWrapper.create()
                .eq("id", attachment.getId())
                .eq("status", AgentDocumentStatus.READING.name())
                .eq("version", attachment.getVersion()));
        if (updated != 1) {
            throw new IllegalStateException("Document attachment ready state commit failed");
        }
        attachment.setStatus(update.getStatus());
        attachment.setCurrentSnapshotId(snapshotId);
        attachment.setVersion(update.getVersion());
    }

    private void markReadFailed(AgentDocumentAttachment attachment, String errorCode, String message) {
        AgentDocumentAttachment update = new AgentDocumentAttachment();
        update.setStatus(AgentDocumentStatus.READ_FAILED.name());
        update.setErrorCode(errorCode);
        update.setErrorMessage(message);
        update.setVersion(attachment.getVersion() + 1L);
        update.setModified(new Date());
        attachmentMapper.updateByQuery(update, QueryWrapper.create()
                .eq("id", attachment.getId())
                .eq("status", AgentDocumentStatus.READING.name())
                .eq("version", attachment.getVersion()));
    }

    private AgentDocumentSnapshot findVersionSnapshot(AgentDocumentAttachment attachment) {
        if (!StringUtils.hasText(attachment.getFileSha256())) {
            return null;
        }
        return snapshotMapper.selectOneByQuery(QueryWrapper.create()
                .eq("attachment_id", attachment.getAttachmentId())
                .eq("file_sha256", attachment.getFileSha256())
                .eq("reader_version", LightweightDocumentReadResult.READER_VERSION)
                .eq("read_policy_version", LightweightDocumentReadResult.READ_POLICY_VERSION)
                .limit(1));
    }

    private AgentDocumentAttachment findAttachment(String attachmentId) {
        return attachmentMapper.selectOneByQuery(QueryWrapper.create()
                .eq("attachment_id", attachmentId)
                .limit(1));
    }

    private long maxBytes(String extension) {
        if (EXCEL_EXTENSIONS.contains(extension)) {
            return properties.getLimits().getExcelMaxBytes().toBytes();
        }
        if (TEXT_EXTENSIONS.contains(extension)) {
            return properties.getLimits().getTextMaxBytes().toBytes();
        }
        return properties.getLimits().getOfficeMaxBytes().toBytes();
    }

    private Path createTempDirectory() throws IOException {
        Path root = Path.of(System.getProperty("java.io.tmpdir"), "easyflow-agent-documents");
        Files.createDirectories(root);
        return Files.createTempDirectory(root, "read-");
    }

    private String userMessage(DocumentReadException error) {
        return switch (error.getErrorCode()) {
            case UNSUPPORTED_DOCUMENT_TYPE -> "不支持该文档类型";
            case DOCUMENT_STRUCTURE_LIMIT_EXCEEDED -> "文档结构超过安全限制";
            case DOCUMENT_ENCRYPTED -> "暂不支持加密文档";
            case DOCUMENT_CORRUPTED -> "文档已损坏或格式不正确";
            case DOCUMENT_NO_READABLE_TEXT -> "未检测到可读取文字";
            case DOCUMENT_READ_CANCELLED -> "文档读取已取消";
            default -> "文档读取失败，请重试";
        };
    }

    private void deleteQuietly(Path path) {
        if (path == null) {
            return;
        }
        try {
            Files.deleteIfExists(path);
        } catch (IOException error) {
            LOG.warn("Agent 文档临时文件清理失败: file={}", path.getFileName(), error);
        }
    }

    private String opaqueId() {
        return UUID.randomUUID().toString().replace("-", "");
    }
}
