import { Router } from "express";
import { z } from "zod";

import { getAgentAuthContext } from "../auth/agentAuth.js";
import {
  agentModelOptions,
  isSupportedModel,
  resolveDefaultModel,
  type SupportedModel,
} from "../chat/models.js";
import { config } from "../config.js";
import { type LearningOrchestrator } from "../learning/orchestrator.js";
import { type SessionTranscriptStore } from "../sessions/transcriptStore.js";
import { logger } from "../logger.js";
import { MemoryStore } from "../memory/store.js";
import { type SessionUiStateStore } from "../sessions/uiStateStore.js";
import { type SessionMetadataStore } from "../sessions/metadataStore.js";
import { type ResultReferenceResolver } from "../results/resolver.js";
import {
  type OpencodeRuntimeAdapter,
} from "../runtime/opencode.js";
import { getRuntimeSessionContext } from "../runtime/sessionContext.js";
import { type ChatSessionBridge } from "../chat/sessionBridge.js";
import { type SessionRecord } from "../sessions/metadataStore.js";
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
import {
  buildPromptWithLearningContext,
  extractLatestFrontendTurn,
  generateSessionTitle,
  shouldGenerateSessionTitle,
  shouldRestoreConversationForRuntime,
} from "./chatSession.js";
import { registerChatAuxiliaryRoutes } from "./chatAuxiliaryRoutes.js";
import { registerChatInteractionRoutes } from "./chatInteractionRoutes.js";
import {
  collectTextContent,
  type PermissionRequestPayload,
  type QuestionRequestPayload,
  streamPromptResponse,
  type TodoUpdatePayload,
} from "./chatStream.js";
import {
  type ActiveRun,
  type RunStatus,
  type StreamSubscriber,
  appendBackendToolArtifact,
  cancelBackendTodos,
  completeBackendProgress,
  createInitialStreamingMessages,
  isObjectRecord,
  toFrontendPermission,
  toPermissionStatus,
  updateLastAssistantMessage,
  updateLastAssistantPermission,
  updateLastAssistantQuestion,
  upsertBackendProgress,
  upsertBackendQuestion,
  upsertBackendTodoUpdate,
} from "./chatUiState.js";

const payloadSchema = z.object({
  message: z.string().min(1).max(10000),
  session_id: z.string().max(128).optional(),
  model: z.string().refine(isSupportedModel, {
    message: "unsupported model",
  }).optional(),
  approval_mode: z.enum(["request", "always"]).optional().default("request"),
});

const createSessionPayloadSchema = z.object({
  session_id: z.string().max(128).optional(),
  parent_session_id: z.string().max(128).optional(),
});

const forkPayloadSchema = z.object({
  session_id: z.string().max(128).optional(),
  keep_message_count: z.coerce.number().int().min(0),
});

const activeRuns = new Map<string, ActiveRun>();
const lastRunStatuses = new Map<string, RunStatus>();

const toSessionUiStateContext = (sessionRecord: SessionRecord) => ({
  sessionId: sessionRecord.sessionId,
});

export const buildForkedSessionUiState = (
  sourceState: { messages?: unknown[] } | null | undefined,
  input: {
    keepMessageCount: number;
    targetSessionId: string;
  },
) => ({
  sessionId: input.targetSessionId,
  isTitleManuallyEdited: false,
  messages: Array.isArray(sourceState?.messages)
    ? sourceState.messages.slice(0, input.keepMessageCount)
    : [],
});

const getSessionRunStatus = (sessionId: string) =>
  activeRuns.get(sessionId)?.status ?? lastRunStatuses.get(sessionId);

const runtimeHasConversation = async (
  runtime: OpencodeRuntimeAdapter,
  sessionId: string,
) => {
  const messages = await runtime.messages(sessionId, 1);
  return messages.some(
    (message) =>
      message.info.role === "user" || message.info.role === "assistant",
  );
};

export const buildChatRouter = (
  sessionBridge: ChatSessionBridge,
  runtime: OpencodeRuntimeAdapter,
  sessionMetadataStore: SessionMetadataStore,
  sessionUiStateStore: SessionUiStateStore,
  memoryStore: MemoryStore,
  sessionTranscriptStore: SessionTranscriptStore,
  learningOrchestrator: LearningOrchestrator,
  resultReferenceResolver: ResultReferenceResolver,
) => {
  const chatRouter = Router();

  chatRouter.get("/models", (_req, res) => {
    res.json({
      default_model: resolveDefaultModel(config.OPENCODE_MODEL),
      models: agentModelOptions,
    });
  });

  chatRouter.post("/sessions", async (req, res) => {
    const parsed = createSessionPayloadSchema.safeParse(req.body ?? {});
    if (!parsed.success) {
      res.status(400).json({
        message: "invalid request payload",
        detail: parsed.error.flatten(),
      });
      return;
    }

    const authContext = getAgentAuthContext(req);
    const projectId = authContext.projectId;
    const userId = authContext.userId;
    const actorKey = toActorKey(userId);
    const projectKey = toProjectKey(projectId);
    const requestedSessionId = parsed.data.session_id?.trim();
    const sessionId = requestedSessionId || (await runtime.createSession()).id;

    const { record, created } = await sessionMetadataStore.ensure({
      actorKey,
      parentSessionId: parsed.data.parent_session_id,
      projectId,
      projectKey,
      sessionId,
      userId,
    });

    res.status(created ? 201 : 200).json({
      session_id: record.sessionId,
      created_at: record.createdAt,
      updated_at: record.updatedAt,
      status: record.status,
      title: record.title,
      parent_session_id: record.parentSessionId,
    });
  });

  chatRouter.get("/sessions", async (req, res) => {
    const authContext = getAgentAuthContext(req);
    const projectId = authContext.projectId;
    const userId = authContext.userId;
    const actorKey = toActorKey(userId);
    const projectKey = toProjectKey(projectId);
    const records = await sessionMetadataStore.list({
      actorKey,
      projectId,
      projectKey,
      userId,
    });
    res.json({
      sessions: records.map((record) => ({
        id: record.sessionId,
        title: record.title ?? "新对话",
        created_at: record.createdAt,
        updated_at: record.updatedAt,
        status: record.status,
        parent_session_id: record.parentSessionId,
        is_streaming: activeRuns.get(record.sessionId)?.status === "running",
        run_status: getSessionRunStatus(record.sessionId),
      })),
    });
  });

  chatRouter.get("/sessions/:session_id", async (req, res) => {
    const sessionId = req.params.session_id?.trim();
    const authContext = getAgentAuthContext(req);
    const projectId = authContext.projectId;
    const userId = authContext.userId;
    const actorKey = toActorKey(userId);
    const projectKey = toProjectKey(projectId);
    if (!sessionId) {
      res.status(400).json({ message: "session_id is required" });
      return;
    }

    const sessionRecord = await sessionMetadataStore.get(
      {
        actorKey,
        projectId,
        projectKey,
        userId,
      },
      sessionId,
    );
    if (!sessionRecord) {
      res.status(404).json({ message: "session not found" });
      return;
    }

    const state = await sessionUiStateStore.read(
      toSessionUiStateContext(sessionRecord),
    );
    res.json({
      id: sessionRecord.sessionId,
      title: sessionRecord.title ?? "新对话",
      is_title_manually_edited: state?.isTitleManuallyEdited ?? false,
      created_at: sessionRecord.createdAt,
      updated_at: sessionRecord.updatedAt,
      status: sessionRecord.status,
      session_id: sessionRecord.sessionId,
      messages: state?.messages ?? [],
      parent_session_id: sessionRecord.parentSessionId,
      is_streaming: activeRuns.get(sessionRecord.sessionId)?.status === "running",
      run_status: getSessionRunStatus(sessionRecord.sessionId),
    });
  });

  chatRouter.get("/sessions/:session_id/runs/current/events", async (req, res) => {
    const sessionId = req.params.session_id?.trim();
    const authContext = getAgentAuthContext(req);
    const projectId = authContext.projectId;
    const userId = authContext.userId;
    const actorKey = toActorKey(userId);
    const projectKey = toProjectKey(projectId);
    if (!sessionId) {
      res.status(400).json({ message: "session_id is required" });
      return;
    }

    const sessionRecord = await sessionMetadataStore.get(
      { actorKey, projectId, projectKey, userId },
      sessionId,
    );
    if (!sessionRecord) {
      res.status(404).json({ message: "session not found" });
      return;
    }

    res.status(200);
    res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
    res.setHeader("Cache-Control", "no-cache");
    res.setHeader("Connection", "keep-alive");
    res.setHeader("X-Accel-Buffering", "no");
    res.flushHeaders?.();

    const run = activeRuns.get(sessionRecord.sessionId);
    const state = await sessionUiStateStore.read(toSessionUiStateContext(sessionRecord));
    res.write(
      toSse("state", {
        session_id: sessionRecord.sessionId,
        messages: state?.messages ?? run?.messages ?? [],
        is_streaming: run?.status === "running",
        run_status: getSessionRunStatus(sessionRecord.sessionId) ?? "completed",
      }),
    );

    if (!run || run.status !== "running") {
      res.end();
      return;
    }

    const subscriber: StreamSubscriber = {
      write: (event, data) => {
        if (!res.writableEnded && !res.destroyed) {
          res.write(toSse(event, data));
        }
      },
      close: () => {
        if (!res.writableEnded && !res.destroyed) {
          res.end();
        }
      },
    };
    run.subscribers.add(subscriber);

    const cleanup = () => {
      run.subscribers.delete(subscriber);
    };
    req.on("close", cleanup);
    res.on("close", cleanup);
  });

  chatRouter.patch("/sessions/:session_id", async (req, res) => {
    const sessionId = req.params.session_id?.trim();
    const title =
      typeof req.body?.title === "string" ? req.body.title.trim() : "";
    const isTitleManuallyEdited =
      typeof req.body?.is_title_manually_edited === "boolean"
        ? req.body.is_title_manually_edited
        : undefined;
    const authContext = getAgentAuthContext(req);
    const projectId = authContext.projectId;
    const userId = authContext.userId;
    const actorKey = toActorKey(userId);
    const projectKey = toProjectKey(projectId);
    if (!sessionId || !title) {
      res.status(400).json({ message: "session_id and title are required" });
      return;
    }
    const sessionRecord = await sessionMetadataStore.get(
      { actorKey, projectId, projectKey, userId },
      sessionId,
    );
    if (!sessionRecord) {
      res.status(404).json({ message: "session not found" });
      return;
    }
    const nextSessionRecord = await sessionMetadataStore.touch(sessionRecord, { title });
    const state = await sessionUiStateStore.read(
      toSessionUiStateContext(nextSessionRecord),
    );
    if (state) {
      await sessionUiStateStore.write(
        toSessionUiStateContext(nextSessionRecord),
        {
        ...state,
        isTitleManuallyEdited:
          isTitleManuallyEdited ?? state.isTitleManuallyEdited,
        },
      );
    }
    res.json({
      id: nextSessionRecord.sessionId,
      title: nextSessionRecord.title,
      updated_at: nextSessionRecord.updatedAt,
    });
  });

  chatRouter.delete("/sessions/:session_id", async (req, res) => {
    const sessionId = req.params.session_id?.trim();
    const authContext = getAgentAuthContext(req);
    const projectId = authContext.projectId;
    const userId = authContext.userId;
    const actorKey = toActorKey(userId);
    const projectKey = toProjectKey(projectId);
    if (!sessionId) {
      res.status(400).json({ message: "session_id is required" });
      return;
    }
    const sessionRecord = await sessionMetadataStore.get(
      { actorKey, projectId, projectKey, userId },
      sessionId,
    );
    if (!sessionRecord) {
      res.status(204).end();
      return;
    }
    await sessionUiStateStore.remove(toSessionUiStateContext(sessionRecord));
    await sessionBridge.deleteSession({
      clientSessionId: sessionRecord.sessionId,
      sessionId: sessionRecord.sessionId,
    });
    activeRuns.delete(sessionRecord.sessionId);
    lastRunStatuses.delete(sessionRecord.sessionId);
    await sessionMetadataStore.remove(sessionRecord);
    res.status(204).end();
  });

  registerChatAuxiliaryRoutes(chatRouter, {
    activeRuns,
    lastRunStatuses,
    resultReferenceResolver,
    sessionBridge,
    sessionMetadataStore,
    sessionUiStateStore,
  });

  registerChatInteractionRoutes(chatRouter, {
    activeRuns,
    runtime,
    sessionMetadataStore,
    sessionUiStateStore,
  });

  chatRouter.post("/sessions/:session_id/forks", async (req, res) => {
    const parsed = forkPayloadSchema.safeParse({
      ...req.body,
      session_id: req.params.session_id,
    });
    if (!parsed.success) {
      res.status(400).json({
        message: "invalid request payload",
        detail: parsed.error.flatten(),
      });
      return;
    }

    try {
      const authContext = getAgentAuthContext(req);
      const projectId = authContext.projectId;
      const traceId = req.header("x-trace-id") ?? undefined;
      const userId = authContext.userId;

      const actorKey = toActorKey(userId);
      const projectKey = toProjectKey(projectId);
      const sourceSessionId = parsed.data.session_id?.trim();
      const sourceSessionRecord = sourceSessionId
        ? await sessionMetadataStore.get(
            {
              actorKey,
              projectId,
              projectKey,
              userId,
            },
            sourceSessionId,
          )
        : null;
      if (!sourceSessionId || !sourceSessionRecord) {
        res.status(404).json({ message: "source session not found" });
        return;
      }
      const forkSession = await runtime.createSession();
      const { record: targetSessionRecord } = await sessionMetadataStore.ensure({
        actorKey,
        parentSessionId: sourceSessionId,
        projectId,
        projectKey,
        sessionId: forkSession.id,
        userId,
      });
      const nextSessionId = targetSessionRecord.sessionId;

      await sessionTranscriptStore.cloneThread(
        {
          actorKey,
          clientSessionId: sourceSessionId,
          projectKey,
          sessionId: sourceSessionId,
        },
        {
          actorKey,
          clientSessionId: nextSessionId,
          projectKey,
          sessionId: nextSessionId,
        },
        parsed.data.keep_message_count,
      );
      const sourceState = await sessionUiStateStore.read(
        toSessionUiStateContext(sourceSessionRecord),
      );
      const forkTitle = sourceSessionRecord.title
        ? `${sourceSessionRecord.title} 副本`
        : "新对话副本";
      const titledTargetSessionRecord = await sessionMetadataStore.touch(
        targetSessionRecord,
        { title: forkTitle },
      );
      await sessionUiStateStore.write(
        toSessionUiStateContext(titledTargetSessionRecord),
        buildForkedSessionUiState(sourceState, {
          keepMessageCount: parsed.data.keep_message_count,
          targetSessionId: nextSessionId,
        }),
      );

      logger.info(
        {
          sourceSessionId: parsed.data.session_id,
          sessionId: nextSessionId,
          traceId,
          projectId,
          keepMessageCount: parsed.data.keep_message_count,
        },
        "forked chat session",
      );

      res.status(200).json({
        session_id: nextSessionId,
      });
    } catch (error) {
      const detail = error instanceof Error ? error.message : String(error);
      logger.error({ err: error }, "chat fork failed");
      res.status(500).json({
        message: "chat fork failed",
        detail,
      });
    }
  });

  chatRouter.post("/sessions/:session_id/runs", async (req, res) => {
    const parsed = payloadSchema.safeParse({
      ...req.body,
      session_id: req.params.session_id,
    });
    if (!parsed.success) {
      res.status(400).json({
        message: "invalid request payload",
        detail: parsed.error.flatten(),
      });
      return;
    }

    try {
      const authContext = getAgentAuthContext(req);
      const accessToken = authContext.accessToken;
      const projectId = authContext.projectId;
      const traceId = req.header("x-trace-id") ?? undefined;
      const userId = authContext.userId;
      const actorKey = toActorKey(userId);
      const projectKey = toProjectKey(projectId);
      const requestedSessionId = parsed.data.session_id?.trim();
      const existingSessionRecord = requestedSessionId
        ? await sessionMetadataStore.get(
            { actorKey, projectId, projectKey, userId },
            requestedSessionId,
          )
        : null;
      const hadExistingRuntimeSession = Boolean(existingSessionRecord);

      const { binding, requestContext, created } = await sessionBridge.resolve({
        sessionId: requestedSessionId,
        accessToken,
        network: authContext.network,
        projectId,
        traceId,
        tokenExpiresAt: authContext.tokenExpiresAt,
        userId,
      });
      const { record: ensuredSessionRecord, created: sessionCreated } =
        await sessionMetadataStore.ensure({
          actorKey,
          projectId,
          projectKey,
          sessionId: binding.sessionId,
          userId,
        });
      const activeSessionRecord = await sessionMetadataStore.touch(ensuredSessionRecord);
      const hasRuntimeConversation = hadExistingRuntimeSession
        ? await runtimeHasConversation(runtime, binding.sessionId)
        : false;
      const shouldRestoreConversation = shouldRestoreConversationForRuntime({
        hadExistingSessionRecord: hadExistingRuntimeSession,
        runtimeHasConversation: hasRuntimeConversation,
      });
      const historyContext = {
        actorKey: requestContext.actorKey,
        clientSessionId: requestContext.clientSessionId,
        projectKey: requestContext.projectKey,
        sessionId: requestContext.clientSessionId,
      };
      const initialSessionState = await sessionUiStateStore.read(
        toSessionUiStateContext(activeSessionRecord),
      );
      const persistedMessages = initialSessionState?.messages ?? [];
      const baseMessages = persistedMessages;
      if (activeRuns.get(activeSessionRecord.sessionId)?.status === "running") {
        res.status(409).json({
          message: "session is already streaming",
          session_id: activeSessionRecord.sessionId,
        });
        return;
      }
      const recentTurns = await sessionTranscriptStore.getRecentTurns(historyContext, 8);

      logger.info(
        {
          clientSessionId: requestContext.clientSessionId,
          sessionId: binding.sessionId,
          created: created || sessionCreated,
          model: parsed.data.model,
          approvalMode: parsed.data.approval_mode,
          traceId: requestContext.traceId,
          projectId: requestContext.projectId,
        },
        "processing chat request",
      );

      res.status(200);
      res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
      res.setHeader("Cache-Control", "no-cache");
      res.setHeader("Connection", "keep-alive");
      res.setHeader("X-Accel-Buffering", "no");
      res.flushHeaders?.();

      const clientSessionId = requestContext.clientSessionId;
      let streamClosed = false;
      const abortController = new AbortController();
      sessionBridge.registerAbortController(clientSessionId, abortController);
      const initialMessages = createInitialStreamingMessages(
        baseMessages,
        parsed.data.message,
      );
      const activeRun: ActiveRun = {
        clientSessionId,
        controller: abortController,
        messages: initialMessages,
        pendingPermissions: new Map(),
        pendingQuestions: new Map(),
        status: "running",
        subscribers: new Set(),
      };
      activeRuns.set(clientSessionId, activeRun);
      lastRunStatuses.set(clientSessionId, "running");
      const sessionUiStateContext = toSessionUiStateContext(activeSessionRecord);
      let persistQueue = sessionUiStateStore.write(sessionUiStateContext, {
        sessionId: activeSessionRecord.sessionId,
        isTitleManuallyEdited: initialSessionState?.isTitleManuallyEdited ?? false,
        messages: initialMessages,
      });
      const queueSessionUiStatePersist = () => {
        const snapshot = {
          sessionId: activeSessionRecord.sessionId,
          isTitleManuallyEdited: initialSessionState?.isTitleManuallyEdited ?? false,
          messages: activeRun.messages,
        };
        persistQueue = persistQueue
          .catch((error) => {
            logger.warn(
              { err: error, sessionId: clientSessionId },
              "failed to persist previous chat stream state",
            );
          })
          .then(() => sessionUiStateStore.write(sessionUiStateContext, snapshot));
        return persistQueue;
      };
      const primarySubscriber: StreamSubscriber = {
        write: (event, data) => {
          if (!streamClosed && !res.writableEnded && !res.destroyed) {
            res.write(toSse(event, data));
          }
        },
        close: () => {
          if (!res.writableEnded && !res.destroyed) {
            res.end();
          }
        },
      };
      activeRun.subscribers.add(primarySubscriber);
      const handleClientClose = () => {
        streamClosed = true;
        activeRun.subscribers.delete(primarySubscriber);
      };

      req.on("close", handleClientClose);
      res.on("close", handleClientClose);

      const publish = (event: string, data: Record<string, unknown>) => {
        if (event === "token") {
          activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
            ...message,
            content: `${typeof message.content === "string" ? message.content : ""}${typeof data.content === "string" ? data.content : ""}`,
            isError: false,
          }));
        } else if (event === "progress") {
          activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
            ...message,
            progress: upsertBackendProgress(message.progress, data),
          }));
        } else if (event === "done") {
          activeRun.status = "completed";
          lastRunStatuses.set(clientSessionId, "completed");
          activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
            ...message,
            content:
              typeof message.content === "string" && message.content.trim()
                ? message.content
                : "Agent 已完成处理，但没有生成文本回答。请查看过程记录，或换个更具体的问题重试。",
            progress: completeBackendProgress(message.progress),
          }));
        } else if (event === "error") {
          activeRun.status = activeRun.status === "aborted" ? "aborted" : "error";
          lastRunStatuses.set(clientSessionId, activeRun.status);
          activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
            ...message,
            content:
              typeof message.content === "string" && message.content.trim()
                ? message.content
                : `⚠️ **错误：** ${typeof data.message === "string" ? data.message : "unknown error"}`,
            isError: true,
            progress: completeBackendProgress(message.progress),
            todos: cancelBackendTodos(message.todos),
          }));
        } else if (event === "auth_required") {
          activeRun.status = "error";
          lastRunStatuses.set(clientSessionId, "error");
          activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
            ...message,
            content:
              typeof data.message === "string"
                ? `⚠️ **${data.message}**`
                : "⚠️ **登录态已过期，请刷新登录后重试**",
            isError: true,
            progress: completeBackendProgress(message.progress),
            todos: cancelBackendTodos(message.todos),
          }));
        } else if (event === "permission_request") {
          const payload = data as PermissionRequestPayload;
          activeRun.pendingPermissions.set(payload.request_id, payload);
          activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
            ...message,
            permissions: [
              ...(Array.isArray(message.permissions) ? message.permissions : []),
              toFrontendPermission(payload),
            ],
          }));
        } else if (event === "permission_response") {
          const requestId =
            typeof data.request_id === "string" ? data.request_id : undefined;
          const reply =
            data.reply === "once" || data.reply === "always" || data.reply === "reject"
              ? data.reply
              : undefined;
          if (requestId && reply) {
            activeRun.pendingPermissions.delete(requestId);
            activeRun.messages = updateLastAssistantPermission(
              activeRun.messages,
              requestId,
              (permission) => ({
                ...permission,
                status: toPermissionStatus(reply),
                repliedAt: Date.now(),
              }),
            );
          }
        } else if (event === "question_request") {
          const payload = data as QuestionRequestPayload;
          let shouldTrackQuestion = true;
          if (payload.tool?.callID) {
            if (payload.request_id !== payload.tool.callID) {
              activeRun.pendingQuestions.delete(payload.tool.callID);
            } else {
              const hasActionableQuestion = [...activeRun.pendingQuestions.values()].some(
                (question) =>
                  question.tool?.callID === payload.tool?.callID &&
                  question.request_id !== payload.tool?.callID,
              );
              if (hasActionableQuestion) {
                activeRun.messages = updateLastAssistantMessage(
                  activeRun.messages,
                  (message) => ({
                    ...message,
                    questions: upsertBackendQuestion(message.questions, payload),
                  }),
                );
                shouldTrackQuestion = false;
              }
            }
          }
          if (shouldTrackQuestion) {
            activeRun.pendingQuestions.set(payload.request_id, payload);
            activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
              ...message,
              questions: upsertBackendQuestion(message.questions, payload),
            }));
          }
        } else if (event === "question_response") {
          const requestId =
            typeof data.request_id === "string" ? data.request_id : undefined;
          if (requestId) {
            activeRun.pendingQuestions.delete(requestId);
            activeRun.messages = updateLastAssistantQuestion(
              activeRun.messages,
              requestId,
              (question) => ({
                ...question,
                status: data.rejected === true ? "rejected" : "answered",
                repliedAt: Date.now(),
                answers: Array.isArray(data.answers) ? data.answers : question.answers,
                error: undefined,
              }),
            );
          }
        } else if (event === "todo_update") {
          const payload = data as TodoUpdatePayload;
          activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
            ...message,
            todos: upsertBackendTodoUpdate(message.todos, payload),
          }));
        } else if (event === "tool_call") {
          activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
            ...message,
            artifacts: appendBackendToolArtifact(message.artifacts, data),
          }));
        }

        for (const subscriber of activeRun.subscribers) {
          subscriber.write(event, data);
        }
        void queueSessionUiStatePersist().catch((error) => {
          logger.warn({ err: error, sessionId: clientSessionId }, "failed to persist chat stream state");
        });
      };

      try {
        const preparedMessage = await buildPromptWithLearningContext(
          memoryStore,
          requestContext.actorKey,
          requestContext.projectKey,
          {
            recentTurns,
            persistedMessages: baseMessages,
            message: parsed.data.message,
            restoreConversation: shouldRestoreConversation,
          },
        );
        const streamResult = await streamPromptResponse({
          runtime,
          sessionId: binding.sessionId,
          clientSessionId,
          message: preparedMessage,
          model: parsed.data.model,
          approvalMode: parsed.data.approval_mode,
          traceId: requestContext.traceId,
          projectId: requestContext.projectId,
          signal: abortController.signal,
          write: (event, data) => {
            publish(event, data);
          },
        });
        await persistQueue.catch((error) => {
          logger.warn({ err: error, sessionId: clientSessionId }, "failed to persist chat stream state");
        });

        const latestRuntimeContext = getRuntimeSessionContext(binding.sessionId);
        if (latestRuntimeContext?.authExpired) {
          publish("auth_required", {
            session_id: clientSessionId,
            reason: latestRuntimeContext.authExpired.reason,
            message: latestRuntimeContext.authExpired.message,
          });
          return;
        }

        if (!streamResult.aborted && !streamResult.failed) {
          const messages = await runtime.messages(binding.sessionId, 60);
          const assistantMessage = [...messages]
            .reverse()
            .find((message) => message.info.role === "assistant");
          const assistantText = collectTextContent(assistantMessage?.parts ?? []);
          const latestSessionRecord =
            (await sessionMetadataStore.get(
              { actorKey, projectId, projectKey, userId },
              activeSessionRecord.sessionId,
            )) ?? activeSessionRecord;
          const latestSessionState = await sessionUiStateStore.read(
            toSessionUiStateContext(latestSessionRecord),
          );
          const existingSessionTitle = latestSessionRecord.title;
          let sessionTitle = existingSessionTitle;
          const shouldGenerateTitle = shouldGenerateSessionTitle({
            recentTurnCount: recentTurns.length,
            isTitleManuallyEdited:
              latestSessionState?.isTitleManuallyEdited ?? false,
          });
          if (shouldGenerateTitle) {
            sessionTitle = await generateSessionTitle(runtime, {
              sessionId: binding.sessionId,
              latestAssistantMessage: assistantText,
              latestUserMessage: parsed.data.message,
              fallbackTitle: existingSessionTitle,
            });
          }
          const nextSessionRecord = await sessionMetadataStore.touch(latestSessionRecord, {
            ...(sessionTitle && sessionTitle !== existingSessionTitle
              ? { title: sessionTitle }
              : {}),
          });
          if (
            shouldGenerateTitle &&
            sessionTitle &&
            sessionTitle !== existingSessionTitle
          ) {
            publish("session_title", {
              session_id: clientSessionId,
              title: sessionTitle,
            });
            await persistQueue.catch((error) => {
              logger.warn({ err: error, sessionId: clientSessionId }, "failed to persist chat stream state");
            });
          }
          const latestTurn = extractLatestFrontendTurn(activeRun.messages);
          if (latestTurn) {
            void learningOrchestrator.onTurnCompleted({
              ...latestTurn,
              requestContext,
              sessionId: clientSessionId,
            }).catch((error) => {
              logger.warn(
                { err: error, sessionId: clientSessionId },
                "stream-completed learning failed",
              );
            });
          }
        }
      } finally {
        if (abortController.signal.aborted) {
          activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
            ...message,
            content:
              typeof message.content === "string" && message.content.trim()
                ? message.content
                : "⚠️ **请求已中断**",
            isError: true,
            progress: completeBackendProgress(message.progress),
            todos: cancelBackendTodos(message.todos),
          }));
          void queueSessionUiStatePersist().catch((error) => {
            logger.warn({ err: error, sessionId: clientSessionId }, "failed to persist aborted chat stream state");
          });
        }
        await persistQueue.catch((error) => {
          logger.warn({ err: error, sessionId: clientSessionId }, "failed to persist chat stream state");
        });
        sessionBridge.finalizeRequest(clientSessionId);
        activeRun.status = abortController.signal.aborted
          ? activeRun.status === "aborted"
            ? "aborted"
            : "aborted"
          : activeRun.status === "running"
            ? "completed"
            : activeRun.status;
        lastRunStatuses.set(clientSessionId, activeRun.status);
        for (const subscriber of activeRun.subscribers) {
          subscriber.close();
        }
        activeRun.subscribers.clear();
        if (
          activeRun.pendingPermissions.size === 0 &&
          activeRun.pendingQuestions.size === 0
        ) {
          activeRuns.delete(clientSessionId);
        }
        streamClosed = true;
        req.off("close", handleClientClose);
        res.off("close", handleClientClose);
      }

      if (!res.writableEnded && !res.destroyed) {
        res.end();
      }
    } catch (error) {
      const detail = error instanceof Error ? error.message : String(error);
      logger.error({ err: error }, "chat stream failed");
      if (res.headersSent) {
        if (!res.writableEnded && !res.destroyed) {
          res.write(toSse("error", {
            message: "chat stream failed",
            detail,
          }));
          res.end();
        }
        return;
      }
      res.status(500).json({
        message: "chat stream failed",
        detail,
      });
    }
  });

  return chatRouter;
};

const toSse = (event: string, data: Record<string, unknown>) =>
  `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
