import type {
  AdminDashboardSummary,
  AdminUserRow,
  AuthStatus,
  CapabilityDefinition,
  CapabilityMap,
  ChatSaveAnalysis,
  ChatSaveResult,
  PolicyDefinition,
  PolicyMap,
  SkillDefinition,
  SkillMap,
  BillingConfig,
  HarnessBootstrapResponse,
  InsufficientBalanceDetails,
  LedgerEntry,
  LlmProviderDefinition,
  LlmProviderKeyRow,
  LlmGlobalSettings,
  LlmConnectionTestResult,
  RechargeOrder,
  Message,
  MindSpace,
  MindSpaceAsset,
  MindSpaceAgentJob,
  MindSpaceCleanupItem,
  MindSpacePage,
  MindSpacePageDeletePreview,
  MindSpacePageDeleteResult,
  MindSpaceQuota,
  MindSpaceSaveCategory,
  MindSpacePublication,
  MindSpacePublicationStats,
  MindSpacePublishCheck,
  PlazaCategory,
  PlazaPostBrief,
  MindSpaceRedactedCopyResult,
  MindSpaceUpload,
  PortalUser,
  Session,
  SessionEvent,
  SessionListResponse,
  UsageRecord,
  BalanceUpdate,
  UserNotification,
  PlanDefinition,
  ActiveSubscription,
} from '../types';
import { CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES } from '../utils/imageUpload';
import { normalizeConversationMessages, normalizeUserMessageForApi } from '../utils/message';

const API = '/api';
const DEFAULT_API_TIMEOUT_MS = 20_000;

export class ApiError extends Error {
  readonly status: number;
  readonly code?: string;
  readonly details?: InsufficientBalanceDetails | Record<string, unknown>;

  constructor(
    status: number,
    message: string,
    code?: string,
    details?: InsufficientBalanceDetails | Record<string, unknown>,
  ) {
    super(sanitizeUserFacingErrorMessage(message));
    this.name = 'ApiError';
    this.status = status;
    this.code = code;
    this.details = details;
  }
}

function sanitizeUserFacingErrorMessage(message: string) {
  const normalized = String(message ?? '').trim();
  if (!normalized) return normalized;
  if (!/goose|goosed/i.test(normalized)) return normalized;
  if (/超时|timeout/i.test(normalized)) {
    return '后端连接超时，请确认后端服务正常后重试';
  }
  if (/不可用|连接失败|failed to fetch|networkerror|fetch failed|upstream|econn|enotfound/i.test(normalized)) {
    return '后端连接失败，请稍后重试';
  }
  return normalized
    .replace(/\bgoosed\b/gi, '后端服务')
    .replace(/\bgoose\b/gi, '后端');
}

async function parseErrorResponse(res: Response): Promise<{
  message: string;
  code?: string;
  details?: InsufficientBalanceDetails | Record<string, unknown>;
}> {
  const text = await res.text().catch(() => '');
  try {
    const body = JSON.parse(text) as Record<string, unknown>;
    const nested =
      body.error && typeof body.error === 'object'
        ? (body.error as Record<string, unknown>)
        : body;
    const message =
      typeof nested.message === 'string'
        ? nested.message
        : typeof body.message === 'string'
          ? body.message
          : text;
    const code =
      typeof nested.code === 'string'
        ? nested.code
        : typeof body.code === 'string'
          ? body.code
          : undefined;
    const details = nested.details ?? body.details;
    if (code === 'INSUFFICIENT_BALANCE') {
      return {
        message: sanitizeUserFacingErrorMessage(message),
        code,
        details: {
          code: 'INSUFFICIENT_BALANCE' as const,
          balanceCents: Number((details as Record<string, unknown>)?.balanceCents ?? body.balanceCents ?? 0),
          minRechargeCents: Number(
            (details as Record<string, unknown>)?.minRechargeCents ?? body.minRechargeCents ?? 500,
          ),
          suggestedTiers: Array.isArray((details as Record<string, unknown>)?.suggestedTiers)
            ? ((details as Record<string, unknown>).suggestedTiers as unknown[]).map((value) => Number(value))
            : Array.isArray(body.suggestedTiers)
              ? body.suggestedTiers.map((value) => Number(value))
              : [],
        },
      };
    }
    return {
      message: sanitizeUserFacingErrorMessage(message),
      code,
      details: details && typeof details === 'object'
        ? (details as InsufficientBalanceDetails | Record<string, unknown>)
        : undefined,
    };
  } catch {
    return { message: sanitizeUserFacingErrorMessage(text || res.statusText) };
  }
}

let unauthorizedHandler: (() => void) | null = null;
let unauthorizedHandling = false;

export function setUnauthorizedHandler(handler: (() => void) | null) {
  unauthorizedHandler = handler;
  if (handler) unauthorizedHandling = false;
}

export function resetUnauthorizedGuard() {
  unauthorizedHandling = false;
}

function notifyUnauthorized() {
  if (!unauthorizedHandler || unauthorizedHandling) return;
  unauthorizedHandling = true;
  unauthorizedHandler();
}

async function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
  const controller = new AbortController();
  const upstreamSignal = init?.signal;
  const timeout = window.setTimeout(() => controller.abort(), DEFAULT_API_TIMEOUT_MS);

  const abortFromUpstream = () => controller.abort();
  if (upstreamSignal) {
    if (upstreamSignal.aborted) controller.abort();
    else upstreamSignal.addEventListener('abort', abortFromUpstream, { once: true });
  }

  try {
    return await fetch(input, {
      ...init,
      signal: controller.signal,
    });
  } finally {
    window.clearTimeout(timeout);
    upstreamSignal?.removeEventListener('abort', abortFromUpstream);
  }
}

async function portalFetch<T>(path: string, init?: RequestInit): Promise<T> {
  let res: Response;
  try {
    res = await fetchWithTimeout(path, {
      ...init,
      headers: {
        'Content-Type': 'application/json',
        ...init?.headers,
      },
    });
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }

  if (res.status === 401) {
    notifyUnauthorized();
    const text = await res.text().catch(() => '');
    throw new ApiError(401, text || '未授权，请重新登录');
  }

  if (!res.ok) {
    const parsed = await parseErrorResponse(res);
    throw new ApiError(
      res.status,
      parsed.message || `${res.status} ${res.statusText}`,
      parsed.code,
      parsed.details,
    );
  }

  if (res.status === 204) return undefined as T;
  const text = await res.text().catch(() => '');
  if (text.trimStart().startsWith('<')) {
    throw new ApiError(res.status, '服务器返回了意外的页面，请确认后端服务已启动并已重启');
  }
  try {
    return JSON.parse(text) as T;
  } catch {
    throw new ApiError(res.status, '服务器响应格式错误');
  }
}

function formatNetworkError(err: unknown) {
  const message = err instanceof Error ? err.message : '网络请求失败';
  if (err instanceof DOMException && err.name === 'AbortError') {
    return '后端连接超时，请确认后端服务正常后重试';
  }
  if (message.includes('Failed to fetch') || message.includes('NetworkError')) {
    return '无法连接后端服务，请先运行: pnpm dev 或 node server.mjs';
  }
  return sanitizeUserFacingErrorMessage(message);
}

function sanitizeSessionEvent(event: SessionEvent): SessionEvent {
  if (event.type !== 'Error') return event;
  return { ...event, error: sanitizeUserFacingErrorMessage(event.error) };
}

async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
  let res: Response;
  try {
    res = await fetchWithTimeout(`${API}${path}`, {
      ...init,
      headers: {
        'Content-Type': 'application/json',
        ...init?.headers,
      },
    });
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }

  if (res.status === 401) {
    notifyUnauthorized();
    const text = await res.text().catch(() => '');
    throw new ApiError(401, text || '未授权，请重新登录');
  }

  if (!res.ok) {
    const parsed = await parseErrorResponse(res);
    throw new ApiError(
      res.status,
      parsed.message || `${res.status} ${res.statusText}`,
      parsed.code,
      parsed.details,
    );
  }

  if (res.status === 204) return undefined as T;
  const rawText = await res.text().catch(() => '');
  if (rawText.trimStart().startsWith('<')) {
    throw new ApiError(res.status, '服务器返回了意外的页面，请确认后端服务已启动并已重启');
  }
  try {
    return JSON.parse(rawText) as T;
  } catch {
    throw new ApiError(res.status, '服务器响应格式错误');
  }
}

export async function startSession(): Promise<Session> {
  return apiFetch<Session>('/agent/start', {
    method: 'POST',
    body: JSON.stringify({}),
  });
}

export async function bootstrapProjectMemory(
  sessionId: string,
  query: string,
  force = false,
): Promise<HarnessBootstrapResponse> {
  return apiFetch<HarnessBootstrapResponse>('/agent/harness_bootstrap', {
    method: 'POST',
    body: JSON.stringify({ sessionId, query, force }),
  });
}

export async function rememberProjectContext(
  sessionId: string,
  content: string,
  title: string,
): Promise<void> {
  await apiFetch('/agent/harness_remember', {
    method: 'POST',
    body: JSON.stringify({ sessionId, content, title }),
  });
}

export type WechatAuthConfig = {
  enabled: boolean;
  inWechat?: boolean;
  scanEnabled?: boolean;
  openScanEnabled?: boolean;
  openAppId?: string;
  oauthCallbackUrl?: string;
};

export type WechatPendingProfile = {
  nickname: string | null;
  avatarUrl: string | null;
  returnTo: string;
};

export type WechatBindingStatus = {
  enabled: boolean;
  bound: boolean;
  nickname?: string | null;
  avatarUrl?: string | null;
};

export type WechatAgentRouteStatus = {
  enabled: boolean;
  bound: boolean;
  appId: string | null;
  openid: string | null;
  agentSessionId: string | null;
  routeStatus: string | null;
  updatedAt: number | null;
};

export type WechatJsSdkSignature = {
  appId: string;
  timestamp: number;
  nonceStr: string;
  signature: string;
  url: string;
  jsApiList: string[];
};

export async function getWechatAuthConfig(): Promise<WechatAuthConfig> {
  try {
    const response = await fetch('/auth/wechat/config');
    if (!response.ok) return { enabled: false };
    return (await response.json()) as WechatAuthConfig;
  } catch {
    return { enabled: false };
  }
}

export async function getWechatJsSdkSignature(url: string): Promise<WechatJsSdkSignature> {
  let response: Response;
  try {
    response = await fetch(`/auth/wechat/js-sdk-signature?url=${encodeURIComponent(url)}`);
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }
  const body = (await response.json().catch(() => null)) as
    | (WechatJsSdkSignature & { message?: string })
    | null;
  if (!response.ok || !body?.signature) {
    throw new ApiError(response.status, body?.message ?? '微信语音初始化失败');
  }
  return body;
}

export async function checkAuth(): Promise<AuthStatus> {
  try {
    const response = await fetch('/auth/status');
    if (!response.ok) return { authenticated: false };
    const status = (await response.json()) as AuthStatus;
    if (status.authenticated) resetUnauthorizedGuard();
    if (status.authenticated && status.mode === 'user' && !status.capabilities) {
      try {
        const me = await getMe();
        return {
          ...status,
          capabilities: me.capabilities,
          grantedSkills: me.grantedSkills,
          user: me.user,
        };
      } catch {
        return status;
      }
    }
    return status;
  } catch {
    return { authenticated: false };
  }
}

export async function getMe(): Promise<{
  user: PortalUser;
  capabilities?: CapabilityMap;
  grantedSkills?: string[];
  unrestricted?: boolean;
}> {
  return portalFetch('/auth/me');
}

export async function getMyUsage(): Promise<UsageRecord[]> {
  const result = await portalFetch<{ records: UsageRecord[] }>('/auth/usage');
  return result.records ?? [];
}

export async function getMyBillingLedger(limit = 30): Promise<LedgerEntry[]> {
  const query = limit ? `?limit=${encodeURIComponent(String(limit))}` : '';
  const result = await portalFetch<{ entries: LedgerEntry[] }>(`/auth/billing/ledger${query}`);
  return result.entries ?? [];
}

export async function getBillingConfig(): Promise<BillingConfig> {
  return portalFetch('/auth/billing/config');
}

export async function getAvailablePlans(): Promise<{
  plans: PlanDefinition[];
  subscription: ActiveSubscription | null;
  balanceCents: number;
}> {
  return portalFetch('/auth/billing/plans');
}

export async function purchaseSubscription(planType: string, autoRenew = false): Promise<{
  subscription: ActiveSubscription;
  balanceCents: number;
}> {
  return portalFetch('/auth/billing/subscribe', {
    method: 'POST',
    body: JSON.stringify({ planType, autoRenew }),
  });
}

export async function setAutoRenew(enabled: boolean): Promise<{ ok: boolean; updated: boolean }> {
  return portalFetch('/auth/billing/auto-renew', {
    method: 'POST',
    body: JSON.stringify({ enabled }),
  });
}

export async function createRechargeOrder(input: {
  amountCents: number;
  payScene: 'native' | 'h5' | 'jsapi';
}): Promise<RechargeOrder> {
  const result = await portalFetch<{ order: RechargeOrder }>('/auth/billing/recharge-orders', {
    method: 'POST',
    body: JSON.stringify(input),
  });
  return result.order;
}

export async function getRechargeOrder(orderId: string): Promise<{
  order: RechargeOrder;
  balanceCents: number | null;
}> {
  return portalFetch(`/auth/billing/recharge-orders/${orderId}`);
}

export async function purchaseSpaceQuota(sizeMb: number): Promise<{
  quota: MindSpaceQuota;
  balanceCents: number;
  purchasedMb: number;
  costCents: number;
}> {
  return portalFetch('/auth/billing/space-purchase', {
    method: 'POST',
    body: JSON.stringify({ sizeMb }),
  });
}

export async function getMindSpace(): Promise<MindSpace> {
  const result = await apiFetch<{ data: MindSpace }>('/mindspace/v1/space');
  return result.data;
}

export async function listMindSpaceCleanupItems(): Promise<{
  items: MindSpaceCleanupItem[];
  totalBytes: number;
}> {
  const result = await apiFetch<{
    data: { items: MindSpaceCleanupItem[]; totalBytes: number };
  }>('/mindspace/v1/space/cleanup');
  return result.data;
}

export async function runMindSpaceCleanup(itemIds: string[]): Promise<{
  removedCount: number;
  freedBytes: number;
  quota?: MindSpaceQuota;
}> {
  const result = await apiFetch<{
    data: { removedCount: number; freedBytes: number; quota?: MindSpaceQuota };
  }>('/mindspace/v1/space/cleanup', {
    method: 'POST',
    body: JSON.stringify({ item_ids: itemIds }),
  });
  return result.data;
}

export async function listMindSpaceAssets(
  categoryCode?: string,
): Promise<MindSpaceAsset[]> {
  const query = categoryCode ? `?category_code=${encodeURIComponent(categoryCode)}` : '';
  const result = await apiFetch<{ data: MindSpaceAsset[] }>(`/mindspace/v1/assets${query}`);
  return result.data;
}

export async function uploadMindSpaceAsset(
  categoryId: string,
  file: File,
): Promise<MindSpaceAsset> {
  if (file.type.startsWith('image/') && file.size > CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES) {
    throw new ApiError(
      413,
      `图片文件过大，当前 ${(file.size / 1024 / 1024).toFixed(2)}MB，请先压缩到 ${CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES / 1024 / 1024}MB 以下再上传。`,
    );
  }

  const created = await apiFetch<{ data: MindSpaceUpload }>('/mindspace/v1/uploads', {
    method: 'POST',
    body: JSON.stringify({
      category_id: categoryId,
      filename: file.name,
      size_bytes: file.size,
      declared_mime_type: file.type || null,
    }),
  });

  try {
    const contentResponse = await fetch(created.data.uploadUrl, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/octet-stream' },
      body: file,
    });
    if (!contentResponse.ok) {
      const body = (await contentResponse.json().catch(() => null)) as {
        error?: { message?: string };
      } | null;
      throw new ApiError(
        contentResponse.status,
        body?.error?.message ?? '文件内容上传失败',
      );
    }
    const completed = await apiFetch<{ data: MindSpaceAsset }>(
      `/mindspace/v1/uploads/${created.data.id}/complete`,
      { method: 'POST', body: JSON.stringify({}) },
    );
    return completed.data;
  } catch (error) {
    await apiFetch(`/mindspace/v1/uploads/${created.data.id}`, {
      method: 'DELETE',
    }).catch(() => {});
    throw error;
  }
}

export async function deleteMindSpaceAsset(assetId: string): Promise<void> {
  await apiFetch(`/mindspace/v1/assets/${assetId}`, { method: 'DELETE' });
}

export async function getMindSpacePageDeletePreview(
  pageId: string,
): Promise<MindSpacePageDeletePreview> {
  const result = await apiFetch<{ data: MindSpacePageDeletePreview }>(
    `/mindspace/v1/pages/${encodeURIComponent(pageId)}/delete-preview`,
  );
  return result.data;
}

export async function deleteMindSpacePage(pageId: string): Promise<MindSpacePageDeleteResult> {
  const result = await apiFetch<{ data: MindSpacePageDeleteResult }>(
    `/mindspace/v1/pages/${encodeURIComponent(pageId)}`,
    { method: 'DELETE' },
  );
  return result.data;
}

export async function listMindSpacePages(status?: string): Promise<MindSpacePage[]> {
  const query = status ? `?status=${encodeURIComponent(status)}` : '';
  const result = await apiFetch<{ data: MindSpacePage[] }>(`/mindspace/v1/pages${query}`);
  return result.data;
}

export async function getMindSpacePage(pageId: string): Promise<MindSpacePage> {
  const result = await apiFetch<{ data: MindSpacePage }>(
    `/mindspace/v1/pages/${encodeURIComponent(pageId)}`,
  );
  return result.data;
}

export async function bindMindSpacePageLiveEdit(
  pageId: string,
  sessionId: string,
  options?: { parentSessionId?: string },
): Promise<{ sessionId: string; pageId: string; parentSessionId?: string }> {
  const result = await apiFetch<{ data: { sessionId: string; pageId: string; parentSessionId?: string } }>(
    `/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/bind`,
    {
      method: 'POST',
      body: JSON.stringify({
        session_id: sessionId,
        ...(options?.parentSessionId ? { parent_session_id: options.parentSessionId } : {}),
      }),
    },
  );
  return result.data;
}

export async function forkMindSpacePageEditSession(
  pageId: string,
  parentSessionId: string,
  h5ApiBase?: string | null,
): Promise<{ sessionId: string; pageId: string; parentSessionId: string }> {
  const result = await apiFetch<{
    data: { sessionId: string; pageId: string; parentSessionId: string };
  }>(`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/fork-session`, {
    method: 'POST',
    body: JSON.stringify({
      parent_session_id: parentSessionId,
      ...(h5ApiBase ? { h5_api_base: h5ApiBase } : {}),
    }),
  });
  return result.data;
}

export async function closeMindSpacePageEditSession(
  pageId: string,
  input: {
    sessionId: string;
    parentSessionId?: string | null;
    summary?: string;
  },
): Promise<{ sessionId: string; pageId: string; merged: boolean }> {
  const result = await apiFetch<{
    data: { sessionId: string; pageId: string; merged: boolean };
  }>(`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/close-session`, {
    method: 'POST',
    body: JSON.stringify({
      session_id: input.sessionId,
      parent_session_id: input.parentSessionId ?? undefined,
      summary: input.summary ?? '',
    }),
  });
  return result.data;
}

export async function getMindSpacePageLiveRevision(pageId: string): Promise<{
  pageId: string;
  versionNo: number;
  updatedAt: number;
  liveRevision: number;
}> {
  const result = await apiFetch<{
    data: { pageId: string; versionNo: number; updatedAt: number; liveRevision: number };
  }>(`/mindspace/v1/pages/${encodeURIComponent(pageId)}/live-edit/revision`);
  return result.data;
}

export async function analyzeChatSave(input: {
  sessionId: string;
  messageId: string;
  selectedLinkIndex?: number;
  previewTitle?: string;
  previewSummary?: string;
}): Promise<ChatSaveAnalysis> {
  const result = await apiFetch<{ data: ChatSaveAnalysis }>(
    '/mindspace/v1/pages/analyze-chat-save',
    {
      method: 'POST',
      body: JSON.stringify({
        session_id: input.sessionId,
        message_id: input.messageId,
        selected_link_index: input.selectedLinkIndex ?? 0,
        preview_title: input.previewTitle,
        preview_summary: input.previewSummary,
      }),
    },
  );
  return result.data;
}

export function buildChatSavePreviewFrameUrl(input: {
  sessionId: string;
  messageId: string;
  selectedLinkIndex?: number;
}) {
  const params = new URLSearchParams({
    session_id: input.sessionId,
    message_id: input.messageId,
    selected_link_index: String(input.selectedLinkIndex ?? 0),
  });
  return `/api/mindspace/v1/pages/chat-save-preview?${params.toString()}`;
}

export function buildChatSaveThumbnailUrl(input: {
  sessionId: string;
  messageId: string;
  selectedLinkIndex?: number;
  previewTitle?: string;
  previewSummary?: string;
  cacheKey?: number | string;
}) {
  const params = new URLSearchParams({
    session_id: input.sessionId,
    message_id: input.messageId,
    selected_link_index: String(input.selectedLinkIndex ?? 0),
  });
  const title = input.previewTitle?.trim();
  const summary = input.previewSummary?.trim();
  if (title) params.set('preview_title', title);
  if (summary) params.set('preview_summary', summary);
  if (input.cacheKey != null) params.set('v', String(input.cacheKey));
  return `/api/mindspace/v1/pages/chat-save-thumbnail?${params.toString()}`;
}

export async function fetchPreviewAsset(path: string): Promise<string> {
  let res: Response;
  try {
    res = await fetch(path, { credentials: 'same-origin' });
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }
  if (res.status === 401) {
    notifyUnauthorized();
    throw new ApiError(401, '未授权，请重新登录');
  }
  if (!res.ok) {
    const parsed = await parseErrorResponse(res);
    throw new ApiError(res.status, parsed.message || `${res.status} ${res.statusText}`, parsed.code);
  }
  const blob = await res.blob();
  return URL.createObjectURL(blob);
}

export async function saveChatMessageAsPage(input: {
  sessionId: string;
  messageId: string;
  title: string;
  summary?: string;
  templateId?: string;
  categoryCode?: MindSpaceSaveCategory;
  selectedLinkIndex?: number;
  acknowledgedFindingIds?: string[];
}): Promise<ChatSaveResult> {
  const result = await apiFetch<{ data: ChatSaveResult }>(
    '/mindspace/v1/pages/save-from-chat',
    {
      method: 'POST',
      body: JSON.stringify({
        session_id: input.sessionId,
        message_id: input.messageId,
        title: input.title,
        summary: input.summary,
        template_id: input.templateId ?? 'editorial',
        category_code: input.categoryCode ?? 'draft',
        selected_link_index: input.selectedLinkIndex ?? 0,
        acknowledged_finding_ids: input.acknowledgedFindingIds,
        page_type: input.templateId === 'report' ? 'report' : 'article',
      }),
    },
  );
  return result.data;
}

export async function createMindSpacePageFromAsset(input: {
  assetId: string;
  title?: string;
  summary?: string;
}): Promise<MindSpacePage> {
  const result = await apiFetch<{ data: { page: MindSpacePage } }>(
    '/mindspace/v1/pages/from-asset',
    {
      method: 'POST',
      body: JSON.stringify({
        asset_id: input.assetId,
        title: input.title,
        summary: input.summary,
      }),
    },
  );
  return result.data.page;
}

export async function createMindSpacePage(input: {
  title: string;
  summary?: string;
  content: string;
  templateId: string;
}): Promise<MindSpacePage> {
  const result = await apiFetch<{ data: MindSpacePage }>('/mindspace/v1/pages', {
    method: 'POST',
    body: JSON.stringify({
      title: input.title,
      summary: input.summary,
      content: input.content,
      template_id: input.templateId,
      page_type: input.templateId === 'report' ? 'report' : 'article',
    }),
  });
  return result.data;
}

export async function updateMindSpacePage(
  pageId: string,
  input: {
    expectedVersion: number;
    title: string;
    summary: string;
    content: string;
    templateId: string;
    changeNote?: string;
  },
): Promise<MindSpacePage> {
  const result = await apiFetch<{ data: MindSpacePage }>(
    `/mindspace/v1/pages/${encodeURIComponent(pageId)}`,
    {
      method: 'PUT',
      body: JSON.stringify({
        expected_version: input.expectedVersion,
        title: input.title,
        summary: input.summary,
        content: input.content,
        template_id: input.templateId,
        page_type: input.templateId === 'report' ? 'report' : 'article',
        change_note: input.changeNote,
      }),
    },
  );
  return result.data;
}

export async function fetchMindSpacePageDraftPreview(
  pageId: string,
  input: {
    title: string;
    summary: string;
    content: string;
    templateId: string;
  },
): Promise<string> {
  let res: Response;
  try {
    res = await fetch(`/api/mindspace/v1/pages/${encodeURIComponent(pageId)}/preview-draft`, {
      method: 'POST',
      credentials: 'same-origin',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        title: input.title,
        summary: input.summary,
        content: input.content,
        template_id: input.templateId,
      }),
    });
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }
  if (res.status === 401) {
    notifyUnauthorized();
    throw new ApiError(401, '未授权，请重新登录');
  }
  if (!res.ok) {
    const parsed = await parseErrorResponse(res);
    throw new ApiError(res.status, parsed.message || `${res.status} ${res.statusText}`, parsed.code);
  }
  return res.text();
}

export function openMindSpaceDraftPreviewWindow(html: string) {
  const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const opened = window.open(url, '_blank', 'noopener,noreferrer');
  if (!opened) {
    URL.revokeObjectURL(url);
    throw new ApiError(0, '无法打开新窗口，请检查浏览器是否拦截弹窗');
  }
  window.setTimeout(() => URL.revokeObjectURL(url), 120_000);
}

export async function uploadMindSpacePageThumbnail(
  pageId: string,
  input: {
    imageBase64: string;
    mimeType?: string;
    title?: string;
    summary?: string;
    content?: string;
  },
): Promise<{ updatedAt: number }> {
  const result = await apiFetch<{ data: { updatedAt: number } }>(
    `/mindspace/v1/pages/${encodeURIComponent(pageId)}/thumbnail/upload`,
    {
      method: 'POST',
      body: JSON.stringify({
        image_base64: input.imageBase64,
        mime_type: input.mimeType,
        title: input.title,
        summary: input.summary,
        html: input.content,
      }),
    },
  );
  return result.data;
}

export async function regenerateMindSpacePageThumbnail(
  pageId: string,
  input: {
    title?: string;
    summary?: string;
    content?: string;
    useAi?: boolean;
    instruction?: string;
  } = {},
): Promise<{ updatedAt: number; content?: string | null }> {
  const result = await apiFetch<{ data: { updatedAt: number; content?: string | null } }>(
    `/mindspace/v1/pages/${encodeURIComponent(pageId)}/thumbnail/regenerate`,
    {
      method: 'POST',
      body: JSON.stringify({
        title: input.title,
        summary: input.summary,
        html: input.content,
        use_ai: input.useAi ?? false,
        instruction: input.instruction,
      }),
    },
  );
  return result.data;
}

export async function checkMindSpacePagePublication(
  pageId: string,
  input: {
    pageVersionId: string;
    accessMode: MindSpacePublishCheck['accessMode'];
    urlSlug: string;
    password?: string;
    expiresAt?: number | null;
  },
): Promise<MindSpacePublishCheck> {
  const result = await apiFetch<{ data: MindSpacePublishCheck }>(
    `/mindspace/v1/pages/${encodeURIComponent(pageId)}/publish-check`,
    {
      method: 'POST',
      body: JSON.stringify({
        page_version_id: input.pageVersionId,
        access_mode: input.accessMode,
        url_slug: input.urlSlug,
        password: input.password,
        expires_at: input.expiresAt,
      }),
    },
  );
  return result.data;
}

export async function publishMindSpacePage(
  pageId: string,
  input: {
    pageVersionId: string;
    accessMode: MindSpacePublishCheck['accessMode'];
    urlSlug: string;
    acknowledgedFindingIds: string[];
    password?: string;
    expiresAt?: number | null;
  },
): Promise<MindSpacePublication> {
  const result = await apiFetch<{ data: MindSpacePublication }>(
    `/mindspace/v1/pages/${encodeURIComponent(pageId)}/publish`,
    {
      method: 'POST',
      body: JSON.stringify({
        page_version_id: input.pageVersionId,
        access_mode: input.accessMode,
        url_slug: input.urlSlug,
        password: input.password,
        expires_at: input.expiresAt,
        acknowledged_finding_ids: input.acknowledgedFindingIds,
      }),
    },
  );
  return result.data;
}

export async function redactMindSpacePage(
  pageId: string,
  input: {
    pageVersionId: string;
    expectedVersion: number;
    title: string;
    summary: string;
    content: string;
  },
): Promise<MindSpaceRedactedCopyResult> {
  const result = await apiFetch<{ data: MindSpaceRedactedCopyResult }>(
    `/mindspace/v1/pages/${encodeURIComponent(pageId)}/redact`,
    {
      method: 'POST',
      body: JSON.stringify({
        page_version_id: input.pageVersionId,
        expected_version: input.expectedVersion,
        title: input.title,
        summary: input.summary,
        content: input.content,
      }),
    },
  );
  return result.data;
}

export async function fixMindSpacePagePublication(
  pageId: string,
  input: {
    pageVersionId: string;
    expectedVersion: number;
    title: string;
    summary: string;
    content: string;
  },
): Promise<MindSpaceRedactedCopyResult> {
  const result = await apiFetch<{ data: MindSpaceRedactedCopyResult }>(
    `/mindspace/v1/pages/${encodeURIComponent(pageId)}/publish-fix`,
    {
      method: 'POST',
      body: JSON.stringify({
        page_version_id: input.pageVersionId,
        expected_version: input.expectedVersion,
        title: input.title,
        summary: input.summary,
        content: input.content,
      }),
    },
  );
  return result.data;
}

/** @deprecated use redactMindSpacePage */
export async function createMindSpaceRedactedCopy(
  pageId: string,
  pageVersionId: string,
): Promise<MindSpaceRedactedCopyResult> {
  const page = await getMindSpacePage(pageId);
  return redactMindSpacePage(pageId, {
    pageVersionId,
    expectedVersion: page.versionNo,
    title: page.title,
    summary: page.summary,
    content: page.content ?? '',
  });
}

export async function offlineMindSpacePublication(publicationId: string): Promise<void> {
  await apiFetch(
    `/mindspace/v1/publications/${encodeURIComponent(publicationId)}/offline`,
    { method: 'POST', body: JSON.stringify({}) },
  );
}

export async function getMindSpacePublicationStats(
  publicationId: string,
): Promise<MindSpacePublicationStats> {
  const result = await apiFetch<{ data: MindSpacePublicationStats }>(
    `/mindspace/v1/publications/${encodeURIComponent(publicationId)}/stats`,
  );
  return result.data;
}

export async function listPlazaCategories(): Promise<PlazaCategory[]> {
  const result = await apiFetch<{ data: { categories: PlazaCategory[] } }>('/plaza/v1/categories');
  return result.data.categories;
}

export async function publishPageToPlaza(input: {
  publicationId: string;
  categoryId: string;
  tags?: string[];
  coverUrl?: string;
  allowComment?: boolean;
}): Promise<PlazaPostBrief> {
  const result = await apiFetch<{ data: { post: PlazaPostBrief } }>('/plaza/v1/posts', {
    method: 'POST',
    body: JSON.stringify({
      publication_id: input.publicationId,
      category_id: input.categoryId,
      tags: input.tags ?? [],
      cover_url: input.coverUrl ?? '',
      allow_comment: input.allowComment ?? true,
    }),
  });
  return result.data.post;
}

export async function createMindSpaceAgentJob(input: {
  jobType: string;
  instruction: string;
  allowedAssetIds: string[];
  outputType?: 'page_draft' | 'html_page' | 'markdown';
  outputCategoryId?: string;
  idempotencyKey?: string;
  locale?: string;
  timezone?: string;
  capabilities?: {
    network?: boolean;
    shell?: boolean;
    createPage?: boolean;
  };
}): Promise<MindSpaceAgentJob> {
  const result = await apiFetch<{ data: MindSpaceAgentJob }>('/mindspace/v1/agent/jobs', {
    method: 'POST',
    body: JSON.stringify({
      job_type: input.jobType,
      instruction: input.instruction,
      allowed_asset_ids: input.allowedAssetIds,
      output_type: input.outputType ?? 'page_draft',
      output_category_id: input.outputCategoryId,
      idempotency_key: input.idempotencyKey,
      locale: input.locale,
      timezone: input.timezone,
      capabilities: input.capabilities,
    }),
  });
  return result.data;
}

export async function getMindSpaceAgentJob(jobId: string): Promise<MindSpaceAgentJob> {
  const result = await apiFetch<{ data: MindSpaceAgentJob }>(
    `/mindspace/v1/agent/jobs/${encodeURIComponent(jobId)}`,
  );
  return result.data;
}

export type MindSpaceListPage = {
  total?: number;
  offset?: number;
  limit?: number;
  has_more?: boolean;
};

export async function listMindSpaceAgentJobs(options?: {
  limit?: number;
  offset?: number;
}): Promise<{ items: MindSpaceAgentJob[]; page: MindSpaceListPage }> {
  const limit = options?.limit ?? 10;
  const offset = options?.offset ?? 0;
  const result = await apiFetch<{ data: MindSpaceAgentJob[]; page?: MindSpaceListPage }>(
    `/mindspace/v1/agent/jobs?limit=${encodeURIComponent(String(limit))}&offset=${encodeURIComponent(String(offset))}`,
  );
  return { items: result.data, page: result.page ?? {} };
}

export async function runMindSpaceAgentJob(
  jobId: string,
): Promise<{ started: boolean; jobId: string }> {
  const result = await apiFetch<{ data: { started: boolean; jobId: string } }>(
    `/mindspace/v1/agent/jobs/${encodeURIComponent(jobId)}/run`,
    { method: 'POST', body: JSON.stringify({}) },
  );
  return result.data;
}

export async function cancelMindSpaceAgentJob(jobId: string): Promise<MindSpaceAgentJob> {
  const result = await apiFetch<{ data: MindSpaceAgentJob }>(
    `/mindspace/v1/agent/jobs/${encodeURIComponent(jobId)}/cancel`,
    { method: 'POST', body: JSON.stringify({}) },
  );
  return result.data;
}

export async function retryMindSpaceAgentJob(jobId: string): Promise<MindSpaceAgentJob> {
  const result = await apiFetch<{ data: MindSpaceAgentJob }>(
    `/mindspace/v1/agent/jobs/${encodeURIComponent(jobId)}/retry`,
    { method: 'POST', body: JSON.stringify({}) },
  );
  return result.data;
}

export async function getAdminDashboardSummary(): Promise<AdminDashboardSummary> {
  const result = await portalFetch<{ summary: AdminDashboardSummary }>('/admin-api/summary');
  return result.summary;
}

export async function listAdminUsage(userId?: string): Promise<UsageRecord[]> {
  const query = userId ? `?userId=${encodeURIComponent(userId)}` : '';
  const result = await portalFetch<{ records: UsageRecord[] }>(`/admin-api/usage${query}`);
  return result.records ?? [];
}

export async function listAdminLedger(userId?: string): Promise<LedgerEntry[]> {
  const query = userId ? `?userId=${encodeURIComponent(userId)}` : '';
  const result = await portalFetch<{ entries: LedgerEntry[] }>(`/admin-api/ledger${query}`);
  return result.entries ?? [];
}

export async function listCapabilityCatalog(): Promise<CapabilityDefinition[]> {
  const result = await portalFetch<{ catalog: CapabilityDefinition[] }>(
    '/admin-api/capabilities/catalog',
  );
  return result.catalog ?? [];
}

export async function getRoleCapabilities(role: 'user' = 'user'): Promise<{
  role: string;
  capabilities: CapabilityMap;
}> {
  return portalFetch(`/admin-api/capabilities/role/${role}`);
}

export async function updateRoleCapabilities(
  role: 'user',
  capabilities: CapabilityMap,
): Promise<{ role: string; capabilities: CapabilityMap }> {
  return portalFetch(`/admin-api/capabilities/role/${role}`, {
    method: 'PUT',
    body: JSON.stringify({ capabilities }),
  });
}

export async function getUserCapabilities(userId: string): Promise<{
  userId: string;
  role: string;
  unrestricted: boolean;
  capabilities: CapabilityMap;
  overrides: CapabilityMap;
}> {
  return portalFetch(`/admin-api/users/${userId}/capabilities`);
}

export async function updateUserCapabilities(
  userId: string,
  capabilities: CapabilityMap,
): Promise<{
  userId: string;
  capabilities: CapabilityMap;
}> {
  return portalFetch(`/admin-api/users/${userId}/capabilities`, {
    method: 'PUT',
    body: JSON.stringify({ capabilities }),
  });
}

export async function clearUserCapabilityOverrides(userId: string): Promise<void> {
  await portalFetch(`/admin-api/users/${userId}/capabilities`, { method: 'DELETE' });
}

export async function listPolicyCatalog(): Promise<PolicyDefinition[]> {
  const result = await portalFetch<{ catalog: PolicyDefinition[] }>('/admin-api/policies/catalog');
  return result.catalog ?? [];
}

export async function getRolePolicies(role: 'user' = 'user'): Promise<{
  role: string;
  policies: PolicyMap;
}> {
  return portalFetch(`/admin-api/policies/role/${role}`);
}

export async function updateRolePolicies(
  role: 'user',
  policies: PolicyMap,
): Promise<{ role: string; policies: PolicyMap }> {
  return portalFetch(`/admin-api/policies/role/${role}`, {
    method: 'PUT',
    body: JSON.stringify({ policies }),
  });
}

export async function getUserPolicies(userId: string): Promise<{
  userId: string;
  role: string;
  unrestricted: boolean;
  policies: PolicyMap;
  overrides: PolicyMap;
}> {
  return portalFetch(`/admin-api/users/${userId}/policies`);
}

export async function updateUserPolicies(
  userId: string,
  policies: PolicyMap,
): Promise<{
  userId: string;
  policies: PolicyMap;
}> {
  return portalFetch(`/admin-api/users/${userId}/policies`, {
    method: 'PUT',
    body: JSON.stringify({ policies }),
  });
}

export async function clearUserPolicyOverrides(userId: string): Promise<void> {
  await portalFetch(`/admin-api/users/${userId}/policies`, { method: 'DELETE' });
}

export async function listSkillCatalog(): Promise<SkillDefinition[]> {
  const result = await portalFetch<{ catalog: SkillDefinition[] }>('/admin-api/skills/catalog');
  return result.catalog ?? [];
}

export async function getRoleSkills(role: 'user' = 'user'): Promise<{
  role: string;
  skills: SkillMap;
}> {
  return portalFetch(`/admin-api/skills/role/${role}`);
}

export async function updateRoleSkills(
  role: 'user',
  skills: SkillMap,
): Promise<{ role: string; skills: SkillMap }> {
  return portalFetch(`/admin-api/skills/role/${role}`, {
    method: 'PUT',
    body: JSON.stringify({ skills }),
  });
}

export async function getUserSkills(userId: string): Promise<{
  userId: string;
  role: string;
  skills: SkillMap;
  grantedSkills: string[];
  overrides: SkillMap;
}> {
  return portalFetch(`/admin-api/users/${userId}/skills`);
}

export async function updateUserSkills(
  userId: string,
  skills: SkillMap,
): Promise<{
  userId: string;
  skills: SkillMap;
  grantedSkills: string[];
}> {
  return portalFetch(`/admin-api/users/${userId}/skills`, {
    method: 'PUT',
    body: JSON.stringify({ skills }),
  });
}

export async function clearUserSkillOverrides(userId: string): Promise<void> {
  await portalFetch(`/admin-api/users/${userId}/skills`, { method: 'DELETE' });
}

export async function login(username: string, password: string): Promise<PortalUser | null> {
  let response: Response;
  try {
    response = await fetch('/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password }),
    });
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }
  const body = (await response.json().catch(() => null)) as {
    message?: string;
    user?: PortalUser;
    mode?: string;
  } | null;
  if (!response.ok) {
    throw new ApiError(response.status, body?.message ?? '登录失败');
  }
  resetUnauthorizedGuard();
  if (body?.user) {
    try {
      const me = await getMe();
      return me.user;
    } catch {
      return body.user;
    }
  }
  return null;
}

export async function loginLegacy(password: string): Promise<void> {
  let response: Response;
  try {
    response = await fetch('/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ password }),
    });
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }
  if (!response.ok) {
    const body = (await response.json().catch(() => null)) as { message?: string } | null;
    throw new ApiError(response.status, body?.message ?? '登录失败');
  }
}

export async function resetPassword(
  username: string,
  email: string,
  password: string,
): Promise<void> {
  let response: Response;
  try {
    response = await fetch('/auth/reset-password', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, email, password }),
    });
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }
  const body = (await response.json().catch(() => null)) as { message?: string } | null;
  if (!response.ok) {
    throw new ApiError(response.status, body?.message ?? '重置失败');
  }
}

export async function getWechatPendingProfile(token: string): Promise<WechatPendingProfile> {
  let response: Response;
  try {
    response = await fetch(`/auth/wechat/pending/${encodeURIComponent(token)}`);
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }
  const body = (await response.json().catch(() => null)) as WechatPendingProfile & {
    message?: string;
  };
  if (!response.ok) {
    throw new ApiError(response.status, body?.message ?? '绑定会话无效');
  }
  return body;
}

export async function completeWechatRegister(pendingToken: string): Promise<{
  user: PortalUser;
  returnTo: string;
}> {
  let response: Response;
  try {
    response = await fetch('/auth/wechat/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ pendingToken }),
    });
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }
  const body = (await response.json().catch(() => null)) as {
    message?: string;
    user?: PortalUser;
    returnTo?: string;
  };
  if (!response.ok || !body?.user) {
    throw new ApiError(response.status, body?.message ?? '微信注册失败');
  }
  return { user: body.user, returnTo: body.returnTo ?? '/' };
}

export async function completeWechatBind(
  pendingToken: string,
  username: string,
  password: string,
): Promise<{ user: PortalUser; returnTo: string }> {
  let response: Response;
  try {
    response = await fetch('/auth/wechat/bind', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ pendingToken, username, password }),
    });
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }
  const body = (await response.json().catch(() => null)) as {
    message?: string;
    user?: PortalUser;
    returnTo?: string;
  };
  if (!response.ok || !body?.user) {
    throw new ApiError(response.status, body?.message ?? '绑定失败');
  }
  return { user: body.user, returnTo: body.returnTo ?? '/' };
}

export async function getWechatBindingStatus(): Promise<WechatBindingStatus> {
  try {
    const response = await fetch('/auth/wechat/status');
    if (!response.ok) return { enabled: false, bound: false };
    return (await response.json()) as WechatBindingStatus;
  } catch {
    return { enabled: false, bound: false };
  }
}

export async function listNotifications(status = 'unread', limit = 20): Promise<UserNotification[]> {
  const params = new URLSearchParams();
  if (status && status !== 'all') params.set('status', status);
  params.set('limit', String(limit));
  const response = await fetch(`/auth/notifications?${params}`);
  if (!response.ok) {
    throw new ApiError(response.status, '读取通知失败');
  }
  const body = (await response.json()) as { notifications?: UserNotification[] };
  return body.notifications ?? [];
}

export function subscribeNotificationEvents({
  onNotification,
  onSync,
}: {
  onNotification: (notification: UserNotification) => void;
  onSync?: () => void;
}): () => void {
  const source = new EventSource('/auth/notifications/events', { withCredentials: true });
  source.addEventListener('notification', (event) => {
    try {
      const payload = JSON.parse((event as MessageEvent).data) as {
        notification?: UserNotification;
      };
      if (payload.notification) onNotification(payload.notification);
    } catch {
      onSync?.();
    }
  });
  source.addEventListener('sync', () => onSync?.());
  source.onerror = () => {
    // EventSource reconnects automatically; consumers can keep their last state.
  };
  return () => source.close();
}

export async function markNotificationRead(notificationId: string): Promise<void> {
  const response = await fetch(`/auth/notifications/${encodeURIComponent(notificationId)}/read`, {
    method: 'POST',
  });
  if (!response.ok) {
    throw new ApiError(response.status, '通知状态更新失败');
  }
}

export async function markAllNotificationsRead(): Promise<void> {
  const response = await fetch('/auth/notifications/read-all', {
    method: 'POST',
  });
  if (!response.ok) {
    throw new ApiError(response.status, '全部已读失败');
  }
}

export async function deleteNotification(notificationId: string): Promise<void> {
  const response = await fetch(`/auth/notifications/${encodeURIComponent(notificationId)}`, {
    method: 'DELETE',
  });
  if (!response.ok && response.status !== 204) {
    throw new ApiError(response.status, '删除通知失败');
  }
}

export async function clearNotifications(status = 'all'): Promise<void> {
  const response = await fetch(`/auth/notifications?status=${encodeURIComponent(status)}`, {
    method: 'DELETE',
  });
  if (!response.ok) {
    throw new ApiError(response.status, '清空通知失败');
  }
}

export async function getWechatAgentRouteStatus(): Promise<WechatAgentRouteStatus> {
  try {
    const response = await fetch('/auth/wechat/agent-route');
    if (!response.ok) {
      return {
        enabled: false,
        bound: false,
        appId: null,
        openid: null,
        agentSessionId: null,
        routeStatus: null,
        updatedAt: null,
      };
    }
    return (await response.json()) as WechatAgentRouteStatus;
  } catch {
    return {
      enabled: false,
      bound: false,
      appId: null,
      openid: null,
      agentSessionId: null,
      routeStatus: null,
      updatedAt: null,
    };
  }
}

export async function resetWechatAgentRoute(): Promise<WechatAgentRouteStatus> {
  let response: Response;
  try {
    response = await fetch('/auth/wechat/agent-route/reset', {
      method: 'POST',
    });
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }
  const body = (await response.json().catch(() => null)) as {
    message?: string;
    route?: WechatAgentRouteStatus;
  } | null;
  if (!response.ok || !body?.route) {
    throw new ApiError(response.status, body?.message ?? '重建公众号 Agent 路由失败');
  }
  return body.route;
}

export type WechatScanSession =
  | {
      mode: 'open';
      state: string;
      openAppId: string;
      redirectUri: string;
      expiresInMs: number;
    }
  | {
      mode: 'mp';
      state: string;
      qrUrl: string;
      expiresInMs: number;
    };

export async function startWechatScanLogin(returnTo?: string): Promise<WechatScanSession> {
  const params = new URLSearchParams();
  if (returnTo) params.set('return_to', returnTo);
  const query = params.toString();
  let response: Response;
  try {
    response = await fetch(`/auth/wechat/scan/start${query ? `?${query}` : ''}`, {
      method: 'POST',
    });
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }
  const body = (await response.json().catch(() => null)) as {
    message?: string;
    mode?: 'open' | 'mp';
    state?: string;
    openAppId?: string;
    redirectUri?: string;
    qrUrl?: string;
    expiresInMs?: number;
  };
  if (!response.ok || !body?.state || !body?.mode) {
    throw new ApiError(response.status, body?.message ?? '无法启动扫码登录');
  }
  if (body.mode === 'open') {
    if (!body.openAppId || !body.redirectUri) {
      throw new ApiError(response.status, body?.message ?? '无法启动扫码登录');
    }
    return {
      mode: 'open',
      state: body.state,
      openAppId: body.openAppId,
      redirectUri: body.redirectUri,
      expiresInMs: body.expiresInMs ?? 600000,
    };
  }
  if (!body.qrUrl) {
    throw new ApiError(response.status, body?.message ?? '无法启动扫码登录');
  }
  return {
    mode: 'mp',
    state: body.state,
    qrUrl: body.qrUrl,
    expiresInMs: body.expiresInMs ?? 600000,
  };
}

export async function pollWechatScanLogin(state: string): Promise<{
  status: 'pending' | 'complete' | 'binding_gate' | 'error' | 'expired';
  pendingToken?: string;
  message?: string;
}> {
  let response: Response;
  try {
    response = await fetch(`/auth/wechat/scan/poll?state=${encodeURIComponent(state)}`);
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }
  const body = (await response.json().catch(() => null)) as {
    status?: 'pending' | 'complete' | 'binding_gate' | 'error' | 'expired';
    pendingToken?: string;
    message?: string;
  };
  if (!response.ok) {
    throw new ApiError(response.status, body?.message ?? '扫码状态查询失败');
  }
  return {
    status: body?.status ?? 'error',
    pendingToken: body?.pendingToken,
    message: body?.message,
  };
}

export async function register(
  username: string,
  password: string,
  displayName?: string,
  email?: string,
  attribution?: {
    utm_source?: string;
    utm_medium?: string;
    utm_campaign?: string;
    ref?: string;
  },
): Promise<PortalUser> {
  let response: Response;
  try {
    response = await fetch('/auth/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        username,
        password,
        displayName,
        email,
        ...(attribution ?? {}),
      }),
    });
  } catch (err) {
    throw new ApiError(0, formatNetworkError(err));
  }
  const body = (await response.json().catch(() => null)) as {
    message?: string;
    user?: PortalUser;
  } | null;
  if (!response.ok) {
    throw new ApiError(response.status, body?.message ?? '注册失败');
  }
  if (!body?.user) throw new ApiError(response.status, '注册失败');
  return body.user;
}

export async function listAdminUsers(): Promise<AdminUserRow[]> {
  const result = await portalFetch<{ users: AdminUserRow[] }>('/admin-api/users');
  return result.users ?? [];
}

export async function createAdminUser(payload: {
  username: string;
  password: string;
  displayName?: string;
  workspaceRoot?: string;
  balanceCents?: number;
  role?: 'user' | 'admin';
  email?: string;
}): Promise<PortalUser> {
  const result = await portalFetch<{ user: PortalUser }>('/admin-api/users', {
    method: 'POST',
    body: JSON.stringify(payload),
  });
  return result.user;
}

export async function updateAdminUser(
  userId: string,
  payload: Partial<{
    displayName: string;
    status: 'active' | 'suspended' | 'disabled';
    workspaceRoot: string;
    balanceCents: number;
    role: 'user' | 'admin';
  }>,
): Promise<PortalUser> {
  const result = await portalFetch<{ user: PortalUser }>(`/admin-api/users/${userId}`, {
    method: 'PATCH',
    body: JSON.stringify(payload),
  });
  return result.user;
}

export async function rechargeUser(
  userId: string,
  amountCents: number,
  note?: string,
): Promise<PortalUser> {
  const result = await portalFetch<{ user: PortalUser }>(`/admin-api/users/${userId}/recharge`, {
    method: 'POST',
    body: JSON.stringify({ amountCents, note }),
  });
  return result.user;
}

export async function listLlmProviderCatalog(): Promise<LlmProviderDefinition[]> {
  const result = await portalFetch<{ catalog: LlmProviderDefinition[] }>(
    '/admin-api/llm-providers/catalog',
  );
  return result.catalog ?? [];
}

export async function listLlmProviderKeys(): Promise<LlmProviderKeyRow[]> {
  const result = await portalFetch<{ keys: LlmProviderKeyRow[] }>('/admin-api/llm-providers/keys');
  return result.keys ?? [];
}

export async function createLlmProviderKey(payload: {
  providerId: string;
  name: string;
  apiKey: string;
  defaultModel?: string;
  apiUrl?: string;
  basePath?: string;
  models?: string | string[];
  engine?: string;
  relayProvider?: string;
}): Promise<LlmProviderKeyRow> {
  const result = await portalFetch<{ key: LlmProviderKeyRow }>('/admin-api/llm-providers/keys', {
    method: 'POST',
    body: JSON.stringify(payload),
  });
  return result.key;
}

export async function updateLlmProviderKey(
  keyId: string,
  payload: {
    name?: string;
    apiKey?: string;
    defaultModel?: string;
    status?: 'active' | 'disabled';
    apiUrl?: string;
    basePath?: string;
    models?: string | string[];
    engine?: string;
    relayProvider?: string;
  },
): Promise<LlmProviderKeyRow> {
  const result = await portalFetch<{ key: LlmProviderKeyRow }>(
    `/admin-api/llm-providers/keys/${keyId}`,
    {
      method: 'PATCH',
      body: JSON.stringify(payload),
    },
  );
  return result.key;
}

export async function selectLlmProviderKey(keyId: string): Promise<LlmProviderKeyRow> {
  const result = await portalFetch<{ key: LlmProviderKeyRow }>(
    `/admin-api/llm-providers/keys/${keyId}/select`,
    { method: 'POST' },
  );
  return result.key;
}

export async function deleteLlmProviderKey(keyId: string): Promise<void> {
  await portalFetch(`/admin-api/llm-providers/keys/${keyId}`, { method: 'DELETE' });
}

export async function syncLlmProviderToGoosed(): Promise<{ synced: boolean }> {
  return portalFetch('/admin-api/llm-providers/sync', { method: 'POST' });
}

export async function getLlmGlobalSettings(): Promise<LlmGlobalSettings> {
  const result = await portalFetch<{ global: LlmGlobalSettings }>('/admin-api/llm-providers/global');
  return result.global;
}

export async function setLlmGlobalModel(model: string): Promise<{ global: LlmGlobalSettings }> {
  return portalFetch('/admin-api/llm-providers/global', {
    method: 'PUT',
    body: JSON.stringify({ model }),
  });
}

export async function testLlmProviderDraft(payload: {
  providerId: string;
  name: string;
  apiKey: string;
  apiUrl?: string;
  basePath?: string;
  models?: string | string[];
  defaultModel?: string;
  testModel?: string;
  engine?: string;
  relayProvider?: string;
}): Promise<LlmConnectionTestResult> {
  return portalFetch('/admin-api/llm-providers/test', {
    method: 'POST',
    body: JSON.stringify(payload),
  });
}

export async function testLlmProviderKey(
  keyId: string,
  model?: string,
): Promise<LlmConnectionTestResult> {
  return portalFetch(`/admin-api/llm-providers/keys/${keyId}/test`, {
    method: 'POST',
    body: JSON.stringify(model ? { model } : {}),
  });
}

export async function logout(): Promise<void> {
  if (import.meta.env.DEV) return;
  await fetch('/auth/logout', { method: 'POST' });
}

export async function resumeSession(
  sessionId: string,
  options?: { skipReconcile?: boolean },
): Promise<Session> {
  const result = await apiFetch<{ session: Session }>('/agent/resume', {
    method: 'POST',
    body: JSON.stringify({
      session_id: sessionId,
      load_model_and_extensions: true,
      ...(options?.skipReconcile ? { skip_reconcile: true } : {}),
    }),
  });
  return result.session;
}

export async function readConfig(key: string): Promise<string | null> {
  return apiFetch<string | null>('/config/read', {
    method: 'POST',
    body: JSON.stringify({ key, is_secret: false }),
  });
}

export async function updateProvider(
  sessionId: string,
  provider: string,
  model: string,
): Promise<void> {
  await apiFetch('/agent/update_provider', {
    method: 'POST',
    body: JSON.stringify({
      session_id: sessionId,
      provider,
      model,
    }),
  });
}

export async function applyLocalLlmFallback(sessionId: string): Promise<{
  ok: boolean;
  providerId?: string;
  model?: string;
  source?: string;
  message?: string;
}> {
  return apiFetch('/llm/apply-local-fallback', {
    method: 'POST',
    body: JSON.stringify({ session_id: sessionId }),
  });
}

export async function listSessions(): Promise<Session[]> {
  const result = await apiFetch<SessionListResponse>('/sessions');
  return result.sessions ?? [];
}

export async function getSession(sessionId: string): Promise<Session> {
  return apiFetch<Session>(`/sessions/${sessionId}`);
}

export async function deleteChatSession(sessionId: string): Promise<void> {
  await apiFetch(`/sessions/${sessionId}`, { method: 'DELETE' });
}

export async function loadSessionDetail(
  sessionId: string,
  hints?: { messageCount?: number; updatedAt?: string },
): Promise<{
  session: Session;
  messages: Message[];
}> {
  const params = new URLSearchParams();
  if (hints?.messageCount != null) params.set('hint_mc', String(hints.messageCount));
  if (hints?.updatedAt) params.set('hint_ua', hints.updatedAt);
  const qs = params.size ? `?${params.toString()}` : '';
  const detail = await apiFetch<Session>(`/sessions/${sessionId}${qs}`);
  const messages = normalizeConversationMessages(
    (detail.conversation ?? []).filter((m) => m.metadata?.userVisible),
  );
  return { session: detail, messages };
}

export async function sendReply(
  sessionId: string,
  requestId: string,
  userMessage: Message,
): Promise<void> {
  await apiFetch(`/sessions/${sessionId}/reply`, {
    method: 'POST',
    body: JSON.stringify({
      request_id: requestId,
      user_message: normalizeUserMessageForApi(userMessage),
    }),
  });
}

export async function cancelRequest(sessionId: string, requestId: string): Promise<void> {
  await apiFetch(`/sessions/${sessionId}/cancel`, {
    method: 'POST',
    body: JSON.stringify({ request_id: requestId }),
  });
}

export async function confirmTool(
  sessionId: string,
  id: string,
  action: 'allow_once' | 'deny_once' | 'always_allow',
): Promise<void> {
  await apiFetch('/action-required/tool-confirmation', {
    method: 'POST',
    body: JSON.stringify({
      sessionId,
      id,
      action,
      principalType: 'Tool',
    }),
  });
}

type SubscribeOptions = {
  pauseWhenHidden?: boolean;
  onBalance?: (update: BalanceUpdate) => void;
};

export function subscribeSessionEvents(
  sessionId: string,
  onEvent: (event: SessionEvent) => void,
  onError: (error: Error) => void,
  options: SubscribeOptions = {},
): () => void {
  const { pauseWhenHidden = true, onBalance } = options;
  let lastEventId: string | undefined;
  let closed = false;
  let paused = false;
  let activeController: AbortController | null = null;

  const run = async (controller: AbortController) => {
    while (!closed && !controller.signal.aborted && !paused) {
      try {
        const headers: Record<string, string> = { Accept: 'text/event-stream' };
        if (lastEventId) headers['Last-Event-ID'] = lastEventId;

        const res = await fetch(`${API}/sessions/${sessionId}/events`, {
          headers,
          signal: controller.signal,
        });

        if (res.status === 401) {
          notifyUnauthorized();
          throw new ApiError(401, 'SSE 未授权');
        }

        if (!res.ok || !res.body) {
          throw new ApiError(res.status, `SSE failed: ${res.status}`);
        }

        const reader = res.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';

        while (!closed && !controller.signal.aborted && !paused) {
          const { done, value } = await reader.read();
          if (done) break;

          buffer += decoder.decode(value, { stream: true });
          const chunks = buffer.split('\n\n');
          buffer = chunks.pop() ?? '';

          for (const chunk of chunks) {
            let eventId: string | undefined;
            let eventName: string | undefined;
            let data: string | undefined;
            for (const line of chunk.split('\n')) {
              if (line.startsWith('id:')) eventId = line.slice(3).trim();
              if (line.startsWith('event:')) eventName = line.slice(6).trim();
              if (line.startsWith('data:')) data = line.slice(5).trim();
            }
            if (eventId) lastEventId = eventId;
            if (!data) continue;
            try {
              if (eventName === 'balance') {
                const payload = JSON.parse(data) as BalanceUpdate & { balanceCents?: number };
                if (typeof payload.balanceCents === 'number') {
                  onBalance?.({
                    balanceCents: payload.balanceCents,
                    tokensUsed:
                      typeof payload.tokensUsed === 'number' ? payload.tokensUsed : undefined,
                    lastUsage: payload.lastUsage,
                  });
                }
                continue;
              }
              onEvent(sanitizeSessionEvent(JSON.parse(data) as SessionEvent));
            } catch {
              // ignore malformed frames
            }
          }
        }
      } catch (err) {
        if (controller.signal.aborted || closed || paused) return;
        onError(err instanceof Error ? err : new Error(String(err)));
        await new Promise((r) => setTimeout(r, 1500));
      }
    }
  };

  const startConnection = () => {
    activeController?.abort();
    activeController = new AbortController();
    void run(activeController);
  };

  const handleVisibility = () => {
    if (!pauseWhenHidden) return;
    if (document.hidden) {
      paused = true;
      activeController?.abort();
    } else if (paused) {
      paused = false;
      startConnection();
    }
  };

  if (pauseWhenHidden) {
    document.addEventListener('visibilitychange', handleVisibility);
  }

  startConnection();
  return () => {
    closed = true;
    activeController?.abort();
    if (pauseWhenHidden) {
      document.removeEventListener('visibilitychange', handleVisibility);
    }
  };
}
