package tech.easyflow.agent.runtime.artifact;

import com.easyagents.agent.runtime.tool.AgentToolContext;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.update.UpdateChain;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ResponseStatusException;
import tech.easyflow.agent.config.AgentWorkspaceProperties;
import tech.easyflow.agent.entity.AgentArtifact;
import tech.easyflow.agent.mapper.AgentArtifactMapper;
import tech.easyflow.agent.runtime.workspace.AgentWorkspaceResolver;
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
import tech.easyflow.chatlog.service.ChatSessionQueryService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.core.runtime.ChatRuntimeExtKeys;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.charset.StandardCharsets;
import java.nio.file.StandardOpenOption;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Date;
import java.util.Enumeration;
import java.util.HexFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * Agent Artifact 发布、归属校验和生命周期服务。
 */
@Service
public class AgentArtifactService {

    /** 草稿产物模式。 */
    public static final String MODE_DRAFT = "DRAFT";
    /** 正式聊天产物模式。 */
    public static final String MODE_FORMAL = "FORMAL";

    private static final Logger LOG = LoggerFactory.getLogger(AgentArtifactService.class);
    private static final int MAX_FILE_NAME_LENGTH = 255;
    private static final int MAX_ZIP_ENTRIES = 256;
    private static final int MAX_ZIP_ENTRY_NAME_LENGTH = 1024;
    private static final long MAX_ZIP_CENTRAL_DIRECTORY_SIZE = 1024L * 1024;
    private static final long MAX_ZIP_DECLARED_SIZE = 512L * 1024 * 1024;
    private static final long MAX_ZIP_COMPRESSION_RATIO = 200L;
    private static final int ZIP_CENTRAL_DIRECTORY_SIGNATURE = 0x02014B50;
    private static final int ZIP_CENTRAL_DIRECTORY_HEADER_SIZE = 46;
    private static final int ZIP_EOCD_SIGNATURE = 0x06054B50;
    private static final int ZIP_EOCD_MIN_SIZE = 22;
    private static final int ZIP_EOCD_MAX_SIZE = 65_557;
    private static final int ZIP_UINT16_MAX = 0xFFFF;
    private static final long ZIP_UINT32_MAX = 0xFFFF_FFFFL;
    private static final long PUBLISH_RECOVERY_TIMEOUT_SECONDS = 10 * 60L;
    private static final String UNAVAILABLE_STATUS = "UNAVAILABLE";

    private final AgentArtifactMapper mapper;
    private final AgentArtifactObjectStorage objectStorage;
    private final AgentWorkspaceResolver workspaceResolver;
    private final AgentWorkspaceProperties workspaceProperties;
    private ChatSessionQueryService chatSessionQueryService;

    /**
     * 创建 Artifact 服务。
     *
     * @param mapper 状态账本 Mapper
     * @param objectStorage 私有对象存储
     * @param workspaceResolver 工作区解析器
     * @param workspaceProperties 工作区限制
     */
    public AgentArtifactService(AgentArtifactMapper mapper,
                                AgentArtifactObjectStorage objectStorage,
                                AgentWorkspaceResolver workspaceResolver,
                                AgentWorkspaceProperties workspaceProperties) {
        this.mapper = mapper;
        this.objectStorage = objectStorage;
        this.workspaceResolver = workspaceResolver;
        this.workspaceProperties = workspaceProperties;
    }

    /**
     * 延迟注入会话查询服务，避免会话投影扩展初始化形成依赖环。
     *
     * @param chatSessionQueryService 会话查询服务
     */
    @Autowired
    @Lazy
    public void setChatSessionQueryService(ChatSessionQueryService chatSessionQueryService) {
        this.chatSessionQueryService = chatSessionQueryService;
    }

    /**
     * 将当前会话工作区中的普通文件发布为私有 Artifact。
     *
     * @param workspace 当前会话绝对工作区
     * @param relativePath 工作区相对文件路径
     * @param requestedFileName 可选展示文件名
     * @param mode DRAFT 或 FORMAL
     * @param context 可信 Tool 调用上下文
     * @return 安全产物视图
     */
    public AgentArtifactView publish(Path workspace,
                                     String relativePath,
                                     String requestedFileName,
                                     String mode,
                                     AgentToolContext context) {
        ToolIdentity identity = requireIdentity(context, mode);
        Path file = workspaceResolver.resolveExistingFile(workspace, relativePath);
        long size = fileSize(file);
        if (size > workspaceProperties.getMaxSingleFileSize().toBytes()) {
            throw new AgentArtifactOperationException(
                    "WORKSPACE_QUOTA_EXCEEDED", "文件超过允许发布的单文件大小", false);
        }
        String fileName = safeFileName(requestedFileName, file.getFileName().toString());
        String mimeType = mimeType(file);
        String artifactId = opaqueId();
        String objectKey = "artifacts/%s/%s/%s/content%s".formatted(
                identity.tenantId(), identity.agentId(), artifactId, safeExtension(fileName));
        AgentArtifact artifact = publishingRecord(
                artifactId, fileName, mimeType, size, objectKey, identity, context);
        mapper.insert(artifact);

        boolean uploadAttempted = false;
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            String etag;
            try (InputStream raw = Files.newInputStream(file);
                 DigestInputStream input = new DigestInputStream(raw, digest)) {
                uploadAttempted = true;
                etag = objectStorage.put(objectKey, input, size, mimeType);
            }
            String sha256 = HexFormat.of().formatHex(digest.digest());
            verifyStoredObject(objectKey, size, sha256);
            boolean changed = UpdateChain.of(new AgentArtifact(), mapper)
                    .set(AgentArtifact::getSha256, sha256)
                    .set(AgentArtifact::getStorageEtag, etag)
                    .set(AgentArtifact::getStatus, AgentArtifactStatus.AVAILABLE.name())
                    .set(AgentArtifact::getNextRetryAt, null)
                    .set(AgentArtifact::getLastErrorCode, null)
                    .set(AgentArtifact::getModified, new Date())
                    .set(AgentArtifact::getModifiedBy, identity.userId())
                    .eq(AgentArtifact::getId, artifact.getId())
                    .eq(AgentArtifact::getStatus, AgentArtifactStatus.PUBLISHING.name())
                    .update();
            if (!changed) {
                throw new AgentArtifactOperationException(
                        "ARTIFACT_PUBLISH_FAILED", "提交产物发布状态失败", true);
            }
            artifact.setSha256(sha256);
            artifact.setStorageEtag(etag);
            artifact.setStatus(AgentArtifactStatus.AVAILABLE.name());
            workspaceResolver.touch(workspace);
            return toView(artifact);
        } catch (AgentArtifactOperationException error) {
            compensatePublishFailure(artifact, uploadAttempted, error.getCode(), error);
            throw error;
        } catch (NoSuchAlgorithmException | IOException error) {
            compensatePublishFailure(artifact, uploadAttempted, "ARTIFACT_PUBLISH_FAILED", error);
            throw new AgentArtifactOperationException(
                    "ARTIFACT_PUBLISH_FAILED", "读取并发布工作区文件失败", true);
        } catch (RuntimeException error) {
            compensatePublishFailure(artifact, uploadAttempted, "ARTIFACT_PUBLISH_FAILED", error);
            throw new AgentArtifactOperationException(
                    "ARTIFACT_PUBLISH_FAILED", "产物发布失败", true);
        }
    }

    /**
     * 校验当前登录用户并返回可下载账本。
     *
     * @param artifactId 对外 Artifact ID
     * @param account 当前账号
     * @param expectedAgentId 当前页面 Agent ID
     * @param expectedMode 当前页面聊天模式
     * @param expectedSessionId 当前页面正式会话 ID
     * @param expectedRuntimeSessionId 当前页面草稿 Runtime 会话 ID
     * @return 可下载记录
     */
    public AgentArtifact requireDownload(String artifactId,
                                         LoginAccount account,
                                         BigInteger expectedAgentId,
                                         String expectedMode,
                                         BigInteger expectedSessionId,
                                         String expectedRuntimeSessionId) {
        if (!StringUtils.hasText(artifactId) || account == null
                || account.getId() == null || account.getTenantId() == null
                || expectedAgentId == null || expectedAgentId.signum() <= 0
                || !StringUtils.hasText(expectedMode)) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "产物不存在");
        }
        AgentArtifact artifact = mapper.selectOneByQuery(QueryWrapper.create()
                .eq(AgentArtifact::getTenantId, account.getTenantId())
                .eq(AgentArtifact::getArtifactId, artifactId));
        if (artifact == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "产物不存在");
        }
        if (!account.getId().equals(artifact.getOwnerUserId())) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "无权下载该产物");
        }
        validateDownloadScope(
                artifact, expectedAgentId, expectedMode, expectedSessionId, expectedRuntimeSessionId);
        if (artifact.getExpiresAt() != null && artifact.getExpiresAt().before(new Date())) {
            markDeletePending(artifact);
            throw new ResponseStatusException(HttpStatus.GONE, "产物已过期");
        }
        if (!AgentArtifactStatus.AVAILABLE.name().equals(artifact.getStatus())) {
            throw new ResponseStatusException(HttpStatus.CONFLICT, "产物当前不可下载");
        }
        return artifact;
    }

    /**
     * 打开已鉴权账本对应的对象流。
     *
     * @param artifact 已鉴权记录
     * @return 对象输入流
     * @throws IOException 对象读取失败
     */
    public InputStream openDownload(AgentArtifact artifact) throws IOException {
        if (artifact == null || !AgentArtifactObjectStorage.PLATFORM.equals(artifact.getStoragePlatform())) {
            throw new IOException("Artifact 存储平台不匹配");
        }
        return objectStorage.open(artifact.getObjectKey());
    }

    /**
     * 用当前 Artifact 账本批量覆盖会话历史中的安全产物投影。
     *
     * @param messages 同一正式会话的一页或全部消息
     * @param tenantId 当前登录租户 ID
     * @param ownerUserId 当前登录用户 ID
     * @param chatSessionId 已鉴权的聊天会话 ID
     * @throws IllegalArgumentException 可信归属不完整时抛出
     */
    public void projectHistoryArtifacts(List<ChatMessageRecord> messages,
                                        BigInteger tenantId,
                                        BigInteger ownerUserId,
                                        BigInteger agentId,
                                        BigInteger chatSessionId) {
        requirePositive(tenantId, "tenantId");
        requirePositive(ownerUserId, "ownerUserId");
        requirePositive(agentId, "agentId");
        requirePositive(chatSessionId, "chatSessionId");
        if (messages == null || messages.isEmpty()) {
            return;
        }
        Set<BigInteger> roundIds = collectHistoryRoundIds(messages, chatSessionId);
        if (roundIds.isEmpty()) {
            return;
        }
        List<AgentArtifact> artifacts = mapper.selectListByQuery(QueryWrapper.create()
                .eq(AgentArtifact::getTenantId, tenantId)
                .eq(AgentArtifact::getOwnerUserId, ownerUserId)
                .eq(AgentArtifact::getAgentId, agentId)
                .eq(AgentArtifact::getChatMode, MODE_FORMAL)
                .eq(AgentArtifact::getChatSessionId, chatSessionId)
                .in(AgentArtifact::getRoundId, roundIds)
                .orderBy(AgentArtifact::getId, true));
        Map<String, AgentArtifact> ledgerById = new LinkedHashMap<>();
        Map<BigInteger, List<AgentArtifact>> ledgerByRound = new LinkedHashMap<>();
        for (AgentArtifact artifact : artifacts) {
            if (artifact != null && StringUtils.hasText(artifact.getArtifactId())) {
                ledgerById.put(artifact.getArtifactId(), artifact);
                ledgerByRound.computeIfAbsent(artifact.getRoundId(), ignored -> new ArrayList<>()).add(artifact);
            }
        }
        for (ChatMessageRecord message : messages) {
            projectMessageArtifacts(message, chatSessionId, ledgerById, ledgerByRound);
        }
    }

    /**
     * 将正式聊天会话的全部产物标记为待删除。
     *
     * @param chatSessionId 聊天会话 ID
     */
    public void markSessionDeletePending(BigInteger tenantId,
                                         BigInteger ownerUserId,
                                         BigInteger agentId,
                                         BigInteger chatSessionId) {
        requirePositive(tenantId, "tenantId");
        requirePositive(ownerUserId, "ownerUserId");
        requirePositive(agentId, "agentId");
        requirePositive(chatSessionId, "chatSessionId");
        AgentArtifact update = new AgentArtifact();
        update.setStatus(AgentArtifactStatus.DELETE_PENDING.name());
        update.setNextRetryAt(new Date());
        update.setModified(new Date());
        mapper.updateByQuery(update, QueryWrapper.create()
                .eq(AgentArtifact::getTenantId, tenantId)
                .eq(AgentArtifact::getOwnerUserId, ownerUserId)
                .eq(AgentArtifact::getAgentId, agentId)
                .eq(AgentArtifact::getChatMode, MODE_FORMAL)
                .eq(AgentArtifact::getChatSessionId, chatSessionId)
                .in(AgentArtifact::getStatus,
                        AgentArtifactStatus.PUBLISHING.name(),
                        AgentArtifactStatus.AVAILABLE.name(),
                        AgentArtifactStatus.FAILED.name(),
                        AgentArtifactStatus.DELETE_FAILED.name()));
    }

    /**
     * 将指定草稿 Runtime 会话的产物标记为待删除。
     *
     * @param runtimeSessionId 草稿会话 ID
     * @param tenantId 租户 ID
     * @param ownerUserId 所有者用户 ID
     */
    public void markDraftSessionDeletePending(String runtimeSessionId,
                                              BigInteger tenantId,
                                              BigInteger ownerUserId) {
        if (!StringUtils.hasText(runtimeSessionId) || tenantId == null || ownerUserId == null) {
            return;
        }
        AgentArtifact update = new AgentArtifact();
        update.setStatus(AgentArtifactStatus.DELETE_PENDING.name());
        update.setNextRetryAt(new Date());
        update.setModified(new Date());
        mapper.updateByQuery(update, QueryWrapper.create()
                .eq(AgentArtifact::getTenantId, tenantId)
                .eq(AgentArtifact::getChatMode, MODE_DRAFT)
                .eq(AgentArtifact::getRuntimeSessionId, runtimeSessionId)
                .eq(AgentArtifact::getOwnerUserId, ownerUserId)
                .in(AgentArtifact::getStatus,
                        AgentArtifactStatus.PUBLISHING.name(),
                        AgentArtifactStatus.AVAILABLE.name(),
                        AgentArtifactStatus.FAILED.name(),
                        AgentArtifactStatus.DELETE_FAILED.name()));
    }

    /**
     * 删除一条待清理对象并更新终态。
     *
     * @param artifact 待清理记录
     */
    public void deleteObject(AgentArtifact artifact) {
        if (artifact == null) {
            return;
        }
        try {
            objectStorage.delete(artifact.getObjectKey());
            AgentArtifact update = new AgentArtifact();
            update.setStatus(AgentArtifactStatus.DELETED.name());
            update.setNextRetryAt(null);
            update.setLastErrorCode(null);
            update.setModified(new Date());
            mapper.updateByQuery(update, QueryWrapper.create().eq(AgentArtifact::getId, artifact.getId()));
        } catch (RuntimeException error) {
            int retries = artifact.getRetryCount() == null ? 1 : artifact.getRetryCount() + 1;
            AgentArtifact update = new AgentArtifact();
            update.setStatus(AgentArtifactStatus.DELETE_FAILED.name());
            update.setRetryCount(retries);
            update.setNextRetryAt(Date.from(Instant.now().plusSeconds(Math.min(3_600L, 60L << Math.min(retries, 5)))));
            update.setLastErrorCode("ARTIFACT_STORAGE_UNAVAILABLE");
            update.setModified(new Date());
            mapper.updateByQuery(update, QueryWrapper.create().eq(AgentArtifact::getId, artifact.getId()));
            LOG.error("Agent Artifact object cleanup failed, artifactId={}", artifact.getArtifactId(), error);
        }
    }

    /**
     * 转换数据库记录为安全视图。
     *
     * @param artifact 账本记录
     * @return 安全视图
     */
    public AgentArtifactView toView(AgentArtifact artifact) {
        String downloadUrl = AgentArtifactStatus.AVAILABLE.name().equals(artifact.getStatus())
                ? downloadUrl(artifact)
                : null;
        return new AgentArtifactView(
                1, artifact.getArtifactId(), artifact.getFileName(), artifact.getMimeType(),
                artifact.getSizeBytes() == null ? 0L : artifact.getSizeBytes(), artifact.getSha256(),
                downloadUrl, artifact.getStatus());
    }

    private ToolIdentity requireIdentity(AgentToolContext context, String mode) {
        if (context == null || context.getRuntimeContext() == null) {
            throw new AgentArtifactOperationException("ARTIFACT_ACCESS_DENIED", "产物调用上下文缺失", false);
        }
        try {
            BigInteger tenantId = positiveId(context.getRuntimeContext().getTenantId());
            BigInteger userId = positiveId(context.getRuntimeContext().getUserId());
            BigInteger agentId = positiveId(context.getAgentId());
            String sessionId = context.getSessionId();
            if (!StringUtils.hasText(sessionId) || !StringUtils.hasText(context.getRequestId())
                    || !StringUtils.hasText(context.getToolCallId())) {
                throw new IllegalArgumentException();
            }
            String safeMode = MODE_DRAFT.equals(mode) ? MODE_DRAFT : MODE_FORMAL;
            BigInteger chatSessionId = MODE_FORMAL.equals(safeMode) ? positiveId(sessionId) : null;
            BigInteger roundId = MODE_FORMAL.equals(safeMode)
                    ? positiveId(String.valueOf(context.getRuntimeContext().getMetadata()
                    .get(ChatRuntimeExtKeys.CURRENT_ROUND_ID))) : null;
            Integer variantIndex = MODE_FORMAL.equals(safeMode)
                    ? positiveInteger(context.getRuntimeContext().getMetadata()
                    .get(ChatRuntimeExtKeys.CURRENT_VARIANT_INDEX)) : null;
            return new ToolIdentity(
                    tenantId, userId, agentId, sessionId, chatSessionId, roundId, variantIndex, safeMode);
        } catch (RuntimeException error) {
            throw new AgentArtifactOperationException("ARTIFACT_ACCESS_DENIED", "产物调用归属不完整", false);
        }
    }

    private AgentArtifact publishingRecord(String artifactId,
                                             String fileName,
                                             String mimeType,
                                             long size,
                                             String objectKey,
                                             ToolIdentity identity,
                                             AgentToolContext context) {
        Date now = new Date();
        AgentArtifact artifact = new AgentArtifact();
        artifact.setArtifactId(artifactId);
        artifact.setTenantId(identity.tenantId());
        artifact.setAgentId(identity.agentId());
        artifact.setOwnerUserId(identity.userId());
        artifact.setChatMode(identity.mode());
        artifact.setChatSessionId(identity.chatSessionId());
        artifact.setRoundId(identity.roundId());
        artifact.setVariantIndex(identity.variantIndex());
        artifact.setRuntimeSessionId(identity.runtimeSessionId());
        artifact.setRequestId(context.getRequestId());
        artifact.setToolCallId(context.getToolCallId());
        artifact.setFileName(fileName);
        artifact.setMimeType(mimeType);
        artifact.setSizeBytes(size);
        artifact.setStoragePlatform(AgentArtifactObjectStorage.PLATFORM);
        artifact.setObjectKey(objectKey);
        artifact.setStatus(AgentArtifactStatus.PUBLISHING.name());
        artifact.setNextRetryAt(Date.from(Instant.now().plusSeconds(PUBLISH_RECOVERY_TIMEOUT_SECONDS)));
        artifact.setExpiresAt(MODE_DRAFT.equals(identity.mode())
                ? Date.from(Instant.now().plusSeconds(24 * 60 * 60L)) : null);
        artifact.setRetryCount(0);
        artifact.setCreated(now);
        artifact.setCreatedBy(identity.userId());
        artifact.setModified(now);
        artifact.setModifiedBy(identity.userId());
        artifact.setIsDeleted(0);
        return artifact;
    }

    private void validateDownloadScope(AgentArtifact artifact,
                                       BigInteger expectedAgentId,
                                       String expectedMode,
                                       BigInteger expectedSessionId,
                                       String expectedRuntimeSessionId) {
        if (!Objects.equals(artifact.getAgentId(), expectedAgentId)
                || !artifact.getChatMode().equalsIgnoreCase(expectedMode)) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "产物不属于当前 Agent 会话");
        }
        if (MODE_FORMAL.equals(artifact.getChatMode())) {
            if (expectedSessionId == null || expectedSessionId.signum() <= 0
                    || StringUtils.hasText(expectedRuntimeSessionId)) {
                throw new ResponseStatusException(HttpStatus.FORBIDDEN, "产物不属于当前正式会话");
            }
            if (!Objects.equals(artifact.getChatSessionId(), expectedSessionId)
                    || chatSessionQueryService == null || artifact.getChatSessionId() == null) {
                throw new ResponseStatusException(HttpStatus.GONE, "产物所属会话已失效");
            }
            ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(artifact.getChatSessionId());
            boolean valid = summary != null
                    && !Integer.valueOf(1).equals(summary.getIsDeleted())
                    && "AGENT".equals(summary.getAssistantCode())
                    && Objects.equals(summary.getTenantId(), artifact.getTenantId())
                    && Objects.equals(summary.getUserId(), artifact.getOwnerUserId())
                    && Objects.equals(summary.getAssistantId(), artifact.getAgentId());
            if (!valid) {
                throw new ResponseStatusException(HttpStatus.GONE, "产物所属会话已失效");
            }
            return;
        }
        boolean validDraft = MODE_DRAFT.equals(artifact.getChatMode())
                && expectedSessionId == null
                && artifact.getAgentId() != null && artifact.getAgentId().signum() > 0
                && StringUtils.hasText(artifact.getRuntimeSessionId())
                && Objects.equals(artifact.getRuntimeSessionId(), expectedRuntimeSessionId)
                && artifact.getChatSessionId() == null;
        if (!validDraft) {
            throw new ResponseStatusException(HttpStatus.CONFLICT, "产物归属记录不完整");
        }
    }

    private String downloadUrl(AgentArtifact artifact) {
        String base = "/api/v1/agent/artifacts/%s/content?agentId=%s&mode=%s".formatted(
                artifact.getArtifactId(), artifact.getAgentId(), artifact.getChatMode());
        if (MODE_FORMAL.equals(artifact.getChatMode())) {
            return base + "&sessionId=" + artifact.getChatSessionId();
        }
        return base + "&runtimeSessionId=" + artifact.getRuntimeSessionId();
    }

    private Set<BigInteger> collectHistoryRoundIds(List<ChatMessageRecord> messages,
                                                   BigInteger chatSessionId) {
        Set<BigInteger> roundIds = new LinkedHashSet<>();
        for (ChatMessageRecord message : messages) {
            if (message != null && Objects.equals(message.getSessionId(), chatSessionId)
                    && message.getRoundId() != null) {
                roundIds.add(message.getRoundId());
            }
        }
        return roundIds;
    }

    private void projectMessageArtifacts(ChatMessageRecord message,
                                         BigInteger chatSessionId,
                                         Map<String, AgentArtifact> ledgerById,
                                         Map<BigInteger, List<AgentArtifact>> ledgerByRound) {
        if (message == null || !Objects.equals(message.getSessionId(), chatSessionId)) {
            return;
        }
        Map<String, Object> originalPayload = message.getContentPayload();
        Object rawArtifacts = originalPayload == null ? null : originalPayload.get("artifacts");
        List<?> list = rawArtifacts instanceof List<?> values ? values : List.of();
        boolean assistantMessage = "assistant".equalsIgnoreCase(message.getSenderRole());
        List<AgentArtifact> variantArtifacts = ledgerByRound.getOrDefault(message.getRoundId(), List.of())
                .stream()
                .filter(ledger -> Objects.equals(ledger.getVariantIndex(), message.getVariantIndex()))
                .toList();
        if (list.isEmpty() && (!assistantMessage || variantArtifacts.isEmpty())) {
            return;
        }
        List<Map<String, Object>> projected = new ArrayList<>(list.size());
        Set<String> projectedIds = new LinkedHashSet<>();
        for (Object item : list) {
            if (!(item instanceof Map<?, ?> oldView)) {
                continue;
            }
            String artifactId = safeString(oldView.get("artifactId"));
            AgentArtifact ledger = ledgerById.get(artifactId);
            boolean sameRound = ledger != null
                    && Objects.equals(ledger.getRoundId(), message.getRoundId())
                    && Objects.equals(ledger.getVariantIndex(), message.getVariantIndex())
                    && Objects.equals(message.getSessionId(), chatSessionId);
            projected.add(sameRound ? toView(ledger).toMap() : unavailableView(oldView, artifactId));
            if (StringUtils.hasText(artifactId)) {
                projectedIds.add(artifactId);
            }
        }
        if (assistantMessage) {
            for (AgentArtifact ledger : variantArtifacts) {
                if (projectedIds.add(ledger.getArtifactId())) {
                    projected.add(toView(ledger).toMap());
                }
            }
        }
        Map<String, Object> payload = originalPayload == null
                ? new LinkedHashMap<>() : new LinkedHashMap<>(originalPayload);
        payload.put("artifacts", projected);
        message.setContentPayload(payload);
    }

    private Map<String, Object> unavailableView(Map<?, ?> oldView, String artifactId) {
        Object sizeValue = oldView.get("size");
        long size = sizeValue instanceof Number number ? Math.max(0L, number.longValue()) : 0L;
        return new AgentArtifactView(
                1,
                artifactId,
                safeString(oldView.get("fileName")),
                safeString(oldView.get("mimeType")),
                size,
                safeString(oldView.get("sha256")),
                null,
                UNAVAILABLE_STATUS).toMap();
    }

    private String safeString(Object value) {
        return value instanceof String text ? text : null;
    }

    private void requirePositive(BigInteger value, String field) {
        if (value == null || value.signum() <= 0) {
            throw new IllegalArgumentException(field + " must be positive");
        }
    }

    private void compensatePublishFailure(AgentArtifact artifact,
                                          boolean uploadAttempted,
                                          String code,
                                          Throwable error) {
        boolean cleanupFailed = false;
        if (uploadAttempted) {
            try {
                objectStorage.delete(artifact.getObjectKey());
            } catch (RuntimeException cleanupError) {
                cleanupFailed = true;
                error.addSuppressed(cleanupError);
            }
        }
        AgentArtifact update = new AgentArtifact();
        update.setStatus(cleanupFailed
                ? AgentArtifactStatus.DELETE_FAILED.name() : AgentArtifactStatus.FAILED.name());
        update.setLastErrorCode(code);
        update.setRetryCount(cleanupFailed ? 1 : 0);
        update.setNextRetryAt(cleanupFailed ? new Date() : null);
        update.setModified(new Date());
        QueryWrapper condition = QueryWrapper.create()
                .eq(AgentArtifact::getId, artifact.getId());
        if (cleanupFailed) {
            condition.in(AgentArtifact::getStatus,
                    AgentArtifactStatus.PUBLISHING.name(),
                    AgentArtifactStatus.DELETE_PENDING.name());
        } else {
            // 会话删除已把记录置为 DELETE_PENDING 时，保留删除意图交给调度器幂等收口。
            condition.eq(AgentArtifact::getStatus, AgentArtifactStatus.PUBLISHING.name());
        }
        mapper.updateByQuery(update, condition);
        LOG.error("Agent Artifact publish failed, artifactId={}", artifact.getArtifactId(), error);
    }

    private void markDeletePending(AgentArtifact artifact) {
        AgentArtifact update = new AgentArtifact();
        update.setStatus(AgentArtifactStatus.DELETE_PENDING.name());
        update.setNextRetryAt(new Date());
        update.setModified(new Date());
        mapper.updateByQuery(update, QueryWrapper.create()
                .eq(AgentArtifact::getId, artifact.getId())
                .eq(AgentArtifact::getStatus, AgentArtifactStatus.AVAILABLE.name()));
    }

    private long fileSize(Path file) {
        try {
            return Files.size(file);
        } catch (IOException error) {
            throw new AgentArtifactOperationException(
                    "WORKSPACE_FILE_NOT_FOUND", "读取工作区文件大小失败", true, error);
        }
    }

    private String mimeType(Path file) {
        byte[] header = new byte[512];
        int length;
        try (InputStream input = Files.newInputStream(file)) {
            length = input.read(header);
        } catch (IOException error) {
            throw new AgentArtifactOperationException(
                    "WORKSPACE_FILE_NOT_FOUND", "读取工作区文件类型失败", true, error);
        }
        if (length < 0) {
            return "application/octet-stream";
        }
        if (startsWith(header, length, new byte[]{(byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A})) {
            return "image/png";
        }
        if (startsWith(header, length, new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF})) {
            return "image/jpeg";
        }
        if (startsWith(header, length, "GIF87a".getBytes(StandardCharsets.US_ASCII))
                || startsWith(header, length, "GIF89a".getBytes(StandardCharsets.US_ASCII))) {
            return "image/gif";
        }
        if (startsWith(header, length, "%PDF-".getBytes(StandardCharsets.US_ASCII))) {
            return "application/pdf";
        }
        if (startsWith(header, length, new byte[]{'P', 'K', 0x03, 0x04})
                || startsWith(header, length, new byte[]{'P', 'K', 0x05, 0x06})
                || startsWith(header, length, new byte[]{'P', 'K', 0x07, 0x08})) {
            return officeOpenXmlMime(file);
        }
        if (isSafeUtf8Text(header, length)) {
            return "text/plain";
        }
        return "application/octet-stream";
    }

    private String officeOpenXmlMime(Path file) {
        if (!hasBoundedClassicZipDirectory(file)) {
            return "application/zip";
        }
        boolean contentTypes = false;
        String documentType = null;
        long declaredSize = 0L;
        try (ZipFile zipFile = new ZipFile(file.toFile())) {
            if (zipFile.size() > MAX_ZIP_ENTRIES) {
                return "application/zip";
            }
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            int inspected = 0;
            while (entries.hasMoreElements() && inspected++ < MAX_ZIP_ENTRIES) {
                ZipEntry entry = entries.nextElement();
                if (isSuspiciousZipEntry(entry, declaredSize)) {
                    return "application/zip";
                }
                declaredSize += Math.max(0L, entry.getSize());
                String name = entry.getName();
                contentTypes |= "[Content_Types].xml".equals(name);
                if (name.startsWith("word/")) {
                    documentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
                } else if (name.startsWith("xl/")) {
                    documentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
                } else if (name.startsWith("ppt/")) {
                    documentType = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
                }
            }
        } catch (IOException ignored) {
            return "application/zip";
        }
        return contentTypes && documentType != null ? documentType : "application/zip";
    }

    /**
     * 在构造 {@link ZipFile} 前以常量内存校验经典 ZIP 的中央目录边界和实际条目数。
     *
     * @param file 待识别 ZIP 文件
     * @return 中央目录可安全交给 ZipFile 解析时为 true
     */
    private boolean hasBoundedClassicZipDirectory(Path file) {
        try (SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ)) {
            long archiveSize = channel.size();
            if (archiveSize < ZIP_EOCD_MIN_SIZE) {
                return false;
            }
            int tailSize = (int) Math.min(archiveSize, ZIP_EOCD_MAX_SIZE);
            long tailOffset = archiveSize - tailSize;
            ByteBuffer tail = readZipAt(channel, tailOffset, tailSize);
            int eocdIndex = findZipEocd(tail);
            if (eocdIndex < 0) {
                return false;
            }
            int diskNumber = unsignedZipShort(tail, eocdIndex + 4);
            int directoryDisk = unsignedZipShort(tail, eocdIndex + 6);
            int entriesOnDisk = unsignedZipShort(tail, eocdIndex + 8);
            int totalEntries = unsignedZipShort(tail, eocdIndex + 10);
            long directorySize = unsignedZipInt(tail, eocdIndex + 12);
            long directoryOffset = unsignedZipInt(tail, eocdIndex + 16);
            if (diskNumber == ZIP_UINT16_MAX || directoryDisk == ZIP_UINT16_MAX
                    || entriesOnDisk == ZIP_UINT16_MAX || totalEntries == ZIP_UINT16_MAX
                    || directorySize == ZIP_UINT32_MAX || directoryOffset == ZIP_UINT32_MAX
                    || diskNumber != 0 || directoryDisk != 0 || entriesOnDisk != totalEntries
                    || totalEntries > MAX_ZIP_ENTRIES
                    || directorySize > MAX_ZIP_CENTRAL_DIRECTORY_SIZE) {
                return false;
            }
            long eocdOffset = tailOffset + eocdIndex;
            long directoryEnd = Math.addExact(directoryOffset, directorySize);
            if (directoryEnd != eocdOffset || directoryEnd > archiveSize) {
                return false;
            }
            return hasExpectedCentralDirectoryEntries(
                    channel, directoryOffset, directoryEnd, totalEntries);
        } catch (IOException | ArithmeticException ignored) {
            return false;
        }
    }

    /**
     * 有界扫描中央目录头，防止伪造较小 EOCD 条目数绕过预检。
     *
     * @param channel ZIP 文件通道
     * @param position 中央目录起点
     * @param end 中央目录终点
     * @param expectedEntries EOCD 声明条目数
     * @return 实际结构和数量一致时为 true
     * @throws IOException 读取失败或文件截断时抛出
     */
    private boolean hasExpectedCentralDirectoryEntries(SeekableByteChannel channel,
                                                       long position,
                                                       long end,
                                                       int expectedEntries) throws IOException {
        int actualEntries = 0;
        while (position < end) {
            if (end - position < ZIP_CENTRAL_DIRECTORY_HEADER_SIZE) {
                return false;
            }
            ByteBuffer header = readZipAt(channel, position, ZIP_CENTRAL_DIRECTORY_HEADER_SIZE);
            if (header.getInt(0) != ZIP_CENTRAL_DIRECTORY_SIGNATURE) {
                return false;
            }
            long variableSize = (long) unsignedZipShort(header, 28)
                    + unsignedZipShort(header, 30)
                    + unsignedZipShort(header, 32);
            position = Math.addExact(position,
                    Math.addExact((long) ZIP_CENTRAL_DIRECTORY_HEADER_SIZE, variableSize));
            if (position > end || ++actualEntries > MAX_ZIP_ENTRIES) {
                return false;
            }
        }
        return position == end && actualEntries == expectedEntries;
    }

    /**
     * 在文件尾缓冲区中定位与注释长度一致的 EOCD。
     *
     * @param tail ZIP 文件尾缓冲区
     * @return EOCD 相对偏移，未找到时返回 -1
     */
    private int findZipEocd(ByteBuffer tail) {
        for (int index = tail.limit() - ZIP_EOCD_MIN_SIZE; index >= 0; index--) {
            if (tail.getInt(index) == ZIP_EOCD_SIGNATURE) {
                int commentLength = unsignedZipShort(tail, index + 20);
                if (index + ZIP_EOCD_MIN_SIZE + commentLength == tail.limit()) {
                    return index;
                }
            }
        }
        return -1;
    }

    /**
     * 从通道指定位置完整读取固定长度的小端序数据。
     *
     * @param channel ZIP 文件通道
     * @param position 起始偏移
     * @param length 读取长度
     * @return 已翻转的小端序缓冲区
     * @throws IOException 读取失败或文件截断时抛出
     */
    private ByteBuffer readZipAt(SeekableByteChannel channel, long position, int length) throws IOException {
        if (position < 0L || length < 0 || position > channel.size() - length) {
            throw new IOException("ZIP record exceeds archive bounds");
        }
        ByteBuffer buffer = ByteBuffer.allocate(length).order(ByteOrder.LITTLE_ENDIAN);
        channel.position(position);
        while (buffer.hasRemaining()) {
            if (channel.read(buffer) <= 0) {
                throw new IOException("ZIP record is truncated");
            }
        }
        buffer.flip();
        return buffer;
    }

    /**
     * 读取小端序无符号 16 位整数。
     *
     * @param buffer 来源缓冲区
     * @param offset 字段偏移
     * @return 无符号整数值
     */
    private int unsignedZipShort(ByteBuffer buffer, int offset) {
        return Short.toUnsignedInt(buffer.getShort(offset));
    }

    /**
     * 读取小端序无符号 32 位整数。
     *
     * @param buffer 来源缓冲区
     * @param offset 字段偏移
     * @return 无符号长整数值
     */
    private long unsignedZipInt(ByteBuffer buffer, int offset) {
        return Integer.toUnsignedLong(buffer.getInt(offset));
    }

    /**
     * 仅依据 central directory 元数据识别可能造成过量展开的 ZIP 条目。
     *
     * @param entry ZIP 条目元数据
     * @param accumulatedSize 已累计声明展开大小
     * @return 条目超出结构探测安全边界时为 true
     */
    private boolean isSuspiciousZipEntry(ZipEntry entry, long accumulatedSize) {
        String name = entry.getName();
        long size = entry.getSize();
        long compressedSize = entry.getCompressedSize();
        if (name == null || name.length() > MAX_ZIP_ENTRY_NAME_LENGTH
                || size < 0L || compressedSize < 0L
                || size > MAX_ZIP_DECLARED_SIZE - accumulatedSize) {
            return true;
        }
        if (size == 0L) {
            return false;
        }
        return compressedSize == 0L
                || (double) size / (double) compressedSize > MAX_ZIP_COMPRESSION_RATIO;
    }

    private void verifyStoredObject(String objectKey, long expectedSize, String expectedSha256) throws IOException {
        AgentArtifactObjectStorage.StoredObjectMetadata metadata = objectStorage.stat(objectKey);
        if (metadata == null || metadata.size() != expectedSize) {
            throw new AgentArtifactOperationException(
                    "ARTIFACT_STORAGE_VERIFY_FAILED", "产物对象大小校验失败", true);
        }
        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException error) {
            throw new IllegalStateException("SHA-256 算法不可用", error);
        }
        long actualSize = 0L;
        byte[] buffer = new byte[8192];
        try (InputStream raw = objectStorage.open(objectKey);
             DigestInputStream input = new DigestInputStream(raw, digest)) {
            int read;
            while ((read = input.read(buffer)) != -1) {
                if (read > 0) {
                    actualSize += read;
                }
            }
        }
        String actualSha256 = HexFormat.of().formatHex(digest.digest());
        if (actualSize != expectedSize || !expectedSha256.equals(actualSha256)) {
            throw new AgentArtifactOperationException(
                    "ARTIFACT_STORAGE_VERIFY_FAILED", "产物对象内容校验失败", true);
        }
    }

    private boolean startsWith(byte[] source, int length, byte[] prefix) {
        if (length < prefix.length) {
            return false;
        }
        for (int index = 0; index < prefix.length; index++) {
            if (source[index] != prefix[index]) {
                return false;
            }
        }
        return true;
    }

    private boolean isSafeUtf8Text(byte[] source, int length) {
        for (int index = 0; index < length; index++) {
            int value = source[index] & 0xFF;
            if (value == 0 || value < 0x09 || value > 0x0D && value < 0x20) {
                return false;
            }
        }
        String decoded = new String(source, 0, length, StandardCharsets.UTF_8);
        return !decoded.contains("\uFFFD");
    }

    private String safeFileName(String requested, String fallback) {
        String value = StringUtils.hasText(requested) ? requested.trim() : fallback;
        value = value.replaceAll("[\\r\\n\\u0000-\\u001f\\u007f]", "_");
        if (value.contains("/") || value.contains("\\\\") || ".".equals(value) || "..".equals(value)) {
            throw new AgentArtifactOperationException("ARTIFACT_PUBLISH_FAILED", "产物文件名不合法", false);
        }
        if (value.length() > MAX_FILE_NAME_LENGTH) {
            value = value.substring(0, MAX_FILE_NAME_LENGTH);
        }
        return value;
    }

    private String safeExtension(String fileName) {
        int dot = fileName.lastIndexOf('.');
        if (dot < 0 || dot == fileName.length() - 1) {
            return "";
        }
        String extension = fileName.substring(dot).toLowerCase(Locale.ROOT);
        return extension.matches("\\.[a-z0-9]{1,16}") ? extension : "";
    }

    private BigInteger positiveId(String value) {
        BigInteger id = new BigInteger(value);
        if (id.signum() <= 0) {
            throw new IllegalArgumentException();
        }
        return id;
    }

    private Integer positiveInteger(Object value) {
        int number = value instanceof Number numeric
                ? numeric.intValue() : Integer.parseInt(String.valueOf(value));
        if (number <= 0) {
            throw new IllegalArgumentException();
        }
        return number;
    }

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

    private record ToolIdentity(BigInteger tenantId,
                                BigInteger userId,
                                BigInteger agentId,
                                String runtimeSessionId,
                                BigInteger chatSessionId,
                                BigInteger roundId,
                                Integer variantIndex,
                                String mode) {
    }
}
