package com.easyagents.agent.runtime.event.observer;

import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
import com.easyagents.agent.runtime.event.AgentRuntimeObserver;
import com.easyagents.agent.runtime.skill.AgentSkillBinding;
import com.easyagents.agent.runtime.skill.AgentSkillLoadCall;
import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext;
import io.agentscope.core.hook.HookEvent;
import io.agentscope.core.hook.PostActingEvent;
import io.agentscope.core.hook.PreActingEvent;
import io.agentscope.core.message.ContentBlock;
import io.agentscope.core.message.TextBlock;
import io.agentscope.core.message.ToolResultBlock;
import io.agentscope.core.message.ToolUseBlock;
import io.agentscope.core.skill.SkillBox;
import io.agentscope.core.tool.Toolkit;
import reactor.core.publisher.Mono;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.StringJoiner;

/**
 * 监听 AgentScope 工具执行生命周期，并发射 Skill 旁路事件。
 *
 * <p>该观察器只做 Easy-Agents 的旁路监察，不修改 AgentScope {@link HookEvent}。
 * Skill 加载工具 {@link AgentSkillRuntimeContext#LOAD_SKILL_TOOL_NAME} 本身仍是
 * AgentScope 主线路中的工具调用；本观察器只把它翻译成调用方可展示的
 * {@link AgentRuntimeEventType#SKILL_CALL}、{@link AgentRuntimeEventType#SKILL_RESULT}
 * 和 {@link AgentRuntimeEventType#SKILL_FAILED}。已激活 Skill 内部的普通工具调用会
 * 被翻译成 {@link AgentRuntimeEventType#SKILL_STEP}。</p>
 *
 * <p>Skill 是否激活以 AgentScope {@link SkillBox} 和 {@link Toolkit#getActiveGroups()}
 * 为准，本地 {@link AgentSkillRuntimeContext} 只缓存旁路展示所需的归属状态。</p>
 */
public class SkillExecutionObserver implements AgentRuntimeObserver {

    private final AgentRuntimeEventBridge eventBridge;
    private final AgentSkillRuntimeContext skillContext;
    private final SkillBox skillBox;

    /**
     * 创建 Skill 执行观察器。
     *
     * @param eventBridge 旁路事件桥
     * @param skillContext Skill 运行时上下文
     */
    public SkillExecutionObserver(AgentRuntimeEventBridge eventBridge,
                                  AgentSkillRuntimeContext skillContext) {
        this(eventBridge, skillContext, null);
    }

    /**
     * 创建 Skill 执行观察器。
     *
     * @param eventBridge 旁路事件桥
     * @param skillContext Skill 运行时上下文
     * @param skillBox AgentScope SkillBox，作为 Skill 激活状态的权威来源
     */
    public SkillExecutionObserver(AgentRuntimeEventBridge eventBridge,
                                  AgentSkillRuntimeContext skillContext,
                                  SkillBox skillBox) {
        this.eventBridge = eventBridge;
        this.skillContext = skillContext;
        this.skillBox = skillBox;
    }

    /**
     * 观察 Skill 加载和已激活 Skill 内部工具执行。
     *
     * @param event AgentScope Hook 事件
     * @return 完成信号
     */
    @Override
    public Mono<Void> observe(HookEvent event) {
        if (skillContext == null) {
            return Mono.empty();
        }
        if (event instanceof PreActingEvent preActingEvent) {
            observePreActing(preActingEvent);
            return Mono.empty();
        }
        if (event instanceof PostActingEvent postActingEvent) {
            observePostActing(postActingEvent);
        }
        return Mono.empty();
    }

    private void observePreActing(PreActingEvent event) {
        ToolUseBlock toolUse = event.getToolUse();
        if (toolUse == null) {
            return;
        }
        syncSkillState(toolUse.getName(), event.getToolkit());
        if (skillContext.isSkillLoadTool(toolUse.getName())) {
            emitSkillCall(event, toolUse);
            return;
        }
        AgentSkillBinding activeBinding = skillContext.getActiveToolBinding(toolUse.getName());
        if (activeBinding != null) {
            emitSkillStepCall(toolUse, activeBinding);
        }
    }

    private void observePostActing(PostActingEvent event) {
        ToolResultBlock result = event.getToolResult();
        ToolUseBlock toolUse = event.getToolUse();
        String toolName = result == null ? toolName(toolUse) : result.getName();
        if (skillContext.isSkillLoadTool(toolName)) {
            emitSkillResult(result, toolUse, event.getToolkit());
            return;
        }
        syncSkillState(toolName, event.getToolkit());
        AgentSkillBinding activeBinding = skillContext.getActiveToolBinding(toolName);
        if (activeBinding != null) {
            emitSkillStepResult(result, toolUse, activeBinding);
        }
    }

    private void emitSkillCall(PreActingEvent event, ToolUseBlock toolUse) {
        AgentSkillLoadCall call = skillContext.rememberLoadCall(toolUse.getId(), toolUse.getInput());
        if (!skillContext.markLoadCallEmitted(toolUse.getId())) {
            return;
        }
        AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.SKILL_CALL);
        runtimeEvent.setToolCallId(toolUse.getId());
        runtimeEvent.getPayload().put("toolCallId", toolUse.getId());
        runtimeEvent.getPayload().put("toolName", toolUse.getName());
        runtimeEvent.getPayload().put("input", toolUse.getInput());
        runtimeEvent.getPayload().put("status", "RUNNING");
        runtimeEvent.getPayload().put("source", "HOOK");
        runtimeEvent.getPayload().put("phase", "PRE_ACTING");
        appendSkillLoadPayload(runtimeEvent, call);
        runtimeEvent.getMetadata().putAll(nullToEmpty(toolUse.getMetadata()));
        eventBridge.emit(runtimeEvent);
    }

    private void emitSkillResult(ToolResultBlock result, ToolUseBlock toolUse, Toolkit toolkit) {
        String toolCallId = result == null ? toolCallId(toolUse) : result.getId();
        AgentSkillLoadCall call = skillContext.removeLoadCall(toolCallId);
        boolean active = syncSkillState(call, toolkit);
        AgentRuntimeEvent runtimeEvent = eventBridge.event(skillResultType(result, active));
        runtimeEvent.setToolCallId(toolCallId);
        runtimeEvent.getPayload().put("toolCallId", toolCallId);
        runtimeEvent.getPayload().put("toolName", result == null ? toolName(toolUse) : result.getName());
        runtimeEvent.getPayload().put("text", resultText(result));
        runtimeEvent.getPayload().put("status", runtimeEvent.getEventType() == AgentRuntimeEventType.SKILL_RESULT
                ? "SUCCESS"
                : "FAILED");
        runtimeEvent.getPayload().put("success", runtimeEvent.getEventType() == AgentRuntimeEventType.SKILL_RESULT);
        runtimeEvent.getPayload().put("suspended", result != null && result.isSuspended());
        runtimeEvent.getPayload().put("active", active);
        runtimeEvent.getPayload().put("source", "HOOK");
        runtimeEvent.getPayload().put("phase", "POST_ACTING");
        if (result != null) {
            runtimeEvent.getMetadata().putAll(nullToEmpty(result.getMetadata()));
        }
        appendSkillLoadPayload(runtimeEvent, call);
        eventBridge.emit(runtimeEvent);
    }

    private void emitSkillStepCall(ToolUseBlock toolUse, AgentSkillBinding binding) {
        AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.SKILL_STEP);
        runtimeEvent.setToolCallId(toolUse.getId());
        runtimeEvent.getPayload().put("toolCallId", toolUse.getId());
        runtimeEvent.getPayload().put("name", toolUse.getName());
        runtimeEvent.getPayload().put("toolName", toolUse.getName());
        runtimeEvent.getPayload().put("input", toolUse.getInput());
        runtimeEvent.getPayload().put("content", toolUse.getContent());
        runtimeEvent.getPayload().put("stepType", "TOOL_CALL");
        runtimeEvent.getPayload().put("stepName", toolUse.getName());
        runtimeEvent.getPayload().put("status", "RUNNING");
        runtimeEvent.getPayload().put("source", "HOOK");
        runtimeEvent.getPayload().put("phase", "PRE_ACTING");
        appendSkillPayload(runtimeEvent.getPayload(), binding);
        appendSkillPayload(runtimeEvent.getMetadata(), binding);
        runtimeEvent.getMetadata().putAll(nullToEmpty(toolUse.getMetadata()));
        eventBridge.emit(runtimeEvent);
    }

    private void emitSkillStepResult(ToolResultBlock result,
                                     ToolUseBlock toolUse,
                                     AgentSkillBinding binding) {
        AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.SKILL_STEP);
        String toolCallId = result == null ? toolCallId(toolUse) : result.getId();
        String toolName = result == null ? toolName(toolUse) : result.getName();
        runtimeEvent.setToolCallId(toolCallId);
        runtimeEvent.getPayload().put("toolCallId", toolCallId);
        runtimeEvent.getPayload().put("name", toolName);
        runtimeEvent.getPayload().put("toolName", toolName);
        runtimeEvent.getPayload().put("text", resultText(result));
        runtimeEvent.getPayload().put("suspended", result != null && result.isSuspended());
        runtimeEvent.getPayload().put("stepType", "TOOL_RESULT");
        runtimeEvent.getPayload().put("stepName", toolName);
        runtimeEvent.getPayload().put("status", success(result) ? "SUCCESS" : "FAILED");
        runtimeEvent.getPayload().put("success", success(result));
        runtimeEvent.getPayload().put("source", "HOOK");
        runtimeEvent.getPayload().put("phase", "POST_ACTING");
        appendSkillPayload(runtimeEvent.getPayload(), binding);
        appendSkillPayload(runtimeEvent.getMetadata(), binding);
        if (result != null) {
            runtimeEvent.getMetadata().putAll(nullToEmpty(result.getMetadata()));
        }
        eventBridge.emit(runtimeEvent);
    }

    private AgentRuntimeEventType skillResultType(ToolResultBlock result, boolean active) {
        return success(result) && active ? AgentRuntimeEventType.SKILL_RESULT : AgentRuntimeEventType.SKILL_FAILED;
    }

    private boolean success(ToolResultBlock result) {
        if (result == null) {
            return false;
        }
        Object success = result.getMetadata() == null ? null : result.getMetadata().get("success");
        return !(success instanceof Boolean) || Boolean.TRUE.equals(success);
    }

    private String resultText(ToolResultBlock result) {
        if (result == null || result.getOutput() == null || result.getOutput().isEmpty()) {
            return "";
        }
        StringJoiner joiner = new StringJoiner("\n");
        for (ContentBlock output : result.getOutput()) {
            if (output instanceof TextBlock textBlock && textBlock.getText() != null) {
                joiner.add(textBlock.getText());
            } else if (output != null) {
                joiner.add(output.toString());
            }
        }
        return joiner.toString();
    }

    private boolean syncSkillState(AgentSkillLoadCall call, Toolkit toolkit) {
        if (call == null) {
            return false;
        }
        boolean active = isAgentScopeSkillActive(call.getSkillId(), toolkit);
        skillContext.syncSkillActive(call.getSkillId(), active);
        return active;
    }

    private void syncSkillState(String toolName, Toolkit toolkit) {
        AgentSkillBinding binding = skillContext.getToolBinding(toolName);
        if (binding == null) {
            return;
        }
        skillContext.syncSkillActive(binding.getSkillId(), isAgentScopeSkillActive(binding.getSkillId(), toolkit));
    }

    private boolean isAgentScopeSkillActive(String skillId, Toolkit toolkit) {
        if (skillId == null || skillId.isBlank()) {
            return false;
        }
        if (skillBox != null && skillBox.isSkillActive(skillId)) {
            return true;
        }
        return toolkit != null && toolkit.getActiveGroups().contains(skillToolGroupName(skillId));
    }

    private String skillToolGroupName(String skillId) {
        return skillId + "_skill_tools";
    }

    private void appendSkillLoadPayload(AgentRuntimeEvent event, AgentSkillLoadCall call) {
        if (call == null) {
            return;
        }
        event.getPayload().put("skillId", call.getSkillId());
        event.getPayload().put("skillName", call.getSkillName());
        event.getPayload().put("skillDisplayName", call.getSkillDisplayName());
        event.getPayload().put("skillBoxId", call.getSkillBoxId());
        event.getPayload().put("path", call.getPath());
        event.getMetadata().put("skillId", call.getSkillId());
        event.getMetadata().put("skillName", call.getSkillName());
        event.getMetadata().put("skillDisplayName", call.getSkillDisplayName());
        event.getMetadata().put("skillBoxId", call.getSkillBoxId());
    }

    private void appendSkillPayload(Map<String, Object> target, AgentSkillBinding binding) {
        if (binding == null || target == null) {
            return;
        }
        target.put("skillId", binding.getSkillId());
        target.put("skillName", binding.getSkillName());
        target.put("skillDisplayName", binding.getSkillDisplayName());
        target.put("skillBoxId", binding.getSkillBoxId());
    }

    private String toolCallId(ToolUseBlock toolUse) {
        return toolUse == null ? null : toolUse.getId();
    }

    private String toolName(ToolUseBlock toolUse) {
        return toolUse == null ? null : toolUse.getName();
    }

    private Map<String, Object> nullToEmpty(Map<String, Object> map) {
        return map == null ? new LinkedHashMap<>() : map;
    }
}
