import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { In, Repository } from 'typeorm';
import { NetaClawAgentEntity } from '../entity/agent.js';
import { NetaClawSubagentSessionEntity } from '../entity/subagent_session.js';
import { runAgent, AgentConfig, AgentRunParams } from '../runtime/agent.js';
import { buildSystemPrompt } from '../runtime/prompt_builder.js';
import { SkillLoaderService } from './skill_loader.js';
import { NetaClawModelChannelService } from './model_channel.js';
import { NetaClawAgentService } from './agent.js';
import { NetaClawSessionService } from '../gateway/session.js';
import {
  SUBAGENT_WORKER_PROTOCOL_VERSION,
  SubagentProxyToolCallEvent,
  SubagentToolManifestItem,
  SubagentWorkerEvent,
  SubagentWorkerPolicy,
  SubagentWorkerResult,
} from '../subagent/process_protocol.js';
import { SubagentProcessRunner } from '../subagent/process_runner.js';
import {
  NetaClawToolResolverService,
  ResolveToolResult,
} from './tool_resolver.js';
import type { ModelCapability } from '../entity/model_channel.js';
import * as path from 'path';
import { injectToolRuntimeContext } from '../tools/runtime_context.js';
import type { NetaToolRuntimeBizContext } from '../tools/runtime_context.js';
import { resolveToolPath } from '../tools/path_utils.js';
import { buildSubprocessWorkerPolicy, normalizeWorkspaceRoots } from '../tools/runtime_policy.js';
import { buildSubagentEvidenceSummary, normalizeSubagentToolResults } from './subagent_evidence.js';

type SubagentSourceMode = 'preset';
type SupportedSubagentSourceType = 'preset';

export interface NetaClawSubagentTask {
  goal: string;
  context?: string;
  mode?: SubagentSourceMode;
  agentId?: number | null;
  agentName?: string | null;
  toolNames?: string[];
  workspaceRoots?: string[];
  bizContext?: NetaToolRuntimeBizContext;
  maxToolRounds?: number;
}

export interface NetaClawSubagentParentModelContext {
  model: string;
  apiKey: string;
  baseUrl?: string;
  capability?: string;
}

export interface NetaClawSubagentRunSingleContext {
  sessionId: string;
  parentMessageId: number;
  parentToolCallId?: string | null;
  parentAgent?: Partial<NetaClawAgentEntity> | null;
  parentModel: NetaClawSubagentParentModelContext;
  sessionCwd?: string | null;
  task: NetaClawSubagentTask;
  sortOrder?: number;
}

export interface NetaClawSubagentRunBatchContext
  extends Omit<NetaClawSubagentRunSingleContext, 'task' | 'sortOrder'> {
  tasks: NetaClawSubagentTask[];
  maxConcurrent?: number;
}

export interface NetaClawSubagentRunSummary {
  id: number;
  sessionId: string;
  parentMessageId: number;
  parentToolCallId: string | null;
  parentAgentId: number | null;
  sourceType: SupportedSubagentSourceType;
  presetAgentId: number | null;
  name: string;
  goal: string;
  context: string | null;
  status: 'completed';
  model: string;
  summary: string;
  toolNames: string[];
  tokenUsage: {
    inputTokens?: number;
    outputTokens?: number;
    totalTokens?: number;
  } | null;
  resultPayload: Record<string, unknown>;
  sortOrder: number;
  error: null;
}

export interface NetaClawSubagentRunFailureSummary {
  id: number;
  sessionId: string;
  parentMessageId: number;
  parentToolCallId: string | null;
  parentAgentId: number | null;
  sourceType: SupportedSubagentSourceType;
  presetAgentId: number | null;
  name: string;
  goal: string;
  context: string | null;
  status: 'failed';
  model: string;
  summary: string;
  toolNames: string[];
  tokenUsage: null;
  resultPayload: Record<string, unknown>;
  sortOrder: number;
  error: string;
}

export type NetaClawSubagentRunResult =
  | NetaClawSubagentRunSummary
  | NetaClawSubagentRunFailureSummary;

export class SubagentExecutionError extends Error {
  cause?: unknown;

  sessionRecordId?: number;
}

@Provide()
@Scope(ScopeEnum.Singleton)
export class NetaClawSubagentService {
  @Config('netaclaw.subagent')
  subagentDefaults: {
    enabled?: boolean;
    maxConcurrent?: number;
    allowedToolNames?: string[];
    executionMode?: 'in_process' | 'subprocess';
    process?: {
      workerPath?: string;
      timeoutMs?: number;
      workspaceRoots?: string[];
      allowShell?: boolean;
      readonly?: boolean;
    };
  };

  @InjectEntityModel(NetaClawSubagentSessionEntity)
  subagentRepo: Repository<NetaClawSubagentSessionEntity>;

  @Inject()
  toolResolver: NetaClawToolResolverService;

  @Inject()
  agentService: NetaClawAgentService;

  @Inject()
  sessionService: NetaClawSessionService;

  @Inject()
  channelService: NetaClawModelChannelService;

  @Inject()
  skillLoader: SkillLoaderService;

  agentRunner: (params: AgentRunParams) => ReturnType<typeof runAgent> = runAgent;

  processRunner?: SubagentProcessRunner;

  buildRestrictedPrompt(ctx: NetaClawSubagentRunSingleContext): string {
    const sections = [
      'You are a session subagent for a delegated goal from the parent agent.',
      'This is a restricted execution context. You cannot ask the user questions.',
      'You cannot delegate further subagents.',
      'You cannot use memory or clarify tools.',
      'Your output is for the parent agent summary, not for direct user delivery.',
      `Delegated goal:\n${ctx.task.goal}`,
    ];

    const parentPrompt = ctx.parentAgent?.systemPrompt?.trim();
    if (parentPrompt) {
      sections.push(`Parent system prompt context:\n${parentPrompt}`);
    }

    if (ctx.task.context?.trim()) {
      sections.push(`Delegation context:\n${ctx.task.context.trim()}`);
    }

    return sections.join('\n\n');
  }

  async runSingle(ctx: NetaClawSubagentRunSingleContext): Promise<NetaClawSubagentRunResult> {
    if (ctx.sessionCwd === undefined) {
      ctx.sessionCwd = await this.resolveSessionCwd(ctx.sessionId, ctx.parentAgent?.id ?? undefined);
    }
    const sourceType = this.resolveSourceType(ctx);
    const presetAgentId = ctx.task.agentId ?? null;
    const sortOrder = ctx.sortOrder ?? 0;
    const name = this.resolveAgentName(ctx);
    const startedAt = new Date();
    const record = await this.subagentRepo.save(this.subagentRepo.create({
      sessionId: ctx.sessionId,
      parentMessageId: ctx.parentMessageId,
      parentToolCallId: ctx.parentToolCallId ?? null,
      parentAgentId: ctx.parentAgent?.id ?? null,
      sourceType,
      presetAgentId,
      name,
      goal: ctx.task.goal,
      context: ctx.task.context ?? null,
      status: 'running',
      sortOrder,
      startedAt,
    }));

    let resolvedToolset: Pick<ResolveToolResult, 'tools' | 'toolNames' | 'toolManifest' | 'toolRuntimeRoutes'> | null = null;

    let execution: {
      agentConfig: AgentConfig;
      resolvedToolset: Pick<ResolveToolResult, 'tools' | 'toolNames' | 'toolManifest' | 'toolRuntimeRoutes'>;
    } | null = null;

    try {
      execution = await this.preparePresetExecution(ctx);

      resolvedToolset = execution.resolvedToolset;
      this.assertExecutableEvidenceRoutes(ctx, resolvedToolset);

      const result = await this.runPreparedExecution(
        ctx,
        record.id,
        execution.agentConfig,
        resolvedToolset
      );

      const finalOutput = this.resolveFinalOutput(ctx, result);
      const evidenceSummary = buildSubagentEvidenceSummary(ctx.task, result.toolResults);
      this.assertEvidenceRequirements(ctx, resolvedToolset, result.toolCallCount, finalOutput);

      const tokenUsage = this.normalizeTokenUsage(result.usage);
        const resultPayload = {
          finalContent: finalOutput,
          rawFinalContent: result.finalContent,
          finalOutput,
          toolCallCount: result.toolCallCount,
          toolResults: normalizeSubagentToolResults(result.toolResults),
          evidenceSummary,
          proxiedToolNames: this.extractProxiedToolNames(result),
          pendingProxyToolNames: this.extractPendingProxyToolNames(result),
          processEvents: this.normalizeProcessEvents(result.metadata?.processEvents),
          toolRuntimeRoutes: resolvedToolset.toolRuntimeRoutes ?? [],
        };

      await this.subagentRepo.update(record.id, {
        status: 'completed',
        model: execution.agentConfig.model,
        toolNames: resolvedToolset.toolNames,
        summary: finalOutput,
        resultPayload,
        tokenUsage,
        error: null,
        endedAt: new Date(),
      });

      return {
        id: record.id,
        sessionId: ctx.sessionId,
        parentMessageId: ctx.parentMessageId,
        parentToolCallId: ctx.parentToolCallId ?? null,
        parentAgentId: ctx.parentAgent?.id ?? null,
        sourceType,
        presetAgentId,
        name,
        goal: ctx.task.goal,
        context: ctx.task.context ?? null,
        status: 'completed',
        model: execution?.agentConfig.model ?? ctx.parentModel.model,
        summary: finalOutput,
        toolNames: resolvedToolset.toolNames,
        tokenUsage,
        resultPayload,
        sortOrder,
        error: null,
      };
    } catch (error) {
      const errorMessage = this.toErrorMessage(error);
      const failedResult: NetaClawSubagentRunFailureSummary = {
        id: record.id,
        sessionId: ctx.sessionId,
        parentMessageId: ctx.parentMessageId,
        parentToolCallId: ctx.parentToolCallId ?? null,
        parentAgentId: ctx.parentAgent?.id ?? null,
        sourceType,
        presetAgentId,
        name,
        goal: ctx.task.goal,
        context: ctx.task.context ?? null,
        status: 'failed',
        model: execution?.agentConfig.model ?? ctx.parentModel.model,
        summary: '',
        toolNames: resolvedToolset?.toolNames ?? [],
        tokenUsage: null,
        resultPayload: resolvedToolset
          ? {
              processEvents: this.normalizeProcessEvents(execution?.agentConfig ? (execution as any)?.processEvents : undefined),
              toolRuntimeRoutes: resolvedToolset.toolRuntimeRoutes ?? [],
            }
          : {},
        sortOrder,
        error: errorMessage,
      };

      await this.subagentRepo.update(record.id, {
        status: 'failed',
        model: execution?.agentConfig.model ?? ctx.parentModel.model,
        toolNames: resolvedToolset?.toolNames ?? null,
        resultPayload: resolvedToolset
          ? {
              processEvents: this.normalizeProcessEvents(execution?.agentConfig ? (execution as any)?.processEvents : undefined),
              toolRuntimeRoutes: resolvedToolset.toolRuntimeRoutes ?? [],
            }
          : undefined,
        error: errorMessage,
        endedAt: new Date(),
      });
      return failedResult;
    }
  }

  async runBatch(ctx: NetaClawSubagentRunBatchContext): Promise<NetaClawSubagentRunResult[]> {
    const tasks = ctx.tasks ?? [];
    if (tasks.length === 0) {
      return [];
    }

    const maxConcurrent = Math.max(1, ctx.maxConcurrent ?? 3);
    const results: NetaClawSubagentRunResult[] = [];

    for (let index = 0; index < tasks.length; index += maxConcurrent) {
      const chunk = tasks.slice(index, index + maxConcurrent);
      const settledResults = await Promise.allSettled(
        chunk.map((task, offset) => this.runSingle({
          ...ctx,
          task,
          sortOrder: index + offset,
        }))
      );
      settledResults.forEach((result, offset) => {
        if (result.status === 'fulfilled') {
          results.push(result.value);
          return;
        }

        throw this.createExecutionError(index + offset, result.reason);
      });
    }

    return results;
  }

  private resolveSourceType(ctx: NetaClawSubagentRunSingleContext): SupportedSubagentSourceType {
    this.applySingleAllowedPresetDefault(ctx);
    if (!ctx.task.agentId && !ctx.task.agentName) {
      const allowedIds = ctx.parentAgent?.subagentConfig?.allowedPresetAgentIds;
      if (Array.isArray(allowedIds) && allowedIds.length > 1) {
        throw new Error(
          `Multiple preset subagents are allowed (${allowedIds.join(', ')}); specify agentId or agentName explicitly.`
        );
      }
      throw new Error('Preset subagent requires agentId or agentName');
    }
    return 'preset';
  }

  private applySingleAllowedPresetDefault(ctx: NetaClawSubagentRunSingleContext) {
    if (ctx.task.agentId || ctx.task.agentName) {
      return;
    }

    const allowedIds = ctx.parentAgent?.subagentConfig?.allowedPresetAgentIds;
    if (Array.isArray(allowedIds) && allowedIds.length === 1) {
      ctx.task.mode = 'preset';
      ctx.task.agentId = allowedIds[0];
    }
  }

  private async preparePresetExecution(
    ctx: NetaClawSubagentRunSingleContext
  ): Promise<{
    agentConfig: AgentConfig;
    resolvedToolset: Pick<ResolveToolResult, 'tools' | 'toolNames' | 'toolPromptHints' | 'toolManifest' | 'toolRuntimeRoutes'>;
  }> {
    const agent = await this.resolvePresetAgent(ctx);
    this.assertPresetAllowed(ctx, agent);

    const modelInfo = await this.resolvePresetModel(agent, ctx.parentModel);
    const runtimePolicy = this.buildPreResolvedRuntimePolicy(ctx);
    const resolved = await this.toolResolver.resolve({
      agent,
      modelCapability: this.resolveModelCapability(modelInfo.capability),
      hasSkills: !!agent.skills?.length,
      delegationRole: 'subagent',
      runtimePolicy,
    });
    // v4 临时调试:打印 subagent 解析出的工具集 + 禁用原因
    // eslint-disable-next-line no-console
    console.log('[subagent DEBUG] preset agentId=%s toolNames=%j disabledReasons=%j requestedToolNames=%j',
      agent.id, resolved.toolNames, resolved.disabledReasons, ctx.task.toolNames);
    const resolvedToolset = this.constrainToolset(resolved, ctx.task.toolNames);
    // eslint-disable-next-line no-console
    console.log('[subagent DEBUG] after constrain toolNames=%j', resolvedToolset.toolNames);
    const { prompt } = buildSystemPrompt({
      agentSystemPrompt: [
        agent.systemPrompt || '',
        'You are running as a session subagent delegated by a parent agent.',
        'Do not ask the user questions. Return a concise result for the parent agent.',
        this.buildExecutionDisciplinePrompt(ctx, agent, resolvedToolset),
        `Delegated goal:\n${ctx.task.goal}`,
        ctx.task.context?.trim() ? `Delegation context:\n${ctx.task.context.trim()}` : '',
      ].filter(Boolean).join('\n\n'),
      modelId: (agent.modelConfig as any)?.modelId || '',
      availableToolNames: resolvedToolset.toolNames,
      skills: agent.skills || [],
      skillLoader: this.skillLoader,
      memoryEnabled: false,
      toolPromptHints: resolvedToolset.toolPromptHints,
    });

    const cfg = (agent.config || {}) as any;

    return {
      resolvedToolset,
      agentConfig: {
        name: agent.name,
        systemPrompt: prompt,
        model: modelInfo.model,
        apiKey: modelInfo.apiKey,
        baseUrl: modelInfo.baseUrl,
        maxToolRounds: ctx.task.maxToolRounds
          ?? cfg?.middleware?.maxToolRounds
          ?? cfg?.middleware?.maxToolCalls
          ?? 10,
      },
    };
  }

  private async resolvePresetAgent(ctx: NetaClawSubagentRunSingleContext): Promise<NetaClawAgentEntity> {
    let agent = ctx.task.agentId
      ? await this.agentService.info(ctx.task.agentId)
      : null;

    if (!agent && ctx.task.agentName) {
      const requestedName = ctx.task.agentName.trim();
      agent = await this.agentService.agentRepo.findOneBy({ name: requestedName } as any);

      if (!agent) {
        const byLabel = await this.agentService.agentRepo.findOne({
          where: [
            { label: requestedName, status: 1 } as any,
            { name: requestedName, status: 1 } as any,
          ],
          order: { createTime: 'DESC' },
        });
        agent = byLabel ?? null;
      }
    }

    if (!agent) {
      throw new Error('Preset subagent requires a valid agentId or agentName');
    }

    if (agent.status !== 1) {
      throw new Error(`Preset subagent "${agent.name}" is not published`);
    }

    return agent;
  }

  private assertPresetAllowed(ctx: NetaClawSubagentRunSingleContext, agent: NetaClawAgentEntity) {
    const allowedIds = ctx.parentAgent?.subagentConfig?.allowedPresetAgentIds;
    if (Array.isArray(allowedIds) && allowedIds.length > 0 && !allowedIds.includes(agent.id)) {
      throw new Error(`Preset subagent "${agent.name}" is not allowed by parent agent policy`);
    }
  }

  private async resolvePresetModel(
    agent: NetaClawAgentEntity,
    fallback: NetaClawSubagentParentModelContext
  ): Promise<NetaClawSubagentParentModelContext> {
    const mc = (agent.modelConfig || {}) as any;
    if (mc.channelId && mc.channelId > 0) {
      const resolved = await this.channelService.resolveForAgent(mc.channelId, mc.modelId || '');
      return {
        model: `${resolved.provider}:${resolved.model}`,
        apiKey: resolved.apiKey,
        baseUrl: resolved.baseUrl,
        capability: resolved.capability,
      };
    }

    return {
      model: mc.modelId ? `openai:${mc.modelId}` : fallback.model,
      apiKey: mc.apiKey || fallback.apiKey,
      baseUrl: mc.apiUrl || fallback.baseUrl,
      capability: fallback.capability,
    };
  }

  private resolveModelCapability(capability?: string): ModelCapability {
    if (capability === 'vision' || capability === 'multimodal') {
      return capability;
    }

    return 'text';
  }

  private resolveAgentName(ctx: NetaClawSubagentRunSingleContext): string {
    return ctx.task.agentName?.trim()
      || `${ctx.parentAgent?.name ?? 'session'}-subagent`;
  }

  private resolveMaxToolRounds(ctx: NetaClawSubagentRunSingleContext): number {
    const parentConfig = ctx.parentAgent?.config as
      | { middleware?: { maxToolRounds?: number; maxToolCalls?: number } }
      | undefined;

    return ctx.task.maxToolRounds
      ?? parentConfig?.middleware?.maxToolRounds
      ?? parentConfig?.middleware?.maxToolCalls
      ?? 10;
  }

  private async runPreparedExecution(
    ctx: NetaClawSubagentRunSingleContext,
    recordId: number,
    agentConfig: AgentConfig,
    resolvedToolset: Pick<ResolveToolResult, 'tools' | 'toolNames' | 'toolManifest' | 'toolRuntimeRoutes'>
  ): Promise<SubagentWorkerResult> {
    const executionMode = this.subagentDefaults?.executionMode ?? 'in_process';
    if (executionMode === 'subprocess') {
      const runner = this.getProcessRunner();
      const processEvents: Record<string, unknown>[] = [];
      return runner.run({
        protocolVersion: SUBAGENT_WORKER_PROTOCOL_VERSION,
        runId: `subagent-${recordId}`,
        sessionId: ctx.sessionId,
        parentMessageId: ctx.parentMessageId,
        parentToolCallId: ctx.parentToolCallId ?? null,
        parentAgentId: ctx.parentAgent?.id ?? null,
        task: ctx.task,
        agentConfig,
        toolNames: resolvedToolset.toolNames,
        toolManifest: resolvedToolset.toolManifest,
        policy: this.deriveProcessPolicy(ctx, resolvedToolset.toolManifest),
        timeoutMs: this.subagentDefaults?.process?.timeoutMs,
        metadata: {
          sourceType: this.resolveSourceType(ctx),
          sortOrder: ctx.sortOrder ?? 0,
        },
      }, event => {
        processEvents.push(this.projectProcessEvent(event));
      }, event => this.executeMainProcessProxyTool(event, resolvedToolset, ctx)).then(result => ({
        ...result,
        metadata: {
          ...(result.metadata ?? {}),
          processEvents,
        },
      })).catch(error => {
        const normalizedError = error instanceof Error ? error.message : String(error ?? 'Unknown error');
        processEvents.push({
          type: 'run_error',
          runId: `subagent-${recordId}`,
          timestamp: new Date().toISOString(),
          error: normalizedError,
        });
        throw Object.assign(error instanceof Error ? error : new Error(normalizedError), {
          processEvents,
        });
      });
    }

    const runId = `subagent-${recordId}`;
    const processEvents: Record<string, unknown>[] = [{
      type: 'run_start',
      runId,
      timestamp: new Date().toISOString(),
      envelope: {
        sessionId: ctx.sessionId,
        parentMessageId: ctx.parentMessageId,
        parentToolCallId: ctx.parentToolCallId ?? null,
        parentAgentId: ctx.parentAgent?.id ?? null,
      },
    }];

    // v4: 解析 preset agent 信息以注入 currentAgent 到 tool runtime context
    const presetAgentForRuntime = ctx.task.agentId
      ? await this.agentService.info(ctx.task.agentId)
      : null;
    const subagentRuntimeCurrentAgent = presetAgentForRuntime ? {
      id: presetAgentForRuntime.id,
      name: presetAgentForRuntime.name ?? '',
      modelChannelId: (presetAgentForRuntime.modelConfig as any)?.channelId ?? presetAgentForRuntime.auxiliaryModelChannelId ?? null,
      toolsets: presetAgentForRuntime.toolsets ?? [],
    } : undefined;

    return this.agentRunner({
      agentConfig,
      tools: resolvedToolset.tools,
      toolNames: resolvedToolset.toolNames,
      userMessage: ctx.task.goal,
      history: [],
      beforeToolCall: async ({ toolCallId, name, label, args }) => {
        // v4: 注入 currentAgent 让 weixin_send_text 等 tool 拿到 modelChannelId
        const runtimeArgs = injectToolRuntimeContext(args as Record<string, unknown>, {
          sessionCwd: ctx.sessionCwd ?? null,
          workspaceRoots: this.deriveProcessPolicy(ctx, resolvedToolset.toolManifest).workspaceRoots,
          bizContext: ctx.task.bizContext,
          currentAgent: subagentRuntimeCurrentAgent,
        });
        processEvents.push({
          type: 'tool_call',
          runId,
          timestamp: new Date().toISOString(),
          name,
          label,
          toolCallId,
          args: runtimeArgs,
        });
        return { args: runtimeArgs as Record<string, unknown> };
      },
      afterToolCall: async ({ toolCallId, name, label, result, isError }) => {
        processEvents.push({
          type: 'tool_result',
          runId,
          timestamp: new Date().toISOString(),
          name,
          label,
          toolCallId,
          result,
          ...(isError ? { error: result } : {}),
        });
      },
      onToken: text => {
        processEvents.push({
          type: 'token',
          runId,
          timestamp: new Date().toISOString(),
          text,
        });
      },
      onThinking: text => {
        processEvents.push({
          type: 'thinking',
          runId,
          timestamp: new Date().toISOString(),
          text,
        });
      },
    }).then(result => ({
      ...result,
      metadata: {
        processEvents: [
          ...processEvents,
          {
            type: 'run_end',
            runId,
            timestamp: new Date().toISOString(),
            result: {
              finalContent: result.finalContent,
              finalOutput: result.finalContent,
              toolCallCount: result.toolCallCount,
            },
          },
        ],
      },
    })).catch(error => {
      const normalizedError = error instanceof Error ? error.message : String(error ?? 'Unknown error');
      processEvents.push({
        type: 'run_error',
        runId,
        timestamp: new Date().toISOString(),
        error: normalizedError,
      });
      throw Object.assign(error instanceof Error ? error : new Error(normalizedError), {
        processEvents,
      });
    });
  }

  private getProcessRunner(): SubagentProcessRunner {
    if (!this.processRunner) {
      this.processRunner = new SubagentProcessRunner({
        workerPath: this.subagentDefaults?.process?.workerPath,
        timeoutMs: this.subagentDefaults?.process?.timeoutMs,
      });
    }

    return this.processRunner;
  }

  private async resolveSessionCwd(sessionId: string, agentId?: number): Promise<string | null> {
    try {
      const session = await this.sessionService.getSessionTreeSession(sessionId, agentId);
      return typeof session?.cwd === 'string' && session.cwd.trim().length > 0
        ? session.cwd.trim()
        : null;
    } catch {
      return null;
    }
  }

  private deriveProcessPolicy(
    ctx: NetaClawSubagentRunSingleContext,
    toolManifest: SubagentToolManifestItem[] = []
  ): SubagentWorkerPolicy {
    return buildSubprocessWorkerPolicy({
      ...this.buildPreResolvedRuntimePolicy(ctx),
      toolManifest,
    });
  }

  private buildPreResolvedRuntimePolicy(
    ctx: NetaClawSubagentRunSingleContext
  ): {
    sessionCwd: string | null;
    workspaceRoots: string[];
    allowShell?: boolean;
    readonly?: boolean;
  } {
    const configuredProcess = this.subagentDefaults?.process;
    const configuredWorkspaceRoots = normalizeWorkspaceRoots(configuredProcess?.workspaceRoots || []);
    const taskWithWorkspaceRoots = ctx.task as NetaClawSubagentTask & {
      workspaceRoots?: unknown;
    };
    const taskWorkspaceRoots = Array.isArray(taskWithWorkspaceRoots.workspaceRoots)
      ? taskWithWorkspaceRoots.workspaceRoots.filter((value): value is string => typeof value === 'string')
      : [];
    const inferredWorkspaceRoots = this.deriveWorkspaceRootsFromTaskText(ctx);
    const workspaceRoots = normalizeWorkspaceRoots([
      ctx.sessionCwd ?? undefined,
      ...taskWorkspaceRoots,
      ...inferredWorkspaceRoots,
      ...configuredWorkspaceRoots,
    ]);

    return {
      sessionCwd: ctx.sessionCwd ?? null,
      workspaceRoots,
      allowShell: configuredProcess?.allowShell,
      readonly: configuredProcess?.readonly,
    };
  }

  private constrainToolset(
    resolved: Pick<ResolveToolResult, 'tools' | 'toolNames' | 'toolPromptHints' | 'toolManifest' | 'toolRuntimeRoutes'>,
    requestedToolNames?: string[] 
  ): Pick<ResolveToolResult, 'tools' | 'toolNames' | 'toolPromptHints' | 'toolManifest' | 'toolRuntimeRoutes'> {
    const requested = Array.isArray(requestedToolNames)
      ? [...new Set(requestedToolNames.filter(Boolean))]
      : [];

    if (requested.length === 0) {
        return {
          tools: resolved.tools,
          toolNames: resolved.toolNames,
          toolManifest: resolved.toolManifest,
          toolRuntimeRoutes: resolved.toolRuntimeRoutes ?? [],
          toolPromptHints: resolved.toolPromptHints,
        };
    }

    const allowed = new Set(requested);
    const tools = resolved.tools.filter(tool => allowed.has(tool.name));
    const toolNames = resolved.toolNames.filter(name => allowed.has(name));
    const toolManifest = resolved.toolManifest.filter(item => allowed.has(item.name));
    const toolRuntimeRoutes = (resolved.toolRuntimeRoutes ?? []).filter(item => allowed.has(item.name));
    const toolPromptHints = Object.fromEntries(
      Object.entries(resolved.toolPromptHints || {}).filter(([name]) => allowed.has(name))
    );

    return { tools, toolNames, toolManifest, toolRuntimeRoutes, toolPromptHints };
  }

  private buildExecutionDisciplinePrompt(
    ctx: NetaClawSubagentRunSingleContext,
    agent: NetaClawAgentEntity,
    resolvedToolset: Pick<ResolveToolResult, 'toolNames'>
  ): string {
    const toolList = resolvedToolset.toolNames.length > 0
      ? resolvedToolset.toolNames.join(', ')
      : '(none)';
    const skillList = Array.isArray(agent.skills) && agent.skills.length > 0
      ? agent.skills.join(', ')
      : '(none)';

    return [
      'Execution contract:',
      `- Available tools for this delegated run: ${toolList}.`,
      `- Available preset skills for this delegated run: ${skillList}.`,
      '- Before answering, inspect the available tools and skills injected below and use the appropriate ones instead of guessing.',
      '- If the task depends on filesystem state, commands, search results, counts, listings, paths, or any external evidence, you must call tools before answering.',
      '- If a matching skill is available and the task clearly fits it, load it with `read_skill` and follow it.',
      '- Never fabricate counts, file paths, command results, or success states.',
      '- If the required tool is unavailable or a tool call fails, explicitly report failure and the reason instead of guessing.',
      '- Your final answer must be grounded in tool evidence gathered during this run.',
      '- Final answer requirements: state the answer directly; do not finish with only reasoning, thinking, or tool attempts.',
      '- If the task asks to count files, include the count and the filenames or evidence used for the count.',
      '- If you cannot determine the answer, say exactly why in the final answer.',
    ].join('\n');
  }

  private assertEvidenceRequirements(
    ctx: NetaClawSubagentRunSingleContext,
    resolvedToolset: Pick<ResolveToolResult, 'toolNames' | 'toolRuntimeRoutes'>,
    toolCallCount?: number,
    finalOutput?: string
  ) {
    if (!this.requiresExternalEvidence(ctx, resolvedToolset)) {
      return;
    }

    this.assertExecutableEvidenceRoutes(ctx, resolvedToolset);

    if ((toolCallCount ?? 0) > 0 && finalOutput && finalOutput !== '(no output)') {
      return;
    }

    throw new Error(
      'Subagent did not produce a usable final answer for a task that requires external evidence'
    );
  }

  private resolveFinalOutput(
    ctx: NetaClawSubagentRunSingleContext,
    result: SubagentWorkerResult
  ): string {
    const candidates = [
      typeof result.finalOutput === 'string' ? result.finalOutput : '',
      typeof result.finalContent === 'string' ? result.finalContent : '',
    ];

    for (const candidate of candidates) {
      const normalized = this.normalizeFinalOutput(candidate);
      if (normalized) {
        return normalized;
      }
    }

    const evidenceSummary = buildSubagentEvidenceSummary(ctx.task, result.toolResults);
    if (evidenceSummary?.summary) {
      return evidenceSummary.summary;
    }

    return '(no output)';
  }

  private normalizeFinalOutput(value: string): string {
    const trimmed = value.trim();
    if (!trimmed) {
      return '';
    }

    const withoutThinkBlocks = trimmed
      .replace(/<think>[\s\S]*?<\/think>/gi, '')
      .trim();

    return withoutThinkBlocks;
  }

  private projectProcessEvent(event: SubagentWorkerEvent): Record<string, unknown> {
    switch (event.type) {
      case 'run_start':
        return {
          type: event.type,
          runId: event.runId,
          timestamp: event.timestamp,
          envelope: event.envelope ?? {},
        };
      case 'tool_call':
        return {
          type: event.type,
          runId: event.runId,
          timestamp: event.timestamp,
          name: event.name,
          label: event.label,
          toolCallId: event.toolCallId,
          args: event.args ?? {},
        };
      case 'tool_result':
        return {
          type: event.type,
          runId: event.runId,
          timestamp: event.timestamp,
          name: event.name,
          label: event.label,
          toolCallId: event.toolCallId,
          result: event.result,
        };
      case 'proxy_tool_call':
        return {
          type: event.type,
          runId: event.runId,
          timestamp: event.timestamp,
          requestId: event.requestId,
          name: event.name,
          toolCallId: event.toolCallId,
          args: event.args ?? {},
        };
      case 'run_end':
        return {
          type: event.type,
          runId: event.runId,
          timestamp: event.timestamp,
          result: {
            finalContent: event.result.finalContent,
            finalOutput: event.result.finalOutput,
            toolCallCount: event.result.toolCallCount,
          },
        };
      case 'run_error':
        return {
          type: event.type,
          runId: event.runId,
          timestamp: event.timestamp,
          error: event.error,
          details: event.details ?? {},
        };
      case 'token':
      case 'thinking':
      case 'log':
        return {
          type: event.type,
          runId: event.runId,
          timestamp: event.timestamp,
          text: event.text,
        };
      default:
        return {
          type: 'unknown',
          runId: (event as any)?.runId,
          timestamp: (event as any)?.timestamp,
        };
    }
  }

  private normalizeProcessEvents(value: unknown): Record<string, unknown>[] {
    if (!Array.isArray(value)) {
      return [];
    }

    return value
      .filter(item => item && typeof item === 'object' && !Array.isArray(item))
      .map(item => ({ ...(item as Record<string, unknown>) }));
  }

  private requiresExternalEvidence(
    ctx: NetaClawSubagentRunSingleContext,
    resolvedToolset: Pick<ResolveToolResult, 'toolNames'>
  ): boolean {
    const toolNames = new Set(resolvedToolset.toolNames || []);
    const hasEvidenceTool =
      toolNames.has('bash')
      || toolNames.has('list_dir')
      || toolNames.has('read_file')
      || toolNames.has('find_files')
      || toolNames.has('grep')
      || toolNames.has('read_skill');

    if (!hasEvidenceTool) {
      return false;
    }

    const text = `${ctx.task.goal}\n${ctx.task.context ?? ''}`.toLowerCase();
    return /(count|统计|多少|files?|文件|desktop|桌面|xlsx|xls|pdf|目录|路径|path|list|列出|find|search|scan|扫描|number of)/i.test(text);
  }

  private assertExecutableEvidenceRoutes(
    ctx: NetaClawSubagentRunSingleContext,
    resolvedToolset: Pick<ResolveToolResult, 'toolNames' | 'toolRuntimeRoutes'>
  ) {
    if (!this.requiresExternalEvidence(ctx, resolvedToolset)) {
      return;
    }

    const evidenceToolNames = new Set(['bash', 'list_dir', 'read_file', 'find_files', 'grep']);
    const relevantRoutes = (resolvedToolset.toolRuntimeRoutes ?? []).filter(route =>
      evidenceToolNames.has(route.name)
      && resolvedToolset.toolNames.includes(route.name)
    );

    if (relevantRoutes.length === 0) {
      throw new Error(
        'Subagent evidence task has no filesystem/search tools available in the resolved toolset'
      );
    }

    const hasExecutableRoute = relevantRoutes.some(route =>
      route.runtimeRoute === 'worker-local' || route.runtimeRoute === 'main-process-proxy'
    );

    if (hasExecutableRoute) {
      return;
    }

    const blockedSummary = relevantRoutes
      .map(route => `${route.name}:${route.blockedReason ?? 'disabled'}`)
      .join(', ');
    throw new Error(
      `Subagent evidence task has no executable filesystem/search tools under current worker policy (${blockedSummary})`
    );
  }

  private normalizeTokenUsage(
    usage?: { inputTokens?: number; outputTokens?: number }
  ): NetaClawSubagentRunSummary['tokenUsage'] {
    if (!usage) {
      return null;
    }

    const inputTokens = usage.inputTokens ?? 0;
    const outputTokens = usage.outputTokens ?? 0;

    return {
      inputTokens,
      outputTokens,
      totalTokens: inputTokens + outputTokens,
    };
  }

  private extractPendingProxyToolNames(result: SubagentWorkerResult): string[] {
    const value = result.metadata?.pendingProxyToolNames;
    return Array.isArray(value)
      ? value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
      : [];
  }

  private extractProxiedToolNames(result: SubagentWorkerResult): string[] {
    const value = result.metadata?.proxiedToolNames;
    return Array.isArray(value)
      ? value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
      : [];
  }

  private async executeMainProcessProxyTool(
    event: SubagentProxyToolCallEvent,
    resolvedToolset: Pick<ResolveToolResult, 'tools' | 'toolManifest'>,
    ctx: NetaClawSubagentRunSingleContext
  ): Promise<string> {
    const manifest = resolvedToolset.toolManifest.find(item => item.name === event.name);
    if (!manifest || manifest.workerRoutingHint !== 'main-process-proxy') {
      throw new Error(`Tool "${event.name}" is not approved for main-process proxy execution`);
    }

    const tool = resolvedToolset.tools.find(item => item.name === event.name);
    if (!tool) {
      throw new Error(`Tool "${event.name}" is not available in the resolved subagent toolset`);
    }

    this.assertMainProcessProxyPolicy(manifest, event.args, this.deriveProcessPolicy(ctx, resolvedToolset.toolManifest));

    // v4: 注入完整 runtime context(含 currentAgent + bizContext),让 weixin_send_text 等 tool 能拿到 modelChannelId
    const presetAgent = ctx.task.agentId
      ? await this.agentService.info(ctx.task.agentId)
      : null;
    const runtimeArgs = injectToolRuntimeContext(event.args as Record<string, unknown>, {
      sessionCwd: ctx.sessionCwd ?? null,
      workspaceRoots: this.deriveProcessPolicy(ctx, resolvedToolset.toolManifest).workspaceRoots,
      bizContext: ctx.task.bizContext,
      currentAgent: presetAgent ? {
        id: presetAgent.id,
        name: presetAgent.name ?? '',
        modelChannelId: (presetAgent.modelConfig as any)?.channelId ?? presetAgent.auxiliaryModelChannelId ?? null,
        toolsets: presetAgent.toolsets ?? [],
      } : undefined,
    });
    const result = await tool.execute(event.toolCallId, runtimeArgs as any);
    return typeof result === 'string' ? result : JSON.stringify(result);
  }

  private assertMainProcessProxyPolicy(
    manifest: SubagentToolManifestItem,
    params: Record<string, unknown>,
    policy: SubagentWorkerPolicy
  ) {
    if (manifest.requiresWrite && policy.readonly !== false) {
      throw new Error(`Subagent main-process proxy policy blocks write-capable tool: ${manifest.name}`);
    }

    if (manifest.requiresShell) {
      if (policy.allowShell !== true) {
        throw new Error('Subagent main-process proxy policy blocks shell execution');
      }
      this.assertPathWithinPolicy(
        typeof params.cwd === 'string' ? params.cwd : process.cwd(),
        policy,
        'cwd'
      );
      return;
    }

    if (typeof params.path === 'string') {
      this.assertPathWithinPolicy(params.path, policy, 'path');
    }
  }

  private assertPathWithinPolicy(
    candidate: string,
    policy: SubagentWorkerPolicy,
    fieldName: string
  ) {
    const baseCwd = (policy.workspaceRoots ?? []).find(root => typeof root === 'string' && root.trim().length > 0) || process.cwd();
    const normalizedCandidate = resolveToolPath(candidate, baseCwd);
    const roots = [...new Set((policy.workspaceRoots ?? [])
      .filter(root => typeof root === 'string' && root.trim().length > 0)
      .map(root => path.resolve(root)))];

    if (roots.length === 0) {
      throw new Error(`Subagent main-process proxy policy requires workspace roots before using ${fieldName}`);
    }

    const allowed = roots.some(root =>
      normalizedCandidate === root || normalizedCandidate.startsWith(`${root}${path.sep}`)
    );
    if (!allowed) {
      throw new Error(`Subagent main-process proxy policy blocks ${fieldName} outside workspace roots: ${normalizedCandidate}`);
    }
  }

  private deriveWorkspaceRoots(
    ctx: NetaClawSubagentRunSingleContext,
    toolManifest: SubagentToolManifestItem[],
    configuredWorkspaceRoots: string[]
  ): string[] {
    const taskWithWorkspaceRoots = ctx.task as NetaClawSubagentTask & {
      workspaceRoots?: unknown;
    };
    const taskWorkspaceRoots = Array.isArray(taskWithWorkspaceRoots.workspaceRoots)
      ? taskWithWorkspaceRoots.workspaceRoots.filter((value): value is string => typeof value === 'string')
      : [];
    const inferredWorkspaceRoots = this.deriveWorkspaceRootsFromTaskText(ctx);
    const roots = normalizeWorkspaceRoots([
      ctx.sessionCwd ?? undefined,
      ...taskWorkspaceRoots,
      ...inferredWorkspaceRoots,
      ...configuredWorkspaceRoots,
    ]);

    const needsWorkspaceRoot = toolManifest.some(item => {
      if (this.getWorkerRoutingHint(item) !== 'local' && this.getWorkerRoutingHint(item) !== 'main-process-proxy') {
        return false;
      }
      return item.name === 'bash'
        || item.name === 'read_file'
        || item.name === 'list_dir'
        || item.name === 'find_files'
        || item.name === 'grep'
        || item.name === 'write_file'
        || item.name === 'edit'
        || item.name === 'patch';
    });

    return needsWorkspaceRoot ? roots : configuredWorkspaceRoots;
  }

  private deriveWorkspaceRootsFromTaskText(
    ctx: NetaClawSubagentRunSingleContext
  ): string[] {
    const text = `${ctx.task.goal}\n${ctx.task.context ?? ''}`.toLowerCase();
    const roots: string[] = [];

    if (/(^|[^a-z])desktop([^a-z]|$)|桌面/.test(text)) {
      const desktopDir = path.join(process.env.USERPROFILE ?? '', 'Desktop');
      if (desktopDir.trim().length > 0) {
        roots.push(desktopDir);
      }
    }

    return roots;
  }

  private getWorkerRoutingHint(
    item: SubagentToolManifestItem
  ): 'local' | 'main-process-proxy' | 'disabled' {
    return item.workerRoutingHint ?? (item.supportedInWorker ? 'local' : 'disabled');
  }

  private createExecutionError(recordId: number, cause: unknown): SubagentExecutionError {
    const error = new SubagentExecutionError(
      `Subagent execution failed: ${this.toErrorMessage(cause)}`
    );
    error.name = 'SubagentExecutionError';
    error.cause = cause;
    error.sessionRecordId = recordId;
    return error;
  }

  private toErrorMessage(error: unknown): string {
    if (error instanceof Error) {
      return error.message;
    }
    if (typeof error === 'string') {
      return error;
    }
    return 'Unknown subagent failure';
  }
}
