import { Config, Inject, Init, Logger, OnWSConnection, OnWSMessage, WSController } from '@midwayjs/core';
import { ILogger } from '@midwayjs/logger';
import { Context } from '@midwayjs/socketio';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { randomUUID } from 'crypto';
import { NetaClawSessionService } from './session.js';
import { ChatAttachment, ClientMessage, ServerEvent } from './protocol.js';
import { runAgent, AgentConfig } from '../runtime/agent.js';
import { TodoStore } from '../runtime/todo_store.js';
import { getModelMaxTokens } from '../runtime/thinking.js';
import { initDefaultProviders } from '../runtime/model_selection.js';
import { NetaClawAgentService } from '../service/agent.js';
import { SkillLoaderService } from '../service/skill_loader.js';
import { NetaClawModelChannelService } from '../service/model_channel.js';
import { NetaClawMemoryEntity } from '../entity/memory.js';
import { NetaClawMemoryTypeEntity } from '../entity/memory_type.js';
import { AgentMemoryConfig } from '../memory/provider.js';
import { createMemoryProvider } from '../memory/factory.js';
import { buildSystemPrompt } from '../runtime/prompt_builder.js';
import { CompactionReason, CompactionService } from '../runtime/compaction.js';
import { estimateHistoryTokens } from '../runtime/token_utils.js';
import { NetaClawSessionEntity } from '../entity/session.js';
import { NetaClawAgentEntity } from '../entity/agent.js';
import type { NetaClawAgentRuntimeConfig } from '../entity/agent.js';
import {
  NetaClawToolResolverService,
  SessionSubagentDelegationContext,
} from '../service/tool_resolver.js';
import {
  ChatOrchestratorSubagentContext,
  ChatOrchestratorSubagentRecord,
  ChatOrchestratorSubagentTaskDescriptor,
  NetaClawChatOrchestratorService,
} from '../service/chat_orchestrator.js';
import {
  NetaClawSubagentRunResult,
  NetaClawSubagentService,
  NetaClawSubagentTask,
} from '../service/subagent.js';
import { injectToolRuntimeContext } from '../tools/runtime_context.js';
import type { ResolvedToolRuntimeRoute } from '../service/tool_resolver.js';
import { detectToolRisk } from './tool_risk.js';
import {
  ProcessEventBuffer,
  RuntimeProcessEvent,
  appendRuntimeProcessLog,
  buildProcessLogRef,
} from '../runtime/process_events.js';
import {
  ToolExecutionEntry,
  buildToolExecutionMetadata,
  buildToolRouteMap,
  toStructuredToolResult,
} from '../runtime/tool_execution_metadata.js';

const clarifyResolvers = new Map<string, { resolve: (answer: string) => void }>();
const toolConfirmResolvers = new Map<string, { resolve: (confirmed: boolean) => void }>();

@WSController('/netaclaw')
export class NetaClawGateway {
  @Config('netaclaw.subagent')
  subagentDefaults: {
    enabled?: boolean;
    maxConcurrent?: number;
    allowedToolNames?: string[];
  };

  @Inject()
  ctx: Context;

  @Logger()
  logger: ILogger;

  @Inject()
  sessionService: NetaClawSessionService;

  @Inject()
  agentService: NetaClawAgentService;

  @Inject()
  skillLoader: SkillLoaderService;

  @Inject()
  channelService: NetaClawModelChannelService;

  @Inject()
  compactionService: CompactionService;

  @Inject()
  toolResolver: NetaClawToolResolverService;

  @Inject()
  chatOrchestrator: NetaClawChatOrchestratorService;

  @Inject()
  subagentService: NetaClawSubagentService;

  @InjectEntityModel(NetaClawMemoryEntity)
  memoryRepo: Repository<NetaClawMemoryEntity>;

  @InjectEntityModel(NetaClawMemoryTypeEntity)
  memoryTypeRepo: Repository<NetaClawMemoryTypeEntity>;

  @InjectEntityModel(NetaClawSessionEntity)
  sessionRepo: Repository<NetaClawSessionEntity>;

  private sessionState = new Map<string, {
    thinkLevel?: string;
    todoStore?: TodoStore;
    compactionInFlight?: boolean;
    drainingPendingChat?: boolean;
    pendingChat?: Array<{ content: string; agentName?: string; agentId?: number }>;
  }>();

  @Init()
  async init() {
    initDefaultProviders();
  }

  @OnWSConnection()
  async onConnect() {
    this.logger.info('[NetaClaw] 客户端已连接: %s', this.ctx.id);
  }

  @OnWSMessage('message')
  async onMessage(data: string) {
    let msg: ClientMessage;
    try {
      msg = JSON.parse(data);
    } catch {
      this.send({ type: 'error', sessionId: '', message: '无效的 JSON 消息' });
      return;
    }

    if (msg.type === 'ping') {
      this.send({ type: 'pong' });
      return;
    }

    if (msg.type === 'chat') {
      const slashCmd = this.parseSlashCommand(msg.content);
      if (slashCmd && slashCmd.command === 'compact') {
        await this.handleCompact(msg.sessionId, 'manual');
        return;
      }
      await this.handleChat(msg.sessionId, msg.content, msg.agentName, msg.agentId, msg.attachments);
      return;
    }

    if (msg.type === 'compact') {
      await this.handleCompact(msg.sessionId, 'manual');
      return;
    }

    if (msg.type === 'set_thinking_level') {
      const state = this.sessionState.get(msg.sessionId) ?? {};
      state.thinkLevel = msg.level;
      this.sessionState.set(msg.sessionId, state);
      this.logger.info('[NetaClaw] 设置思考级别: sessionId=%s, level=%s', msg.sessionId, msg.level);
      return;
    }

    if (msg.type === 'clarify_response') {
      const entry = clarifyResolvers.get(msg.requestId);
      if (entry) {
        clarifyResolvers.delete(msg.requestId);
        entry.resolve(msg.answer);
      }
    }

    if (msg.type === 'tool_confirmation_response') {
      const entry = toolConfirmResolvers.get(msg.requestId);
      if (entry) {
        toolConfirmResolvers.delete(msg.requestId);
        entry.resolve(msg.confirmed);
      }
    }
  }

  private parseSlashCommand(content: string): { command: string; args: string } | null {
    const trimmed = content.trim();
    if (!trimmed.startsWith('/')) return null;
    const spaceIdx = trimmed.indexOf(' ');
    if (spaceIdx === -1) return { command: trimmed.slice(1).toLowerCase(), args: '' };
    return { command: trimmed.slice(1, spaceIdx).toLowerCase(), args: trimmed.slice(spaceIdx + 1) };
  }

  private async handleCompact(
    sessionId: string,
    reason: CompactionReason,
    agentInfo?: NetaClawAgentEntity,
    drainPending = true,
  ) {
    const state = this.sessionState.get(sessionId) ?? {};
    this.sessionState.set(sessionId, state);

    if (state.compactionInFlight) {
      this.logger.warn('[NetaClaw] 压缩已在进行中，跳过: sessionId=%s', sessionId);
      return;
    }
    state.compactionInFlight = true;

    try {
      const historyWithId = await this.sessionService.loadHistoryWithId(sessionId);
      if (historyWithId.length === 0) {
        this.send({ type: 'compaction_error', sessionId, data: { message: '没有可压缩的历史消息', retryable: false } });
        return;
      }

      if (!agentInfo) {
        const session = await this.sessionRepo.findOneBy({ sessionId });
        if (session?.agentId) {
          agentInfo = await this.agentService.info(session.agentId);
        }
      }

      const mc = (agentInfo?.modelConfig || {}) as any;
      const model = mc.modelId
        ? `openai:${mc.modelId}`
        : (process.env.NETACLAW_MODEL ?? 'anthropic:claude-sonnet-4-20250514');
      const maxTokens = getModelMaxTokens(model);
      const threshold = agentInfo?.compactionThreshold ?? 70;
      const thresholdTokens = Math.floor(maxTokens * threshold / 100);
      const auxChannelId = agentInfo?.auxiliaryModelChannelId ?? undefined;

      this.send({ type: 'compaction_start', sessionId, data: { reason, messageCount: historyWithId.length } });

      const result = await this.compactionService.compact(
        sessionId,
        historyWithId,
        thresholdTokens,
        auxChannelId,
        reason,
      );

      if (result.success) {
        await this.sessionService.updateSessionMetadata(sessionId, {
          hasCompactedView: true,
          compactionSummaryMessageId: result.summaryMessageId,
          lastCompactionAt: new Date().toISOString(),
        });
        this.send({
          type: 'compaction_done',
          sessionId,
          data: {
            success: result.success,
            compactedCount: result.compactedCount,
            keepRecentCount: result.keepRecentCount,
            summaryMessageId: result.summaryMessageId,
            summaryContent: result.summaryContent,
            tokensBefore: result.tokensBefore,
            tokensAfter: result.tokensAfter,
          },
        });
      } else {
        this.send({
          type: 'compaction_error',
          sessionId,
          data: { message: result.errorMessage ?? '压缩失败', retryable: false },
        });
      }

      this.logger.info(
        '[NetaClaw] 压缩完成: reason=%s, success=%s, compacted=%d, kept=%d',
        reason,
        result.success,
        result.compactedCount,
        result.keepRecentCount,
      );
    } catch (err: any) {
      this.logger.error('[NetaClaw] 压缩执行错误: %s', err.message);
      this.send({ type: 'compaction_error', sessionId, data: { message: err.message, retryable: true } });
    } finally {
      state.compactionInFlight = false;

      const pending = drainPending ? state.pendingChat : undefined;
      if (pending && pending.length > 0) {
        state.pendingChat = [];
        for (const p of pending) {
          try {
            await this.handleChat(sessionId, p.content, p.agentName, p.agentId);
          } catch (e: any) {
            this.logger.error('[NetaClaw] 处理排队消息失败: %s', e.message);
          }
        }
      }
    }
  }

  private async handleChat(sessionId: string, content: string, agentName?: string, agentId?: number, attachments?: ChatAttachment[]) {
    this.logger.info(
      '[NetaClaw] handleChat 开始: sessionId=%s, agentId=%s, content=%s',
      sessionId,
      agentId,
      content?.slice(0, 50),
    );
    const requestedAgentName = agentName ?? 'default';
    const sid = await this.sessionService.getOrCreateSession(sessionId, requestedAgentName, agentId);

    const sessionSt = this.sessionState.get(sid) ?? {};
    this.sessionState.set(sid, sessionSt);
    if (sessionSt.compactionInFlight) {
      if (!sessionSt.pendingChat) sessionSt.pendingChat = [];
      if (sessionSt.pendingChat.length >= 5) {
        this.logger.warn('[NetaClaw] 排队消息已达上限，丢弃: sessionId=%s', sid);
        this.send({ type: 'error', sessionId: sid, message: '压缩进行中，消息队列已满，请稍后再试' });
        return;
      }
      this.logger.info('[NetaClaw] 压缩进行中，消息排队: sessionId=%s', sid);
      sessionSt.pendingChat.push({ content, agentName, agentId });
      return;
    }

    let agentConfig: AgentConfig | undefined;
    let runtimeTools: any[] = [];
    let resolvedToolNames: string[] | undefined;
    let supportThinking = false;
    let supportThinkingLevel = false;
    let resolvedAgentInfo: NetaClawAgentEntity | undefined;
    let resolvedModelCapability: any = 'text';
    let memoryConfig: AgentMemoryConfig | undefined;
    let memoryTypes: Array<{ key: string; name: string; description: string }> | undefined;
    let hasExistingMemory = false;

    if (agentId) {
      const agentInfo = await this.agentService.info(agentId);
      if (agentInfo) {
        resolvedAgentInfo = agentInfo;
        const mc = (agentInfo.modelConfig || {}) as any;
        const cfg: NetaClawAgentRuntimeConfig = agentInfo.config || {};
        let model = mc.modelId
          ? `openai:${mc.modelId}`
          : (process.env.NETACLAW_MODEL ?? 'anthropic:claude-sonnet-4-20250514');
        let apiKey = mc.apiKey || process.env.NETACLAW_API_KEY || '';
        let baseUrl = mc.apiUrl;
        let modelCapability: any = 'text';
        let channelId: number | undefined;
        let channelName: string | undefined;
        let channelSupplier: string | undefined;

        if (mc.channelId && mc.channelId > 0) {
          try {
            const resolved = await this.channelService.resolveForAgent(mc.channelId, mc.modelId || '');
            model = `${resolved.provider}:${resolved.model}`;
            apiKey = resolved.apiKey;
            baseUrl = resolved.baseUrl;
            modelCapability = resolved.capability;
            channelId = resolved.channelId;
            channelName = resolved.channelName;
            channelSupplier = resolved.channelSupplier;
            supportThinking = resolved.supportThinking;
            supportThinkingLevel = resolved.supportThinkingLevel;
            resolvedModelCapability = resolved.capability;
            this.logger.info(
              '[NetaClaw] 渠道解析成功: channelId=%s, channelName=%s, supplier=%s, provider=%s, model=%s, supportThinking=%s, supportThinkingLevel=%s, hasApiKey=%s',
              mc.channelId,
              resolved.channelName,
              resolved.channelSupplier,
              resolved.provider,
              resolved.model,
              supportThinking,
              supportThinkingLevel,
              !!resolved.apiKey,
            );
          } catch (e: any) {
            this.logger.warn('[NetaClaw] 渠道解析失败: %s', e.message);
          }
        }

        resolvedModelCapability = modelCapability;
        memoryConfig = cfg?.memory;
        if (memoryConfig?.enabled) {
          const provider = createMemoryProvider(memoryConfig, this.memoryRepo);
          const userId = 'anonymous';
          const existingCount = await provider.count({ agentName: agentInfo.name, userId });
          hasExistingMemory = existingCount > 0;
          try {
            const types = await this.memoryTypeRepo.find();
            if (types.length > 0) {
              memoryTypes = types.map(t => ({ key: t.key, name: t.name, description: t.description }));
            }
          } catch { /* fallback: prompt_guidance 会用内置类型 */ }
        }

        const resolvedTools = await this.toolResolver.resolve({
          agent: agentInfo,
          modelCapability,
          memoryEnabled: !!memoryConfig?.enabled,
          hasSkills: !!agentInfo.skills?.length,
          userId: 'anonymous',
        });
        runtimeTools = resolvedTools.tools;
        resolvedToolNames = resolvedTools.toolNames;

        const { prompt: systemPrompt } = buildSystemPrompt({
          agentSystemPrompt: agentInfo.systemPrompt || '',
          modelId: mc.modelId || '',
          availableToolNames: resolvedTools.toolNames,
          skills: agentInfo.skills || [],
          skillLoader: this.skillLoader,
          memoryEnabled: !!memoryConfig?.enabled,
          memoryTypes,
          hasExistingMemory,
          toolPromptHints: resolvedTools.toolPromptHints,
        });

        agentConfig = {
          name: agentInfo.name,
          systemPrompt,
          model,
          apiKey,
          baseUrl,
          channelId,
          channelName,
          channelSupplier,
          maxToolRounds: cfg?.middleware?.maxToolRounds ?? cfg?.middleware?.maxToolCalls ?? 20,
          temperature: cfg?.temperature,
          maxTokens: cfg?.maxTokens,
          defaultThinkLevel: cfg?.thinking?.defaultLevel,
        };
      }
    }

    this.logger.info(
      '[NetaClaw] agentConfig 构建完成: model=%s, baseUrl=%s, hasApiKey=%s',
      agentConfig?.model,
      agentConfig?.baseUrl,
      !!agentConfig?.apiKey,
    );

    if (!agentConfig) {
      agentConfig = {
        name: requestedAgentName,
        systemPrompt: '你是 NetaClaw 电商运营助手，帮助用户管理电商平台的商品、订单和营销活动。',
        model: process.env.NETACLAW_MODEL ?? 'anthropic:claude-sonnet-4-20250514',
        apiKey: process.env.NETACLAW_API_KEY ?? '',
        maxToolRounds: 20,
      };
    }

    try {
      this.logger.info('[NetaClaw] 开始调用 runAgent...');
      let thinkingText = '';
      const toolExecutions: ToolExecutionEntry[] = [];
      let processEventBuffer: ProcessEventBuffer | undefined;
      const failRunningTools = (message: string, assistantEntryId?: string) => {
        for (const te of toolExecutions) {
          if (te.status !== 'running') continue;
          te.status = 'error';
          te.result = te.result || message;
          const failedEvent: RuntimeProcessEvent = {
            version: 1,
            operationId: `tool-${te.toolCallId}`,
            targetType: 'tool',
            source: 'runtime',
            status: 'failed',
            level: 'error',
            message,
            timestamp: new Date().toISOString(),
            sequence: (te.processEvents?.length || 0) + 1,
          };
          te.processEvents = [...(te.processEvents || []), failedEvent].slice(-50);
          te.processLogRef = buildProcessLogRef({
            sessionId: sid,
            operationId: failedEvent.operationId,
            toolCallId: te.toolCallId,
          });
          processEventBuffer?.push(failedEvent);
          this.send({
            type: 'tool_call_completed',
            sessionId: sid,
            data: {
                    toolCallId: te.toolCallId,
                    assistantEntryId,
                    toolName: te.name,
                    toolLabel: te.label,
                    result: te.result,
                    status: 'error',
                  },
                });
        }
      };

      const state = this.sessionState.get(sid) ?? {};
      const todoStore = state.todoStore ?? new TodoStore();
      state.todoStore = todoStore;
      this.sessionState.set(sid, state);
      let history: any[] = [];

      const orchestrated = await this.chatOrchestrator.executeSessionTreeChat({
        sessionId: sid,
        requestedAgentName,
        message: content,
        userMessageMetadata: attachments?.length ? { attachments } : undefined,
        agentId,
        cwd: resolvedAgentInfo?.config?.session?.cwd,
        realtime: {
          onEntryAdded: event => this.send({
            type: 'session_entry_added',
            sessionId: event.sessionId,
            data: {
              entry: event.entry,
              session: event.session,
            },
          }),
          onEntryUpdated: event => this.send({
            type: 'session_entry_updated',
            sessionId: event.sessionId,
            data: {
              entry: event.entry,
              session: event.session,
            },
          }),
          onLeafChanged: event => this.send({
            type: 'session_leaf_changed',
            sessionId: event.sessionId,
            data: {
              leafEntryId: event.leafEntryId,
              session: event.session,
            },
          }),
          onAssistantStreamStart: event => this.send({
            type: 'assistant_stream_start',
            sessionId: event.sessionId,
            data: {
              assistantEntryId: event.assistantEntryId,
            },
          }),
          onAssistantStreamDelta: event => this.send({
            type: 'assistant_stream_delta',
            sessionId: event.sessionId,
            data: {
              assistantEntryId: event.assistantEntryId,
              text: event.text,
            },
          }),
          onAssistantStreamEnd: event => this.send({
            type: 'assistant_stream_end',
            sessionId: event.sessionId,
            data: {
              assistantEntryId: event.assistantEntryId,
            },
          }),
        },
        runner: async ({ history: preparedHistory, assistantEntryId, subagents, onAssistantDelta }) => {
          history = preparedHistory;
          processEventBuffer = new ProcessEventBuffer(event => {
            const te = toolExecutions.find(item => `tool-${item.toolCallId}` === event.operationId);
            const toolCallId = te?.toolCallId || event.operationId.replace(/^tool-/, '');
            this.send({
              type: 'tool_process_event',
              sessionId: sid,
              data: {
                ...event,
                toolCallId,
                assistantEntryId,
                toolName: te?.name || 'unknown',
                toolLabel: te?.label,
                sequence: event.sequence || 0,
              },
            });
          });
          const runtimeSnapshot = await this.sessionService.getRuntimeSnapshot(sid, agentId);
          const sessionCwd =
            typeof runtimeSnapshot.session?.cwd === 'string' && runtimeSnapshot.session.cwd.trim().length > 0
              ? runtimeSnapshot.session.cwd.trim()
              : null;
          const workspaceRoots = sessionCwd ? [sessionCwd] : [];
          const runtimeUserId = Number(runtimeSnapshot.session?.userId);
          const auditUserId = Number.isFinite(runtimeUserId) ? runtimeUserId : undefined;
          let effectiveTools = runtimeTools;
          let effectiveToolNames = resolvedToolNames;
          let effectiveAgentConfig = agentConfig!;
          let effectiveToolManifest: ResolvedToolRuntimeRoute[] = [];

          if (this.isSubagentEnabled(resolvedAgentInfo)) {
            const subagentContext = await this.createSessionSubagentContext({
              sessionId: sid,
              assistantMessageId: 0,
              parentAgent: resolvedAgentInfo,
              parentModelCapability: resolvedModelCapability,
              agentConfig: effectiveAgentConfig,
              subagents,
            });
            const resolvedTools = await this.toolResolver.resolve({
              agent: resolvedAgentInfo,
              modelCapability: resolvedModelCapability,
              memoryEnabled: !!memoryConfig?.enabled,
              hasSkills: !!resolvedAgentInfo.skills?.length,
              userId: 'anonymous',
              delegationRole: 'supervisor',
              subagentContext,
              runtimePolicy: {
                sessionCwd,
                workspaceRoots,
              },
            });
            effectiveTools = resolvedTools.tools;
            effectiveToolNames = resolvedTools.toolNames;
            effectiveToolManifest = resolvedTools.toolRuntimeRoutes || [];

            const { prompt: systemPrompt } = buildSystemPrompt({
              agentSystemPrompt: resolvedAgentInfo.systemPrompt || '',
              modelId: ((resolvedAgentInfo.modelConfig || {}) as any).modelId || '',
              availableToolNames: resolvedTools.toolNames,
              skills: resolvedAgentInfo.skills || [],
              skillLoader: this.skillLoader,
              memoryEnabled: !!memoryConfig?.enabled,
              memoryTypes,
              hasExistingMemory,
              toolPromptHints: resolvedTools.toolPromptHints,
            });
            effectiveAgentConfig = {
              ...effectiveAgentConfig,
              systemPrompt,
            };
          }

          let result;
          try {
            result = await runAgent({
              agentConfig: effectiveAgentConfig,
              tools: effectiveTools,
              toolNames: effectiveToolNames,
              userMessage: content,
              userMessageMetadata: attachments?.length ? { attachments } : undefined,
              history: preparedHistory,
              sessionThinkLevel: supportThinking ? (state.thinkLevel as any) : 'off',
              todoStore,
              onTodoUpdate: data => {
                void this.sessionService.updateSessionMetadata(sid, { todoState: data });
                this.send({ type: 'todo_update', sessionId: sid, data });
              },
              onThinkingDelta: text => {
                thinkingText += text;
                this.send({ type: 'thinking_delta', sessionId: sid, data: { text } });
              },
              onToken: text => {
                onAssistantDelta?.(text);
                this.send({ type: 'token', sessionId: sid, content: text });
              },
              onThinking: text => {
                thinkingText += text;
                this.send({ type: 'thinking_delta', sessionId: sid, data: { text } });
              },
              beforeToolCall: async ({ label, name, args, toolCallId }) => {
                const runtimeArgs = injectToolRuntimeContext(args, {
                  sessionCwd,
                  workspaceRoots,
                  userId: auditUserId,
                  currentAgent: resolvedAgentInfo ? {
                    id: resolvedAgentInfo.id,
                    name: resolvedAgentInfo.name ?? '',
                    modelChannelId: (resolvedAgentInfo.modelConfig as any)?.channelId ?? resolvedAgentInfo.auxiliaryModelChannelId ?? null,
                    toolsets: resolvedAgentInfo.toolsets ?? [],
                  } : undefined,
                });
                const projectionId = toolCallId || randomUUID();
                const toolRouteMap = buildToolRouteMap(effectiveToolManifest);
                const routeMeta = toolRouteMap.get(name);
                toolExecutions.push({
                  toolCallId: projectionId,
                  name,
                  label: label || name,
                  args: runtimeArgs,
                  status: 'running',
                  runtimeRoute: routeMeta?.runtimeRoute,
                  workerRoutingHint: routeMeta?.workerRoutingHint,
                  blockedReason: routeMeta?.blockedReason ?? null,
                  policySources: routeMeta?.policySources ?? ['toolManifest'],
                });
                this.send({ type: 'tool_call', sessionId: sid, toolName: name, toolLabel: label || name, args: runtimeArgs, toolCallId: projectionId } as any);
                this.send({
                  type: 'tool_call_started',
                  sessionId: sid,
                  data: {
                    toolCallId: projectionId,
                    assistantEntryId,
                    toolName: name,
                    toolLabel: label || name,
                    args: runtimeArgs,
                    status: 'running',
                    runtimeRoute: routeMeta?.runtimeRoute,
                    workerRoutingHint: routeMeta?.workerRoutingHint,
                    blockedReason: routeMeta?.blockedReason ?? null,
                    policySources: routeMeta?.policySources ?? ['toolManifest'],
                  },
                });

                const risk = detectToolRisk(name, runtimeArgs);
                if (risk) {
                  const confirmId = randomUUID();
                  this.send({
                    type: 'tool_confirmation_request',
                    sessionId: sid,
                    data: { requestId: confirmId, toolName: name, toolLabel: label || name, args: runtimeArgs, riskReason: risk },
                  } as any);
                  const confirmed = await new Promise<boolean>(resolve => {
                    toolConfirmResolvers.set(confirmId, { resolve });
                  });
                  if (!confirmed) {
                    return { block: true, reason: '用户拒绝了此操作' };
                  }
                }

                return { args: runtimeArgs };
              },
              onToolCall: (name, args, toolCallId) => {
                const te = [...toolExecutions].reverse().find(t => {
                  if (toolCallId) return t.toolCallId === toolCallId;
                  return t.name === name && t.status === 'running';
                });
                if (te) {
                  te.args = args;
                }
              },
              onToolProcessEvent: ({ toolCallId, operationId, toolName, event }) => {
                const projectionId = toolCallId || operationId.replace(/^tool-/, '');
                const te = [...toolExecutions].reverse().find(t => t.toolCallId === projectionId)
                  || [...toolExecutions].reverse().find(t => t.name === toolName && t.status === 'running');
                const processEvent: RuntimeProcessEvent = {
                  ...event,
                  sequence: (te?.processEvents?.length || 0) + 1,
                };
                if (te) {
                  te.processEvents = [...(te.processEvents || []), processEvent].slice(-50);
                  te.processLogRef = appendRuntimeProcessLog({
                    sessionId: sid,
                    operationId: processEvent.operationId,
                    toolCallId: te.toolCallId,
                    event: processEvent,
                  });
                }
                processEventBuffer?.push(processEvent);
              },
              afterToolCall: async ({ label, name, result: res, rawResult, toolCallId, isError }) => {
                const te = [...toolExecutions].reverse().find(t => {
                  if (toolCallId) return t.toolCallId === toolCallId;
                  return t.name === name && t.status === 'running';
                });
                const projectionId = te?.toolCallId || toolCallId || randomUUID();
                const structured = toStructuredToolResult(rawResult);
                this.logger.info(
                  '[NetaClaw] afterToolCall: name=%s, rawResult.type=%s, structured.type=%s, hasUrl=%s, hasImages=%s',
                  name,
                  rawResult && typeof rawResult === 'object' ? (rawResult as any).type : typeof rawResult,
                  structured?.type,
                  structured?.type === 'image' ? !!structured.url : '-',
                  structured?.type === 'images' ? structured.images?.length : '-',
                );
                if (te) {
                  te.label = label || te.label || name;
                  te.result = res;
                  te.rawResult = rawResult;
                  te.status = isError ? 'error' : 'done';
                }
                this.send({
                  type: 'tool_result',
                  sessionId: sid,
                  toolName: name,
                  toolLabel: label || te?.label || name,
                  result: res,
                  rawResult: toStructuredToolResult(rawResult),
                  toolCallId: projectionId,
                } as any);
                this.send({
                  type: 'tool_result_streamed',
                  sessionId: sid,
                  data: {
                    toolCallId: projectionId,
                    assistantEntryId,
                    toolName: name,
                    toolLabel: label || te?.label || name,
                    result: res,
                    rawResult: toStructuredToolResult(rawResult),
                    runtimeRoute: (te as any)?.runtimeRoute,
                    workerRoutingHint: (te as any)?.workerRoutingHint,
                    blockedReason: (te as any)?.blockedReason ?? null,
                    policySources: (te as any)?.policySources ?? ['toolManifest'],
                  },
                });
                this.send({
                  type: 'tool_call_completed',
                  sessionId: sid,
                  data: {
                    toolCallId: projectionId,
                    assistantEntryId,
                    toolName: name,
                    toolLabel: label || te?.label || name,
                    result: res,
                    rawResult: toStructuredToolResult(rawResult),
                    status: isError ? 'error' : 'done',
                    runtimeRoute: (te as any)?.runtimeRoute,
                    workerRoutingHint: (te as any)?.workerRoutingHint,
                    blockedReason: (te as any)?.blockedReason ?? null,
                    policySources: (te as any)?.policySources ?? ['toolManifest'],
                  },
                });
              },
              onToolResult: (name, res, toolCallId, rawResult) => {
                const te = [...toolExecutions].reverse().find(t => {
                  if (toolCallId) return t.toolCallId === toolCallId;
                  return t.name === name && t.status === 'running';
                });
                if (te) {
                  te.result = res;
                  te.rawResult = rawResult;
                }
              },
              onClarifyRequest: async (question, choices) => {
                const requestId = randomUUID();
                this.send({ type: 'clarify_request', sessionId: sid, data: { requestId, question, choices } });
                return new Promise<string>(resolve => {
                  clarifyResolvers.set(requestId, { resolve });
                });
              },
            });
          } catch (err: any) {
            failRunningTools(err?.message || 'Agent execution failed', assistantEntryId);
            processEventBuffer?.flushAll();
            err.runtimeMetadata = {
              ...(err?.runtimeMetadata || {}),
              ...buildToolExecutionMetadata(toolExecutions),
            };
            throw err;
          }
          processEventBuffer?.flushAll();

          const metadata: Record<string, unknown> = {
            ...buildToolExecutionMetadata(toolExecutions),
          };
          if (attachments?.length) {
            metadata.attachments = attachments;
          }
          if (result.usage) {
            const inp = result.usage.inputTokens || 0;
            const out = result.usage.outputTokens || 0;
            metadata.tokenUsage = {
              total: { input: inp, output: out, total: inp + out },
            };
          }
          if (todoStore.hasItems()) {
            metadata.todoState = {
              todos: todoStore.read(),
              summary: todoStore.getSummary(),
            };
          }

          return {
            ...result,
            thinking: result.thinking || thinkingText || undefined,
            metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
          };
        },
      });
      const result = {
        finalContent: orchestrated.finalContent,
        usage: orchestrated.usage,
        toolCallCount: orchestrated.toolCallCount,
      };

      await this.sessionService.updateSessionMetadata(sid, {
        todoState: todoStore.hasItems()
          ? {
              todos: todoStore.read(),
              summary: todoStore.getSummary(),
            }
          : null,
      });

      if (thinkingText) {
        this.send({ type: 'thinking_done', sessionId: sid, data: {} as Record<string, never> });
      }

      if (result.usage) {
        const inp = result.usage.inputTokens || 0;
        const out = result.usage.outputTokens || 0;
        const historyTokens = estimateHistoryTokens(history);
        const usedTokens = Math.round(historyTokens + inp);
        const maxTokens = getModelMaxTokens(agentConfig.model);
        let thresholdPercent: number | undefined;
        if (agentId) {
          const agInfo = await this.agentService.info(agentId);
          if (agInfo && agInfo.compactionThreshold > 0) {
            thresholdPercent = agInfo.compactionThreshold;
          }
        }
        this.send({
          type: 'token_update',
          sessionId: sid,
          data: {
            current: {
              inputTokens: inp,
              outputTokens: out,
              totalTokens: inp + out,
              apiCalls: (result.usage as any).apiCalls || 1,
            },
            context: {
              usedTokens,
              maxTokens,
              percent: Math.min(100, Math.round((usedTokens / maxTokens) * 100)),
              thresholdPercent,
              estimated: false,
            },
          },
        });
      }

      let thinkingCapability: any;
      if (!supportThinking) {
        thinkingCapability = { supported: false, adaptive: false, levels: ['off'], defaultLevel: 'off' };
      } else if (!supportThinkingLevel) {
        thinkingCapability = { supported: true, adaptive: false, levels: ['medium'], defaultLevel: 'medium' };
      } else {
        thinkingCapability = { supported: true, adaptive: false, levels: ['off', 'low', 'medium', 'high'], defaultLevel: 'medium' };
      }

      const doneUsage = result.usage
        ? {
            inputTokens: result.usage.inputTokens || 0,
            outputTokens: result.usage.outputTokens || 0,
          }
        : undefined;
      this.send({ type: 'done', sessionId: sid, usage: doneUsage, thinkingCapability });
      this.logger.info('[NetaClaw] Agent 执行完成: tokens=%j', result.usage);
    } catch (err: any) {
      this.logger.error('[NetaClaw] Agent 执行错误: %s', err.message);
      this.send({ type: 'error', sessionId: sid, message: err.message });
    } finally {
      await this.drainPendingChats(sid);
    }
  }

  private async drainPendingChats(sessionId: string) {
    const state = this.sessionState.get(sessionId);
    if (!state || state.drainingPendingChat || state.compactionInFlight) {
      return;
    }

    state.drainingPendingChat = true;
    try {
      while (state.pendingChat && state.pendingChat.length > 0) {
        const pending = state.pendingChat;
        state.pendingChat = [];
        for (const chat of pending) {
          try {
            await this.handleChat(sessionId, chat.content, chat.agentName, chat.agentId);
          } catch (error: any) {
            this.logger.error('[NetaClaw] 处理排队消息失败: %s', error.message);
          }
        }
      }
    } finally {
      state.drainingPendingChat = false;
    }
  }

  private send(event: ServerEvent) {
    this.ctx.emit('message', JSON.stringify(event));
  }

  private async createSessionSubagentContext(params: {
    sessionId: string;
    assistantMessageId: number;
    parentAgent: NetaClawAgentEntity;
    parentModelCapability: string;
    agentConfig: AgentConfig;
    subagents: ChatOrchestratorSubagentContext;
  }): Promise<SessionSubagentDelegationContext> {
    const defaultToolNames = params.parentAgent.subagentConfig?.allowedToolNames
      ?? this.subagentDefaults?.allowedToolNames;
    const defaultMaxConcurrent = params.parentAgent.subagentConfig?.maxConcurrent
      ?? this.subagentDefaults?.maxConcurrent;
    const parentModel = {
      model: params.agentConfig.model,
      apiKey: params.agentConfig.apiKey ?? '',
      baseUrl: params.agentConfig.baseUrl,
      capability: params.parentModelCapability,
    };
    const presetAgents = await this.buildPresetMemberAgents(params.parentAgent);

    const normalizeTask = (task: NetaClawSubagentTask): NetaClawSubagentTask => ({
      ...task,
      toolNames: task.toolNames !== undefined ? task.toolNames : defaultToolNames,
    });
    const toTaskDescriptor = (task: NetaClawSubagentTask): ChatOrchestratorSubagentTaskDescriptor => ({
      goal: task.goal,
      context: task.context,
      mode: task.mode,
      agentName: task.agentName,
      agentId: task.agentId,
      toolNames: task.toolNames,
    });
    const toSubagentRecord = (result: NetaClawSubagentRunResult): ChatOrchestratorSubagentRecord => ({
      id: result.id,
      sortOrder: result.sortOrder,
      name: result.name,
      goal: result.goal,
      status: result.status,
      summary: result.summary,
      error: result.error,
    });

    const emitBatchStart = (
      batchId: string,
      mode: 'single' | 'parallel',
      tasks: ChatOrchestratorSubagentTaskDescriptor[]
    ) => {
      params.subagents.onBatchStart({
        batchId,
        sessionId: params.sessionId,
        parentMessageId: params.assistantMessageId,
        mode,
        tasks,
      });
      this.send({
        type: 'subagent_run_started',
        sessionId: params.sessionId,
        parentMessageId: params.assistantMessageId,
        data: {
          batchId,
          mode,
          tasks,
          toolRuntimeRoutes: this.filterToolRuntimeRoutesForSubagentTasks(
            (tasks[0] as any)?.toolRuntimeRoutes,
            tasks
          ),
        },
      });
    };

    let subagentEventSequence = 0;

    const emitUpdate = (
      batchId: string,
      subagent: Record<string, unknown>,
      options?: {
        kind?: 'progress' | 'result' | 'status';
        status?: 'in_flight' | 'completed' | 'failed';
      },
    ) => {
      const sequence = ++subagentEventSequence;
      const timestamp = new Date().toISOString();
      const eventPayload = {
        kind: options?.kind ?? 'result',
        status: options?.status ?? 'completed',
        subagent,
      } as const;
      params.subagents.onUpdate({
        batchId,
        sessionId: params.sessionId,
        parentMessageId: params.assistantMessageId,
        sequence,
        timestamp,
        event: eventPayload,
      });
      this.send({
        type: 'subagent_event',
        sessionId: params.sessionId,
        parentMessageId: params.assistantMessageId,
        data: {
          batchId,
          sequence,
          timestamp,
          event: eventPayload,
        },
      });
    };

    const emitDone = (batchId: string, results: Record<string, unknown>[]) => {
      params.subagents.onDone({
        batchId,
        sessionId: params.sessionId,
        parentMessageId: params.assistantMessageId,
        results,
      });
      this.send({
        type: 'subagent_run_completed',
        sessionId: params.sessionId,
        parentMessageId: params.assistantMessageId,
        data: {
          batchId,
          results,
        },
      });
    };

    return {
      kind: 'session-subagent',
      sessionId: params.sessionId,
      parentMessageId: params.assistantMessageId,
      memberAgents: presetAgents,
      runSingle: async (task, options) => {
        const normalizedTask = normalizeTask(task);
        const batchId = `single:${params.assistantMessageId}:${options?.parentToolCallId ?? randomUUID()}`;
        emitBatchStart(batchId, 'single', [toTaskDescriptor(normalizedTask)]);
        const result = await this.subagentService.runSingle({
          sessionId: params.sessionId,
          parentMessageId: params.assistantMessageId,
          parentToolCallId: options?.parentToolCallId ?? null,
          parentAgent: params.parentAgent,
          parentModel,
          task: normalizedTask,
          sortOrder: 0,
        });
        const record = toSubagentRecord(result);
        emitUpdate(batchId, record as Record<string, unknown>, {
          kind: record.status === 'failed' ? 'status' : 'result',
          status: record.status === 'failed' ? 'failed' : 'completed',
        });
        emitDone(batchId, [record as Record<string, unknown>]);
        return result;
      },
      runBatch: async (tasks, options) => {
        const normalizedTasks = tasks.map(task => normalizeTask(task));
        const batchId = `batch:${params.assistantMessageId}:${options?.parentToolCallId ?? randomUUID()}`;
        emitBatchStart(batchId, 'parallel', normalizedTasks.map(toTaskDescriptor));
        const results = await this.subagentService.runBatch({
          sessionId: params.sessionId,
          parentMessageId: params.assistantMessageId,
          parentToolCallId: options?.parentToolCallId ?? null,
          parentAgent: params.parentAgent,
          parentModel,
          tasks: normalizedTasks,
          maxConcurrent: defaultMaxConcurrent,
        });
        const records = results.map(toSubagentRecord);
        records.forEach(record => emitUpdate(batchId, record as Record<string, unknown>, {
          kind: record.status === 'failed' ? 'status' : 'result',
          status: record.status === 'failed' ? 'failed' : 'completed',
        }));
        emitDone(batchId, records as Record<string, unknown>[]);
        return results;
      },
    };
  }

  private async buildPresetMemberAgents(parentAgent: NetaClawAgentEntity) {
    const allowedIds = parentAgent.subagentConfig?.allowedPresetAgentIds;
    if (!Array.isArray(allowedIds) || allowedIds.length === 0) {
      return [];
    }

    const agents = await Promise.all(
      allowedIds.map(async id => {
        const agent = await this.agentService.info(id);
        if (!agent || agent.status !== 1) {
          return null;
        }
        return {
          id: agent.id,
          name: agent.name,
          label: agent.label,
        };
      })
    );

    return agents.filter(Boolean);
  }

  private isSubagentEnabled(agent: NetaClawAgentEntity | null | undefined): boolean {
    return agent?.subagentConfig?.enabled
      ?? this.subagentDefaults?.enabled
      ?? false;
  }

  private filterToolRuntimeRoutesForSubagentTasks(
    routes: unknown,
    tasks: ChatOrchestratorSubagentTaskDescriptor[]
  ): ResolvedToolRuntimeRoute[] {
    if (!Array.isArray(routes)) {
      return [];
    }

    const requestedToolNames = new Set<string>();
    for (const task of tasks) {
      const toolNames = Array.isArray(task.toolNames) ? task.toolNames : [];
      for (const name of toolNames) {
        if (typeof name === 'string' && name.trim().length > 0) {
          requestedToolNames.add(name.trim());
        }
      }
    }

    if (requestedToolNames.size === 0) {
      return routes as ResolvedToolRuntimeRoute[];
    }

    return (routes as ResolvedToolRuntimeRoute[]).filter(route =>
      requestedToolNames.has(route.name)
    );
  }
}
