import { Config, Provide, Scope, ScopeEnum, Inject } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { randomUUID } from 'crypto';
import { NetaClawSessionService } from '../gateway/session.js';
import { SkillLoaderService } from './skill_loader.js';
import { runAgent, AgentConfig } from '../runtime/agent.js';
import { AnyAgentTool, ToolResultContent } from '../tools/common.js';
import { initDefaultProviders } from '../runtime/model_selection.js';
import { NetaClawAgentService } from './agent.js';
import { NetaClawAgentEntity } from '../entity/agent.js';
import type { NetaClawAgentRuntimeConfig } from '../entity/agent.js';
import { NetaClawModelChannelService } from './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 {
  NetaClawToolResolverService,
  ResolvedToolRuntimeRoute,
  SessionSubagentDelegationContext,
} from './tool_resolver.js';
import {
  ChatOrchestratorSubagentContext,
  ChatOrchestratorSubagentRecord,
  ChatOrchestratorSubagentTaskDescriptor,
  NetaClawChatOrchestratorService,
} from './chat_orchestrator.js';
import {
  NetaClawSubagentRunResult,
  NetaClawSubagentService,
  NetaClawSubagentTask,
} from './subagent.js';
import { TodoStore } from '../runtime/todo_store.js';
import { injectToolRuntimeContext } from '../tools/runtime_context.js';
import type { NetaToolRuntimeBizContext } from '../tools/runtime_context.js';
import { detectToolRisk } from '../gateway/tool_risk.js';
import { GroupInteractionDeclineError } from '../runtime/errors.js';
import {
  RuntimeProcessEvent,
  appendRuntimeProcessLog,
} from '../runtime/process_events.js';
import {
  ToolExecutionEntry,
  buildToolExecutionMetadata,
  buildToolRouteMap,
} from '../runtime/tool_execution_metadata.js';

export interface AgentChannelRiskConfirmRequest {
  toolName: string;
  toolLabel?: string;
  args: Record<string, unknown>;
  riskReason: string;
}


@Provide()
@Scope(ScopeEnum.Singleton)
export class NetaClawAgentExecutorService {
  @Config('netaclaw.subagent')
  subagentDefaults: {
    enabled?: boolean;
    maxConcurrent?: number;
    allowedToolNames?: string[];
  };

  @Inject()
  sessionService: NetaClawSessionService;

  @Inject()
  agentService: NetaClawAgentService;

  @Inject()
  skillLoader: SkillLoaderService;

  @Inject()
  channelService: NetaClawModelChannelService;

  @Inject()
  toolResolver: NetaClawToolResolverService;

  @Inject()
  chatOrchestrator: NetaClawChatOrchestratorService;

  @Inject()
  subagentService: NetaClawSubagentService;

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

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

  protected agentRunner = runAgent;

  async execute(params: {
    sessionId?: string;
    message: string;
    agentId?: number;
    agentName?: string;
    userId?: string;
    sessionMetadata?: Record<string, unknown>;
    onToolCall?: (name: string, args: Record<string, unknown>, toolCallId?: string) => void;
    onToolResult?: (
      name: string,
      result: string,
      toolCallId?: string,
      rawResult?: ToolResultContent | string
    ) => void;
    onToolProcessEvent?: (input: {
      toolCallId?: string;
      operationId: string;
      toolName: string;
      args: Record<string, unknown>;
      event: unknown;
    }) => void;
    onClarifyRequest?: (question: string, choices?: string[]) => Promise<string>;
    onRiskConfirmRequest?: (req: AgentChannelRiskConfirmRequest) => Promise<boolean>;
    chatScope?: 'dm' | 'group';
    bizContext?: NetaToolRuntimeBizContext;
  }) {
    initDefaultProviders();

    const requestedAgentName = params.agentName ?? 'default';
    const agentEntity = params.agentId
      ? await this.agentService.info(params.agentId)
      : await this.agentService.agentRepo.findOneBy({ name: requestedAgentName });

    const agentName = agentEntity?.name || requestedAgentName;

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

    if (agentEntity) {
      const mc = (agentEntity.modelConfig || {}) as any;
      const cfg: NetaClawAgentRuntimeConfig = agentEntity.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;

      if (mc.channelId && mc.channelId > 0) {
        const resolved = await this.channelService.resolveForAgent(mc.channelId, mc.modelId || '');
        model = `${resolved.provider}:${resolved.model}`;
        apiKey = resolved.apiKey;
        baseUrl = resolved.baseUrl;
        resolvedModelCapability = resolved.capability;
      }

      // 仅检查是否已有记忆，不做静默读取
      memoryConfig = cfg.memory;
      if (memoryConfig?.enabled) {
        const provider = createMemoryProvider(memoryConfig, this.memoryRepo);
        const userId = params.userId ?? 'anonymous';
        const existingCount = await provider.count({ agentName: agentEntity.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 */ }
      }

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

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

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

    const result = await this.chatOrchestrator.executeSessionTreeChat({
      sessionId: params.sessionId,
      requestedAgentName: agentName,
      message: params.message,
      agentId: agentEntity?.id,
      userId: params.userId,
      sessionMetadata: params.sessionMetadata,
      runner: async ({ sessionId, history, subagents, userMessage }) => {
        const runtimeSnapshot = await this.sessionService.getRuntimeSnapshot(sessionId, agentEntity?.id);
        const sessionCwd =
          typeof runtimeSnapshot.session?.cwd === 'string'
          && runtimeSnapshot.session.cwd.trim().length > 0
            ? runtimeSnapshot.session.cwd.trim()
            : null;
        const workspaceRoots = sessionCwd ? [sessionCwd] : [];

        let effectiveTools = runtimeTools;
        let effectiveToolNames = resolvedToolNames;
        let effectiveAgentConfig = agentConfig;
        let effectiveToolManifest: ResolvedToolRuntimeRoute[] = [];

        if (agentEntity && this.isSubagentEnabled(agentEntity)) {
          const subagentContext = await this.createSessionSubagentContext({
            sessionId,
            assistantMessageId: 0,
            parentAgent: agentEntity,
            parentModelCapability: resolvedModelCapability,
            agentConfig,
            subagents,
            bizContext: params.bizContext,
          });
          const resolvedTools = await this.toolResolver.resolve({
            agent: agentEntity,
            modelCapability: resolvedModelCapability,
            memoryEnabled: !!memoryConfig?.enabled,
            hasSkills: !!agentEntity.skills?.length,
            userId: params.userId,
            delegationRole: 'supervisor',
            subagentContext,
            runtimePolicy: { sessionCwd, workspaceRoots },
          });

          effectiveTools = resolvedTools.tools;
          effectiveToolNames = resolvedTools.toolNames;
          effectiveToolManifest = resolvedTools.toolRuntimeRoutes || [];

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

          effectiveAgentConfig = {
            ...agentConfig,
            systemPrompt: this.withChannelBizPrompt(systemPrompt, params.bizContext),
          };
        } else if (agentEntity) {
          const resolvedTools = await this.toolResolver.resolve({
            agent: agentEntity,
            modelCapability: resolvedModelCapability,
            memoryEnabled: !!memoryConfig?.enabled,
            hasSkills: !!agentEntity.skills?.length,
            userId: params.userId,
            runtimePolicy: { sessionCwd, workspaceRoots },
          });
          effectiveTools = resolvedTools.tools;
          effectiveToolNames = resolvedTools.toolNames;
          effectiveToolManifest = resolvedTools.toolRuntimeRoutes || [];
        }

        const toolRouteMap = buildToolRouteMap(effectiveToolManifest);
        const toolExecutions: ToolExecutionEntry[] = [];
        let thinkingText = '';
        const todoStore = new TodoStore();
        effectiveAgentConfig = {
          ...effectiveAgentConfig,
          systemPrompt: this.withChannelBizPrompt(effectiveAgentConfig.systemPrompt, params.bizContext),
        };

        let runResult;
        try {
          runResult = await this.agentRunner({
            agentConfig: effectiveAgentConfig,
            tools: effectiveTools,
            toolNames: effectiveToolNames,
            userMessage,
            history,
            todoStore,
            onThinkingDelta: text => {
              thinkingText += text;
            },
            onThinking: text => {
              thinkingText += text;
            },
            beforeToolCall: async ({ label, name, args, toolCallId }) => {
              const runtimeArgs = injectToolRuntimeContext(args, {
                sessionCwd,
                workspaceRoots,
                userId: params.userId,
                bizContext: params.bizContext,
                // v4: 注入 currentAgent 让 weixin_send_text 等 tool 能拿到 modelChannelId
                currentAgent: agentEntity ? {
                  id: agentEntity.id,
                  name: agentEntity.name ?? '',
                  modelChannelId: (agentEntity.modelConfig as any)?.channelId ?? agentEntity.auxiliaryModelChannelId ?? null,
                  toolsets: agentEntity.toolsets ?? [],
                } : undefined,
              });
              const projectionId = toolCallId || randomUUID();
              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'],
              });

              const risk = detectToolRisk(name, runtimeArgs);
              if (risk) {
                if (params.chatScope === 'group') {
                  throw new GroupInteractionDeclineError(
                    `⚠️ 群内禁止高风险操作：${label || name}\n原因：${risk}\n👉 请点我头像私聊 bot 继续`,
                  );
                }
                if (!params.onRiskConfirmRequest) {
                  return { block: true, reason: `未配置风险确认通道，阻断执行：${risk}` };
                }
                const confirmed = await params.onRiskConfirmRequest({
                  toolName: name,
                  toolLabel: label,
                  args: runtimeArgs,
                  riskReason: risk,
                });
                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;
              }
              params.onToolCall?.(name, args, toolCallId);
            },
            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,
                  operationId: processEvent.operationId,
                  toolCallId: te.toolCallId,
                  event: 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';
              });
              if (te) {
                te.label = label || te.label || name;
                te.result = res;
                te.rawResult = rawResult;
                te.status = isError ? 'error' : 'done';
              }
            },
            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;
              }
              params.onToolResult?.(name, res, toolCallId, rawResult);
            },
            onClarifyRequest: params.onClarifyRequest,
          });
        } catch (err: any) {
          if (err?.isInteractionDecline) {
            for (const te of toolExecutions) {
              if (te.status === 'running') {
                te.status = 'error';
                te.result = te.result || (err as GroupInteractionDeclineError).declineMessage;
              }
            }
            return {
              messages: [],
              finalContent: (err as GroupInteractionDeclineError).declineMessage,
              thinking: thinkingText || undefined,
              usage: undefined,
              toolCallCount: toolExecutions.length,
              metadata: {
                ...buildToolExecutionMetadata(toolExecutions),
              },
            } as any;
          }
          for (const te of toolExecutions) {
            if (te.status === 'running') {
              te.status = 'error';
              te.result = te.result || (err?.message || 'Agent execution failed');
            }
          }
          err.runtimeMetadata = {
            ...(err?.runtimeMetadata || {}),
            ...buildToolExecutionMetadata(toolExecutions),
          };
          throw err;
        }

        const metadata: Record<string, unknown> = {
          ...buildToolExecutionMetadata(toolExecutions),
        };
        if (runResult.usage) {
          const inp = runResult.usage.inputTokens || 0;
          const out = runResult.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 {
          ...runResult,
          thinking: runResult.thinking || thinkingText || undefined,
          metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
        };
      },
    });

    const resultWithMetadata = result as typeof result & { metadata?: Record<string, unknown> };
    return {
      sessionId: result.sessionId,
      content: result.content,
      usage: result.usage,
      toolCallCount: result.toolCallCount,
      toolExecutions: (resultWithMetadata.metadata as any)?.skillExecutions,
      metadata: resultWithMetadata.metadata,
      agentId: agentEntity?.id,
      agentName: agentConfig.name,
      assistantEntryId: result.assistantEntryId,
      subagentBatches: result.subagentBatches,
    };
  }

  private withChannelBizPrompt(systemPrompt: string, bizContext?: NetaToolRuntimeBizContext): string {
    if (!bizContext?.channelId && !bizContext?.roomName) return systemPrompt;
    const lines = [
      '',
      '当前消息来自微信群上下文：',
      bizContext.channelId !== undefined ? `- channelId=${bizContext.channelId}` : null,
      typeof bizContext.roomName === 'string' && bizContext.roomName.trim()
        ? `- 当前群名：${bizContext.roomName.trim()}`
        : null,
      typeof bizContext.roomId === 'string' && bizContext.roomId.trim()
        ? `- 当前 roomId：${bizContext.roomId.trim()}`
        : null,
      typeof bizContext.senderId === 'string' && bizContext.senderId.trim()
        ? `- 当前发言人ID：${bizContext.senderId.trim()}`
        : null,
      typeof bizContext.senderWxid === 'string' && bizContext.senderWxid.trim()
        ? `- 当前发言人微信ID：${bizContext.senderWxid.trim()}`
        : null,
      typeof bizContext.senderDisplayName === 'string' && bizContext.senderDisplayName.trim()
        ? `- 当前发言人显示名：${bizContext.senderDisplayName.trim()}`
        : null,
      typeof bizContext.senderRemark === 'string' && bizContext.senderRemark.trim()
        ? `- 当前发言人备注：${bizContext.senderRemark.trim()}`
        : null,
      typeof bizContext.senderNickName === 'string' && bizContext.senderNickName.trim()
        ? `- 当前发言人昵称：${bizContext.senderNickName.trim()}`
        : null,
      '- 当用户在当前群里要求回复、发送或测试时，默认目标就是当前群，不要再询问群名。',
      '- 调用 weixin_send_text 时使用当前群名作为 roomName；channelId 会由运行时上下文注入。',
    ].filter(Boolean);
    return `${systemPrompt}\n${lines.join('\n')}`;
  }

  private async createSessionSubagentContext(params: {
    sessionId: string;
    assistantMessageId: number;
    parentAgent: NetaClawAgentEntity;
    parentModelCapability: string;
    agentConfig: AgentConfig;
    subagents: ChatOrchestratorSubagentContext;
    bizContext?: NetaToolRuntimeBizContext;
  }): 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 normalizeTask = (task: NetaClawSubagentTask): NetaClawSubagentTask => ({
      ...task,
      toolNames: task.toolNames !== undefined ? task.toolNames : defaultToolNames,
      bizContext: task.bizContext ?? params.bizContext,
    });

    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,
      });
    };

    const emitUpdate = (batchId: string, subagent: ChatOrchestratorSubagentRecord) => {
      params.subagents.onUpdate({
        batchId,
        sessionId: params.sessionId,
        parentMessageId: params.assistantMessageId,
        sequence: 0,
        timestamp: new Date().toISOString(),
        event: {
          kind: subagent.status === 'failed' ? 'status' : 'result',
          status: subagent.status === 'failed' ? 'failed' : 'completed',
          subagent,
        },
      });
    };

    const emitDone = (batchId: string, results: ChatOrchestratorSubagentRecord[]) => {
      params.subagents.onDone({
        batchId,
        sessionId: params.sessionId,
        parentMessageId: params.assistantMessageId,
        results,
      });
    };

    const presetAgents = await this.buildPresetMemberAgents(params.parentAgent);

    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);
        emitDone(batchId, [record]);
        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));
        emitDone(batchId, records);
        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): boolean {
    return agent?.subagentConfig?.enabled
      ?? this.subagentDefaults?.enabled
      ?? false;
  }
}
