"use client";

import { useCallback, useEffect, useRef, useState } from "react";

import { abortAgentChat, forkAgentChat, rejectAgentQuestion, replyAgentPermission, replyAgentQuestion, resumeAgentChatStream, streamAgentChat } from "@/lib/chatStream";
import type { PermissionReply, StreamEvent } from "@/lib/chatStream";
import type { AgentArtifact, ChatSessionSummary, Message } from "../GlobalChatbox.types";
import { cloneMessages } from "../globalChatboxUtils";
import { createEmptyChatState, deleteChatSession, listChatSessions, loadChatSessionById, updateChatSessionTitle } from "../chatStorage";
import { applyQuestionResponse, cancelRunningTodos, completeRunningProgress, createAssistantMessage, createTodoUpdateFromEvent, createUserMessage, dedupeQuestionsAcrossMessages, finalizeAssistantMessageAfterAbort, normalizeSessionTodos, toPermissionStatus, upsertPermission, upsertProgress, upsertQuestionAcrossMessages } from "./agentChatSessionState";
import type { PromptRunOptions, UseAgentChatSessionOptions } from "./useAgentChatSession.types";

const TOKEN_PLAYBACK_INTERVAL_MS = 16;
const TOKEN_PLAYBACK_BASE_CHARS = 28;
const TOKEN_PLAYBACK_MAX_CHARS = 160;

const sliceCodePoints = (value: string, count: number) =>
  Array.from(value).slice(0, count).join("");

let cachedSegmenter: Intl.Segmenter | null | undefined;

const getSegmenter = () => {
  if (cachedSegmenter !== undefined) return cachedSegmenter;
  cachedSegmenter =
    typeof Intl !== "undefined" && "Segmenter" in Intl
      ? new Intl.Segmenter("zh", { granularity: "word" })
      : null;
  return cachedSegmenter;
};

const getPlaybackChunkSize = (bufferLength: number) => {
  if (bufferLength >= 600) return TOKEN_PLAYBACK_MAX_CHARS;
  if (bufferLength >= 300) return 112;
  if (bufferLength >= 140) return 72;
  if (bufferLength >= 64) return 44;
  return TOKEN_PLAYBACK_BASE_CHARS;
};

const takeNextTokenPlaybackChunk = (content: string, maxChars: number) => {
  if (content.length <= maxChars) return content;
  const targetChars = Math.max(12, Math.floor(maxChars * 0.68));

  const segmenter = getSegmenter();
  if (segmenter) {
    let chunk = "";
    for (const segment of segmenter.segment(content)) {
      chunk += segment.segment;
      if (
        chunk.length >= maxChars ||
        (chunk.length >= targetChars &&
          /[\s，。！？、；：,.!?;:]/u.test(segment.segment))
      ) {
        return chunk;
      }
    }
  }

  const phrase = content.match(/^.{1,12}?[\s，。！？、；：,.!?;:]+/u)?.[0];
  if (phrase) return phrase;

  const cjkChunk = content.match(
    /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+/u,
  )?.[0];
  if (cjkChunk) return sliceCodePoints(cjkChunk, Math.min(maxChars, 18));

  const wordChunk = content.match(/^\S+\s*/u)?.[0];
  if (wordChunk) {
    return wordChunk.length <= maxChars
      ? wordChunk
      : sliceCodePoints(wordChunk, maxChars);
  }

  return sliceCodePoints(content, Math.min(maxChars, 12));
};

export const useAgentChatSession = ({
  projectId,
  onToolCall,
  onBeforeSend,
  getModel,
  getApprovalMode,
}: UseAgentChatSessionOptions) => {
  const hydrationNonceRef = useRef(0);

  const [messages, setMessages] = useState<Message[]>([]);
  const [sessionTitle, setSessionTitle] = useState<string | undefined>(undefined);
  const [isSessionTitleManuallyEdited, setIsSessionTitleManuallyEdited] = useState(false);
  const [sessionId, setSessionId] = useState<string | undefined>(undefined);
  const [chatSessions, setChatSessions] = useState<ChatSessionSummary[]>([]);
  const [isStreaming, setIsStreaming] = useState(false);
  const [isHydrating, setIsHydrating] = useState(true);
  const [loadingSessionId, setLoadingSessionId] = useState<string | undefined>(undefined);
  const abortRef = useRef<AbortController | null>(null);
  const sessionIdRef = useRef<string | undefined>(undefined);
  const messagesRef = useRef<Message[]>([]);
  const resumeStreamingSessionRef = useRef<((sessionId: string) => void) | null>(null);
  const isSessionTitleManuallyEditedRef = useRef(false);
  const cancelPromiseRef = useRef<Promise<void> | null>(null);
  const titleUpdateNonceRef = useRef(0);
  const pendingTokenRef = useRef<{
    assistantMessageId: string;
    content: string;
  } | null>(null);
  const tokenPlaybackIntervalRef = useRef<number | null>(null);

  useEffect(() => {
    sessionIdRef.current = sessionId;
  }, [sessionId]);

  useEffect(() => {
    messagesRef.current = messages;
  }, [messages]);

  const applyTokenContent = useCallback((assistantMessageId: string, content: string) => {
    if (!content) return;
    setMessages((prev) => {
      const next = prev.map((message) =>
        message.id === assistantMessageId
          ? {
              ...message,
              content: message.content + content,
              isError: false,
            }
          : message,
      );
      messagesRef.current = next;
      return next;
    });
  }, []);

  const cancelTokenPlayback = useCallback(() => {
    const intervalId = tokenPlaybackIntervalRef.current;
    if (intervalId === null) return;
    window.clearInterval(intervalId);
    tokenPlaybackIntervalRef.current = null;
  }, []);

  const flushPendingTokens = useCallback(() => {
    const pending = pendingTokenRef.current;
    pendingTokenRef.current = null;
    cancelTokenPlayback();
    if (!pending) return;
    applyTokenContent(pending.assistantMessageId, pending.content);
  }, [applyTokenContent, cancelTokenPlayback]);

  const scheduleTokenPlayback = useCallback(() => {
    if (tokenPlaybackIntervalRef.current !== null) return;
    const id = window.setInterval(() => {
      const pending = pendingTokenRef.current;
      if (!pending) {
        window.clearInterval(id);
        tokenPlaybackIntervalRef.current = null;
        return;
      }

      const chunk = takeNextTokenPlaybackChunk(
        pending.content,
        getPlaybackChunkSize(pending.content.length),
      );
      if (!chunk) {
        window.clearInterval(id);
        tokenPlaybackIntervalRef.current = null;
        pendingTokenRef.current = null;
        return;
      }

      const remaining = pending.content.slice(chunk.length);
      pendingTokenRef.current = remaining
        ? { assistantMessageId: pending.assistantMessageId, content: remaining }
        : null;
      applyTokenContent(pending.assistantMessageId, chunk);

      if (!remaining) {
        window.clearInterval(id);
        tokenPlaybackIntervalRef.current = null;
      }
    }, TOKEN_PLAYBACK_INTERVAL_MS);
    tokenPlaybackIntervalRef.current = id;
  }, [applyTokenContent]);

  const queueTokenContent = useCallback(
    (assistantMessageId: string, content: string) => {
      const pending = pendingTokenRef.current;
      if (pending && pending.assistantMessageId !== assistantMessageId) {
        flushPendingTokens();
      }
      pendingTokenRef.current = {
        assistantMessageId,
        content:
          pending?.assistantMessageId === assistantMessageId
            ? pending.content + content
            : content,
      };
      scheduleTokenPlayback();
    },
    [flushPendingTokens, scheduleTokenPlayback],
  );

  useEffect(
    () => () => {
      pendingTokenRef.current = null;
      cancelTokenPlayback();
    },
    [cancelTokenPlayback],
  );


  useEffect(() => {
    isSessionTitleManuallyEditedRef.current = isSessionTitleManuallyEdited;
  }, [isSessionTitleManuallyEdited]);

  useEffect(() => {
    let cancelled = false;

    const hydrate = async () => {
      setIsHydrating(true);

      if (!projectId) {
        sessionIdRef.current = undefined;
        hydrationNonceRef.current += 1;
        titleUpdateNonceRef.current += 1;
        setMessages([]);
        setSessionTitle(undefined);
        setIsSessionTitleManuallyEdited(false);
        setSessionId(undefined);
        setChatSessions([]);
        setIsHydrating(false);
        return;
      }

      try {
        const sessions = await listChatSessions();
        const streamingSession = sessions.find((session) => session.isStreaming);
        const loadedState = streamingSession
          ? await loadChatSessionById(streamingSession.id)
          : createEmptyChatState();
        if (cancelled) return;

        sessionIdRef.current = loadedState.sessionId;
        hydrationNonceRef.current += 1;
        titleUpdateNonceRef.current += 1;

        setMessages(
          normalizeSessionTodos(dedupeQuestionsAcrossMessages(loadedState.messages)),
        );
        setSessionTitle(loadedState.title);
        setIsSessionTitleManuallyEdited(loadedState.isTitleManuallyEdited ?? false);
        setSessionId(loadedState.sessionId);
        setChatSessions(sessions);
        if (
          loadedState.sessionId &&
          (loadedState.isStreaming || streamingSession?.isStreaming)
        ) {
          resumeStreamingSessionRef.current?.(loadedState.sessionId);
        }
      } catch (error) {
        console.error("[GlobalChatbox] Failed to hydrate chat state:", error);
      } finally {
        if (!cancelled) {
          setIsHydrating(false);
        }
      }
    };

    void hydrate();

    return () => {
      cancelled = true;
    };
  }, [projectId]);

  const appendArtifact = useCallback((messageId: string, artifact: AgentArtifact) => {
    setMessages((prev) =>
      prev.map((message) =>
        message.id === messageId
          ? {
              ...message,
              artifacts: [...(message.artifacts ?? []), artifact],
            }
          : message,
      ),
    );
  }, []);

  const getLastAssistantMessageId = useCallback((fallback?: string) => {
    const assistant = [...messagesRef.current]
      .reverse()
      .find((message) => message.role === "assistant");
    return assistant?.id ?? fallback;
  }, []);

  const applyStreamEvent = useCallback(
    (
      event: StreamEvent,
      options?: {
        assistantMessageId?: string;
      },
    ) => {
      if (event.type !== "token") {
        flushPendingTokens();
      }

      if (
        event.type !== "session_title" &&
        "sessionId" in event &&
        event.sessionId &&
        event.sessionId !== sessionIdRef.current
      ) {
        sessionIdRef.current = event.sessionId;
        setSessionId(event.sessionId);
      }

      if (event.type === "state") {
        const nextMessages = normalizeSessionTodos(
          dedupeQuestionsAcrossMessages(cloneMessages(event.messages as Message[])),
        );
        messagesRef.current = nextMessages;
        setMessages(nextMessages);
        setIsStreaming(event.isStreaming);
        return;
      }

      if (event.type === "session_title") {
        const nextTitle = event.title.trim();
        if (nextTitle && !isSessionTitleManuallyEditedRef.current) {
          const currentSessionId = sessionIdRef.current;
          const targetSessionId = event.sessionId || currentSessionId;
          if (targetSessionId === currentSessionId) {
            setSessionTitle(nextTitle);
          }
          if (targetSessionId) {
            const currentNonce = ++titleUpdateNonceRef.current;
            void updateChatSessionTitle(targetSessionId, nextTitle, {
              isTitleManuallyEdited: false,
            })
              .then(() => listChatSessions())
              .then((sessions) => {
                if (titleUpdateNonceRef.current !== currentNonce) return;
                setChatSessions(sessions);
              })
              .catch((error) => {
                console.error("[GlobalChatbox] Failed to persist session title:", error);
              });
          }
        }
        return;
      }

      const assistantMessageId = getLastAssistantMessageId(options?.assistantMessageId);
      if (!assistantMessageId) {
        return;
      }

      if (event.type === "token") {
        queueTokenContent(assistantMessageId, event.content);
      } else if (event.type === "progress") {
        setMessages((prev) =>
          prev.map((message) =>
            message.id === assistantMessageId
              ? { ...message, progress: upsertProgress(message.progress, event) }
              : message,
          ),
        );
      } else if (event.type === "tool_call") {
        onToolCall(event, {
          assistantMessageId,
          appendArtifact,
        });
      } else if (event.type === "permission_request") {
        setMessages((prev) =>
          prev.map((message) =>
            message.id === assistantMessageId
              ? {
                  ...message,
                  permissions: upsertPermission(message.permissions, event),
                }
              : message,
          ),
        );
      } else if (event.type === "permission_response") {
        setMessages((prev) =>
          prev.map((message) => {
            if (message.id !== assistantMessageId || !message.permissions?.length) {
              return message;
            }
            return {
              ...message,
              permissions: message.permissions.map((permission) =>
                permission.requestId === event.requestId
                  ? {
                      ...permission,
                      status: toPermissionStatus(event.reply),
                      repliedAt: Date.now(),
                      error: undefined,
                    }
                  : permission,
              ),
            };
          }),
        );
      } else if (event.type === "question_request") {
        setMessages((prev) =>
          upsertQuestionAcrossMessages(prev, event, assistantMessageId),
        );
      } else if (event.type === "question_response") {
        setMessages((prev) =>
          prev.map((message) =>
            message.questions?.some((question) => question.requestId === event.requestId)
              ? {
                  ...message,
                  questions: applyQuestionResponse(message.questions, event),
                }
              : message,
          ),
        );
      } else if (event.type === "todo_update") {
        setMessages((prev) =>
          normalizeSessionTodos(
            prev,
            createTodoUpdateFromEvent(event),
            assistantMessageId,
          ),
        );
      } else if (event.type === "done") {
        setMessages((prev) =>
          prev.map((message) => {
            if (message.id !== assistantMessageId) return message;
            const completedProgress = completeRunningProgress(message.progress);
            if (
              message.content.trim().length === 0 &&
              !(message.artifacts?.length)
            ) {
              return {
                ...message,
                content:
                  "Agent 已完成处理，但没有生成文本回答。请查看过程记录，或换个更具体的问题重试。",
                progress: completedProgress,
              };
            }
            return { ...message, progress: completedProgress };
          }),
        );
        setIsStreaming(false);
      } else if (event.type === "error") {
        setMessages((prev) =>
          prev.map((message) =>
            message.id === assistantMessageId
              ? {
                  ...message,
                  content: message.content || `⚠️ **错误：** ${event.message}`,
                  isError: true,
                  progress: completeRunningProgress(message.progress),
                  todos: cancelRunningTodos(message.todos),
                }
              : message,
          ),
        );
        setIsStreaming(false);
      } else if (event.type === "auth_required") {
        setMessages((prev) =>
          prev.map((message) =>
            message.id === assistantMessageId
              ? {
                  ...message,
                  content: message.content || `⚠️ **${event.message}**`,
                  isError: true,
                  progress: completeRunningProgress(message.progress),
                  todos: cancelRunningTodos(message.todos),
                }
              : message,
          ),
        );
        setIsStreaming(false);
      }
    },
    [
      appendArtifact,
      flushPendingTokens,
      getLastAssistantMessageId,
      onToolCall,
      queueTokenContent,
    ],
  );

  const resumeStreamingSession = useCallback(
    (nextSessionId: string) => {
      const controller = new AbortController();
      abortRef.current?.abort();
      abortRef.current = controller;
      setIsStreaming(true);

      void resumeAgentChatStream({
        sessionId: nextSessionId,
        signal: controller.signal,
        onEvent: (event) => applyStreamEvent(event),
      })
        .catch((error) => {
          flushPendingTokens();
          if (!controller.signal.aborted) {
            console.error("[GlobalChatbox] Failed to resume chat stream:", error);
            setIsStreaming(false);
          }
        })
        .finally(() => {
          flushPendingTokens();
          if (abortRef.current === controller) {
            abortRef.current = null;
          }
        });
    },
    [applyStreamEvent, flushPendingTokens],
  );
  resumeStreamingSessionRef.current = resumeStreamingSession;

  const runPrompt = useCallback(
    async ({
      prompt: rawPrompt,
      sessionIdOverride,
      preparedMessages,
      userMessage,
      assistantMessage,
    }: PromptRunOptions) => {
      const prompt = rawPrompt.trim();
      if (!prompt || isStreaming || isHydrating) return;

      await cancelPromiseRef.current?.catch(() => undefined);
      onBeforeSend?.();

      const nextUserMessage = userMessage ?? createUserMessage(prompt);
      const nextAssistantMessage = assistantMessage ?? createAssistantMessage();
      const nextMessages =
        preparedMessages ??
        [...messages, nextUserMessage, nextAssistantMessage];

      const clonedNextMessages = cloneMessages(nextMessages);
      setIsStreaming(true);
      messagesRef.current = clonedNextMessages;
      setMessages(clonedNextMessages);
      if (sessionIdOverride !== undefined) {
        sessionIdRef.current = sessionIdOverride;
        setSessionId(sessionIdOverride);
      }

      const controller = new AbortController();
      abortRef.current = controller;

      try {
        await streamAgentChat({
          message: prompt,
          sessionId: sessionIdOverride ?? sessionIdRef.current,
          model: getModel?.(),
          approvalMode: getApprovalMode?.(),
          signal: controller.signal,
          onEvent: (event) =>
            applyStreamEvent(event, {
              assistantMessageId: nextAssistantMessage.id,
            }),
        });
      } catch (error) {
        flushPendingTokens();
        if (controller.signal.aborted) {
          setMessages((prev) =>
            prev
              .map((message) =>
                message.id === nextAssistantMessage.id
                  ? finalizeAssistantMessageAfterAbort(message)
                  : message,
              )
              .filter(
                (message) =>
                  !(
                    message.id === nextAssistantMessage.id &&
                    message.role === "assistant" &&
                    message.content.trim().length === 0 &&
                    !(message.artifacts?.length) &&
                    !(message.progress?.length) &&
                    !message.todos
                  ),
              ),
          );
          return;
        }
        setMessages((prev) =>
          prev.map((message) =>
            message.id === nextAssistantMessage.id
              ? {
                  ...message,
                  content: `⚠️ **错误：** ${String(error)}`,
                  isError: true,
                  progress: completeRunningProgress(message.progress),
                }
              : message,
          ),
        );
        setIsStreaming(false);
      } finally {
        flushPendingTokens();
        abortRef.current = null;
        setIsStreaming(false);
      }
    },
    [
      applyStreamEvent,
      flushPendingTokens,
      getApprovalMode,
      getModel,
      isHydrating,
      isStreaming,
      messages,
      onBeforeSend,
    ],
  );

  const abort = useCallback(() => {
    const controller = abortRef.current;
    controller?.abort();
    flushPendingTokens();
    setIsStreaming(false);
    const assistantMessageId = getLastAssistantMessageId();

    if (assistantMessageId) {
      setMessages((prev) =>
        prev.map((message) =>
          message.id === assistantMessageId
            ? finalizeAssistantMessageAfterAbort(message)
            : message,
        ),
      );
    }

    const cancelPromise = abortAgentChat(sessionIdRef.current).catch((error) => {
      console.error("[GlobalChatbox] Failed to abort agent session:", error);
    });
    const trackedCancelPromise = cancelPromise.finally(() => {
      if (cancelPromiseRef.current === trackedCancelPromise) {
        cancelPromiseRef.current = null;
      }
    });
    cancelPromiseRef.current = trackedCancelPromise;
  }, [flushPendingTokens, getLastAssistantMessageId]);

  const replyPermission = useCallback(
    async (requestId: string, reply: PermissionReply) => {
      const target = messagesRef.current
        .flatMap((message) => message.permissions ?? [])
        .find((permission) => permission.requestId === requestId);
      if (!target || target.status === "submitting") {
        return;
      }

      setMessages((prev) =>
        prev.map((message) =>
          !message.permissions?.some((permission) => permission.requestId === requestId)
            ? message
            : {
                ...message,
                permissions: message.permissions.map((permission) =>
                  permission.requestId === requestId
                    ? { ...permission, status: "submitting", error: undefined }
                    : permission,
                ),
              },
        ),
      );

      try {
        await replyAgentPermission(target.sessionId, requestId, reply);
        setMessages((prev) =>
          prev.map((message) =>
            !message.permissions?.some((permission) => permission.requestId === requestId)
              ? message
              : {
                  ...message,
                  permissions: message.permissions.map((permission) =>
                    permission.requestId === requestId
                      ? {
                          ...permission,
                          status: toPermissionStatus(reply),
                          repliedAt: Date.now(),
                          error: undefined,
                        }
                      : permission,
                  ),
                },
          ),
        );
      } catch (error) {
        setMessages((prev) =>
          prev.map((message) =>
            !message.permissions?.some((permission) => permission.requestId === requestId)
              ? message
              : {
                  ...message,
                  permissions: message.permissions.map((permission) =>
                    permission.requestId === requestId
                      ? {
                          ...permission,
                          status: "error",
                          error: error instanceof Error ? error.message : String(error),
                        }
                      : permission,
                  ),
                },
          ),
        );
      }
    },
    [],
  );

  const replyQuestion = useCallback(
    async (requestId: string, answers: string[][]) => {
      const target = messagesRef.current
        .flatMap((message) => message.questions ?? [])
        .find((question) => question.requestId === requestId);
      if (!target || target.status === "submitting") {
        return;
      }

      setMessages((prev) =>
        prev.map((message) =>
          !message.questions?.some((question) => question.requestId === requestId)
            ? message
            : {
                ...message,
                questions: message.questions.map((question) =>
                  question.requestId === requestId
                    ? { ...question, status: "submitting", error: undefined }
                    : question,
                ),
              },
        ),
      );

      try {
        await replyAgentQuestion(target.sessionId, requestId, answers);
        setMessages((prev) =>
          prev.map((message) =>
            !message.questions?.some((question) => question.requestId === requestId)
              ? message
              : {
                  ...message,
                  questions: message.questions.map((question) =>
                    question.requestId === requestId
                      ? {
                          ...question,
                          status: "answered",
                          answers,
                          repliedAt: Date.now(),
                          error: undefined,
                        }
                      : question,
                  ),
                },
          ),
        );
      } catch (error) {
        setMessages((prev) =>
          prev.map((message) =>
            !message.questions?.some((question) => question.requestId === requestId)
              ? message
              : {
                  ...message,
                  questions: message.questions.map((question) =>
                    question.requestId === requestId
                      ? {
                          ...question,
                          status: "error",
                          error: error instanceof Error ? error.message : String(error),
                        }
                      : question,
                  ),
                },
          ),
        );
      }
    },
    [],
  );

  const rejectQuestion = useCallback(
    async (requestId: string) => {
      const target = messagesRef.current
        .flatMap((message) => message.questions ?? [])
        .find((question) => question.requestId === requestId);
      if (!target || target.status === "submitting") {
        return;
      }

      setMessages((prev) =>
        prev.map((message) =>
          !message.questions?.some((question) => question.requestId === requestId)
            ? message
            : {
                ...message,
                questions: message.questions.map((question) =>
                  question.requestId === requestId
                    ? { ...question, status: "submitting", error: undefined }
                    : question,
                ),
              },
        ),
      );

      try {
        await rejectAgentQuestion(target.sessionId, requestId);
        setMessages((prev) =>
          prev.map((message) =>
            !message.questions?.some((question) => question.requestId === requestId)
              ? message
              : {
                  ...message,
                  questions: message.questions.map((question) =>
                    question.requestId === requestId
                      ? {
                          ...question,
                          status: "rejected",
                          repliedAt: Date.now(),
                          error: undefined,
                        }
                      : question,
                  ),
                },
          ),
        );
      } catch (error) {
        setMessages((prev) =>
          prev.map((message) =>
            !message.questions?.some((question) => question.requestId === requestId)
              ? message
              : {
                  ...message,
                  questions: message.questions.map((question) =>
                    question.requestId === requestId
                      ? {
                          ...question,
                          status: "error",
                          error: error instanceof Error ? error.message : String(error),
                        }
                      : question,
                  ),
                },
          ),
        );
      }
    },
    [],
  );

  const createSession = useCallback(() => {
    if (isHydrating || isStreaming) return;

    flushPendingTokens();
    const controller = abortRef.current;
    controller?.abort();
    hydrationNonceRef.current += 1;
    titleUpdateNonceRef.current += 1;
    sessionIdRef.current = undefined;
    setMessages([]);
    setSessionTitle("新对话");
    setIsSessionTitleManuallyEdited(false);
    setSessionId(undefined);
    setIsStreaming(false);
  }, [flushPendingTokens, isHydrating, isStreaming]);

  const switchSession = useCallback(
    async (nextSessionId: string, optimisticTitle?: string) => {
      if (isHydrating || isStreaming || sessionIdRef.current === nextSessionId) {
        return;
      }

      setIsHydrating(true);
      setLoadingSessionId(nextSessionId);
      const nextTitle = optimisticTitle?.trim();
      if (nextTitle) {
        setSessionTitle(nextTitle);
      }
      try {
        const [nextState, sessions] = await Promise.all([
          loadChatSessionById(nextSessionId),
          listChatSessions(),
        ]);

        hydrationNonceRef.current += 1;
        titleUpdateNonceRef.current += 1;
        sessionIdRef.current = nextState.sessionId;
        setMessages(nextState.messages);
        setSessionTitle(nextState.title);
        setIsSessionTitleManuallyEdited(nextState.isTitleManuallyEdited ?? false);
        setSessionId(nextState.sessionId);
        setChatSessions(sessions);
        if (nextState.sessionId && nextState.isStreaming) {
          resumeStreamingSession(nextState.sessionId);
        } else {
          setIsStreaming(false);
        }
      } catch (error) {
        console.error("[GlobalChatbox] Failed to switch chat session:", error);
      } finally {
        setLoadingSessionId(undefined);
        setIsHydrating(false);
      }
    },
    [isHydrating, isStreaming, resumeStreamingSession],
  );

  const removeSession = useCallback(
    async (targetSessionId: string) => {
      if (isHydrating || isStreaming) return;

      setChatSessions((prev) =>
        prev.filter((session) => session.id !== targetSessionId),
      );

      try {
        const nextActiveSessionId = await deleteChatSession(
          targetSessionId,
        );
        const sessions = await listChatSessions();
        setChatSessions(sessions);

        if (sessionIdRef.current !== targetSessionId) {
          return;
        }

        if (!nextActiveSessionId) {
          hydrationNonceRef.current += 1;
          titleUpdateNonceRef.current += 1;
          sessionIdRef.current = undefined;
          setMessages([]);
          setSessionTitle(undefined);
          setIsSessionTitleManuallyEdited(false);
          setSessionId(undefined);
          return;
        }

        setIsHydrating(true);
        const [nextState, sessionsAfterDelete] = await Promise.all([
          loadChatSessionById(nextActiveSessionId),
          listChatSessions(),
        ]);
        hydrationNonceRef.current += 1;
        titleUpdateNonceRef.current += 1;
        sessionIdRef.current = nextState.sessionId;
        setMessages(nextState.messages);
        setSessionTitle(nextState.title);
        setIsSessionTitleManuallyEdited(nextState.isTitleManuallyEdited ?? false);
        setSessionId(nextState.sessionId);
        setChatSessions(sessionsAfterDelete);
      } catch (error) {
        console.error("[GlobalChatbox] Failed to delete chat session:", error);
        try {
          setChatSessions(await listChatSessions());
        } catch (refreshError) {
          console.error("[GlobalChatbox] Failed to refresh chat sessions:", refreshError);
        }
      } finally {
        setIsHydrating(false);
      }
    },
    [isHydrating, isStreaming],
  );

  const sendPrompt = useCallback(
    async (rawPrompt: string) => {
      await runPrompt({ prompt: rawPrompt });
    },
    [runPrompt],
  );

  const renameSession = useCallback(
    async (targetSessionId: string, nextTitle: string) => {
      const normalizedTitle = nextTitle.trim();
      if (!normalizedTitle || isHydrating) return;

      try {
        await updateChatSessionTitle(targetSessionId, normalizedTitle, {
          isTitleManuallyEdited: true,
        });
        const sessions = await listChatSessions();
        setChatSessions(sessions);

        if (sessionIdRef.current === targetSessionId) {
          setSessionTitle(normalizedTitle);
          setIsSessionTitleManuallyEdited(true);
        }
      } catch (error) {
        console.error("[GlobalChatbox] Failed to rename chat session:", error);
      }
    },
    [isHydrating],
  );

  const createBranch = useCallback(
    async (messageId: string) => {
      if (isHydrating || isStreaming) return;

      const assistantIndex = messages.findIndex(
        (message) => message.id === messageId && message.role === "assistant",
      );
      if (assistantIndex < 0) return;

      const currentSessionId = sessionIdRef.current;
      const keepMessageCount = assistantIndex + 1;
      const copiedMessages = cloneMessages(messages.slice(0, keepMessageCount));
      const forkedSessionId = await forkAgentChat(currentSessionId, keepMessageCount);

      sessionIdRef.current = forkedSessionId;
      setSessionId(forkedSessionId);
      messagesRef.current = copiedMessages;
      setMessages(copiedMessages);
      setIsSessionTitleManuallyEdited(false);
      const forkTitle = sessionTitle ? `${sessionTitle} 副本` : "新对话副本";
      setSessionTitle(forkTitle);
      try {
        setChatSessions(await listChatSessions());
      } catch (error) {
        console.error("[GlobalChatbox] Failed to refresh chat sessions after fork:", error);
      }
    },
    [isHydrating, isStreaming, messages, sessionTitle],
  );

  return {
    messages,
    chatSessions,
    activeSessionId: sessionIdRef.current,
    isHydrating,
    loadingSessionId,
    isStreaming,
    sessionTitle,
    sessionId,
    sendPrompt,
    createBranch,
    abort,
    replyPermission,
    replyQuestion,
    rejectQuestion,
    createSession,
    renameSession,
    removeSession,
    switchSession,
  };
};
