import { useCallback, useEffect, useRef, useState } from 'react';
import {
  ApiError,
  applyLocalLlmFallback,
  bootstrapProjectMemory,
  cancelRequest,
  confirmTool,
  deleteChatSession,
  getMindSpace,
  getMe,
  listNotifications,
  markNotificationRead,
  getSession,
  listSessions,
  loadSessionDetail,
  readConfig,
  rememberProjectContext,
  uploadMindSpaceAsset,
  resumeSession,
  sendReply,
  startSession,
  subscribeNotificationEvents,
  subscribeSessionEvents,
  updateProvider,
} from '../api/client';
import { appConfig } from '../config';
import type {
  CapabilityMap,
  ChatState,
  Message,
  MindSpaceChatContext,
  PortalUser,
  Session,
  SessionEvent,
  ToolConfirmation,
  UserNotification,
} from '../types';
import { buildContextPrefix } from '../utils/mindspaceChatContext';
import { buildUserAddressPrefix } from '../utils/userAddress';
import { buildAutoChatSkillPrefix } from '../../chat-skills.mjs';
import {
  CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
  CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES,
  CHAT_IMAGE_MAX_SIDE,
  compressImageForUpload,
} from '../utils/imageUpload';
import { buildAbsoluteAssetImageUrl } from '../utils/mindspaceCards';
import {
  buildUserMessage,
  normalizeConversationMessages,
  getDisplayText,
  getToolConfirmation,
  getVisibleText,
  isCreditsExhaustedNotification,
  isRelayServerErrorMessage,
  pushMessage,
} from '../utils/message';
import { prependUnique, shouldShowNewChatTitle, sortAndTrim, touchSession } from '../utils/sessions';

const LEGACY_SESSION_KEY = 'tkmind-h5-session-id';

function resolveSessionStorageKey(userId?: string | null): string {
  return userId ? `${LEGACY_SESSION_KEY}:${userId}` : LEGACY_SESSION_KEY;
}

function readStoredSessionId(userId?: string | null): string | null {
  const scopedKey = resolveSessionStorageKey(userId);
  const scopedValue = localStorage.getItem(scopedKey);
  if (scopedValue) return scopedValue;
  const legacyValue = localStorage.getItem(LEGACY_SESSION_KEY);
  if (legacyValue && userId) {
    localStorage.setItem(scopedKey, legacyValue);
    localStorage.removeItem(LEGACY_SESSION_KEY);
    return legacyValue;
  }
  return legacyValue;
}

function writeStoredSessionId(userId: string | null | undefined, sessionId: string) {
  localStorage.setItem(resolveSessionStorageKey(userId), sessionId);
}

function clearStoredSessionId(userId?: string | null) {
  localStorage.removeItem(resolveSessionStorageKey(userId));
  if (!userId) {
    localStorage.removeItem(LEGACY_SESSION_KEY);
  }
}

const INSUFFICIENT_BALANCE_NOTICE = '余额不足，请充值后继续使用';

export { INSUFFICIENT_BALANCE_NOTICE };

export function useTKMindChat(
  user?: PortalUser | null,
  onUserUpdate?: (user: PortalUser) => void,
  capabilities?: CapabilityMap | null,
  grantedSkills?: string[],
) {
  const canUseProjectMemory = Boolean(capabilities?.context_memory);
  const [session, setSession] = useState<Session | null>(null);
  const [sessions, setSessions] = useState<Session[]>([]);
  const [sessionsLoading, setSessionsLoading] = useState(false);
  const [messages, setMessages] = useState<Message[]>([]);
  const [chatState, setChatState] = useState<ChatState>('loading');
  const [error, setError] = useState<string | null>(null);
  const [notice, setNotice] = useState<string | null>(null);
  const [pendingTool, setPendingTool] = useState<ToolConfirmation | null>(null);
  const [memoryLoading, setMemoryLoading] = useState(false);
  const [rechargePrompt, setRechargePrompt] = useState(false);
  const [rechargeForced, setRechargeForced] = useState(false);
  const [subscribePrompt, setSubscribePrompt] = useState(false);
  const [activeNotification, setActiveNotification] = useState<UserNotification | null>(null);
  const seenNotificationIdsRef = useRef<Set<string>>(new Set());

  const activeRequestId = useRef<string | null>(null);
  const unsubscribeRef = useRef<(() => void) | null>(null);
  const connectTokenRef = useRef(0);
  const messagesRef = useRef<Message[]>([]);
  const sessionRef = useRef<Session | null>(null);
  const sessionsRef = useRef<Session[]>([]);
  const rememberedContextRef = useRef<string | null>(null);
  const rememberInFlightRef = useRef(false);
  const fallbackRetriedRef = useRef(new Set<string>());
  const userRef = useRef(user);
  const onUserUpdateRef = useRef(onUserUpdate);
  const chatImageCategoryIdRef = useRef<string | null>(null);

  const dismissNotice = useCallback(() => {
    const currentNotification = activeNotification;
    setNotice(null);
    setActiveNotification(null);
    if (currentNotification) {
      void markNotificationRead(currentNotification.id).catch(() => {});
    }
  }, [activeNotification]);

  const notifyInsufficientBalance = useCallback(() => {
    setNotice(INSUFFICIENT_BALANCE_NOTICE);
    setChatState('idle');
  }, []);

  const openRecharge = useCallback((forced = false) => {
    setRechargeForced(forced);
    setRechargePrompt(true);
  }, []);

  const dismissRecharge = useCallback(() => {
    if (rechargeForced) return;
    setRechargePrompt(false);
  }, [rechargeForced]);

  const completeRecharge = useCallback((nextBalance?: number) => {
    setRechargeForced(false);
    setRechargePrompt(false);
    const currentUser = userRef.current;
    const updateUser = onUserUpdateRef.current;
    if (typeof nextBalance === 'number' && currentUser && updateUser) {
      const prevBalance = currentUser.balanceCents;
      const prevTotal = currentUser.totalCreditCents ?? prevBalance;
      const rechargeDelta = nextBalance - prevBalance;
      updateUser({
        ...currentUser,
        balanceCents: nextBalance,
        totalCreditCents: Math.max(prevTotal + rechargeDelta, nextBalance),
      });
    }
  }, []);

  const openSubscribe = useCallback(() => {
    setSubscribePrompt(true);
  }, []);

  const dismissSubscribe = useCallback(() => {
    setSubscribePrompt(false);
  }, []);

  const completeSubscribe = useCallback((nextBalanceCents: number, subscription: import('../types').ActiveSubscription) => {
    setSubscribePrompt(false);
    const currentUser = userRef.current;
    const updateUser = onUserUpdateRef.current;
    if (currentUser && updateUser) {
      updateUser({
        ...currentUser,
        balanceCents: nextBalanceCents,
        subscription,
        planType: subscription.planType,
      });
    }
  }, []);

  useEffect(() => {
    userRef.current = user;
  }, [user]);

  useEffect(() => {
    onUserUpdateRef.current = onUserUpdate;
  }, [onUserUpdate]);

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

  useEffect(() => {
    sessionRef.current = session;
  }, [session]);

  useEffect(() => {
    sessionsRef.current = sessions;
  }, [sessions]);

  useEffect(() => {
    if (!user?.id) return;
    let cancelled = false;

    const showNotification = (notification: UserNotification) => {
      if (cancelled) return;
      const hasSeen = seenNotificationIdsRef.current.has(notification.id);
      if (activeNotification?.id === notification.id && hasSeen) return;
      seenNotificationIdsRef.current.add(notification.id);
      setActiveNotification(notification);
      setNotice(`${notification.title}\n${notification.body}`.trim());
    };

    const pullNotifications = async () => {
      try {
        const notifications = await listNotifications('unread', 10);
        if (cancelled) return;
        if (notifications.length === 0) {
          seenNotificationIdsRef.current.clear();
          return;
        }
        const currentIds = new Set(notifications.map((item) => item.id));
        seenNotificationIdsRef.current.forEach((id) => {
          if (!currentIds.has(id)) seenNotificationIdsRef.current.delete(id);
        });
        showNotification(notifications[0]);
      } catch {
        // Ignore notification polling failures to avoid disrupting chat.
      }
    };

    const handleForegroundSync = () => {
      if (document.visibilityState === 'visible') {
        void pullNotifications();
      }
    };

    void pullNotifications();
    const unsubscribe = subscribeNotificationEvents({
      onNotification: showNotification,
      onSync: () => void pullNotifications(),
    });
    window.addEventListener('focus', handleForegroundSync);
    document.addEventListener('visibilitychange', handleForegroundSync);

    return () => {
      cancelled = true;
      unsubscribe();
      window.removeEventListener('focus', handleForegroundSync);
      document.removeEventListener('visibilitychange', handleForegroundSync);
    };
  }, [user?.id, activeNotification?.id]);

  const loadProjectMemory = useCallback(async (sessionId: string, force: boolean) => {
    if (!canUseProjectMemory) return null;
    setMemoryLoading(true);
    try {
      const result = await bootstrapProjectMemory(sessionId, appConfig.memoryQuery, force);
      if (force) {
        setNotice(
          result.summary
            ? `项目记忆已刷新（${result.source}）`
            : '未找到可用的项目记忆，当前会话仍可正常使用',
        );
      }
      return result;
    } catch (err) {
      setNotice(`项目记忆加载失败：${err instanceof Error ? err.message : String(err)}`);
      return null;
    } finally {
      setMemoryLoading(false);
    }
  }, [canUseProjectMemory]);

  const rememberRecentContext = useCallback(
    async ({
      silent = false,
      sessionId,
      sessionName,
      recentContext,
    }: {
      silent?: boolean;
      sessionId?: string;
      sessionName?: string;
      recentContext?: string;
    } = {}) => {
      const currentSession = sessionId
        ? {
            id: sessionId,
            name: sessionName ?? sessionRef.current?.name ?? sessionId,
          }
        : sessionRef.current;
      if (!currentSession) return false;
      if (rememberInFlightRef.current) return false;

      const contextText =
        recentContext ??
        messagesRef.current
          .filter((message) => getDisplayText(message).trim())
          .slice(-6)
          .map((message) => `${message.role === 'user' ? 'User' : 'Assistant'}: ${getDisplayText(message)}`)
          .join('\n\n')
          .slice(0, 6_000);

      if (!canUseProjectMemory) {
        return false;
      }

      if (!contextText) {
        if (!silent) setNotice('当前还没有可保存的对话内容');
        return false;
      }

      const contextKey = `${currentSession.id}:${contextText}`;
      if (rememberedContextRef.current === contextKey) {
        return false;
      }

      rememberInFlightRef.current = true;
      if (!silent) {
        setMemoryLoading(true);
      }

      try {
        await rememberProjectContext(
          currentSession.id,
          contextText,
          `H5 会话 ${currentSession.name}`,
        );
        rememberedContextRef.current = contextKey;
        await loadProjectMemory(currentSession.id, true);
        if (!silent) {
          setNotice('最近对话已写入长期记忆，并同步到当前会话');
        }
        return true;
      } catch (err) {
        if (!silent) {
          setNotice(`保存项目记忆失败：${err instanceof Error ? err.message : String(err)}`);
        }
        return false;
      } finally {
        rememberInFlightRef.current = false;
        if (!silent) {
          setMemoryLoading(false);
        }
      }
    },
    [canUseProjectMemory, loadProjectMemory],
  );

  const refreshSessions = useCallback(async () => {
    setSessionsLoading(true);
    try {
      const items = await listSessions();
      setSessions(sortAndTrim(items));
    } catch (err) {
      if (err instanceof ApiError && err.status === 0) {
        setError('网络不可用，无法刷新历史列表');
      }
    } finally {
      setSessionsLoading(false);
    }
  }, []);

  const resolveChatImageUploadCategoryId = useCallback(async () => {
    if (chatImageCategoryIdRef.current) return chatImageCategoryIdRef.current;

    const space = await getMindSpace();
    const category =
      space.categories.find((item) => item.code === 'private') ??
      space.categories.find((item) => item.code === 'oa') ??
      space.categories.find((item) => item.code === 'public') ??
      space.categories[0];
    if (!category) {
      throw new Error('当前空间暂无可用的图片上传分类');
    }

    chatImageCategoryIdRef.current = category.id;
    return category.id;
  }, []);

  const uploadChatImage = useCallback(async (file: File): Promise<string> => {
    if (!file.type.startsWith('image/')) {
      throw new Error('只支持图片文件');
    }
    const compressed = await compressImageForUpload(file, {
      maxInputBytes: CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
      maxOutputBytes: CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES,
      maxDimension: CHAT_IMAGE_MAX_SIDE,
    });
    const categoryId = await resolveChatImageUploadCategoryId();
    const asset = await uploadMindSpaceAsset(categoryId, compressed);
    return buildAbsoluteAssetImageUrl({
      id: asset.id,
      updatedAt: asset.updatedAt,
    });
  }, [resolveChatImageUploadCategoryId]);

  const syncSessionMessages = useCallback(async (sessionId: string) => {
    try {
      const detail = await loadSessionDetail(sessionId);
      if (sessionRef.current?.id !== sessionId) return;
      messagesRef.current = detail.messages;
      setMessages(detail.messages);
      setSession((current) => (current?.id === sessionId ? detail.session : current));
    } catch {
      // Keep the optimistic streamed state if the follow-up sync fails.
    }
  }, []);

  const processEvent = useCallback(
    (event: SessionEvent, requestId: string, sessionId: string) => {
      const raw = event as SessionEvent & { chat_request_id?: string; request_id?: string };
      const eventRequestId = raw.chat_request_id ?? raw.request_id;
      if (eventRequestId && eventRequestId !== requestId) return;

      switch (event.type) {
        case 'Message': {
          const fallbackReason = isCreditsExhaustedNotification(event.message)
            ? 'credits'
            : isRelayServerErrorMessage(event.message)
              ? 'relay'
              : null;
          if (fallbackReason) {
            const retryKey = `${sessionId}:${requestId}:${fallbackReason}`;
            if (!fallbackRetriedRef.current.has(retryKey) && sessionRef.current) {
              fallbackRetriedRef.current.add(retryKey);
              void (async () => {
                try {
                  const fallback = await applyLocalLlmFallback(sessionRef.current!.id);
                  if (!fallback.ok) {
                    throw new Error(fallback.message ?? '本地 LLM 不可用');
                  }
                  setNotice(
                    fallbackReason === 'credits'
                      ? `DeepSeek 额度不足，已切换到本地 ${fallback.model ?? 'qwen2.5-coder:7b'} 并重试…`
                      : `云端 Relay 返回 500（请求体过大），已切换到本地 ${fallback.model ?? 'qwen2.5-coder:7b'} 并重试…`,
                  );
                  const lastUser = [...messagesRef.current]
                    .reverse()
                    .find((message) => message.role === 'user');
                  if (!lastUser) {
                    setChatState('idle');
                    activeRequestId.current = null;
                    return;
                  }
                  const retryRequestId = crypto.randomUUID();
                  activeRequestId.current = retryRequestId;
                  setChatState('streaming');
                  setError(null);
                  await sendReply(sessionRef.current!.id, retryRequestId, lastUser);
                } catch (err) {
                  setError(err instanceof Error ? err.message : String(err));
                  setChatState('error');
                  activeRequestId.current = null;
                }
              })();
            } else {
              setError(
                fallbackReason === 'credits'
                  ? 'DeepSeek 额度已耗尽，且本地 fallback 重试失败或未启用'
                  : '云端 Relay 不可用，且本地 fallback 重试失败或未启用',
              );
              setChatState('error');
              activeRequestId.current = null;
            }
            return;
          }
          if (!event.message.metadata?.userVisible) return;
          setMessages((prev) => {
            const next = pushMessage(prev, event.message);
            messagesRef.current = next;
            return next;
          });
          const confirmation = getToolConfirmation(event.message);
          if (confirmation) {
            setPendingTool(confirmation);
            setChatState('waiting');
          } else {
            setChatState('streaming');
          }
          return;
        }
        case 'UpdateConversation':
          messagesRef.current = normalizeConversationMessages(
            event.conversation.filter((m) => m.metadata?.userVisible),
          );
          setMessages(messagesRef.current);
          return;
        case 'Error':
          setError(event.error);
          setChatState('error');
          activeRequestId.current = null;
          return;
        case 'Finish':
          setChatState('idle');
          setPendingTool(null);
          activeRequestId.current = null;
          setSessions((prev) => touchSession(prev, sessionId, 0));
          if (userRef.current && onUserUpdateRef.current) {
            void getMe()
              .then(({ user: fresh }) => onUserUpdateRef.current?.(fresh))
              .catch(() => {});
          }
          void (async () => {
            await syncSessionMessages(sessionId);
            const recentContext = messagesRef.current
              .filter((message) => getDisplayText(message).trim())
              .slice(-6)
              .map((message) => `${message.role === 'user' ? 'User' : 'Assistant'}: ${getDisplayText(message)}`)
              .join('\n\n')
              .slice(0, 6_000);
            const finishedSession = sessionRef.current;
            void rememberRecentContext({
              silent: true,
              sessionId,
              sessionName: finishedSession?.name,
              recentContext,
            });
          })();
          return;
        default:
          return;
      }
    },
    [rememberRecentContext, syncSessionMessages],
  );

  const subscribeToSession = useCallback(
    (sessionId: string) => {
      unsubscribeRef.current?.();
      unsubscribeRef.current = subscribeSessionEvents(
        sessionId,
        (event) => {
          const rid = activeRequestId.current;
          if (rid) processEvent(event, rid, sessionId);
          else if (event.type === 'ActiveRequests' && event.request_ids.length > 0) {
            activeRequestId.current = event.request_ids[0];
            setChatState('streaming');
          }
        },
        (err) => {
          if (err instanceof ApiError && err.status === 401) return;
          if (err instanceof ApiError && err.status === 402) {
            notifyInsufficientBalance();
            return;
          }
          setError(err.message);
        },
        {
          pauseWhenHidden: true,
          onBalance: (update) => {
            const currentUser = userRef.current;
            const updateUser = onUserUpdateRef.current;
            if (currentUser && updateUser) {
              updateUser({
                ...currentUser,
                balanceCents: update.balanceCents,
                ...(typeof update.tokensUsed === 'number'
                  ? { tokensUsed: update.tokensUsed }
                  : {}),
              });
            }
          },
        },
      );
    },
    [notifyInsufficientBalance, processEvent],
  );

  const resetSessionView = useCallback(() => {
    connectTokenRef.current += 1;
    unsubscribeRef.current?.();
    unsubscribeRef.current = null;
    setError(null);
    setPendingTool(null);
    activeRequestId.current = null;
    messagesRef.current = [];
    setMessages([]);
    setChatState('connecting');
  }, []);

  const connectSession = useCallback(
    async (
      sessionId: string,
      options?: { showLoading?: boolean; skipResume?: boolean; seedSession?: Session },
    ) => {
      const showLoading = options?.showLoading !== false;
      const token = ++connectTokenRef.current;
      unsubscribeRef.current?.();
      unsubscribeRef.current = null;

      if (showLoading) {
        setChatState('loading');
      }
      setError(null);
      setPendingTool(null);
      activeRequestId.current = null;

      const resumed = options?.skipResume
        ? (options.seedSession ?? null)
        : await resumeSession(sessionId);
      if (token !== connectTokenRef.current) return;

      const knownSession = sessionsRef.current.find((s) => s.id === sessionId);
      const hints = knownSession
        ? { messageCount: knownSession.message_count, updatedAt: knownSession.updated_at }
        : undefined;
      const { session: detail, messages: history } = await loadSessionDetail(sessionId, hints);
      if (token !== connectTokenRef.current) return;

      writeStoredSessionId(userRef.current?.id, sessionId);
      setSession({ ...detail, ...(resumed ?? {}), id: sessionId });
      messagesRef.current = history;
      setMessages(history);
      setChatState('idle');
      setSessions((prev) => prependUnique(prev, { ...detail, ...resumed, id: sessionId }));

      subscribeToSession(sessionId);
    },
    [subscribeToSession],
  );

  const ensureProvider = useCallback(async (sessionId: string) => {
    const provider = appConfig.provider || (await readConfig('TKMIND_PROVIDER'));
    const model = appConfig.model || (await readConfig('TKMIND_MODEL'));
    if (provider && model) {
      try {
        await updateProvider(sessionId, provider, model);
      } catch {
        // resumeSession may have already configured the provider
      }
    }
  }, []);

  const retryConnect = useCallback(async () => {
    const sessionId = session?.id ?? readStoredSessionId(userRef.current?.id);
    if (!sessionId) return;
    setError(null);
    try {
      await connectSession(sessionId);
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
      setChatState('error');
    }
  }, [session, connectSession]);

  const connectSessionRef = useRef(connectSession);
  connectSessionRef.current = connectSession;

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

    const boot = async () => {
      try {
        setChatState('loading');
        setError(null);

        const previousSessionId = readStoredSessionId(userRef.current?.id);
        let sessionId: string | null = null;
        let staleSession = false;

        if (previousSessionId) {
          try {
            const previousSession = await getSession(previousSessionId);
            if (shouldShowNewChatTitle(previousSession)) {
              try {
                await deleteChatSession(previousSessionId);
              } catch {
                // Best-effort cleanup: a failed delete should not block the next fresh chat.
              }
            }
            clearStoredSessionId(userRef.current?.id);
          } catch (err) {
            if (err instanceof ApiError && (err.status === 403 || err.status === 404 || err.status === 400)) {
              clearStoredSessionId(userRef.current?.id);
              staleSession = true;
            } else {
              throw err;
            }
          }
        }

        const freshSession = await startSession();
        sessionId = freshSession.id;
        writeStoredSessionId(userRef.current?.id, sessionId);
        if (staleSession) {
          setNotice('上次会话已失效，已为你新建对话');
        }

        if (cancelled) return;
        await connectSessionRef.current(sessionId, {
          skipResume: true,
          seedSession: freshSession,
        });
        if (cancelled) return;
        void loadProjectMemory(sessionId, false);
        void refreshSessions();
      } catch (err) {
        if (!cancelled) {
          if (err instanceof ApiError && err.status === 402) {
            notifyInsufficientBalance();
            return;
          }
          setError(err instanceof Error ? err.message : String(err));
          setChatState('error');
        }
      }
    };

    void boot();
    return () => {
      cancelled = true;
      connectTokenRef.current += 1;
      unsubscribeRef.current?.();
    };
    // Re-run when the signed-in user changes so session storage stays per-user.
  }, [ensureProvider, loadProjectMemory, notifyInsufficientBalance, refreshSessions, user?.id]);

  useEffect(() => {
    const handleVisibility = () => {
      if (!document.hidden && session?.id) {
        void refreshSessions();
      }
    };
    document.addEventListener('visibilitychange', handleVisibility);
    return () => document.removeEventListener('visibilitychange', handleVisibility);
  }, [session?.id, refreshSessions]);

  const switchSession = useCallback(
    (sessionId: string) => {
      if (session?.id === sessionId || chatState === 'streaming') return;

      resetSessionView();
      const token = connectTokenRef.current;
      const target = sessions.find((item) => item.id === sessionId);
      if (target) {
        setSession(target);
        writeStoredSessionId(userRef.current?.id, sessionId);
      }

      void (async () => {
        try {
          await connectSession(sessionId, { showLoading: false });
        } catch (err) {
          if (token !== connectTokenRef.current) return;
          setError(err instanceof Error ? err.message : String(err));
          setChatState('error');
        }
      })();
    },
    [session, chatState, connectSession, resetSessionView, sessions],
  );

  const submit = useCallback(
    async (
      text: string,
      options?: { mindspaceContext?: MindSpaceChatContext },
      imageUrls?: string[],
    ) => {
      const normalizedImageUrls = (imageUrls ?? []).filter((value) => typeof value === 'string' && value.trim());
      if (!session || (!text.trim() && normalizedImageUrls.length === 0)) return;
      if (chatState === 'streaming' || chatState === 'loading' || chatState === 'connecting') return;

      const trimmed = text.trim();
      const mindspacePrefix = options?.mindspaceContext
        ? buildContextPrefix(options.mindspaceContext)
        : '';
      const userPrefix = buildUserAddressPrefix(userRef.current);
      const skillPrefix = buildAutoChatSkillPrefix(trimmed, grantedSkills ?? []);
      const agentPrefix = `${userPrefix}${mindspacePrefix}${skillPrefix}`;
      const userMessage = buildUserMessage(trimmed, {
        agentText: `${agentPrefix}${trimmed}`,
        displayText: trimmed,
        imageUrls: normalizedImageUrls,
      });
      const requestId = crypto.randomUUID();
      activeRequestId.current = requestId;
      messagesRef.current = [...messagesRef.current, userMessage];
      setMessages(messagesRef.current);
      setSessions((prev) => touchSession(prev, session.id, 1));
      setChatState('streaming');
      setError(null);
      setPendingTool(null);

      try {
        await sendReply(session.id, requestId, userMessage);
      } catch (err) {
        setSessions((prev) => touchSession(prev, session.id, -1));
        if (err instanceof ApiError && err.status === 402) {
          notifyInsufficientBalance();
        } else {
          setError(err instanceof Error ? err.message : String(err));
          setChatState('error');
        }
        activeRequestId.current = null;
      }
    },
    [notifyInsufficientBalance, session, chatState, grantedSkills],
  );

  const stop = useCallback(async () => {
    if (!session || !activeRequestId.current) return;
    try {
      await cancelRequest(session.id, activeRequestId.current);
    } finally {
      activeRequestId.current = null;
      setChatState('idle');
    }
  }, [session]);

  const approveTool = useCallback(
    async (allow: boolean) => {
      if (!session || !pendingTool) return;
      await confirmTool(session.id, pendingTool.id, allow ? 'allow_once' : 'deny_once');
      setPendingTool(null);
      setChatState('streaming');
    },
    [session, pendingTool],
  );

  const newSession = useCallback(() => {
    if (chatState === 'streaming') return;

    resetSessionView();
    setSession(null);
    const token = connectTokenRef.current;

    void (async () => {
      try {
        const created = await startSession();
        if (token !== connectTokenRef.current) return;

        writeStoredSessionId(userRef.current?.id, created.id);
        setSession(created);
        setSessions((prev) => prependUnique(prev, created));

        void loadProjectMemory(created.id, false);
        void refreshSessions();

        await ensureProvider(created.id);
        if (token !== connectTokenRef.current) return;

        await connectSession(created.id, { showLoading: false });
      } catch (err) {
        if (token !== connectTokenRef.current) return;
        if (err instanceof ApiError && err.status === 402) {
          notifyInsufficientBalance();
          return;
        }
        setError(err instanceof Error ? err.message : String(err));
        setChatState('error');
      }
    })();
  }, [chatState, connectSession, ensureProvider, loadProjectMemory, notifyInsufficientBalance, refreshSessions, resetSessionView]);

  const refreshProjectMemory = useCallback(async () => {
    if (!session || chatState === 'streaming') return;
    await loadProjectMemory(session.id, true);
  }, [chatState, loadProjectMemory, session]);

  const rememberCurrentContext = useCallback(async () => {
    if (chatState === 'streaming' || chatState === 'loading' || chatState === 'connecting') return;
    await rememberRecentContext({ silent: false });
  }, [chatState, rememberRecentContext]);

  const onSidebarOpen = useCallback(() => {
    void refreshSessions();
  }, [refreshSessions]);

  const deleteSession = useCallback(
    (sessionId: string) => {
      if (chatState === 'streaming') return;

      void (async () => {
        try {
          await deleteChatSession(sessionId);
          const wasActive = sessionRef.current?.id === sessionId;
          const nextSessions = sessions.filter((item) => item.id !== sessionId);
          setSessions(nextSessions);

          if (!wasActive) return;

          const fallbackId = nextSessions[0]?.id;
          if (fallbackId) {
            void switchSession(fallbackId);
            return;
          }

          clearStoredSessionId(userRef.current?.id);
          resetSessionView();
          setSession(null);
          void newSession();
        } catch (err) {
          setNotice(`删除会话失败：${err instanceof Error ? err.message : String(err)}`);
        }
      })();
    },
    [chatState, newSession, resetSessionView, sessions, switchSession],
  );

  return {
    session,
    sessions,
    sessionsLoading,
    messages,
    chatState,
    error,
    notice,
    pendingTool,
    memoryLoading,
    submit,
    stop,
    approveTool,
    newSession,
    rememberCurrentContext,
    refreshProjectMemory,
    switchSession,
    deleteSession,
    refreshSessions,
    retryConnect,
    dismissNotice,
    onSidebarOpen,
    uploadChatImage,
    workingDir: session?.working_dir ?? user?.workspaceRoot ?? '',
    balanceCents: user?.balanceCents,
    totalCreditCents: user?.totalCreditCents,
    tokensUsed: user?.tokensUsed ?? 0,
    subscription: user?.subscription,
    rechargePrompt,
    rechargeForced,
    openRecharge,
    dismissRecharge,
    completeRecharge,
    subscribePrompt,
    openSubscribe,
    dismissSubscribe,
    completeSubscribe,
  };
}
