import { useEffect, useMemo, useRef, useState } from 'react';
import {
  AssistantRuntimeProvider,
  ComposerPrimitive,
  ErrorPrimitive,
  MessagePrimitive,
  ThreadPrimitive,
  useLocalRuntime,
  useMessage,
  useMessagePartText,
  useThread,
  type ChatModelAdapter,
  type ThreadMessage,
  type ThreadMessageLike,
} from '@assistant-ui/react';
import { StreamdownTextPrimitive } from '@assistant-ui/react-streamdown';
import { cjk } from '@streamdown/cjk';
import { code } from '@streamdown/code';
import { math } from '@streamdown/math';
import { mermaid } from '@streamdown/mermaid';
import type { GatewayApiKey } from '@easyai-ai-gateway/contracts';
import { Send } from 'lucide-react';
import { Button, Select } from '../components/ui';
import { GatewayApiError, streamChatCompletionText } from '../api';
import type { PlaygroundMode } from '../types';
import {
  chatUploadAccept as sharedChatUploadAccept,
  mediaUploadSummaryMessage as sharedMediaUploadSummaryMessage,
  openAIContentFromPromptAndUploads,
  PlaygroundReferencePicker,
  uploadPlaygroundFiles as sharedUploadPlaygroundFiles,
  type OpenAIChatContentPart,
  type PlaygroundUpload,
} from './playground-upload';
import {
  ApiKeySelect,
  ModeSwitch,
  PlaygroundGreeting,
  apiKeyNoticeText,
  modeOptions,
  modelOptionLabel,
  placeholderByMode,
  resolveSelectedApiKeyId,
  type ModelOption,
} from './playground-shared';

const CHAT_MESSAGES_STORAGE_KEY = 'easyai:playground:chat-messages:v1';
const CHAT_MESSAGES_STORAGE_LIMIT = 100;
const streamdownPlugins = { cjk, code, math, mermaid };

type OpenAIChatRole = 'assistant' | 'user';

interface StoredOpenAIChatMessage {
  content: OpenAIChatContentPart[] | string;
  createdAt: string;
  id: string;
  role: OpenAIChatRole;
}

type StoredOpenAIChatMessagesById = Record<string, StoredOpenAIChatMessage>;

export function AssistantChatPlayground(props: {
  apiKeySecretsById: Record<string, string>;
  apiKeys: GatewayApiKey[];
  modelOptions: ModelOption[];
  selectedApiKeyId: string;
  selectedModel: string;
  token: string;
  onApiKeyChange: (apiKeyId: string) => void;
  onCreateApiKey: () => void;
  onLogin: () => void;
  onModeChange: (mode: PlaygroundMode) => void;
  onModelChange: (value: string) => void;
}) {
  const activeApiKeyId = resolveSelectedApiKeyId(props.apiKeys, props.apiKeySecretsById, props.selectedApiKeyId);
  const activeApiKeySecret = activeApiKeyId ? props.apiKeySecretsById[activeApiKeyId] ?? '' : '';
  const canRun = Boolean(props.token && props.selectedModel && activeApiKeySecret);
  const apiKeyNotice = apiKeyNoticeText(props.apiKeys, props.apiKeySecretsById);
  const initialStoredMessages = useMemo(() => readStoredOpenAIChatMessages(), []);
  const initialMessages = useMemo(() => initialStoredMessages.map(threadMessageLikeFromOpenAIMessage), [initialStoredMessages]);
  const [storedMessagesById, setStoredMessagesById] = useState<StoredOpenAIChatMessagesById>(() => indexStoredOpenAIChatMessages(initialStoredMessages));
  const [chatUploadMessage, setChatUploadMessage] = useState('');
  const [chatUploads, setChatUploads] = useState<PlaygroundUpload[]>([]);
  const [chatUploading, setChatUploading] = useState(false);
  const chatUploadsRef = useRef(chatUploads);
  const storedMessagesByIdRef = useRef(storedMessagesById);

  useEffect(() => {
    chatUploadsRef.current = chatUploads;
  }, [chatUploads]);

  useEffect(() => {
    storedMessagesByIdRef.current = storedMessagesById;
  }, [storedMessagesById]);

  async function uploadChatFiles(files: File[]) {
    if (!files.length) return;
    if (!props.token) {
      props.onLogin();
      return;
    }
    if (!activeApiKeySecret) {
      setChatUploadMessage('请选择可用于测试的 API Key 后再上传。');
      return;
    }
    setChatUploading(true);
    setChatUploadMessage('');
    try {
      const { items, warnings } = await sharedUploadPlaygroundFiles(activeApiKeySecret, files, {
        allowFiles: true,
        source: 'ai-gateway-playground-chat',
      });
      if (items.length) {
        setChatUploads((current) => [...current, ...items]);
      }
      setChatUploadMessage(warnings[0] ?? (items.length ? `已上传 ${items.length} 个附件。` : ''));
    } catch (err) {
      setChatUploadMessage(err instanceof Error ? err.message : '文件上传失败');
    } finally {
      setChatUploading(false);
    }
  }

  const adapter = useMemo<ChatModelAdapter>(() => ({
    async *run({ abortSignal, messages }) {
      if (!props.token) {
        props.onLogin();
        throw new GatewayApiError('请先登录后再测试模型。');
      }
      if (!activeApiKeySecret) {
        throw new GatewayApiError('请选择可用于测试的 API Key；如果列表为空，请刷新或重新创建一个 Key。');
      }
      if (!props.selectedModel) {
        throw new GatewayApiError('当前没有可用的大模型，请确认用户组权限或平台模型配置。');
      }
      const requestUploads = chatUploadsRef.current;
      const request = buildGatewayChatMessages(messages, requestUploads, storedMessagesByIdRef.current);
      if (request.lastUserMessage) {
        setStoredMessagesById((current) => ({
          ...current,
          [request.lastUserMessage!.id]: request.lastUserMessage!,
        }));
      }
      if (requestUploads.length) {
        chatUploadsRef.current = [];
        setChatUploads([]);
        setChatUploadMessage('');
      }
      let text = '';
      for await (const delta of streamChatCompletionText(
        activeApiKeySecret,
        {
          messages: request.messages,
          model: props.selectedModel,
        },
        abortSignal,
      )) {
        text += delta;
        yield {
          content: [{ type: 'text', text }],
        };
      }
      yield {
        content: [{ type: 'text', text }],
        status: { type: 'complete', reason: 'stop' },
      };
    },
  }), [activeApiKeySecret, props]);
  const runtime = useLocalRuntime(adapter, { initialMessages });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <AssistantChatPersistenceBridge storedMessagesById={storedMessagesById} />
      <ThreadPrimitive.Root className="assistantThreadRoot">
        <ThreadPrimitive.Empty>
          <div className="assistantEmptyStage">
            <AssistantEmptyState
              apiKeyNotice={apiKeyNotice}
              apiKeySecretsById={props.apiKeySecretsById}
              apiKeys={props.apiKeys}
              canRun={canRun}
              modelOptions={props.modelOptions}
              selectedApiKeyId={activeApiKeyId}
              selectedModel={props.selectedModel}
              token={props.token}
              activeApiKeySecret={activeApiKeySecret}
              uploadAccept={sharedChatUploadAccept}
              uploadMessage={chatUploadMessage}
              uploads={chatUploads}
              uploading={chatUploading}
              onApiKeyChange={props.onApiKeyChange}
              onCreateApiKey={props.onCreateApiKey}
              onModeChange={props.onModeChange}
              onModelChange={props.onModelChange}
              onRemoveUpload={(id) => setChatUploads((current) => current.filter((item) => item.id !== id))}
              onUploadFiles={(files) => void uploadChatFiles(files)}
            />
          </div>
        </ThreadPrimitive.Empty>
        <ThreadPrimitive.If empty={false}>
          <div className="assistantShell" data-has-notice={Boolean(apiKeyNotice)}>
            {apiKeyNotice && (
              <div className="assistantApiKeyNotice">
                <span>{apiKeyNotice}</span>
                <Button type="button" size="sm" variant="secondary" onClick={props.onCreateApiKey}>
                  去创建 API Key
                </Button>
              </div>
            )}
            <ThreadPrimitive.Viewport className="assistantThreadViewport">
              <div className="assistantMessageList">
                <ThreadPrimitive.Messages
                  components={{
                    Message: () => (
                      <AssistantMessage storedMessagesById={storedMessagesById} />
                    ),
                  }}
                />
              </div>
              <ThreadPrimitive.ViewportFooter className="assistantComposerDock">
                <AssistantChatComposer
                  apiKeyNotice={apiKeyNotice}
                  apiKeySecretsById={props.apiKeySecretsById}
                  apiKeys={props.apiKeys}
                  canRun={canRun}
                  docked
                  modelOptions={props.modelOptions}
                  placeholder={assistantPlaceholder(props.token, props.selectedModel, activeApiKeySecret)}
                  selectedApiKeyId={activeApiKeyId}
                  selectedModel={props.selectedModel}
                  uploadAccept={sharedChatUploadAccept}
                  uploadMessage={chatUploadMessage}
                  uploads={chatUploads}
                  uploading={chatUploading}
                  onApiKeyChange={props.onApiKeyChange}
                  onCreateApiKey={props.onCreateApiKey}
                  onModeChange={props.onModeChange}
                  onModelChange={props.onModelChange}
                  onRemoveUpload={(id) => setChatUploads((current) => current.filter((item) => item.id !== id))}
                  onUploadFiles={(files) => void uploadChatFiles(files)}
                />
              </ThreadPrimitive.ViewportFooter>
            </ThreadPrimitive.Viewport>
          </div>
        </ThreadPrimitive.If>
      </ThreadPrimitive.Root>
    </AssistantRuntimeProvider>
  );
}

function AssistantChatPersistenceBridge(props: { storedMessagesById: StoredOpenAIChatMessagesById }) {
  const messages = useThread((state) => state.messages);
  const skipInitialEmptyWriteRef = useRef(true);

  useEffect(() => {
    if (skipInitialEmptyWriteRef.current) {
      skipInitialEmptyWriteRef.current = false;
      if (!messages.length && hasStoredChatMessages()) return;
    }
    writeStoredOpenAIChatMessages(messages, props.storedMessagesById);
  }, [messages, props.storedMessagesById]);

  return null;
}

function AssistantEmptyState(props: {
  activeApiKeySecret: string;
  apiKeyNotice: string;
  apiKeySecretsById: Record<string, string>;
  apiKeys: GatewayApiKey[];
  canRun: boolean;
  modelOptions: ModelOption[];
  selectedApiKeyId: string;
  selectedModel: string;
  token: string;
  uploadAccept: string;
  uploadMessage: string;
  uploads: PlaygroundUpload[];
  uploading: boolean;
  onApiKeyChange: (apiKeyId: string) => void;
  onCreateApiKey: () => void;
  onModeChange: (mode: PlaygroundMode) => void;
  onModelChange: (value: string) => void;
  onRemoveUpload: (id: string) => void;
  onUploadFiles: (files: File[]) => void;
}) {
  const activeMode = modeOptions.find((item) => item.value === 'chat') ?? modeOptions[0];
  const placeholder = props.canRun ? placeholderByMode.chat : assistantPlaceholder(props.token, props.selectedModel, props.activeApiKeySecret);

  return (
    <div className="assistantEmpty">
      <ModeSwitch activeMode="chat" onModeChange={props.onModeChange} />
      <PlaygroundGreeting activeMode={activeMode} />
      <AssistantChatComposer
        apiKeyNotice={props.apiKeyNotice}
        apiKeySecretsById={props.apiKeySecretsById}
        apiKeys={props.apiKeys}
        canRun={props.canRun}
        modelOptions={props.modelOptions}
        placeholder={placeholder}
        selectedApiKeyId={props.selectedApiKeyId}
        selectedModel={props.selectedModel}
        uploadAccept={props.uploadAccept}
        uploadMessage={props.uploadMessage}
        uploads={props.uploads}
        uploading={props.uploading}
        onApiKeyChange={props.onApiKeyChange}
        onCreateApiKey={props.onCreateApiKey}
        onModeChange={props.onModeChange}
        onModelChange={props.onModelChange}
        onRemoveUpload={props.onRemoveUpload}
        onUploadFiles={props.onUploadFiles}
      />
    </div>
  );
}

function AssistantChatComposer(props: {
  apiKeyNotice: string;
  apiKeySecretsById: Record<string, string>;
  apiKeys: GatewayApiKey[];
  canRun: boolean;
  docked?: boolean;
  modelOptions: ModelOption[];
  placeholder: string;
  selectedApiKeyId: string;
  selectedModel: string;
  uploadAccept?: string;
  uploadMessage?: string;
  uploads?: PlaygroundUpload[];
  uploading?: boolean;
  onApiKeyChange: (apiKeyId: string) => void;
  onCreateApiKey: () => void;
  onModeChange: (mode: PlaygroundMode) => void;
  onModelChange: (value: string) => void;
  onRemoveUpload?: (id: string) => void;
  onUploadFiles?: (files: File[]) => void;
}) {
  const className = ['playgroundComposer', 'assistantChatComposer', props.docked ? 'assistantDockComposer' : 'assistantEmptyComposer'].join(' ');
  const uploadMessage = props.uploadMessage || sharedMediaUploadSummaryMessage(props.uploads ?? [], 'chat', 'text_to_video');

  return (
    <ComposerPrimitive.Root className={className}>
      <div className="composerBody composerBodyWithReferences">
        <PlaygroundReferencePicker
          accept={props.uploadAccept ?? sharedChatUploadAccept}
          disabled={!props.canRun || !props.onUploadFiles}
          mode="chat"
          uploadLabel="上传附件"
          uploads={props.uploads ?? []}
          uploading={props.uploading}
          onFiles={props.onUploadFiles}
          onRemove={props.onRemoveUpload}
        />
        <div className="composerInputStack">
          <ComposerPrimitive.Input
            className="assistantEmptyInput"
            disabled={!props.canRun}
            placeholder={props.placeholder}
          />
          {uploadMessage && <div className="composerUploadMessage">{uploadMessage}</div>}
        </div>
      </div>
      <div className="composerFooter">
        <Select value="chat" onChange={(event) => props.onModeChange(event.target.value as PlaygroundMode)}>
          {modeOptions.map((item) => <option value={item.value} key={item.value}>{item.label}</option>)}
        </Select>
        <Select
          className="playgroundModelSelect"
          value={props.selectedModel}
          disabled={!props.modelOptions.length}
          onChange={(event) => props.onModelChange(event.target.value)}
        >
          {props.modelOptions.length ? props.modelOptions.map((item) => (
            <option value={item.value} key={item.value}>{modelOptionLabel(item)}</option>
          )) : <option value="">没有可用模型</option>}
        </Select>
        <ApiKeySelect
          apiKeySecretsById={props.apiKeySecretsById}
          apiKeys={props.apiKeys}
          selectedApiKeyId={props.selectedApiKeyId}
          onApiKeyChange={props.onApiKeyChange}
        />
        {props.apiKeyNotice && (
          <Button type="button" size="sm" variant="secondary" onClick={props.onCreateApiKey}>
            创建 API Key
          </Button>
        )}
        <ComposerPrimitive.Send className="composerSendButton" disabled={!props.canRun} aria-label="发送消息">
          <Send size={18} />
        </ComposerPrimitive.Send>
      </div>
    </ComposerPrimitive.Root>
  );
}

function AssistantMessage(props: { storedMessagesById: StoredOpenAIChatMessagesById }) {
  const messageId = useMessage((state) => state.id);
  const messageContent = useMessage((state) => state.content);
  const hasError = useMessage((state) => state.status?.type === 'incomplete' && state.status.reason === 'error');
  const storedMessage = messageId ? props.storedMessagesById[messageId] : undefined;
  const imageParts = imagePartsFromOpenAIContent(storedMessage?.content);
  const hasText = threadMessageContentText(messageContent).trim().length > 0;

  return (
    <MessagePrimitive.Root className="assistantMessage">
      <MessagePrimitive.If user>
        <div className="assistantUserMessage">
          <ChatMessageImagePreviews parts={imageParts} />
          {hasText && (
            <div className="assistantBubble user">
              <MessagePrimitive.Parts components={{ Text: PlainMessageText }} />
            </div>
          )}
        </div>
      </MessagePrimitive.If>
      <MessagePrimitive.If assistant>
        <div className={hasError ? 'assistantBubble assistant error' : 'assistantBubble assistant'}>
          <MessagePrimitive.Parts components={{ Text: AssistantMarkdownText }} />
          <MessagePrimitive.Error>
            <strong>调用失败</strong>
            <ErrorPrimitive.Message className="assistantErrorMessage" />
          </MessagePrimitive.Error>
          {!hasError && (
            <MessagePrimitive.If hasContent={false}>
              <span className="assistantTyping">模型正在回复...</span>
            </MessagePrimitive.If>
          )}
        </div>
      </MessagePrimitive.If>
    </MessagePrimitive.Root>
  );
}

function ChatMessageImagePreviews(props: { parts: Array<{ name: string; url: string }> }) {
  if (!props.parts.length) return null;
  return (
    <div className="assistantUserImageGrid">
      {props.parts.map((item) => (
        <a className="assistantUserImagePreview" href={item.url} key={item.url} rel="noreferrer" target="_blank" title={item.name}>
          <img src={item.url} alt={item.name} loading="lazy" />
        </a>
      ))}
    </div>
  );
}

function PlainMessageText() {
  const { text } = useMessagePartText();
  return <span className="assistantPlainText">{text}</span>;
}

function AssistantMarkdownText() {
  return (
    <StreamdownTextPrimitive
      containerClassName="assistantMarkdown"
      plugins={streamdownPlugins}
      shikiTheme={['github-light', 'github-dark']}
    />
  );
}

interface GatewayChatMessageForRequest extends Record<string, unknown> {
  content: OpenAIChatContentPart[] | string;
  role: OpenAIChatRole;
}

function buildGatewayChatMessages(
  messages: readonly ThreadMessage[],
  uploads: PlaygroundUpload[],
  storedMessagesById: StoredOpenAIChatMessagesById,
) {
  const sourceMessages = messages.filter((message) => message.role === 'user' || message.role === 'assistant');
  let sourceLastUserIndex = -1;
  sourceMessages.forEach((message, index) => {
    if (message.role === 'user') sourceLastUserIndex = index;
  });
  const gatewayMessages: GatewayChatMessageForRequest[] = [];
  let lastUserMessage: StoredOpenAIChatMessage | undefined;
  sourceMessages.forEach((message, index) => {
    const isUploadTarget = uploads.length > 0 && index === sourceLastUserIndex && message.role === 'user';
    const text = threadMessageText(message);
    const preserved = storedMessagesById[message.id];
    const content = isUploadTarget
      ? openAIContentFromPromptAndUploads(text, uploads)
      : preserved?.content ?? text;
    if (!openAIContentHasPayload(content)) return;
    gatewayMessages.push({
      content,
      role: message.role,
    });
    if (isUploadTarget) {
      lastUserMessage = {
        content,
        createdAt: message.createdAt.toISOString(),
        id: message.id,
        role: 'user',
      };
    }
  });
  return { lastUserMessage, messages: gatewayMessages };
}

function readStoredOpenAIChatMessages(): StoredOpenAIChatMessage[] {
  if (typeof window === 'undefined') return [];
  try {
    const raw = window.localStorage.getItem(CHAT_MESSAGES_STORAGE_KEY);
    if (!raw) return [];
    const parsed = JSON.parse(raw) as unknown;
    const record = recordFromUnknown(parsed);
    const source = Array.isArray(parsed) ? parsed : record?.messages;
    if (!Array.isArray(source)) return [];
    const legacyImagesByMessageId = recordFromUnknown(record?.imageUploadsByMessageId);
    return source
      .map((item) => storedOpenAIChatMessageFromStorage(item, legacyImagesByMessageId))
      .filter((item): item is StoredOpenAIChatMessage => Boolean(item))
      .slice(-CHAT_MESSAGES_STORAGE_LIMIT);
  } catch {
    return [];
  }
}

function writeStoredOpenAIChatMessages(messages: readonly ThreadMessage[], preservedMessagesById: StoredOpenAIChatMessagesById) {
  if (typeof window === 'undefined') return;
  try {
    const storedMessages = messages
      .map((message) => storedOpenAIChatMessageFromThread(message, preservedMessagesById[message.id]))
      .filter((item): item is StoredOpenAIChatMessage => Boolean(item))
      .slice(-CHAT_MESSAGES_STORAGE_LIMIT);
    if (!storedMessages.length) {
      window.localStorage.removeItem(CHAT_MESSAGES_STORAGE_KEY);
      return;
    }
    window.localStorage.setItem(CHAT_MESSAGES_STORAGE_KEY, JSON.stringify({
      messages: storedMessages,
      version: 2,
    }));
  } catch {
    // Best effort only: local chat history should not block sending messages.
  }
}

export function clearStoredChatMessages() {
  if (typeof window === 'undefined') return;
  try {
    window.localStorage.removeItem(CHAT_MESSAGES_STORAGE_KEY);
  } catch {
    // Ignore storage errors.
  }
}

function hasStoredChatMessages() {
  if (typeof window === 'undefined') return false;
  try {
    return Boolean(window.localStorage.getItem(CHAT_MESSAGES_STORAGE_KEY));
  } catch {
    return false;
  }
}

function storedOpenAIChatMessageFromThread(message: ThreadMessage, preserved?: StoredOpenAIChatMessage): StoredOpenAIChatMessage | undefined {
  if (message.role !== 'assistant' && message.role !== 'user') return undefined;
  const content = message.role === 'user' && preserved
    ? preserved.content
    : threadMessageText(message);
  if (!openAIContentHasPayload(content)) return undefined;
  return {
    content,
    createdAt: message.createdAt.toISOString(),
    id: message.id,
    role: message.role,
  };
}

function storedOpenAIChatMessageFromStorage(value: unknown, legacyImagesByMessageId?: Record<string, unknown>): StoredOpenAIChatMessage | undefined {
  const record = recordFromUnknown(value);
  if (!record) return undefined;
  const role = record.role === 'assistant' || record.role === 'user' ? record.role : undefined;
  const id = stringFromUnknown(record.id);
  if (!role || !id) return undefined;
  const createdAt = dateStringFromUnknown(record.createdAt) ?? new Date().toISOString();
  let content = openAIContentFromUnknown(record.content);
  const legacyImages = Array.isArray(legacyImagesByMessageId?.[id]) ? legacyImagesByMessageId[id] as unknown[] : [];
  if (role === 'user' && legacyImages.length && (typeof content === 'string' || !content)) {
    content = openAIContentWithLegacyImages(typeof content === 'string' ? content : '', legacyImages);
  }
  if (!content || !openAIContentHasPayload(content)) return undefined;
  return {
    content,
    createdAt,
    id,
    role,
  };
}

function threadMessageLikeFromOpenAIMessage(message: StoredOpenAIChatMessage): ThreadMessageLike {
  return {
    content: threadMessageLikeContentFromOpenAIContent(message.content),
    createdAt: new Date(message.createdAt),
    id: message.id,
    role: message.role,
    status: message.role === 'assistant' ? { type: 'complete', reason: 'stop' } : undefined,
  };
}

function indexStoredOpenAIChatMessages(messages: StoredOpenAIChatMessage[]) {
  return messages.reduce<StoredOpenAIChatMessagesById>((result, message) => {
    result[message.id] = message;
    return result;
  }, {});
}

function threadMessageLikeContentFromOpenAIContent(content: OpenAIChatContentPart[] | string): ThreadMessageLike['content'] {
  const text = openAIContentText(content);
  return text || ' ';
}

function threadMessageText(message: ThreadMessage) {
  return threadMessageContentText(message.content).trim();
}

function threadMessageContentText(content: ThreadMessage['content']) {
  return content
    .map((part) => part.type === 'text' ? part.text : '')
    .join('')
    .trim();
}

function openAIContentFromUnknown(value: unknown): OpenAIChatContentPart[] | string | undefined {
  if (typeof value === 'string') return value;
  if (!Array.isArray(value)) return undefined;
  const parts = value
    .map(openAIContentPartFromUnknown)
    .filter((item): item is OpenAIChatContentPart => Boolean(item));
  return parts.length ? parts : undefined;
}

function openAIContentPartFromUnknown(value: unknown): OpenAIChatContentPart | undefined {
  const record = recordFromUnknown(value);
  if (!record) return undefined;
  if (record.type === 'text' && typeof record.text === 'string') {
    return { type: 'text', text: record.text };
  }
  if (record.type === 'image_url') {
    const imageUrl = recordFromUnknown(record.image_url);
    const url = stringFromUnknown(imageUrl?.url);
    return url ? { type: 'image_url', image_url: { url } } : undefined;
  }
  if (record.type === 'video_url') {
    const videoUrl = recordFromUnknown(record.video_url);
    const url = stringFromUnknown(videoUrl?.url);
    return url ? { type: 'video_url', video_url: { url } } : undefined;
  }
  if (record.type === 'audio_url') {
    const audioUrl = recordFromUnknown(record.audio_url);
    const url = stringFromUnknown(audioUrl?.url);
    return url ? { type: 'audio_url', audio_url: { url } } : undefined;
  }
  if (record.type === 'file_url') {
    const fileUrl = recordFromUnknown(record.file_url);
    const url = stringFromUnknown(fileUrl?.url);
    const filename = stringFromUnknown(fileUrl?.filename) || '文件';
    return url ? { type: 'file_url', file_url: { filename, url } } : undefined;
  }
  return undefined;
}

function openAIContentWithLegacyImages(text: string, images: unknown[]): OpenAIChatContentPart[] {
  const content: OpenAIChatContentPart[] = [];
  if (text.trim()) {
    content.push({ type: 'text', text });
  }
  images.forEach((item) => {
    const record = recordFromUnknown(item);
    const url = stringFromUnknown(record?.url);
    if (url) {
      content.push({ type: 'image_url', image_url: { url } });
    }
  });
  return content;
}

function openAIContentHasPayload(content: OpenAIChatContentPart[] | string) {
  if (typeof content === 'string') return content.trim().length > 0;
  return content.some((part) => {
    if (part.type === 'text') return part.text.trim().length > 0;
    return true;
  });
}

function openAIContentText(content: OpenAIChatContentPart[] | string) {
  if (typeof content === 'string') return content;
  return content
    .map((part) => part.type === 'text' ? part.text : '')
    .join('')
    .trim();
}

function imagePartsFromOpenAIContent(content: OpenAIChatContentPart[] | string | undefined) {
  if (!Array.isArray(content)) return [];
  return content.flatMap((part) => {
    if (part.type !== 'image_url') return [];
    return [{ name: '图片', url: part.image_url.url }];
  });
}

function assistantPlaceholder(token: string, selectedModel: string, apiKeySecret: string) {
  if (!token) return '请先登录后再测试模型';
  if (!apiKeySecret) return '请选择可用于测试的 API Key';
  if (!selectedModel) return '当前没有可用模型';
  return '输入消息，Enter 发送，Shift + Enter 换行';
}

function recordFromUnknown(value: unknown): Record<string, unknown> | undefined {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
  return value as Record<string, unknown>;
}

function stringFromUnknown(value: unknown) {
  return typeof value === 'string' ? value.trim() : '';
}

function dateStringFromUnknown(value: unknown) {
  if (typeof value !== 'string') return undefined;
  const timestamp = Date.parse(value);
  return Number.isNaN(timestamp) ? undefined : new Date(timestamp).toISOString();
}
