package com.easyagents.agui;

import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
import io.agentscope.core.agui.event.AguiEvent;
import io.agentscope.core.util.JsonException;
import io.agentscope.core.util.JsonUtils;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * 将 Easy-Agents 中立运行时事件有序投影为 AG-UI 事件。
 *
 * <p>实例绑定单个 run 且非线程安全。调用方应按运行顺序串行调用 {@link #project(AgentRuntimeEvent)}。</p>
 */
public final class AguiRuntimeEventProjector {

    private static final String DEFAULT_ERROR_MESSAGE = "Agent runtime failed.";

    /** AG-UI thread ID。 */
    private final String threadId;
    /** AG-UI run ID。 */
    private final String runId;
    /** 已开始但尚未收到结果的工具调用。 */
    private final Set<String> knownToolCallIds = new LinkedHashSet<>();

    private boolean runStarted;
    private boolean terminated;
    private String openMessageId;
    private String openReasoningMessageId;
    private long generatedMessageSequence;

    /**
     * 创建不含业务 Custom Event 的投影器。
     *
     * @param threadId AG-UI thread ID
     * @param runId AG-UI run ID
     */
    public AguiRuntimeEventProjector(String threadId, String runId) {
        this.threadId = requireText(threadId, "threadId");
        this.runId = requireText(runId, "runId");
    }

    /**
     * 按输入顺序投影一条运行时事件。
     *
     * @param event Easy-Agents 运行时事件
     * @return 零到多条 AG-UI 官方或补充标准事件
     */
    public List<Object> project(AgentRuntimeEvent event) {
        if (event == null || event.getEventType() == null || terminated) {
            return List.of();
        }
        List<Object> output = new ArrayList<>();
        switch (event.getEventType()) {
            case STARTED -> startRun(output);
            case MESSAGE_DELTA -> projectMessageDelta(event, output);
            case REASONING_STARTED -> startReasoning(event, output);
            case REASONING_DELTA -> projectReasoningDelta(event, output);
            case REASONING_COMPLETED -> closeReasoning(output);
            case TOOL_CALL -> projectToolCall(event, output);
            case TOOL_RESULT -> projectToolResult(event, output);
            case COMPLETED -> finishSuccessfully(output);
            case FAILED -> finishWithError(event, "AGENT_RUNTIME_FAILED", output);
            case CANCELLED -> finishWithError(event, "RUN_CANCELLED", output);
            default -> {
                // EasyFlow 等上层业务扩展由各自协议边界映射为 CUSTOM，通用模块保持业务无关。
            }
        }
        return List.copyOf(output);
    }

    /**
     * 判断当前投影是否已经产生协议终态。
     *
     * @return 已产生 RUN_FINISHED 或 RUN_ERROR 时为 true
     */
    public boolean isTerminated() {
        return terminated;
    }

    private void startRun(List<Object> output) {
        if (!runStarted) {
            output.add(new AguiEvent.RunStarted(threadId, runId));
            runStarted = true;
        }
    }

    private void projectMessageDelta(AgentRuntimeEvent event, List<Object> output) {
        startRun(output);
        String messageId = eventMessageId(event, "assistant");
        if (!Objects.equals(openMessageId, messageId)) {
            closeMessage(output);
            output.add(new AguiEvent.TextMessageStart(threadId, runId, messageId, "assistant"));
            openMessageId = messageId;
        }
        String delta = stringValue(event.getPayload(), "text");
        if (!delta.isEmpty()) {
            output.add(new AguiEvent.TextMessageContent(threadId, runId, messageId, delta));
        }
    }

    private void startReasoning(AgentRuntimeEvent event, List<Object> output) {
        startRun(output);
        if (openReasoningMessageId != null) {
            return;
        }
        openReasoningMessageId = eventMessageId(event, "reasoning");
        output.add(new AguiEvent.ReasoningMessageStart(
                threadId, runId, openReasoningMessageId, "reasoning"));
    }

    private void projectReasoningDelta(AgentRuntimeEvent event, List<Object> output) {
        startReasoning(event, output);
        String delta = stringValue(event.getPayload(), "reasoning");
        if (!delta.isEmpty()) {
            output.add(new AguiEvent.ReasoningMessageContent(
                    threadId, runId, openReasoningMessageId, delta));
        }
    }

    private void projectToolCall(AgentRuntimeEvent event, List<Object> output) {
        startRun(output);
        String toolCallId = firstText(event.getToolCallId(), stringValue(event.getPayload(), "toolCallId"));
        if (toolCallId == null || !knownToolCallIds.add(toolCallId)) {
            return;
        }
        String toolName = firstText(
                stringValue(event.getPayload(), "toolName"),
                stringValue(event.getPayload(), "name"),
                "tool");
        output.add(new AguiEvent.ToolCallStart(threadId, runId, toolCallId, toolName));
        output.add(new AguiEvent.ToolCallArgs(
                threadId, runId, toolCallId, jsonValue(event.getPayload().get("input"))));
        output.add(new AguiEvent.ToolCallEnd(threadId, runId, toolCallId));
    }

    private void projectToolResult(AgentRuntimeEvent event, List<Object> output) {
        startRun(output);
        String toolCallId = firstText(event.getToolCallId(), stringValue(event.getPayload(), "toolCallId"));
        if (toolCallId == null || !knownToolCallIds.contains(toolCallId)) {
            return;
        }
        String messageId = eventMessageId(event, "tool-" + toolCallId);
        String content = nullToEmpty(firstText(
                stringValue(event.getPayload(), "text"),
                event.getPayload().containsKey("result")
                        ? jsonValue(event.getPayload().get("result"))
                        : null));
        output.add(new AguiEvent.ToolCallResult(
                threadId, runId, toolCallId, content, "tool", messageId));
    }

    private void finishSuccessfully(List<Object> output) {
        startRun(output);
        closeOpenFragments(output);
        output.add(new AguiEvent.RunFinished(threadId, runId));
        terminated = true;
    }

    private void finishWithError(AgentRuntimeEvent event, String code, List<Object> output) {
        startRun(output);
        closeOpenFragments(output);
        String message = firstText(
                stringValue(event.getPayload(), "message"),
                stringValue(event.getPayload(), "reason"),
                DEFAULT_ERROR_MESSAGE);
        output.add(new AguiExtendedEvent.RunError(threadId, runId, message, code));
        terminated = true;
    }

    private void closeOpenFragments(List<Object> output) {
        closeReasoning(output);
        closeMessage(output);
    }

    private void closeMessage(List<Object> output) {
        if (openMessageId != null) {
            output.add(new AguiEvent.TextMessageEnd(threadId, runId, openMessageId));
            openMessageId = null;
        }
    }

    private void closeReasoning(List<Object> output) {
        if (openReasoningMessageId != null) {
            output.add(new AguiEvent.ReasoningMessageEnd(
                    threadId, runId, openReasoningMessageId));
            openReasoningMessageId = null;
        }
    }

    private String eventMessageId(AgentRuntimeEvent event, String suffix) {
        String messageId = firstText(
                event.getMessageId(),
                event.getMessage() == null ? null : event.getMessage().getMessageId());
        if (messageId != null) {
            return messageId;
        }
        generatedMessageSequence++;
        return runId + "-" + suffix + "-" + generatedMessageSequence;
    }

    private static String stringValue(Map<String, Object> payload, String key) {
        if (payload == null) {
            return "";
        }
        Object value = payload.get(key);
        return value instanceof String text ? text : "";
    }

    private static String jsonValue(Object value) {
        if (value == null) {
            return "{}";
        }
        if (value instanceof String text) {
            return text;
        }
        try {
            return JsonUtils.getJsonCodec().toJson(value);
        } catch (JsonException exception) {
            throw new IllegalArgumentException("Failed to encode AG-UI payload", exception);
        }
    }

    private static String firstText(String... values) {
        for (String value : values) {
            if (value != null && !value.isBlank()) {
                return value;
            }
        }
        return null;
    }

    private static String requireText(String value, String name) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException(name + " cannot be blank");
        }
        return value;
    }

    private static String nullToEmpty(String value) {
        return value == null ? "" : value;
    }
}
