import { Injectable, ForbiddenException, NotFoundException } from '@nestjs/common';
import { randomUUID } from 'crypto';
import { PrismaService } from '@core/database/prisma/prisma.service';
import type { AgentMessage, AgentMessageType } from '@prisma/client';
import { ProviderRegistry } from '../providers/provider-registry.service';
import type { ProviderMessage, ProviderToolCall } from '../providers/provider.types';
import { ModelRouter } from '../router/model-router.service';
import type { RoutingRequest, TaskType } from '../router/routing.types';
import { TrajectoryService } from '../trajectory/trajectory.service';
import { QuotaService } from '../quota/quota.service';
import { QueryEngine } from '../engine/query-engine.service';
import { ToolRegistry } from '../tools/tool-registry.service';
import { AgentArtifactService } from '../artifact/artifact.service';
import { AgentMemoriesService } from './memories.service';
import { HooksRegistry } from '../hooks/hooks.registry';
import { estimateTokens } from '../utils/token-estimation.util';
import { createLogger } from '@core/observability/logging/config/winston.config';
import { SkipAssertAccess } from '@common/decorators/skip-assert-access.decorator';

const logger = createLogger('AgentMessagesService');

export type StreamEvent =
  | { type: 'message'; message: AgentMessage }
  | { type: 'text_delta'; text: string; iter: number }
  | { type: 'routing'; decision: unknown; decisionId: string }
  | { type: 'ask_user'; turnId: string; question: string; options?: string[] }
  | { type: 'done'; turnId: string; totalLatencyMs: number; iterations: number }
  | { type: 'error'; message: string };

export interface AppendMessageInput {
  sessionId: string;
  organizationId: string;
  turnId: string;
  type: AgentMessageType;
  content?: string;
  payload?: unknown;
  model?: string;
}

@Injectable()
export class AgentMessagesService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly providerRegistry: ProviderRegistry,
    private readonly modelRouter: ModelRouter,
    private readonly trajectory: TrajectoryService,
    private readonly quota: QuotaService,
    private readonly queryEngine: QueryEngine,
    private readonly toolRegistry: ToolRegistry,
    private readonly artifactService: AgentArtifactService,
    private readonly memoriesService: AgentMemoriesService,
    private readonly hooks: HooksRegistry,
  ) {}

  /**
   * PR9: 工具返回结构化数据时自动落 Artifact。
   * 简单启发：output 含 items[] / projects[] / results[] / rows[] 数组 → TABLE。
   */
  private async maybeCreateArtifact(args: {
    organizationId: string;
    userId: string;
    sessionId: string;
    turnId: string;
    toolName: string;
    output: unknown;
  }): Promise<void> {
    try {
      const out = args.output as Record<string, unknown> | null;
      if (!out || typeof out !== 'object') return;
      const arrays = ['items', 'projects', 'results', 'rows'] as const;
      const key = arrays.find((k) => Array.isArray(out[k]));
      if (!key) return;
      const rows = out[key] as Record<string, unknown>[];
      if (rows.length === 0) return;
      const columns = Object.keys(rows[0]).slice(0, 8);
      await this.artifactService.create({
        organizationId: args.organizationId,
        sessionId: args.sessionId,
        turnId: args.turnId,
        createdById: args.userId,
        type: 'TABLE',
        title: `${args.toolName} → ${rows.length} 条结果`,
        data: { columns: columns.map((k) => ({ key: k, label: k })), rows },
      });
    } catch {
      // 落 artifact 失败不阻塞主流程
    }
  }

  @SkipAssertAccess('内部已通过 session.organizationId !== input.organizationId 做归属校验')
  async appendMessage(input: AppendMessageInput): Promise<AgentMessage> {
    const session = await this.prisma.agentSession.findUnique({
      where: { id: input.sessionId },
      select: { organizationId: true, closedAt: true },
    });
    if (!session) throw new NotFoundException('Agent session not found');
    if (session.organizationId !== input.organizationId) {
      throw new ForbiddenException('Cross-organization access denied');
    }
    if (session.closedAt) throw new ForbiddenException('Session is closed');

    const lastMessage = await this.prisma.agentMessage.findFirst({
      where: { sessionId: input.sessionId },
      orderBy: { sequence: 'desc' },
      select: { sequence: true },
    });
    const nextSequence = (lastMessage?.sequence ?? 0) + 1;

    const message = await this.prisma.agentMessage.create({
      data: {
        sessionId: input.sessionId,
        turnId: input.turnId,
        type: input.type,
        content: input.content ?? null,
        payload: input.payload as never,
        model: input.model ?? null,
        sequence: nextSequence,
      },
    });

    await this.prisma.agentSession.update({
      where: { id: input.sessionId },
      data: { updatedAt: new Date() },
    });

    return message;
  }

  /**
   * 一次对话回合：
   *   1. 写 user_prompt（即便后续步骤失败也保留输入）
   *   2. ModelRouter.decide(5 信号) → RoutingDecision
   *   3. 持久化 RoutingDecision
   *   4. ProviderRegistry.invoke(decision.primary)
   *   5. 写 assistant_text + turn_done（含 routing 元数据）
   *   6. 回填 actualLatencyMs（actualCost 待 PR3.6 Cost Tracker）
   *
   * PR3.5 路由层落地，PR3.6 quota 闭环，PR4 TAOR 多 turn 后接续接管。
   */
  /**
   * 入口闸门：校验 session 归属，阻断本 org 用户拿外 org sessionId 投毒 trajectory 哈希链 +
   * 经 summarizer 外发明文。必须先于 trajectory.append / history 读取 / quota / provider 调用。
   */
  /**
   * 在跑的 turn 注册表。POST /agent/messages/:turnId/cancel 通过这个 abort 跑中的 TAOR loop。
   * 只跨 org 不可取消（向量同 sessionId）。
   *
   * TODO(PR0.6 ops baseline): in-memory Map 仅 single-process 安全。多副本部署时 cancel 请求
   * 经 LB 落到不同 replica 会静默 cancelled:false。上 UAT/生产前需换 Redis pub/sub 或
   * sticky session by turnId。决策落点：.learnings/2026-05-16-ffai-agent-ops-deferred.md。
   */
  private readonly activeTurns = new Map<string, { controller: AbortController; orgId: string }>();

  cancelTurn(turnId: string, orgId: string): { cancelled: boolean } {
    const entry = this.activeTurns.get(turnId);
    if (!entry) return { cancelled: false };
    if (entry.orgId !== orgId) {
      throw new ForbiddenException('cross-organization cancel denied');
    }
    entry.controller.abort();
    return { cancelled: true };
  }

  private async assertSessionOwnership(sessionId: string, organizationId: string): Promise<void> {
    const session = await this.prisma.agentSession.findUnique({
      where: { id: sessionId },
      select: { organizationId: true },
    });
    if (!session) throw new NotFoundException(`session ${sessionId} not found`);
    if (session.organizationId !== organizationId) {
      throw new ForbiddenException('cross-organization session access denied');
    }
  }

  async runTurn(args: {
    sessionId: string;
    organizationId: string;
    userId: string;
    prompt: string;
    projectId?: string;
    surface?: RoutingRequest['surface'];
  }): Promise<{ turnId: string; messages: AgentMessage[] }> {
    const turnId = randomUUID();
    const t0 = Date.now();
    const abortController = new AbortController();
    this.activeTurns.set(turnId, { controller: abortController, orgId: args.organizationId });
    try {

    await this.assertSessionOwnership(args.sessionId, args.organizationId);

    // OnTurnStart hook（在 quota / routing / provider 调用前；让 hook 能预算/拦截）
    await this.hooks.fireOnTurnStart({
      organizationId: args.organizationId,
      userId: args.userId,
      sessionId: args.sessionId,
      turnId,
      prompt: args.prompt,
    });

    // PR3.6: 配额硬上限检查（超限直接 ForbiddenException）；软上限 → degrade 后传给 router
    const quotaCheck = await this.quota.assertAllowed({
      organizationId: args.organizationId,
      userId: args.userId,
    });

    // PR4c: 写 turn_started 事件（哈希链起点）
    await this.trajectory.append({
      organizationId: args.organizationId,
      sessionId: args.sessionId,
      turnId,
      eventType: 'TURN_STARTED',
      payload: {
        userId: args.userId,
        surface: args.surface ?? 'web',
        quota: {
          degradeRequested: quotaCheck.degradeRequested,
          remainingTokens: quotaCheck.remainingTokens,
        },
      },
    });

    // 拉历史拼上下文
    const history = await this.prisma.agentMessage.findMany({
      where: {
        sessionId: args.sessionId,
        type: { in: ['USER_PROMPT', 'ASSISTANT_TEXT'] },
      },
      orderBy: { sequence: 'asc' },
    });
    const rawMessages: ProviderMessage[] = [
      ...history
        .filter((m) => m.content)
        .map((m): ProviderMessage => ({
          role: m.type === 'USER_PROMPT' ? 'user' : 'assistant',
          content: m.content!,
        })),
      { role: 'user', content: args.prompt },
    ];
    // PR4b 多层 compaction：先 layer 3（LLM 摘要老历史）再 layer 1/2/5（trim/truncate/sliding）
    const summarized = await this.queryEngine.summarizeOldHistory(rawMessages, {
      estimateTokens,
      summarizeThreshold: 8000,
      keepLastPairs: 3,
    });
    const compacted = this.queryEngine.compactHistory(summarized.messages, {
      tokenBudget: 12000,
      estimateTokens,
    });
    // System prompt 注入顺序：BASE → Persona instructions → Memory
    // 同时把 turn 主循环要用的 planMode/permissionMode 一起拿回，避免后面再跑一次 findUnique
    const sessionRow = await this.prisma.agentSession.findUnique({
      where: { id: args.sessionId },
      select: {
        projectId: true,
        personaId: true,
        planMode: true,
        permissionMode: true,
        persona: { select: { name: true, instructions: true } },
      },
    });
    const systemContent = await this.buildSystemContent(
      args.organizationId,
      args.userId,
      sessionRow,
    );
    const providerMessages: ProviderMessage[] = [
      { role: 'system', content: systemContent },
      ...compacted.messages,
    ];

    // 1. 写 user_prompt
    const userMsg = await this.appendMessage({
      sessionId: args.sessionId,
      organizationId: args.organizationId,
      turnId,
      type: 'USER_PROMPT',
      content: args.prompt,
    });

    // 2. ModelRouter 决策（5 信号）
    const routingRequest: RoutingRequest = {
      taskType: inferTaskType(args.prompt),
      organizationId: args.organizationId,
      userId: args.userId,
      projectId: args.projectId,
      surface: args.surface ?? 'web',
      contextTokens: estimateTokens(providerMessages.map((m) => m.content).join('\n')),
      hasImage: false,
      hasAudio: false,
      turnDepth: 0,
      latencyPriority: 'interactive',
      // PR3.6: 软上限触发 → 自降级到 budget 优先（ModelRouter 倾向便宜模型）
      costPriority: quotaCheck.degradeRequested ? 'budget' : 'standard',
      monthlyBudgetRemaining: quotaCheck.remainingCostUsd,
    };
    const decision = await this.modelRouter.decide(routingRequest);

    // 3. 持久化决策
    const decisionRow = await this.modelRouter.persistDecision({
      organizationId: args.organizationId,
      sessionId: args.sessionId,
      turnId,
      request: routingRequest,
      decision,
    });

    // PR4c: 写 routing_decided 事件
    await this.trajectory.append({
      organizationId: args.organizationId,
      sessionId: args.sessionId,
      turnId,
      eventType: 'ROUTING_DECIDED',
      payload: {
        decisionId: decisionRow.id,
        primary: decision.primary,
        matchSource: decision.matchSource,
        matchedRuleId: decision.matchedRuleId ?? null,
        reasoning: decision.reasoning ?? null,
      },
    });

    // 4. PR4a TAOR 主循环：调 provider → tool_use? → 执行 tool → 再调 → 直到 end_turn
    //    给 LLM 看到的可用工具（按 surface + session mode 过滤）
    //    sessionRow 已在 system prompt 装配处拉回，复用避免重复 findUnique
    const providerTools = this.toolRegistry.listAsProviderTools({
      surface: args.surface ?? 'web',
      planMode: sessionRow?.planMode,
      permissionMode: sessionRow?.permissionMode,
    });

    const writtenMessages: AgentMessage[] = [userMsg];
    let loopMessages = providerMessages;
    let totalInputTokens = 0;
    let totalOutputTokens = 0;
    let finalStopReason = 'end_turn';
    let lastModel = decision.primary.model;
    let assistantText = '';
    const MAX_ITERATIONS = 5;
    let iter = 0;

    while (iter < MAX_ITERATIONS) {
      if (abortController.signal.aborted) {
        finalStopReason = 'cancelled';
        break;
      }
      iter += 1;
      const response = await this.providerRegistry.invoke(
        {
          model: decision.primary.model,
          messages: loopMessages,
          maxTokens: 1024,
          tools: providerTools.length > 0 ? providerTools : undefined,
          routingDecisionId: decisionRow.id,
        },
        { model: decision.primary.model },
      );
      totalInputTokens += response.usage.inputTokens;
      totalOutputTokens += response.usage.outputTokens;
      lastModel = response.resolvedModel ?? response.model;
      finalStopReason = response.stopReason;

      await this.trajectory.append({
        organizationId: args.organizationId,
        sessionId: args.sessionId,
        turnId,
        eventType: 'PROVIDER_INVOKED',
        payload: {
          iter,
          provider: decision.primary.provider,
          model: lastModel,
          usage: response.usage,
          stopReason: response.stopReason,
          toolCallCount: response.toolCalls?.length ?? 0,
        },
      });

      // 没有 tool_calls → 终态：写 assistant_text
      if (response.stopReason !== 'tool_use' || !response.toolCalls?.length) {
        // OnAssistantText hook：允许改写 final text（脱敏 / 修辞）
        const patchedText = await this.hooks.fireOnAssistantText({
          organizationId: args.organizationId,
          userId: args.userId,
          sessionId: args.sessionId,
          turnId,
          text: response.text,
          model: lastModel,
        });
        // M2 auto-extract：剥离 <remember> tag → 写 ai-detected memory，剩下的展示给用户
        const { cleanedText, created } = await this.memoriesService.extractAndPersist(
          args.organizationId,
          args.userId,
          patchedText,
        );
        const assistantMsg = await this.appendMessage({
          sessionId: args.sessionId,
          organizationId: args.organizationId,
          turnId,
          type: 'ASSISTANT_TEXT',
          content: cleanedText,
          model: lastModel,
          payload: {
            iter,
            usage: response.usage,
            providerStopReason: response.stopReason,
            routing: {
              matchSource: decision.matchSource,
              matchedRuleId: decision.matchedRuleId,
              reasoning: decision.reasoning,
            },
            memoriesAutoCreated: created,
          },
        });
        writtenMessages.push(assistantMsg);
        assistantText = cleanedText;
        break;
      }

      // tool_use：拼 assistant message（含 tool_calls）到 loop history，
      // 然后执行每个 tool call，再拼 tool_result 进 loop history
      loopMessages = [
        ...loopMessages,
        {
          role: 'assistant',
          content: response.text ?? '',
          toolCalls: response.toolCalls,
        },
      ];

      for (const call of response.toolCalls) {
        let parsedInput: Record<string, unknown> = {};
        try {
          parsedInput = JSON.parse(call.function.arguments);
        } catch (parseErr) {
          logger.warn(
            `tool_call arguments not valid JSON (tool=${call.function.name} id=${call.id}): ${(parseErr as Error).message}; raw=${call.function.arguments?.slice(0, 200)}`,
          );
          parsedInput = {};
        }

        // 落 TOOL_USE message + trajectory
        const toolUseMsg = await this.appendMessage({
          sessionId: args.sessionId,
          organizationId: args.organizationId,
          turnId,
          type: 'TOOL_USE',
          payload: {
            iter,
            toolCallId: call.id,
            toolName: call.function.name,
            input: parsedInput,
          },
        });
        writtenMessages.push(toolUseMsg);
        await this.trajectory.append({
          organizationId: args.organizationId,
          sessionId: args.sessionId,
          turnId,
          eventType: 'TOOL_CALL',
          payload: { iter, toolCallId: call.id, toolName: call.function.name, input: parsedInput },
        });

        // PreToolUse hooks：可阻断 / 改写 input
        const hookCtx = {
          organizationId: args.organizationId,
          userId: args.userId,
          sessionId: args.sessionId,
          turnId,
          toolName: call.function.name,
          input: parsedInput,
        };
        const preResult = await this.hooks.firePreToolUse(hookCtx);

        // 执行 tool（带 mode 检查 + hook 阻断短路）
        let toolResult: { ok: boolean; output?: unknown; errorMessage?: string };
        const toolStartedAt = Date.now();
        if (preResult.reject) {
          toolResult = { ok: false, errorMessage: `hook_rejected: ${preResult.reason ?? 'no reason'}` };
        } else {
          try {
            this.toolRegistry.assertAvailable(call.function.name, {
              surface: args.surface ?? 'web',
              planMode: sessionRow?.planMode,
              permissionMode: sessionRow?.permissionMode,
            });
            toolResult = await this.toolRegistry.invoke(call.function.name, {
              organizationId: args.organizationId,
              userId: args.userId,
              sessionId: args.sessionId,
              turnId,
              input: preResult.finalInput,
            });
          } catch (err) {
            toolResult = {
              ok: false,
              errorMessage: (err as Error).message,
            };
          }
        }

        // PostToolUse hooks（仅 fire-and-forget；不改 result）
        await this.hooks.firePostToolUse({
          ...hookCtx,
          input: preResult.finalInput,
          ok: toolResult.ok,
          output: toolResult.output,
          errorMessage: toolResult.errorMessage,
          durationMs: Date.now() - toolStartedAt,
        });

        // 落 TOOL_RESULT message + trajectory
        const toolResultMsg = await this.appendMessage({
          sessionId: args.sessionId,
          organizationId: args.organizationId,
          turnId,
          type: 'TOOL_RESULT',
          payload: { iter, toolCallId: call.id, result: toolResult },
        });
        writtenMessages.push(toolResultMsg);
        await this.trajectory.append({
          organizationId: args.organizationId,
          sessionId: args.sessionId,
          turnId,
          eventType: 'TOOL_RESULT',
          payload: { iter, toolCallId: call.id, ok: toolResult.ok },
        });

        // PR9: 工具返回结构化数据 → 落 Artifact 给右栏渲染
        if (toolResult.ok) {
          await this.maybeCreateArtifact({
            organizationId: args.organizationId,
            userId: args.userId,
            sessionId: args.sessionId,
            turnId,
            toolName: call.function.name,
            output: toolResult.output,
          });
        }

        // 拼 tool message 进下轮 loop history
        const toolContent = toolResult.ok
          ? JSON.stringify(toolResult.output)
          : `Error: ${toolResult.errorMessage ?? 'unknown'}`;
        loopMessages = [
          ...loopMessages,
          { role: 'tool', content: toolContent, toolCallId: call.id },
        ];
      }
      // 继续 while loop —— provider 看到 tool_result 后再决策
    }

    if (iter >= MAX_ITERATIONS && finalStopReason === 'tool_use') {
      // 达到上限仍要 tool_use → 强制写一条 assistant 兜底说明
      const fallbackMsg = await this.appendMessage({
        sessionId: args.sessionId,
        organizationId: args.organizationId,
        turnId,
        type: 'ASSISTANT_TEXT',
        content: `（达到 ${MAX_ITERATIONS} 轮工具调用上限，本轮强制终止）${assistantText}`,
        model: lastModel,
        payload: { iter, forcedStop: true },
      });
      writtenMessages.push(fallbackMsg);
      finalStopReason = 'max_iterations';
    }

    // 5. 写 turn_done
    const turnDoneMsg = await this.appendMessage({
      sessionId: args.sessionId,
      organizationId: args.organizationId,
      turnId,
      type: 'TURN_DONE',
      payload: {
        stopReason: finalStopReason,
        iterations: iter,
        usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens },
      },
    });
    writtenMessages.push(turnDoneMsg);
    await this.trajectory.append({
      organizationId: args.organizationId,
      sessionId: args.sessionId,
      turnId,
      eventType: 'TURN_DONE',
      payload: {
        iterations: iter,
        stopReason: finalStopReason,
        totalLatencyMs: Date.now() - t0,
      },
    });

    // 6. 回填 actualLatencyMs + 配额扣减
    const totalTokens = totalInputTokens + totalOutputTokens;
    const estimatedCostUsd = totalTokens * 0.000002;
    await Promise.all([
      this.modelRouter.recordOutcome({
        decisionId: decisionRow.id,
        actualLatencyMs: Date.now() - t0,
        actualCostUsd: estimatedCostUsd,
      }),
      this.quota.recordUsage({
        organizationId: args.organizationId,
        userId: args.userId,
        tokens: totalTokens,
        costUsd: estimatedCostUsd,
      }),
    ]);

    // OnTurnEnd hook（在所有持久化 + 配额扣减完成后；fail-soft）
    await this.hooks.fireOnTurnEnd({
      organizationId: args.organizationId,
      userId: args.userId,
      sessionId: args.sessionId,
      turnId,
      totalLatencyMs: Date.now() - t0,
      iterations: iter,
      stopReason: finalStopReason,
      totalInputTokens,
      totalOutputTokens,
      messages: writtenMessages,
    });

    return { turnId, messages: writtenMessages };
    } finally {
      this.activeTurns.delete(turnId);
    }
  }

  /**
   * PR4a runTurn 的流式版本。同 runTurn 但通过 yield emitter 把中间事件流出去。
   * 控制器把 yield 出来的事件转成 SSE event 写给 client。
   *
   * yield 事件类型：
   *   - 'session'    新建 session 时一次（client 拿到 sessionId）
   *   - 'message'    每条 AgentMessage 写入后一次（user_prompt / tool_use / tool_result / assistant_text / turn_done）
   *   - 'text_delta' assistant 流式增量
   *   - 'routing'    ModelRouter 决策出来后一次
   *   - 'done'       turn 结束
   *   - 'error'      任何阶段失败
   */
  async *runTurnStream(args: {
    sessionId: string;
    organizationId: string;
    userId: string;
    prompt: string;
    projectId?: string;
    surface?: RoutingRequest['surface'];
  }): AsyncGenerator<StreamEvent, void, void> {
    const turnId = randomUUID();
    const t0 = Date.now();
    const abortController = new AbortController();
    this.activeTurns.set(turnId, { controller: abortController, orgId: args.organizationId });

    try {
      await this.assertSessionOwnership(args.sessionId, args.organizationId);

      const quotaCheck = await this.quota.assertAllowed({
        organizationId: args.organizationId,
        userId: args.userId,
      });

      await this.trajectory.append({
        organizationId: args.organizationId,
        sessionId: args.sessionId,
        turnId,
        eventType: 'TURN_STARTED',
        payload: { userId: args.userId, surface: args.surface ?? 'web' },
      });

      const history = await this.prisma.agentMessage.findMany({
        where: { sessionId: args.sessionId, type: { in: ['USER_PROMPT', 'ASSISTANT_TEXT'] } },
        orderBy: { sequence: 'asc' },
      });
      const rawMessages: ProviderMessage[] = [
        ...history
          .filter((m) => m.content)
          .map((m): ProviderMessage => ({
            role: m.type === 'USER_PROMPT' ? 'user' : 'assistant',
            content: m.content!,
          })),
        { role: 'user', content: args.prompt },
      ];
      const summarized = await this.queryEngine.summarizeOldHistory(rawMessages, {
        estimateTokens,
        summarizeThreshold: 8000,
        keepLastPairs: 3,
      });
      const compacted = this.queryEngine.compactHistory(summarized.messages, {
        tokenBudget: 12000,
        estimateTokens,
      });
      // System prompt 注入（与主 runTurn 一致）：BASE + Persona + Memory
      // 同时把 planMode/permissionMode 一起拉回，避免主循环前再跑一次 findUnique
      const sessionRow = await this.prisma.agentSession.findUnique({
        where: { id: args.sessionId },
        select: {
          projectId: true,
          personaId: true,
          planMode: true,
          permissionMode: true,
          persona: { select: { name: true, instructions: true } },
        },
      });
      const systemContent = await this.buildSystemContent(
        args.organizationId,
        args.userId,
        sessionRow,
      );
      let loopMessages: ProviderMessage[] = [
        { role: 'system', content: systemContent },
        ...compacted.messages,
      ];

      const userMsg = await this.appendMessage({
        sessionId: args.sessionId,
        organizationId: args.organizationId,
        turnId,
        type: 'USER_PROMPT',
        content: args.prompt,
      });
      yield { type: 'message', message: userMsg };

      const routingRequest: RoutingRequest = {
        taskType: inferTaskType(args.prompt),
        organizationId: args.organizationId,
        userId: args.userId,
        projectId: args.projectId,
        surface: args.surface ?? 'web',
        contextTokens: estimateTokens(loopMessages.map((m) => m.content).join('\n')),
        hasImage: false,
        hasAudio: false,
        turnDepth: 0,
        latencyPriority: 'interactive',
        costPriority: quotaCheck.degradeRequested ? 'budget' : 'standard',
        monthlyBudgetRemaining: quotaCheck.remainingCostUsd,
      };
      const decision = await this.modelRouter.decide(routingRequest);
      const decisionRow = await this.modelRouter.persistDecision({
        organizationId: args.organizationId,
        sessionId: args.sessionId,
        turnId,
        request: routingRequest,
        decision,
      });
      yield { type: 'routing', decision, decisionId: decisionRow.id };
      await this.trajectory.append({
        organizationId: args.organizationId,
        sessionId: args.sessionId,
        turnId,
        eventType: 'ROUTING_DECIDED',
        payload: { decisionId: decisionRow.id, primary: decision.primary, matchSource: decision.matchSource },
      });

      const providerTools = this.toolRegistry.listAsProviderTools({
        surface: args.surface ?? 'web',
        planMode: sessionRow?.planMode,
        permissionMode: sessionRow?.permissionMode,
      });

      let totalInputTokens = 0;
      let totalOutputTokens = 0;
      let finalStopReason: string = 'end_turn';
      let lastModel = decision.primary.model;
      const MAX_ITER = 5;
      let iter = 0;

      while (iter < MAX_ITER) {
        if (abortController.signal.aborted) {
          finalStopReason = 'cancelled';
          break;
        }
        iter += 1;
        let collectedText = '';
        let collectedToolCalls: ProviderToolCall[] | undefined;
        let stopReason: string = 'end_turn';
        let usage = { inputTokens: 0, outputTokens: 0 };

        for await (const chunk of this.providerRegistry.invokeStream(
          {
            model: decision.primary.model,
            messages: loopMessages,
            maxTokens: 1024,
            tools: providerTools.length > 0 ? providerTools : undefined,
            routingDecisionId: decisionRow.id,
          },
          { model: decision.primary.model },
        )) {
          if (chunk.type === 'text_delta') {
            collectedText += chunk.text;
            yield { type: 'text_delta', text: chunk.text, iter };
          } else if (chunk.type === 'stop') {
            stopReason = chunk.stopReason;
            usage = chunk.usage;
            collectedToolCalls = chunk.toolCalls;
            collectedText = chunk.text || collectedText;
            lastModel = chunk.resolvedModel ?? lastModel;
          }
        }
        totalInputTokens += usage.inputTokens;
        totalOutputTokens += usage.outputTokens;
        finalStopReason = stopReason;
        await this.trajectory.append({
          organizationId: args.organizationId,
          sessionId: args.sessionId,
          turnId,
          eventType: 'PROVIDER_INVOKED',
          payload: { iter, provider: decision.primary.provider, model: lastModel, usage, stopReason, toolCallCount: collectedToolCalls?.length ?? 0 },
        });

        // 终态：写 assistant_text break
        if (stopReason !== 'tool_use' || !collectedToolCalls?.length) {
          // M2 auto-extract：剥离 <remember> tag → 写 ai-detected memory
          const { cleanedText, created } = await this.memoriesService.extractAndPersist(
            args.organizationId,
            args.userId,
            collectedText,
          );
          const assistantMsg = await this.appendMessage({
            sessionId: args.sessionId,
            organizationId: args.organizationId,
            turnId,
            type: 'ASSISTANT_TEXT',
            content: cleanedText,
            model: lastModel,
            payload: { iter, usage, providerStopReason: stopReason, memoriesAutoCreated: created },
          });
          yield { type: 'message', message: assistantMsg };
          break;
        }

        // tool_use：拼 assistant + 执行每个 tool + 拼 tool_result
        loopMessages = [
          ...loopMessages,
          { role: 'assistant', content: collectedText, toolCalls: collectedToolCalls },
        ];

        // PR4a follow-up: 多 tool_use 并发执行（原先串行）+ abort 信号 pre-check 传播。
        // 顺序：① 串行 pre-yield TOOL_USE（保 UI 时间线确定）→ ② Promise.all 并发 invoke
        // → ③ 按原顺序 yield TOOL_RESULT + 拼回 loopMessages（LLM 看到的 tool_result 序与 tool_use 序一致）。
        // 注意：invokeOne 内部 try/catch 把所有异常包成 {ok:false}，永不 reject，Promise.all 足够；
        // 后续若 invokeOne 重构允许 throw，需要改 Promise.allSettled 防止 sibling tool 被一票否决。
        // 限制：tools 内部一旦开跑不会被 abort 中断（需 tool.invoke(signal) 签名改造，进 Phase 2）；
        // 但 abort 在 invoke 启动前会快速短路，与 cancel endpoint 联动改善长 tool 排队的可中断性。
        const parsedInputs: Record<string, unknown>[] = collectedToolCalls.map((call) => {
          try {
            return JSON.parse(call.function.arguments);
          } catch {
            return {};
          }
        });

        // ① pre-yield TOOL_USE in order
        for (let i = 0; i < collectedToolCalls.length; i++) {
          const call = collectedToolCalls[i];
          const toolUseMsg = await this.appendMessage({
            sessionId: args.sessionId,
            organizationId: args.organizationId,
            turnId,
            type: 'TOOL_USE',
            payload: { iter, toolCallId: call.id, toolName: call.function.name, input: parsedInputs[i] },
          });
          yield { type: 'message', message: toolUseMsg };
          await this.trajectory.append({
            organizationId: args.organizationId,
            sessionId: args.sessionId,
            turnId,
            eventType: 'TOOL_CALL',
            payload: { iter, toolCallId: call.id, toolName: call.function.name },
          });
        }

        // ② concurrent invoke; invokeOne 永不 reject（内部 try/catch → {ok:false}），Promise.all 安全
        const invokeOne = async (
          call: ProviderToolCall,
          input: Record<string, unknown>,
        ): Promise<{ ok: boolean; output?: unknown; errorMessage?: string }> => {
          if (abortController.signal.aborted) {
            return { ok: false, errorMessage: 'cancelled' };
          }
          try {
            this.toolRegistry.assertAvailable(call.function.name, {
              surface: args.surface ?? 'web',
              planMode: sessionRow?.planMode,
              permissionMode: sessionRow?.permissionMode,
            });
            return await this.toolRegistry.invoke(call.function.name, {
              organizationId: args.organizationId,
              userId: args.userId,
              sessionId: args.sessionId,
              turnId,
              input,
            });
          } catch (err) {
            return { ok: false, errorMessage: (err as Error).message };
          }
        };
        const settled = await Promise.all(
          collectedToolCalls.map((call, i) => invokeOne(call, parsedInputs[i])),
        );

        // ③ yield TOOL_RESULT in original order + write loopMessages
        // 同 iter 多个 ask_user 时收集全部（不覆盖），后续在 TAOR break 前统一 yield
        const pendingQuestions: Array<{ question: string; options?: string[] }> = [];
        for (let i = 0; i < collectedToolCalls.length; i++) {
          const call = collectedToolCalls[i];
          const toolResult = settled[i];
          // PR4.5 follow-up: ask_user tool 用 pendingQuestion 字段触发 SSE ask_user 事件
          if (
            toolResult.ok &&
            toolResult.output &&
            typeof toolResult.output === 'object' &&
            'pendingQuestion' in (toolResult.output as Record<string, unknown>)
          ) {
            const pq = (toolResult.output as { pendingQuestion?: unknown }).pendingQuestion;
            if (pq && typeof pq === 'object' && 'question' in (pq as Record<string, unknown>)) {
              pendingQuestions.push(pq as { question: string; options?: string[] });
            }
          }
          const toolResultMsg = await this.appendMessage({
            sessionId: args.sessionId,
            organizationId: args.organizationId,
            turnId,
            type: 'TOOL_RESULT',
            payload: { iter, toolCallId: call.id, result: toolResult },
          });
          yield { type: 'message', message: toolResultMsg };
          await this.trajectory.append({
            organizationId: args.organizationId,
            sessionId: args.sessionId,
            turnId,
            eventType: 'TOOL_RESULT',
            payload: { iter, toolCallId: call.id, ok: toolResult.ok },
          });

          if (toolResult.ok) {
            await this.maybeCreateArtifact({
              organizationId: args.organizationId,
              userId: args.userId,
              sessionId: args.sessionId,
              turnId,
              toolName: call.function.name,
              output: toolResult.output,
            });
          }

          const toolContent = toolResult.ok
            ? JSON.stringify(toolResult.output)
            : `Error: ${toolResult.errorMessage ?? 'unknown'}`;
          loopMessages = [
            ...loopMessages,
            { role: 'tool', content: toolContent, toolCallId: call.id },
          ];
        }

        // 任一 tool 返回 pendingQuestion → halt TAOR loop。多个 question 全部 yield 给前端
        // （前端可按顺序逐个渲染或合并展示），不丢任何一个。
        if (pendingQuestions.length > 0) {
          for (const pq of pendingQuestions) {
            yield {
              type: 'ask_user',
              turnId,
              question: pq.question,
              options: pq.options,
            };
          }
          finalStopReason = 'ask_user';
          break;
        }
      }

      const turnDoneMsg = await this.appendMessage({
        sessionId: args.sessionId,
        organizationId: args.organizationId,
        turnId,
        type: 'TURN_DONE',
        payload: { stopReason: finalStopReason, iterations: iter, usage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens } },
      });
      yield { type: 'message', message: turnDoneMsg };
      await this.trajectory.append({
        organizationId: args.organizationId,
        sessionId: args.sessionId,
        turnId,
        eventType: 'TURN_DONE',
        payload: { iterations: iter, stopReason: finalStopReason, totalLatencyMs: Date.now() - t0 },
      });

      const totalTokens = totalInputTokens + totalOutputTokens;
      const estimatedCostUsd = totalTokens * 0.000002;
      await Promise.all([
        this.modelRouter.recordOutcome({
          decisionId: decisionRow.id,
          actualLatencyMs: Date.now() - t0,
          actualCostUsd: estimatedCostUsd,
        }),
        this.quota.recordUsage({
          organizationId: args.organizationId,
          userId: args.userId,
          tokens: totalTokens,
          costUsd: estimatedCostUsd,
        }),
      ]);

      yield { type: 'done', turnId, totalLatencyMs: Date.now() - t0, iterations: iter };
    } catch (err) {
      yield { type: 'error', message: (err as Error).message };
    } finally {
      this.activeTurns.delete(turnId);
    }
  }

  /**
   * 统一拼装 system prompt：BASE → Persona → Memory。runTurn 和 runTurnStream 共用。
   * 入参 sessionRow 是上游已拉回的 session 行（含 projectId / personaId / persona）；
   * null 代表 session 不存在（理论不会发生，TAOR 已校验过 ownership），但兼容处理
   * 让本函数无副作用。
   */
  private async buildSystemContent(
    organizationId: string,
    userId: string,
    sessionRow:
      | {
          projectId: string | null;
          personaId: string | null;
          persona: { name: string; instructions: string | null } | null;
        }
      | null,
  ): Promise<string> {
    const memorySection = await this.memoriesService.buildSystemPromptSection(
      organizationId,
      userId,
      { projectId: sessionRow?.projectId, personaId: sessionRow?.personaId },
    );
    return composeSystemPrompt({
      personaName: sessionRow?.persona?.name ?? null,
      personaInstructions: sessionRow?.persona?.instructions ?? null,
      memorySection,
    });
  }

  /** @deprecated 兼容旧入口；新代码用 runTurn() */
  async runMockTurn(args: {
    sessionId: string;
    organizationId: string;
    prompt: string;
  }): Promise<{ turnId: string; messages: AgentMessage[] }> {
    return this.runTurn({ ...args, userId: '00000000-0000-0000-0000-000000000000' });
  }
}

/**
 * 极简任务类型推断（PR3.5 启动用）。PR3.6 + 启用 LLM-routed 策略后这里被 LLM 分类器替换。
 */
function inferTaskType(prompt: string): TaskType {
  const lower = prompt.toLowerCase();
  if (/翻译|translate/i.test(prompt)) return 'translation';
  if (/分类|classify/i.test(prompt)) return 'classification';
  if (/```|code|代码|function|class /i.test(lower)) return 'code';
  if (/为什么|why|分析|reason|explain/i.test(lower)) return 'reasoning';
  return 'chat';
}


/**
 * 拼装最终 system prompt：BASE → Persona instructions → Memory 三段
 * - persona instructions 没有时跳过该段，保持 BASE_SYSTEM_PROMPT 整洁
 * - memory section 内部已含 header，空时返空串
 */
function composeSystemPrompt(input: {
  personaName: string | null;
  personaInstructions: string | null;
  memorySection: string;
}): string {
  const parts: string[] = [BASE_SYSTEM_PROMPT];
  if (input.personaInstructions && input.personaInstructions.trim()) {
    const name = input.personaName ?? 'Persona';
    parts.push(
      `## 当前 Persona：${name}\n以下是该 persona 的角色指令，对本会话所有回复都生效；其优先级高于 BASE 段中的通用风格指令。\n\n${input.personaInstructions.trim()}`,
    );
  }
  if (input.memorySection) {
    parts.push(input.memorySection);
  }
  return parts.join('\n\n');
}

/**
 * Base system prompt —— 定义 agent 边界 + 工具选择规则。
 * 模块级 const 求值一次复用；不依赖 persona / 用户上下文。
 */
const BASE_SYSTEM_PROMPT = [
    '你是 FF AI Workspace 企业内部助手，为 FF（Faraday Future）员工服务。',
    '',
    '工具选择红线：',
    '- 内部信息（公司制度 / HR 流程 / SOP / 项目数据 / 审批模板 / 内部 FAQ）→ 走 knowledge_query / project_query / approval_submit',
    '- 外部世界（其他公司 / 公开新闻 / 第三方资讯 / 实时事件 / 技术文档查询）→ 走 web_search',
    '- 已有 URL 想读全文 / 用户粘 URL 求总结 → 走 web_fetch（不是 web_search）',
    '- 用户问外部公司（如 "Freddie Future 融资"、"Tesla 新车型"、"某某 API 怎么用"）→ **直接调 web_search，不要试 knowledge_query**',
    '- 拿不准内部 vs 外部时优先 web_search；knowledge_query 失败不要重试',
    '',
    '多步任务规则：',
    '- 用户问一句复杂多步任务（≥ 3 步）→ 先调 TodoWrite 列计划，再逐项执行，每完成一项重写 TodoWrite 标该项 completed=true',
    '- 需要派给 sub-agent 干长活 → 走 TaskCreate（持久化），不要用 TodoWrite',
    '',
    '回答规则：',
    '- 不要臆造数据（融资额、人名、政策细节）。没工具支撑就承认 "我目前没办法查到"',
    '- 工具失败时换思路或反问用户，不要无限重试同一工具',
    '- 中文用户用中文回；英文用户用英文回；混合输入跟用户最后一句的语言',
    '',
    '长期记忆（Memory）沉淀规则：',
    '- 当本轮对话中发现**值得跨会话保留**的事实时，在回答末尾追加：',
    '    <remember category="USER|FEEDBACK|PROJECT|REFERENCE">一句话事实</remember>',
    '  系统会自动收录到该用户的长期记忆，下次对话自动注入。tag 不会展示给用户。',
    '- category 含义：USER=用户偏好/背景 / FEEDBACK=协作约束/纠正/规则 / PROJECT=当前工作上下文 / REFERENCE=外部资源链接',
    '- 只记**事实和约束**，不记当下任务进度。不要复述已经在 Memory 段里出现过的内容。',
    '- 一轮最多写 3 条 remember。每条独立成行，绝对值的事实写 absolute（"2026 年用 Node 22"，不写 "今年"）。',
].join('\n');
