import { Fragment, useEffect, useState, type CSSProperties, type FormEvent, type ReactNode } from 'react';
import type { GatewayApiKey, GatewaySkillBundleMetadata, GatewayTask } from '@easyai-ai-gateway/contracts';
import { BookOpen, Download, ExternalLink, FileJson, KeyRound, Play, Search, Send, Wrench } from 'lucide-react';
import { Badge, Button, Input, Select, Textarea } from '../components/ui';
import { getOpsManagementSkillMetadata, resolveApiAssetUrl } from '../api';
import { applyTaskSubmissionMode, type RunTaskOptions } from '../lib/run-task';
import type { ApiDocSection, LoadState, TaskForm, TaskKind, TaskSubmissionMode } from '../types';
import { ApiKeySelect, apiKeyNoticeText, resolveSelectedApiKeyId } from './playground-shared';
import {
  publicApiCatalogGroups,
  publicApiEndpointCount,
  type PublicApiFamily,
  type PublicApiMethod,
  type PublicApiSurface,
} from './api-docs-catalog';

interface ApiDocItem {
  catalogFamily?: PublicApiFamily;
  catalogSurface?: PublicApiSurface;
  group: string;
  key: ApiDocSection;
  kind?: TaskKind;
  lead: string;
  method?: PublicApiMethod;
  path?: string;
  surface: PublicApiSurface;
  title: string;
}

interface ApiDocParam {
  children?: ApiDocParam[];
  name: string;
  required?: boolean;
  type: string;
  value: string;
}

type ApiGuideSection = Extract<ApiDocSection, 'guideBaseUrl' | 'guideWebhook' | 'guideErrors' | 'guideTesting'>;

interface ApiGuideItem {
  group: '指南';
  key: ApiGuideSection;
  lead: string;
  title: string;
}

export const apiDocs: ApiDocItem[] = [
  { key: 'openApiOverview', surface: 'open', catalogSurface: 'open', group: '接口总览', title: '通用开放接口', lead: '统一的 Gateway 接口面向新业务接入，覆盖账号、模型、生成、异步任务、真人资产和安全集成；生产 Base URL 固定为 https://ai.51easyai.com/api/v1。' },
  { key: 'chat', surface: 'open', group: '文本', kind: 'chat.completions', method: 'POST', path: '/api/v1/chat/completions', title: 'Chat Completions', lead: 'OpenAI 兼容的对话接口，支持本地 API Key 授权、simulation 测试和非流式/流式响应。' },
  { key: 'responses', surface: 'open', group: '文本', kind: 'responses', method: 'POST', path: '/api/v1/responses', title: 'Responses', lead: 'OpenAI 兼容的 Responses 接口，原生支持 input、previous_response_id、工具调用和流式输出；不支持原生 Responses 的模型会由网关转换到 Chat Completions。' },
  { key: 'embeddings', surface: 'open', group: '文本', kind: 'embeddings', method: 'POST', path: '/api/v1/embeddings', title: '文本向量 Embeddings', lead: 'OpenAI 兼容的文本向量接口，可直接用 input 数组或字符串生成 embedding，API Key 需要 embedding 权限。' },
  { key: 'reranks', surface: 'open', group: '文本', kind: 'reranks', method: 'POST', path: '/api/v1/reranks', title: '文本重排序 Reranks', lead: 'OpenAI 风格的重排序接口，传入 query 和 documents 后返回 relevance_score，API Key 需要 rerank 权限。' },
  { key: 'imageGeneration', surface: 'open', group: '图片', kind: 'images.generations', method: 'POST', path: '/api/v1/images/generations', title: '创建图片', lead: 'OpenAI 兼容的图片生成接口，支持 prompt、size、quality 和 simulation 测试。' },
  { key: 'imageEdit', surface: 'open', group: '图片', kind: 'images.edits', method: 'POST', path: '/api/v1/images/edits', title: '编辑图片', lead: 'OpenAI 兼容的图片编辑接口，支持 image、mask、prompt 和 simulation 测试。' },
  { key: 'videoGeneration', surface: 'open', group: '视频', kind: 'videos.generations', method: 'POST', path: '/api/v1/videos/generations', title: '生成视频', lead: '视频生成任务接口，支持文生视频、首尾帧、图片/视频/音频参考，以及时长、分辨率、画幅和声音等模型能力参数。' },
  { key: 'asyncMode', surface: 'open', group: '异步任务', title: '异步模式', lead: '所有 AI 任务创建接口使用同一种异步开启方式：保留原接口和原请求 Body，只需增加 X-Async: true。' },
  { key: 'taskRetrieve', surface: 'open', group: '异步任务', kind: 'tasks.retrieve', method: 'GET', path: '/api/v1/tasks/{taskID}', title: '取回任务', lead: '使用异步提交返回的 taskId 查询任务状态、结果、错误、用量、计费和执行尝试；queued、running、submitting 为进行中状态。' },
  { key: 'pricing', surface: 'open', group: '计费', method: 'POST', path: '/api/v1/pricing/estimate', title: '价格预估', lead: '按请求体估算输入输出 token、模型倍率和折扣后的预估费用。' },
  { key: 'files', surface: 'open', group: '文件', method: 'POST', path: '/api/v1/files/upload', title: '上传文件', lead: '上传在线测试所需的图片、音频或视频资源，后续请求可复用返回的文件 URL。' },
  { key: 'compatApiOverview', surface: 'compatibility', catalogSurface: 'compatibility', group: '接口总览', title: '兼容接口', lead: '为已有厂商 SDK 和存量业务保留请求、响应及任务协议；调用方只需替换 Base URL 和 Gateway API Key。' },
  { key: 'compatGemini', surface: 'compatibility', catalogFamily: 'gemini', group: '协议分类', title: 'Gemini 兼容接口', lead: '保留 generateContent 和 Gemini Files 请求结构，统一通过 /api/v1 前缀接入。' },
  { key: 'compatKling', surface: 'compatibility', catalogFamily: 'kling', group: '协议分类', title: '可灵兼容接口', lead: '覆盖中国区可灵官方 V1 Omni、网关 V1 和 API 2.0；上游 AK/SK 只保存在网关服务端。' },
  { key: 'compatVolces', surface: 'compatibility', catalogFamily: 'volces', group: '协议分类', title: '火山兼容接口', lead: '保留火山内容生成任务的创建、列表、查询和取消协议。' },
  { key: 'compatServerMain', surface: 'compatibility', catalogFamily: 'serverMain', group: '协议分类', title: 'server-main 兼容接口', lead: '保留 server-main 的视频提交与结果轮询响应结构。' },
];

const guideItems: ApiGuideItem[] = [
  { key: 'guideBaseUrl', group: '指南', title: '获取 Base URL 和 API Key', lead: '确认当前部署环境的网关地址，创建 API Key，并使用 Bearer 鉴权调用公开接口。' },
  { key: 'guideWebhook', group: '指南', title: '通知设置-WebHook 参数介绍', lead: '了解任务进度 WebHook 的启用方式、回调结构、可靠投递机制和公开 API 的状态取回方式。' },
  { key: 'guideErrors', group: '指南', title: '错误码', lead: '根据 HTTP 状态、error.code、requestId 和异步任务错误字段快速定位请求失败原因。' },
  { key: 'guideTesting', group: '指南', title: '测试模式', lead: '使用 simulation 完整验证鉴权、路由、队列、限流、进度和结果归一，而不向真实供应商提交任务。' },
];

const taskKindOptions = [
  ['chat.completions', 'Chat'],
  ['responses', 'Responses'],
  ['embeddings', '文本向量'],
  ['reranks', '重排序'],
  ['images.generations', '生图'],
  ['images.edits', '图像编辑'],
  ['videos.generations', '视频生成'],
  ['tasks.retrieve', '取回任务'],
] as const;

const defaultOpsSkillMetadata: GatewaySkillBundleMetadata = {
  name: 'ai-gateway-ops-management',
  version: '',
  displayName: 'AI Gateway 运维管理',
  modules: ['model-runtime'],
  fileName: 'ai-gateway-ops-management.zip',
  downloadPath: '/api/v1/public/skills/ai-gateway-ops-management/download',
  apiDocsJsonPath: '/api/v1/openapi.json',
  apiDocsYamlPath: '/api/v1/openapi.yaml',
};

export function ApiDocsPage(props: {
  activeDocSection: ApiDocSection;
  apiKeySecretsById: Record<string, string>;
  apiKeys: GatewayApiKey[];
  canRun: boolean;
  coreMessage: string;
  coreState: LoadState;
  selectedApiKeyId: string;
  taskForm: TaskForm;
  taskResult: GatewayTask | null;
  onApiKeyChange: (apiKeyId: string) => void;
  onCreateApiKey: () => void;
  onLogin: () => void;
  onDocSectionChange: (value: ApiDocSection) => void;
  onSubmitTask: (event: FormEvent<HTMLFormElement>, options?: RunTaskOptions) => void;
  onTaskFormChange: (value: TaskForm) => void;
}) {
  const activeGuide = guideItems.find((item) => item.key === props.activeDocSection);
  const currentApiDoc = apiDocs.find((item) => item.key === props.activeDocSection) ?? (activeGuide ? undefined : apiDocs[0]);
  const current = activeGuide ?? currentApiDoc ?? apiDocs[0];
  const [opsSkillMetadata, setOpsSkillMetadata] = useState<GatewaySkillBundleMetadata>(defaultOpsSkillMetadata);
  const [docsSearch, setDocsSearch] = useState('');
  const normalizedDocsSearch = docsSearch.trim().toLowerCase();
  const visibleGuideItems = guideItems.filter((item) => guideMatchesSearch(item, normalizedDocsSearch));
  const visibleApiDocs = apiDocs.filter((item) => apiDocMatchesSearch(item, normalizedDocsSearch));
  const isFileDoc = currentApiDoc?.key === 'files';
  const isTaskRetrieveDoc = currentApiDoc?.key === 'taskRetrieve';
  const isAsyncModeDoc = currentApiDoc?.key === 'asyncMode';
  const isCatalogDoc = Boolean(currentApiDoc?.catalogSurface || currentApiDoc?.catalogFamily);
  const runnerAvailable = Boolean(currentApiDoc?.kind && currentApiDoc.method && currentApiDoc.path);
  const runnerModeEnabled = Boolean(runnerAvailable && currentApiDoc?.method !== 'GET');
  const apiKeyNotice = apiKeyNoticeText(props.apiKeys, props.apiKeySecretsById);
  const activeApiKeyId = resolveSelectedApiKeyId(props.apiKeys, props.apiKeySecretsById, props.selectedApiKeyId);
  const [submissionMode, setSubmissionMode] = useState<TaskSubmissionMode>('simulation');
  const [bodyDraft, setBodyDraft] = useState(() => requestBodyExample(props.taskForm, currentApiDoc?.key ?? 'chat', 'simulation'));
  const [bodyError, setBodyError] = useState('');
  const runnerPath = currentApiDoc?.path
    ? isTaskRetrieveDoc
      ? currentApiDoc.path.replace('{taskID}', props.taskForm.taskId?.trim() || '{taskID}')
      : currentApiDoc.path
    : '';

  useEffect(() => {
    if (currentApiDoc?.kind && props.taskForm.kind !== currentApiDoc.kind) {
      props.onTaskFormChange(defaultTaskForDoc(currentApiDoc.kind, props.taskForm, props.taskResult));
    }
  }, [currentApiDoc?.kind, props.taskForm.kind, props.taskResult?.id]);

  useEffect(() => {
    setSubmissionMode('simulation');
    setBodyDraft(requestBodyExample(props.taskForm, currentApiDoc?.key ?? 'chat', 'simulation'));
    setBodyError('');
  }, [currentApiDoc?.key, props.taskForm]);

  useEffect(() => {
    let active = true;
    getOpsManagementSkillMetadata()
      .then((metadata) => {
        if (active) setOpsSkillMetadata(metadata);
      })
      .catch(() => {
        // Keep stable public fallback paths visible when metadata is temporarily unavailable.
      });
    return () => {
      active = false;
    };
  }, []);

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!runnerAvailable) {
      return;
    }
    if (!props.canRun) {
      props.onLogin();
      return;
    }
    if (runnerModeEnabled) {
      const parsed = parseEditableRequestBody(bodyDraft);
      if (parsed.error || !parsed.body) {
        setBodyError(parsed.error);
        return;
      }
      const requestBody = applyTaskSubmissionMode(parsed.body, submissionMode);
      setBodyDraft(JSON.stringify(requestBody, null, 2));
      setBodyError('');
      props.onSubmitTask(event, { requestBody, submissionMode });
      return;
    }
    props.onSubmitTask(event);
  }

  function handleBodyChange(value: string) {
    setBodyDraft(value);
    setBodyError(parseEditableRequestBody(value).error);
  }

  function handleBodyBlur() {
    const parsed = parseEditableRequestBody(bodyDraft);
    if (parsed.error || !parsed.body) {
      setBodyError(parsed.error);
      return;
    }
    setBodyDraft(JSON.stringify(applyTaskSubmissionMode(parsed.body, submissionMode), null, 2));
    setBodyError('');
  }

  function handleSubmissionModeChange(nextMode: TaskSubmissionMode) {
    const parsed = parseEditableRequestBody(bodyDraft);
    if (parsed.error || !parsed.body) {
      setBodyError('请先修正请求 Body 的 JSON 格式，再切换运行模式。');
      return;
    }
    setSubmissionMode(nextMode);
    setBodyDraft(JSON.stringify(applyTaskSubmissionMode(parsed.body, nextMode), null, 2));
    setBodyError('');
  }

  function handleDocClick(item: ApiDocItem) {
    if (item.kind) {
      props.onTaskFormChange(defaultTaskForDoc(item.kind, props.taskForm, props.taskResult));
    }
    props.onDocSectionChange(item.key);
  }

  function handleGuideClick(item: ApiGuideItem) {
    props.onDocSectionChange(item.key);
  }

  function handleKindChange(kind: TaskKind) {
    props.onTaskFormChange(defaultTaskForDoc(kind, props.taskForm, props.taskResult));
    const nextSection = docSectionForKind(kind);
    if (nextSection !== props.activeDocSection) {
      props.onDocSectionChange(nextSection);
    }
  }

  return (
    <div className="apiDocsShell">
      <aside className="docsSidebar">
        <div className="docsBrand">
          <BookOpen size={19} />
          <strong>API Docs</strong>
        </div>
        <label className="docsSearch">
          <Search size={15} />
          <input
            aria-label="搜索 API 文档"
            placeholder="搜索名称或路径"
            value={docsSearch}
            onChange={(event) => setDocsSearch(event.target.value)}
          />
        </label>
        {visibleGuideItems.length > 0 && (
          <DocsGroup
            title="指南"
            items={visibleGuideItems.map((item) => ({
              active: item.key === props.activeDocSection,
              title: item.title,
              onClick: () => handleGuideClick(item),
            }))}
          />
        )}
        {groupDocsBySurface(visibleApiDocs).map((surface) => (
          <section className="docsSurface" data-surface={surface.key} key={surface.key}>
            <div className="docsSurfaceHeader">
              <strong>{apiSurfaceTitle(surface.key)}</strong>
              <span>{publicApiEndpointCount(surface.key)} 项</span>
            </div>
            {groupDocs(surface.items).map((group) => (
              <DocsGroup
                key={group.title}
                title={group.title}
                items={group.items.map((item) => ({
                  active: item.key === props.activeDocSection,
                  method: item.method,
                  title: item.title,
                  onClick: () => handleDocClick(item),
                }))}
              />
            ))}
          </section>
        ))}
        {normalizedDocsSearch && visibleGuideItems.length === 0 && visibleApiDocs.length === 0 && (
          <div className="docsSearchEmpty">没有找到匹配的接口或指南</div>
        )}
      </aside>

      <main className="docsArticle">
        <p className="eyebrow">{current.group}</p>
        <h1>{current.title}</h1>
        {currentApiDoc?.method && currentApiDoc.path && (
          <div className="endpointBar">
            <MethodBadge method={currentApiDoc.method} />
            <code>{currentApiDoc.path}</code>
          </div>
        )}
        <p className="docsLead">{current.lead}</p>

        {isCatalogDoc && currentApiDoc ? (
          <ApiCatalogDetails
            family={currentApiDoc.catalogFamily}
            search={normalizedDocsSearch}
            surface={currentApiDoc.catalogSurface ?? currentApiDoc.surface}
          />
        ) : isAsyncModeDoc ? (
          <DocDetails section="asyncMode" />
        ) : currentApiDoc ? (
          <>
            {!isTaskRetrieveDoc && <OpsManagementSkillCard metadata={opsSkillMetadata} />}

            <section className="paramCard">
              <header>
                <h2>Header 参数</h2>
                <Button type="button" variant="secondary" size="sm">生成代码</Button>
              </header>
              {currentApiDoc.method !== 'GET' && <ParamRow name="Content-Type" type="string" required value={isFileDoc ? 'multipart/form-data' : 'application/json'} />}
              <ParamRow name="Accept" type="string" required value="application/json" />
              <ParamRow name="Authorization" type="string" required value={authorizationDescription(currentApiDoc.key)} />
              {supportsAsyncMode(currentApiDoc.key) && (
                <ParamRow name="X-Async" type="boolean" value="设为 true 后立即返回 202 和 taskId；省略时按接口默认方式执行。" />
              )}
            </section>

            <section className="paramCard">
              <header>
                <h2>{isTaskRetrieveDoc ? 'Path 参数' : 'Body 参数'}</h2>
                {!isTaskRetrieveDoc && <Badge variant="outline">{isFileDoc ? 'multipart/form-data' : 'application/json'}</Badge>}
              </header>
              {isTaskRetrieveDoc ? (
                <ParamRow name="taskID" type="string" required value="异步提交响应中的 taskId，也可直接使用 next.detail 返回的地址。" />
              ) : isFileDoc ? (
                <>
                  <ParamRow name="file" type="file" required value="multipart 文件字段" />
                  <ParamRow name="source" type="string" value="上传来源标记" />
                </>
              ) : (
                <ParamRows rows={bodyParamRows(currentApiDoc.key)} />
              )}
            </section>

            {isTaskRetrieveDoc && <TaskResponseSchema />}
            <DocDetails section={currentApiDoc.key} />
          </>
        ) : activeGuide ? (
          <GuideDetails onCreateApiKey={props.onCreateApiKey} section={activeGuide.key} />
        ) : null}
      </main>

      <aside className="docsRunner">
        {currentApiDoc?.method && currentApiDoc.path && !isAsyncModeDoc ? <form onSubmit={handleSubmit}>
          <header>
            <strong>在线运行</strong>
            <Button type="submit" size="sm" disabled={!runnerAvailable || Boolean(bodyError) || (props.canRun && props.coreState === 'loading')}>
              <Send size={14} />
              {!runnerAvailable ? '暂不支持' : props.canRun ? '发送' : '登录'}
            </Button>
          </header>
          <div className="runnerEndpoint">
            <MethodBadge method={currentApiDoc.method} />
            <span>{runnerPath}</span>
          </div>
          <label className="shLabel">
            API Key
            <ApiKeySelect
              apiKeySecretsById={props.apiKeySecretsById}
              apiKeys={props.apiKeys}
              selectedApiKeyId={activeApiKeyId}
              onApiKeyChange={props.onApiKeyChange}
            />
          </label>
          {apiKeyNotice && (
            <div className="docsKeyNotice">
              <span>{apiKeyNotice}</span>
              <Button type="button" size="sm" variant="secondary" onClick={props.onCreateApiKey}>
                <KeyRound size={14} />
                创建 Key
              </Button>
            </div>
          )}
          {runnerAvailable ? (
            <>
              {runnerModeEnabled && (
                <div className="runnerModeField">
                  <span className="runnerModeLabel">运行模式</span>
                  <div className="runnerModeToggle" role="group" aria-label="运行模式">
                    <button
                      type="button"
                      aria-pressed={submissionMode === 'simulation'}
                      data-active={submissionMode === 'simulation'}
                      onClick={() => handleSubmissionModeChange('simulation')}
                    >
                      测试模式
                    </button>
                    <button
                      type="button"
                      aria-pressed={submissionMode === 'production'}
                      data-active={submissionMode === 'production'}
                      onClick={() => handleSubmissionModeChange('production')}
                    >
                      真实提交
                    </button>
                  </div>
                  <p className="runnerModeHint" data-mode={submissionMode}>
                    {submissionMode === 'simulation'
                      ? '不触达真实供应商，不消耗上游额度。'
                      : '请求将真实提交给供应商，可能产生费用。'}
                  </p>
                </div>
              )}
              <label className="shLabel">
                能力
                <Select value={props.taskForm.kind} onChange={(event) => handleKindChange(event.target.value as TaskKind)}>
                  {taskKindOptions.map(([value, label]) => (
                    <option value={value} key={value}>{label}</option>
                  ))}
                </Select>
              </label>
              {isTaskRetrieveDoc ? (
                <label className="shLabel">
                  Task ID
                  <Input
                    placeholder="输入异步提交返回的 taskId"
                    value={props.taskForm.taskId ?? ''}
                    onChange={(event) => props.onTaskFormChange({ ...props.taskForm, taskId: event.target.value })}
                  />
                </label>
              ) : (
                <div className="runnerBodyField">
                  <label className="shLabel">
                    请求 Body
                    <Textarea
                      aria-describedby={bodyError ? 'docs-runner-body-error' : undefined}
                      aria-invalid={Boolean(bodyError)}
                      value={bodyDraft}
                      onBlur={handleBodyBlur}
                      onChange={(event) => handleBodyChange(event.target.value)}
                    />
                  </label>
                  {bodyError && <span className="runnerBodyError" id="docs-runner-body-error" role="alert">{bodyError}</span>}
                </div>
              )}
              <Button type="submit" disabled={Boolean(bodyError) || (props.canRun && props.coreState === 'loading')}>
                <Play size={15} />
                {!props.canRun
                  ? '登录后运行'
                  : props.coreState === 'loading'
                    ? '运行中'
                    : submissionMode === 'simulation' || !runnerModeEnabled
                      ? '运行测试'
                      : '真实提交'}
              </Button>
            </>
          ) : (
            <div className="docsRunnerUnavailable">该接口暂未接入在线运行，请使用上方 Swagger 定义生成调用代码。</div>
          )}
        </form> : (
          <section className="docsGuideAside">
            <header><strong>{isCatalogDoc ? '接口分类' : isAsyncModeDoc ? '能力说明' : '指南导航'}</strong></header>
            <div>
              <span>当前章节</span>
              <strong>{current.title}</strong>
              <p>{isCatalogDoc
                ? '所有新接入路径统一以 /api/v1 开头。选择兼容协议时只替换 Base URL 和 Gateway API Key。'
                : isAsyncModeDoc
                  ? '异步模式不改变原接口的请求参数，只改变任务提交后的等待和响应方式。'
                  : '使用左侧目录切换其他指南或 API 接口，浏览器前进、后退可保持当前章节。'}</p>
            </div>
          </section>
        )}
        {currentApiDoc?.method && currentApiDoc.path && !isAsyncModeDoc && <section className="runnerResult">
          <strong>返回结果</strong>
          {props.taskResult ? (
            <pre>{JSON.stringify(isTaskRetrieveDoc ? props.taskResult : props.taskResult.result ?? props.taskResult, null, 2)}</pre>
          ) : (
            <div className="emptyState">
              <span>点击发送获取返回结果</span>
            </div>
          )}
          {props.coreMessage && <p>{props.coreMessage}</p>}
        </section>}
      </aside>
    </div>
  );
}

function ApiCatalogDetails(props: { family?: PublicApiFamily; search: string; surface: PublicApiSurface }) {
  const catalogGroups = publicApiCatalogGroups.filter((group) => (
    group.surface === props.surface && (!props.family || group.family === props.family)
  ));
  const visibleGroups = catalogGroups
    .map((group) => {
      const groupMatches = searchText([group.title, group.description, group.family], props.search);
      const endpoints = groupMatches
        ? group.endpoints
        : group.endpoints.filter((endpoint) => searchText([endpoint.method, endpoint.path, endpoint.description], props.search));
      return { ...group, endpoints };
    })
    .filter((group) => group.endpoints.length > 0);
  const endpointCount = catalogGroups.reduce((total, group) => total + group.endpoints.length, 0);
  const visibleEndpointCount = visibleGroups.reduce((total, group) => total + group.endpoints.length, 0);
  const isCompatibility = props.surface === 'compatibility';
  const baseUrl = props.family === 'kling'
    ? 'https://ai.51easyai.com/api/v1/kling'
    : 'https://ai.51easyai.com/api/v1';

  return (
    <>
      <section className="docsCatalogSummary" aria-label="API 接入摘要">
        <article>
          <span>{props.family === 'kling' ? '可灵客户端 Base URL' : '生产 Base URL'}</span>
          <code>{baseUrl}</code>
        </article>
        <article>
          <span>生成接口鉴权</span>
          <code>Authorization: Bearer {'<Gateway API Key>'}</code>
        </article>
        <article>
          <span>接口数量</span>
          <strong>{endpointCount}</strong>
        </article>
      </section>

      <section className="docsCatalogNotice" data-surface={props.surface}>
        <strong>{isCompatibility ? '存量协议接入规则' : '新业务接入规则'}</strong>
        <p>{isCompatibility
          ? '保留对应厂商或存量系统的请求字段、响应包络和任务语义。调用方替换 Base URL 与 Gateway API Key 即可，上游凭据不会下发。'
          : '优先使用通用开放接口。所有路径统一位于 /api/v1；模型能力由模型目录与服务端配置决定，不要求额外传入 modelType。'}</p>
      </section>

      {props.surface === 'open' && <OpenApiUsageNotes />}
      {isCompatibility && <CompatibilityUsageNotes family={props.family} />}

      {props.search && (
        <p className="docsCatalogSearchResult">当前筛选显示 {visibleEndpointCount} / {endpointCount} 项接口</p>
      )}
      {visibleGroups.map((group) => (
        <section className="paramCard docsCatalogGroup" key={group.family}>
          <header>
            <div>
              <h2>{group.title}</h2>
              <p>{group.description}</p>
            </div>
            <Badge variant="outline">{group.endpoints.length} 项</Badge>
          </header>
          <div className="docsCatalogEndpoints">
            {group.endpoints.map((endpoint) => (
              <div className="docsCatalogEndpoint" key={`${endpoint.method}-${endpoint.path}`}>
                <MethodBadge method={endpoint.method} />
                <code>{endpoint.path}</code>
                <span>{endpoint.description}</span>
              </div>
            ))}
          </div>
        </section>
      ))}
      {visibleGroups.length === 0 && (
        <div className="docsCatalogEmpty">当前分类中没有匹配“{props.search}”的接口。</div>
      )}
    </>
  );
}

function OpenApiUsageNotes() {
  return (
    <section className="paramCard docsDetailCard">
      <header><h2>统一调用约定</h2></header>
      <div className="docsStatusGrid">
        <code>Base URL</code><p>SDK 配置到 <code>https://ai.51easyai.com/api/v1</code>，请求资源路径时不要再次拼接 <code>/api/v1</code>。</p>
        <code>同步 / 异步</code><p>生成接口保留原请求 Body；需要持久化异步任务时增加 <code>X-Async: true</code>，再通过 <code>/tasks/{'{taskID}'}</code> 取回结果。</p>
        <code>全模态能力</code><p>模型可直接使用服务端声明的文生图、文生视频、图生视频等能力，不要求调用方显式传入 <code>modelType</code>。</p>
        <code>公开端点</code><p>健康检查和 public 目录无需登录；账号管理使用登录会话，生成与任务接口使用 Gateway API Key。</p>
      </div>
    </section>
  );
}

function CompatibilityUsageNotes(props: { family?: PublicApiFamily }) {
  if (props.family === 'kling') {
    return (
      <section className="paramCard docsDetailCard">
        <header><h2>可灵 V1 / API 2.0 约定</h2></header>
        <div className="docsStatusGrid">
          <code>SDK Base URL</code><p>设置为 <code>https://ai.51easyai.com/api/v1/kling</code>，之后按原客户端继续请求 <code>/v1/...</code> 或 <code>/v2/...</code>。</p>
          <code>O1</code><p>使用 <code>kling-video-o1</code>；兼容别名 <code>kling-o1</code> 会自动归一。</p>
          <code>3.0 Omni</code><p>使用 <code>kling-v3-omni</code>；兼容 <code>kling-3.0-omni</code> 和 <code>kling-3-omni</code>。</p>
          <code>鉴权</code><p>客户端只发送 Gateway API Key。可灵中国区 AK/SK 保存在网关平台配置中。</p>
        </div>
      </section>
    );
  }
  if (props.family === 'gemini') {
    return (
      <section className="paramCard docsDetailCard">
        <header><h2>Gemini 接入约定</h2></header>
        <div className="docsDetailContent">
          <p><code>generateContent</code> 保留 Gemini 内容结构；Files 上传的 <code>version</code> 支持 <code>v1</code> 和 <code>v1beta</code>。新接入路径仍统一放在 Gateway 的 <code>/api/v1</code> 前缀下。</p>
        </div>
      </section>
    );
  }
  if (props.family === 'volces') {
    return (
      <section className="paramCard docsDetailCard">
        <header><h2>火山任务约定</h2></header>
        <div className="docsDetailContent">
          <p>创建、列表、查询和删除接口保留火山任务结构。旧 <code>/api/v3</code> 仅作为兼容别名，不应写入新客户端配置。</p>
        </div>
      </section>
    );
  }
  if (props.family === 'serverMain') {
    return (
      <section className="paramCard docsDetailCard">
        <header><h2>server-main 接入约定</h2></header>
        <div className="docsDetailContent">
          <p>提交接口返回兼容的 <code>submitted</code> 和 <code>task_id</code>，现有调用方继续通过 <code>/ai/result/{'{taskID}'}</code> 轮询结果。</p>
        </div>
      </section>
    );
  }
  return (
    <section className="paramCard docsDetailCard">
      <header><h2>兼容层边界</h2></header>
      <div className="docsDetailContent">
        <p>兼容接口用于无缝迁移已有 SDK 或存量调用。新业务优先选择通用开放接口，以获得统一的异步任务、结果归一、计费和模型能力语义。</p>
      </div>
    </section>
  );
}

function OpsManagementSkillCard(props: { metadata: GatewaySkillBundleMetadata }) {
  const modules = props.metadata.modules.map(skillModuleLabel).join('、');
  return (
    <section className="agentResourceCard">
      <div className="agentResourceIcon">
        <Wrench size={22} />
      </div>
      <div className="agentResourceContent">
        <div className="agentResourceTitle">
          <div>
            <p className="eyebrow">Agent 运维资源</p>
            <h2>{props.metadata.displayName}</h2>
          </div>
          <Badge variant="outline">{props.metadata.version ? `v${props.metadata.version}` : '最新版本'}</Badge>
        </div>
        <p>用于让 Agent 安全操作厂商、基础模型、定价、运行策略、平台、平台模型和 universal 自定义平台。</p>
        <div className="agentResourceModules">
          <span>当前模块</span>
          <strong>{modules || '模型运行时'}</strong>
        </div>
        <div className="agentResourceActions">
          <Button asChild size="sm">
            <a href={resolveApiAssetUrl(props.metadata.downloadPath)}>
              <Download size={14} />
              下载 SKILL
            </a>
          </Button>
          <Button asChild size="sm" variant="secondary">
            <a href={resolveApiAssetUrl(props.metadata.apiDocsJsonPath)} rel="noreferrer" target="_blank">
              <FileJson size={14} />
              Swagger JSON
            </a>
          </Button>
          <Button asChild size="sm" variant="outline">
            <a href={resolveApiAssetUrl(props.metadata.apiDocsYamlPath)} rel="noreferrer" target="_blank">
              <ExternalLink size={14} />
              Swagger YAML
            </a>
          </Button>
          <a
            className="agentResourceMetadataLink"
            href={resolveApiAssetUrl('/api/v1/public/skills/ai-gateway-ops-management/metadata')}
            rel="noreferrer"
            target="_blank"
          >
            查看 metadata
          </a>
        </div>
      </div>
    </section>
  );
}

function skillModuleLabel(module: string) {
  if (module === 'model-runtime') return '模型运行时';
  return module;
}

function DocsGroup(props: {
  items: Array<{ active?: boolean; method?: ApiDocItem['method']; onClick?: () => void; title: string }>;
  title: string;
}) {
  return (
    <section className="docsGroup">
      <h3>{props.title}</h3>
      {props.items.map((item) => (
        <button type="button" data-active={item.active} key={item.title} onClick={item.onClick}>
          <span>{item.title}</span>
          {item.method && <MethodBadge method={item.method} />}
        </button>
      ))}
    </section>
  );
}

function ParamRows(props: { depth?: number; parentPath?: string; rows: ApiDocParam[] }) {
  const depth = props.depth ?? 0;
  const parentPath = props.parentPath ?? '';

  return props.rows.map((row) => {
    const path = parentPath ? `${parentPath}.${row.name}` : row.name;
    return (
      <Fragment key={path}>
        <ParamRow {...row} depth={depth} />
        {row.children?.length ? (
          <ParamRows depth={depth + 1} parentPath={path} rows={row.children} />
        ) : null}
      </Fragment>
    );
  });
}

function ParamRow(props: ApiDocParam & { depth?: number }) {
  const depth = props.depth ?? 0;
  const nameStyle: CSSProperties = depth ? { paddingLeft: `${depth * 20}px` } : {};
  const markerStyle: CSSProperties = depth ? { left: `${(depth - 1) * 20 + 3}px` } : {};

  return (
    <div className="paramRow" data-depth={depth} data-nested={depth > 0 ? 'true' : undefined}>
      <div className="paramName" style={nameStyle}>
        {depth > 0 && <span aria-hidden="true" className="paramTreeMarker" style={markerStyle} />}
        <code>{props.name}</code>
      </div>
      <span>{props.type}</span>
      <p>{props.value}</p>
      <em>{props.required ? '必需' : '可选'}</em>
    </div>
  );
}

function GuideDetails(props: { onCreateApiKey: () => void; section: ApiGuideSection }) {
  if (props.section === 'guideBaseUrl') {
    return (
      <>
        <GuideSection title="1. 确认 Base URL">
          <p>公开 API Base URL 固定以 <code>/api/v1</code> 结尾。接口卡片展示的是完整路径；SDK 配置 Base URL 后只追加资源路径，不要再次添加 <code>/api/v1</code>。</p>
          <pre>{`export EASYAI_BASE_URL="https://your-gateway.example.com/api/v1"\ncurl "$EASYAI_BASE_URL/healthz"`}</pre>
        </GuideSection>
        <GuideSection title="2. 创建并使用 API Key">
          <p>登录后在“用户工作台 → API Key”创建 Key，并为它分配实际需要的 chat、embedding、rerank、image 或 video 权限。密钥只在创建或重置时完整展示，请立即安全保存。</p>
          <div className="docsGuideActions">
            <Button type="button" size="sm" onClick={props.onCreateApiKey}>
              <KeyRound size={14} />
              前往创建 API Key
            </Button>
          </div>
          <pre>{`Authorization: Bearer sk-...\nContent-Type: application/json\nAccept: application/json`}</pre>
        </GuideSection>
      </>
    );
  }

  if (props.section === 'guideWebhook') {
    return (
      <>
        <GuideSection title="任务进度 WebHook">
          <p>Gateway 的任务进度 WebHook 由部署管理员统一配置，用于把任务事件可靠投递到业务服务；它不是公开请求中可任意覆盖的回调地址。</p>
          <pre>{`TASK_PROGRESS_CALLBACK_ENABLED=true\nTASK_PROGRESS_CALLBACK_URL=https://service.example.com/internal/task-progress`}</pre>
          <p>每次状态变化会先写入任务事件和 callback outbox，再异步投递。失败不会阻塞任务执行，并会按 outbox 策略重试。</p>
        </GuideSection>
        <GuideSection title="回调 Body">
          <pre>{`{\n  "taskId": "9f4d8f3d-...",\n  "seq": 3,\n  "eventType": "progress",\n  "status": "running",\n  "phase": "generating",\n  "progress": 0.6,\n  "message": "任务处理中",\n  "payload": {},\n  "simulated": false,\n  "createdAt": "2026-07-17T07:00:00Z"\n}`}</pre>
          <p>公开 API 客户端无需依赖 WebHook：异步提交后可直接使用 <code>next.detail</code> 查询任务，或使用 <code>next.events</code> 订阅进度事件。</p>
        </GuideSection>
      </>
    );
  }

  if (props.section === 'guideErrors') {
    return (
      <>
        <GuideSection title="同步请求错误结构">
          <pre>{`{\n  "error": {\n    "message": "api key scope does not allow this capability",\n    "status": 403,\n    "code": "forbidden"\n  }\n}`}</pre>
          <p>先判断 HTTP 状态，再读取 <code>error.code</code> 和 <code>error.message</code>；响应中包含 <code>requestId</code> 时请一并保存，便于关联上游请求和运行日志。</p>
        </GuideSection>
        <section className="paramCard docsDetailCard">
          <header><h2>常见状态与处理方式</h2></header>
          <div className="docsStatusGrid">
            <code>400 invalid_parameter</code><p>请求结构或参数不受支持，检查字段类型、必填项及模型能力。</p>
            <code>401 unauthorized</code><p>缺少、失效或无法识别的 API Key。</p>
            <code>403 forbidden</code><p>API Key 权限范围不包含当前能力，或账号无权访问该模型。</p>
            <code>404 not_found</code><p>任务、模型或其他目标资源不存在。</p>
            <code>429 rate_limit</code><p>触发 RPM、TPM 或并发限制；按响应和运行策略退避重试。</p>
            <code>5xx provider_failed</code><p>Gateway 或上游供应商暂时失败；结合 retryable、attempts 和 requestId 排查。</p>
          </div>
        </section>
        <GuideSection title="异步任务失败">
          <p>异步提交成功不代表生成成功。任务进入 <code>failed</code> 后，读取任务详情中的 <code>errorCode</code>、<code>errorMessage</code>、<code>requestId</code> 和 <code>attempts</code>。</p>
        </GuideSection>
      </>
    );
  }

  return (
    <>
      <GuideSection title="启用请求级测试模式">
        <p>在请求 Body 中同时设置 <code>runMode: simulation</code> 和 <code>simulation: true</code>。在线运行示例默认使用该配置。</p>
        <pre>{`{\n  "model": "gpt-4o-mini",\n  "messages": [{ "role": "user", "content": "你好" }],\n  "runMode": "simulation",\n  "simulation": true\n}`}</pre>
      </GuideSection>
      <GuideSection title="测试模式的执行边界">
        <p>测试任务仍会执行真实鉴权、模型路由、持久化队列、限流、进度事件和结果归一，因此可用于验证完整 Gateway 链路。</p>
        <p>它不会向真实供应商提交任务、不会创建真实供应商订单，也不会产生真实扣费；任务、事件和结算结果会带有 <code>simulated=true</code> 标记。</p>
      </GuideSection>
    </>
  );
}

function GuideSection(props: { children: ReactNode; title: string }) {
  return (
    <section className="paramCard docsDetailCard">
      <header><h2>{props.title}</h2></header>
      <div className="docsDetailContent">{props.children}</div>
    </section>
  );
}

function TaskResponseSchema() {
  return (
    <section className="paramCard">
      <header>
        <h2>响应 Body</h2>
        <Badge variant="outline">application/json</Badge>
      </header>
      <ParamRows rows={taskResponseParamRows()} />
    </section>
  );
}

function DocDetails(props: { section: ApiDocSection }) {
  if (props.section === 'responses') {
    return (
      <section className="paramCard docsDetailCard">
        <header><h2>连续对话与状态归属</h2></header>
        <div className="docsDetailContent">
          <p>首次请求不传 <code>previous_response_id</code>。后续请求如需由 Responses 链续接，传入上一轮返回的 <code>response.id</code>。</p>
          <p>未传 <code>previous_response_id</code> 时，网关以本轮完整 input 为准，不自动追加本地历史；客户端自行管理的完整上下文始终优先。</p>
          <pre>{`{
  "model": "gpt-4o-mini",
  "input": "继续上一轮回答",
  "previous_response_id": "resp_..."
}`}</pre>
        </div>
      </section>
    );
  }
  if (props.section === 'videoGeneration') {
    return (
      <section className="paramCard docsDetailCard">
        <header><h2>参考素材角色</h2></header>
        <div className="docsDetailContent">
          <p><code>image_url</code> 可使用 <code>first_frame</code>、<code>last_frame</code>、<code>reference_image</code>；<code>video_url</code> 和 <code>audio_url</code> 可作为视频、音频参考。实际数量和组合由所选模型能力约束。</p>
          <p>视频生成通常耗时较长，生产调用建议设置 <code>X-Async: true</code>，再使用响应中的 <code>next.detail</code> 或任务取回接口查询结果。</p>
        </div>
      </section>
    );
  }
  if (props.section === 'asyncMode') {
    return (
      <>
        <section className="paramCard">
          <header><h2>开启方式</h2></header>
          <ParamRow name="X-Async" type="boolean" required value="在原请求 Header 中设置为 true；请求 URL、鉴权方式和 Body 均保持原接口定义不变。" />
        </section>
        <section className="paramCard docsDetailCard">
          <header><h2>同步与异步的区别</h2></header>
          <div className="docsStatusGrid">
            <code>请求 Body</code><p>始终使用原接口的参数。异步模式没有独立的模型、提示词或媒体参数。</p>
            <code>未设置 X-Async</code><p>连接等待任务执行，并按原接口协议返回同步 JSON 或 SSE 流。</p>
            <code>X-Async: true</code><p>任务进入持久化队列，接口立即返回 HTTP 202；客户端通过 taskId 取回状态和最终结果。</p>
            <code>适用范围</code><p>作为 AI 任务创建接口的统一能力使用，不与某一种文本、图片、视频或音频 Body 绑定。</p>
          </div>
        </section>
        <section className="paramCard docsDetailCard">
          <header><h2>异步受理响应</h2></header>
          <div className="docsDetailContent">
            <p>保存 <code>taskId</code>。<code>next.detail</code> 用于取回完整任务结果，<code>next.events</code> 用于订阅进度事件。</p>
            <pre>{`HTTP/1.1 202 Accepted
{
  "taskId": "9f4d8f3d-...",
  "task": {
    "id": "9f4d8f3d-...",
    "status": "queued",
    "asyncMode": true
  },
  "next": {
    "detail": "/api/v1/tasks/9f4d8f3d-...",
    "events": "/api/v1/tasks/9f4d8f3d-.../events"
  }
}`}</pre>
          </div>
        </section>
      </>
    );
  }
  if (props.section === 'taskRetrieve') {
    return (
      <>
        <section className="paramCard docsDetailCard">
          <header><h2>状态与结果读取</h2></header>
          <div className="docsStatusGrid">
            <code>queued / submitting / running</code><p>任务仍在进行，保留 taskId 并继续查询；此时 <code>result</code> 通常为空。</p>
            <code>succeeded</code><p>任务成功，原能力的归一化响应完整保存在 <code>result</code>；用量与结算分别位于 <code>usage</code>、<code>billings</code>。</p>
            <code>failed</code><p>任务失败，读取 <code>errorCode</code>、<code>errorMessage</code>、<code>requestId</code> 和 <code>attempts</code>。</p>
            <code>cancelled</code><p>任务已取消，不再继续执行；结合 <code>submitted</code> 判断是否曾提交到上游。</p>
          </div>
        </section>
        <section className="paramCard docsDetailCard">
          <header><h2>成功响应示例</h2></header>
          <div className="docsDetailContent">
            <p><code>result</code> 的内部字段与原始能力一致：文本、向量、重排序和媒体任务会保留各自的结果结构，外层任务字段保持统一。</p>
            <pre>{`{
  "id": "9f4d8f3d-...",
  "kind": "images.generations",
  "model": "image-model",
  "asyncMode": true,
  "status": "succeeded",
  "result": {
    "data": [{ "url": "https://example.com/result.png" }]
  },
  "usage": { "totalTokens": 0 },
  "billings": [{
    "resourceType": "image",
    "unit": "image",
    "quantity": 1,
    "amount": 0.1,
    "currency": "resource",
    "simulated": false
  }],
  "billingSummary": {
    "lineCount": 1,
    "totalAmount": 0.1,
    "currency": "resource",
    "simulated": false
  },
  "finalChargeAmount": 0.1,
  "attempts": [{
    "attemptNo": 1,
    "provider": "provider-name",
    "status": "succeeded",
    "retryable": false,
    "responseDurationMs": 1280
  }],
  "createdAt": "2026-07-17T07:00:00Z",
  "updatedAt": "2026-07-17T07:00:02Z",
  "finishedAt": "2026-07-17T07:00:02Z"
}`}</pre>
          </div>
        </section>
      </>
    );
  }
  return null;
}

function authorizationDescription(section: ApiDocSection) {
  const scope = section === 'embeddings'
    ? 'embedding'
    : section === 'reranks'
      ? 'rerank'
      : section === 'videoGeneration' || section === 'asyncMode'
        ? 'video'
        : section === 'chat' || section === 'responses'
          ? 'chat'
          : '';
  return `Bearer {{YOUR_API_KEY}}，支持本地 API Key；管理接口仍使用 JWT。${scope ? `当前接口需要 ${scope} 权限。` : ''}`;
}

function supportsAsyncMode(section: ApiDocSection) {
  return section === 'chat'
    || section === 'responses'
    || section === 'embeddings'
    || section === 'reranks'
    || section === 'imageGeneration'
    || section === 'imageEdit'
    || section === 'videoGeneration';
}

function MethodBadge(props: { method: PublicApiMethod }) {
  const variant = props.method === 'GET'
    ? 'success'
    : props.method === 'DELETE'
      ? 'destructive'
      : props.method === 'PATCH'
        ? 'secondary'
        : 'warning';
  return <Badge variant={variant}>{props.method}</Badge>;
}

function groupDocs(items: ApiDocItem[]) {
  const groups = new Map<string, ApiDocItem[]>();
  for (const item of items) {
    groups.set(item.group, [...(groups.get(item.group) ?? []), item]);
  }
  return Array.from(groups, ([title, groupItems]) => ({ title, items: groupItems }));
}

function groupDocsBySurface(items: ApiDocItem[]) {
  const groups = new Map<PublicApiSurface, ApiDocItem[]>();
  for (const item of items) {
    groups.set(item.surface, [...(groups.get(item.surface) ?? []), item]);
  }
  return Array.from(groups, ([key, surfaceItems]) => ({ key, items: surfaceItems }));
}

function apiSurfaceTitle(surface: PublicApiSurface) {
  return surface === 'open' ? '通用开放接口' : '兼容接口';
}

function guideMatchesSearch(item: ApiGuideItem, search: string) {
  return searchText([item.title, item.lead, item.group], search);
}

function apiDocMatchesSearch(item: ApiDocItem, search: string) {
  if (searchText([item.title, item.lead, item.path, item.group, apiSurfaceTitle(item.surface)], search)) {
    return true;
  }
  if (!item.catalogSurface && !item.catalogFamily) return false;
  return publicApiCatalogGroups
    .filter((group) => (
      (!item.catalogSurface || group.surface === item.catalogSurface)
      && (!item.catalogFamily || group.family === item.catalogFamily)
    ))
    .some((group) => searchText([
      group.title,
      group.description,
      ...group.endpoints.flatMap((endpoint) => [endpoint.method, endpoint.path, endpoint.description]),
    ], search));
}

function searchText(values: Array<string | undefined>, search: string) {
  if (!search) return true;
  return values.some((value) => value?.toLowerCase().includes(search));
}

function defaultTaskForDoc(kind: TaskForm['kind'], current: TaskForm, taskResult: GatewayTask | null): TaskForm {
  const next = defaultTaskForKind(kind, current);
  if (kind === 'tasks.retrieve' && !next.taskId && taskResult && !taskResult.id.startsWith('docs-')) {
    return { ...next, taskId: taskResult.id };
  }
  return next;
}

function defaultTaskForKind(kind: TaskForm['kind'], current: TaskForm): TaskForm {
  if (kind === 'chat.completions') return { ...current, kind, model: 'gpt-4o-mini' };
  if (kind === 'responses') {
    return {
      ...current,
      instructions: current.instructions ?? '回答保持简洁。',
      kind,
      model: 'gpt-4o-mini',
      prompt: current.kind === kind ? current.prompt : '用一句话说明 Responses API 的用途。',
    };
  }
  if (kind === 'embeddings') {
    return {
      ...current,
      dimensions: current.dimensions ?? 4,
      kind,
      model: 'text-embedding-v4',
      prompt: current.prompt || 'AI Gateway 提供 OpenAI 兼容接口。',
    };
  }
  if (kind === 'reranks') {
    return {
      ...current,
      documents: current.documents ?? 'AI Gateway 提供 OpenAI 兼容接口。\n图片生成任务支持异步队列。\n文本向量接口返回 embedding 数组。',
      kind,
      model: 'qwen3-rerank',
      prompt: current.prompt || 'OpenAI 兼容接口',
      topN: current.topN ?? 2,
    };
  }
  if (kind === 'images.edits') return { ...current, kind, image: current.image ?? 'https://example.com/source.png', mask: current.mask ?? 'https://example.com/mask.png', model: 'gpt-image-1' };
  if (kind === 'images.generations') return { ...current, kind, model: 'gpt-image-1' };
  if (kind === 'videos.generations') {
    return {
      ...current,
      aspectRatio: current.aspectRatio ?? '16:9',
      duration: current.duration ?? 5,
      kind,
      model: '豆包Seedance-2.0',
      outputAudio: current.outputAudio ?? true,
      prompt: current.kind === kind ? current.prompt : '电影感航拍镜头掠过日出时的雪山。',
      resolution: current.resolution ?? '720p',
    };
  }
  return { ...current, kind, model: 'task' };
}

function requestBodyExample(task: TaskForm, section: ApiDocSection, submissionMode: TaskSubmissionMode) {
  const body = task.kind === 'chat.completions'
    ? { model: task.model, messages: [{ role: 'user', content: task.prompt }], stream: false }
    : task.kind === 'responses'
      ? {
        model: task.model,
        input: task.prompt,
        instructions: task.instructions,
        previous_response_id: task.previousResponseId || undefined,
        store: true,
        stream: false,
      }
      : task.kind === 'embeddings'
        ? { model: task.model, input: embeddingInputExample(task.prompt), dimensions: task.dimensions ?? 4 }
        : task.kind === 'reranks'
          ? { model: task.model, query: task.prompt, documents: rerankDocumentsExample(task.documents), top_n: task.topN ?? 2 }
          : task.kind === 'images.edits'
            ? { model: task.model, prompt: task.prompt, image: task.image, mask: task.mask }
            : task.kind === 'videos.generations'
              ? {
                model: task.model,
                content: [{ type: 'text', text: task.prompt }],
                duration: task.duration ?? 5,
                resolution: task.resolution ?? '720p',
                aspect_ratio: task.aspectRatio ?? '16:9',
                audio: task.outputAudio ?? true,
              }
              : section === 'pricing'
                ? { kind: 'chat.completions', model: 'gpt-4o-mini', messages: [{ role: 'user', content: '你好' }], max_tokens: 512 }
                : { model: task.model, prompt: task.prompt, quality: 'medium', size: '1024x1024' };
  return JSON.stringify(section === 'pricing' ? body : applyTaskSubmissionMode(body, submissionMode), null, 2);
}

function parseEditableRequestBody(value: string): { body: Record<string, unknown> | null; error: string } {
  try {
    const body = JSON.parse(value) as unknown;
    if (!body || typeof body !== 'object' || Array.isArray(body)) {
      return { body: null, error: '请求 Body 必须是 JSON 对象。' };
    }
    return { body: body as Record<string, unknown>, error: '' };
  } catch (error) {
    const detail = error instanceof SyntaxError ? error.message : '无法解析 JSON';
    return { body: null, error: `JSON 格式有误：${detail}` };
  }
}

function taskResponseParamRows(): ApiDocParam[] {
  return [
    { name: 'id', type: 'string', required: true, value: 'Gateway 任务 ID，与异步受理响应中的 taskId 相同' },
    { name: 'kind', type: 'string', required: true, value: '原始任务类型，例如 chat.completions、images.generations 或 videos.generations' },
    { name: 'model', type: 'string', required: true, value: '请求使用的模型 ID 或别名' },
    { name: 'status', type: 'string', required: true, value: 'queued、submitting、running、succeeded、failed 或 cancelled' },
    { name: 'asyncMode', type: 'boolean', required: true, value: '是否以异步模式提交' },
    { name: 'cancellable', type: 'boolean', value: '当前状态是否允许取消' },
    { name: 'submitted', type: 'boolean', value: '任务是否已经提交到上游供应商' },
    { name: 'message', type: 'string', value: '当前状态或取消能力的补充说明' },
    { name: 'requestId', type: 'string', value: '上游请求 ID，用于日志和供应商问题定位' },
    { name: 'result', type: 'object', value: '任务成功后的归一化结果；内部结构与原始文本、向量、重排序、图片、视频或音频接口一致' },
    {
      name: 'usage',
      type: 'object',
      value: '归一化用量数据；媒体任务可能使用能力相关的用量字段',
      children: [
        { name: 'inputTokens', type: 'number', value: '输入 token 数' },
        { name: 'outputTokens', type: 'number', value: '输出 token 数' },
        { name: 'totalTokens', type: 'number', value: '总 token 数' },
      ],
    },
    {
      name: 'billings',
      type: 'array<object>',
      value: '逐项计费明细',
      children: [
        { name: 'resourceType', type: 'string', required: true, value: '计费资源类型，例如 text_input、image 或 video' },
        { name: 'unit', type: 'string', required: true, value: '计费单位，例如 1k_tokens、image 或 5s_video' },
        { name: 'quantity', type: 'number', required: true, value: '计费数量' },
        { name: 'amount', type: 'number', required: true, value: '本计费项金额' },
        { name: 'currency', type: 'string', required: true, value: '金额单位，默认 resource' },
        { name: 'discountFactor', type: 'number', value: '实际应用的折扣系数' },
        { name: 'simulated', type: 'boolean', required: true, value: '是否为模拟计费' },
      ],
    },
    {
      name: 'billingSummary',
      type: 'object',
      value: '计费汇总',
      children: [
        { name: 'lineCount', type: 'number', required: true, value: '计费明细条数' },
        { name: 'totalAmount', type: 'number', required: true, value: '汇总金额' },
        { name: 'amountByCurrency', type: 'object', required: true, value: '按金额单位汇总的金额映射' },
        { name: 'currency', type: 'string', required: true, value: '单一金额单位或 mixed' },
        { name: 'simulated', type: 'boolean', required: true, value: '是否为模拟计费汇总' },
      ],
    },
    { name: 'finalChargeAmount', type: 'number', required: true, value: '最终实际扣费金额；模拟任务不会真实扣费' },
    {
      name: 'attempts',
      type: 'array<object>',
      value: '执行尝试记录；发生重试或切换平台时可能包含多项',
      children: [
        { name: 'attemptNo', type: 'number', required: true, value: '尝试序号，从 1 开始' },
        { name: 'provider', type: 'string', value: '供应商标识' },
        { name: 'modelName', type: 'string', value: 'Gateway 模型名称' },
        { name: 'providerModelName', type: 'string', value: '实际上游模型名称' },
        { name: 'status', type: 'string', required: true, value: '本次尝试状态' },
        { name: 'retryable', type: 'boolean', required: true, value: '失败后是否允许重试' },
        { name: 'simulated', type: 'boolean', required: true, value: '是否由 SimulationClient 执行' },
        { name: 'requestId', type: 'string', value: '本次尝试的上游请求 ID' },
        { name: 'responseDurationMs', type: 'number', required: true, value: '本次尝试的响应耗时（毫秒）' },
        { name: 'errorCode', type: 'string', value: '本次尝试失败时的错误码' },
        { name: 'errorMessage', type: 'string', value: '本次尝试失败时的错误信息' },
      ],
    },
    { name: 'errorCode', type: 'string', value: '任务最终失败时的错误码' },
    { name: 'errorMessage', type: 'string', value: '任务最终失败时的错误信息' },
    { name: 'responseDurationMs', type: 'number', required: true, value: '任务响应耗时（毫秒）' },
    { name: 'createdAt', type: 'string', required: true, value: '任务创建时间，ISO 8601 格式' },
    { name: 'updatedAt', type: 'string', required: true, value: '任务最后更新时间，ISO 8601 格式' },
    { name: 'finishedAt', type: 'string', value: '任务进入终态的时间，ISO 8601 格式' },
  ];
}

function bodyParamRows(section: ApiDocSection): ApiDocParam[] {
  if (section === 'responses') {
    return [
      { name: 'model', type: 'string', required: true, value: '模型 ID 或别名；网关优先选择支持 openai_responses 的平台模型' },
      { name: 'input', type: 'string|array', required: true, value: '本轮输入，可为文本或 Responses input item 数组' },
      { name: 'instructions', type: 'string', value: '本轮系统级指令' },
      { name: 'previous_response_id', type: 'string', value: '上一轮 response.id；用于由客户端显式管理连续对话' },
      { name: 'store', type: 'boolean', value: '是否保存 Responses 链，默认 true' },
      { name: 'stream', type: 'boolean', value: '设为 true 时返回 Responses SSE 事件流' },
      { name: 'tools / tool_choice', type: 'array|object', value: '函数工具定义与选择策略；Chat 回退只支持 function tools' },
      { name: 'reasoning / text', type: 'object', value: '推理强度、文本格式和结构化输出配置' },
      { name: 'background', type: 'boolean', value: 'OpenAI 协议字段；Gateway 的持久化异步任务请使用 X-Async' },
    ];
  }
  if (section === 'embeddings') {
    return [
      { name: 'model', type: 'string', required: true, value: '模型 ID 或别名，例如 text-embedding-v4' },
      { name: 'input', type: 'string|array', required: true, value: '需要向量化的文本或文本数组' },
      { name: 'dimensions', type: 'number', value: '可选向量维度，需模型支持' },
      { name: 'runMode / simulation', type: 'string|boolean', value: '在线测试时使用 simulation，不消耗真实上游额度' },
    ];
  }
  if (section === 'reranks') {
    return [
      { name: 'model', type: 'string', required: true, value: '模型 ID 或别名，例如 qwen3-rerank' },
      { name: 'query', type: 'string', required: true, value: '用于相关性排序的查询文本' },
      { name: 'documents', type: 'array', required: true, value: '候选文档文本数组，至少 1 条' },
      { name: 'top_n', type: 'number', value: '返回前 N 条结果' },
      { name: 'runMode / simulation', type: 'string|boolean', value: '在线测试时使用 simulation，不消耗真实上游额度' },
    ];
  }
  if (section === 'videoGeneration') {
    return [
      { name: 'model', type: 'string', required: true, value: '视频模型 ID 或别名，例如 豆包Seedance-2.0' },
      {
        name: 'content',
        type: 'array<object>',
        required: true,
        value: '文本和参考素材对象数组；每一项由 type 决定应填写的内容字段和 role',
        children: [
          { name: 'type', type: 'string', required: true, value: '内容类型：text、image_url、video_url、audio_url 或 element' },
          { name: 'text', type: 'string', value: 'type=text 时的提示词；配合 role=shot_prompt 可描述指定分镜' },
          {
            name: 'image_url',
            type: 'object',
            value: 'type=image_url 时的图片素材',
            children: [
              { name: 'url', type: 'string', required: true, value: '可公开访问或已上传的图片 URL' },
            ],
          },
          {
            name: 'video_url',
            type: 'object',
            value: 'type=video_url 时的视频素材及参考方式',
            children: [
              { name: 'url', type: 'string', required: true, value: '可公开访问或已上传的视频 URL' },
              { name: 'mime_type', type: 'string', value: '视频 MIME 类型，例如 video/mp4' },
              { name: 'refer_type', type: 'string', value: '参考方式：feature（特征参考）或 base（基础视频）' },
              { name: 'keep_original_sound', type: 'string', value: '是否保留原声：yes 或 no' },
            ],
          },
          {
            name: 'audio_url',
            type: 'object',
            value: 'type=audio_url 时的音频素材',
            children: [
              { name: 'url', type: 'string', required: true, value: '可公开访问或已上传的音频 URL' },
              { name: 'mime_type', type: 'string', value: '音频 MIME 类型，例如 audio/mpeg' },
            ],
          },
          { name: 'role', type: 'string', value: '素材角色，例如 first_frame、last_frame、reference_image、reference_video、reference_audio、element 或 shot_prompt' },
          { name: 'shot_index', type: 'number', value: '素材或提示词对应的分镜序号' },
          { name: 'duration', type: 'number', value: '当前分镜或素材的时长（秒）' },
          { name: 'name', type: 'string', value: '素材或元素名称' },
          {
            name: 'element',
            type: 'object',
            value: 'type=element 时使用的系统元素或内联元素',
            children: [
              { name: 'system_element_id', type: 'string', value: '已存在的系统元素 ID' },
              {
                name: 'inline_element',
                type: 'object',
                value: '随请求提交的内联元素定义',
                children: [
                  { name: 'name', type: 'string', required: true, value: '内联元素名称' },
                  { name: 'description', type: 'string', value: '内联元素描述' },
                  { name: 'frontal_image_url', type: 'string', required: true, value: '元素正面参考图 URL' },
                  {
                    name: 'refer_images',
                    type: 'array<object>',
                    required: true,
                    value: '元素的补充参考图片对象数组',
                    children: [
                      { name: 'url', type: 'string', required: true, value: '参考图片 URL' },
                      { name: 'slot_key', type: 'string', value: '参考图槽位标识' },
                    ],
                  },
                  { name: 'tags', type: 'array<string>', value: '元素标签数组' },
                ],
              },
            ],
          },
        ],
      },
      { name: 'duration', type: 'number', value: '视频时长（秒），允许范围取决于模型能力' },
      { name: 'resolution', type: 'string', value: '输出分辨率，例如 480p、720p、1080p' },
      { name: 'aspect_ratio', type: 'string', value: '画幅，例如 16:9、9:16、1:1 或 adaptive' },
      { name: 'audio', type: 'boolean', value: '是否生成或保留声音，需模型支持 output_audio' },
      {
        name: 'audio_list',
        type: 'array<object>',
        value: '额外音频素材对象数组',
        children: [
          { name: 'url', type: 'string', value: '音频 URL' },
          { name: 'audio_url', type: 'string', value: '音频 URL 的兼容字段' },
          { name: 'name', type: 'string', value: '音频名称' },
        ],
      },
      { name: 'prompt', type: 'string', value: '纯文生视频的快捷字段；网关会转换为 content 文本项' },
      { name: 'runMode / simulation', type: 'string|boolean', value: '在线测试时使用 simulation，不触达真实视频供应商' },
    ];
  }
  if (section === 'pricing') {
    return [
      { name: 'kind', type: 'string', value: '任务类型，默认 chat.completions' },
      { name: 'model', type: 'string', required: true, value: '需要估价的模型 ID 或别名' },
      { name: 'messages / prompt', type: 'array|string', value: '用于估算输入规模的请求内容' },
      { name: 'max_tokens / n', type: 'number', value: '预期输出 token 或媒体数量' },
    ];
  }
  if (section === 'chat') {
    return [
      { name: 'model', type: 'string', required: true, value: '模型 ID 或别名' },
      { name: 'messages', type: 'array', required: true, value: 'OpenAI Chat 消息数组' },
      { name: 'stream', type: 'boolean', value: '设为 true 时返回 Chat Completions SSE' },
      { name: 'tools / tool_choice', type: 'array|object', value: '函数工具定义与选择策略' },
      { name: 'simulation', type: 'boolean', value: '测试模式开关' },
    ];
  }
  return [
    { name: 'model', type: 'string', required: true, value: '模型 ID 或别名' },
    { name: 'prompt', type: 'string', required: true, value: '图片生成或编辑提示词' },
    { name: 'image / mask', type: 'string|array', value: '图像编辑输入与可选遮罩' },
    { name: 'simulation', type: 'boolean', value: '测试模式开关' },
  ];
}

function docSectionForKind(kind: TaskKind): ApiDocSection {
  if (kind === 'responses') return 'responses';
  if (kind === 'embeddings') return 'embeddings';
  if (kind === 'reranks') return 'reranks';
  if (kind === 'images.generations') return 'imageGeneration';
  if (kind === 'images.edits') return 'imageEdit';
  if (kind === 'videos.generations') return 'videoGeneration';
  if (kind === 'tasks.retrieve') return 'taskRetrieve';
  return 'chat';
}

function embeddingInputExample(prompt: string) {
  const lines = splitLines(prompt);
  return lines.length > 1 ? lines : (lines[0] ?? prompt);
}

function rerankDocumentsExample(value?: string) {
  return splitLines(value ?? '').length
    ? splitLines(value ?? '')
    : ['AI Gateway 提供 OpenAI 兼容接口。', '图片生成任务支持异步队列。'];
}

function splitLines(value: string) {
  return value
    .split(/\n+/)
    .map((item) => item.trim())
    .filter(Boolean);
}
