<script setup>
// API 对接文档 — OpenAI-compatible. Lists live models and shows ready-to-run
// curl / Python(openai SDK) examples for image + video, wired to this
// deployment's base URL and the caller's model ids.
import { ref, computed, onMounted } from 'vue'
import { auth } from '../auth'
import { api } from '../api'
import { points } from '../credits'
import Icon from '../components/Icon.vue'

const base = computed(() => location.origin)            // /v1 is same-origin (dev: Vite proxy)
const keyHint = computed(() => auth.user?.api_keys?.[0]?.key_preview || 'YOUR_API_KEY')

const models = ref([])
onMounted(async () => {
  const r = await api('/managed-models')
  if (r.ok) models.value = (r.data?.data || []).filter((m) => m.enabled !== false)
})

const imageModels = computed(() => models.value.filter((m) => m.type === 'image'))
const videoModels = computed(() => models.value.filter((m) => m.type === 'video'))
const sampleImage = computed(() => imageModels.value[0]?.id || 'firefly-image-4')
const sampleVideo = computed(() => videoModels.value[0]?.id || 'firefly-kling3')
const sampleSeconds = computed(() => String(videoModels.value[0]?.durations?.[0] || '8s').replace(/s$/, ''))

function priceOf(m) {
  if (m.type === 'video') {
    // Video charge = resolution price + duration price; show the combined range.
    const rv = Object.values(m.prices || {}).filter((v) => v != null).map(Number)
    const dv = Object.values(m.duration_prices || {}).filter((v) => v != null).map(Number)
    if (!rv.length || !dv.length) return '—'
    const lo = Math.min(...rv) + Math.min(...dv)
    const hi = Math.max(...rv) + Math.max(...dv)
    return lo === hi ? `${points(lo)} 积分` : `${points(lo)}–${points(hi)} 积分`
  }
  const vals = Object.values(m.prices || {}).filter((v) => v != null).map(Number)
  if (!vals.length) return '—'
  const lo = Math.min(...vals), hi = Math.max(...vals)
  return lo === hi ? `${points(lo)} 积分` : `${points(lo)}–${points(hi)} 积分`
}

// ---- request parameter tables ----
const imageParams = [
  ['model', 'string', '必填', '模型 id,见上表(图像)'],
  ['prompt', 'string', '必填', '文字描述'],
  ['size', 'string', '可选', '宽x高,如 "1024x1024"。同时决定「比例」+「分辨率档」(按长边)。具体怎么填见下方对照表;留空 = 1:1 · 2K'],
]
const editParams = [
  ['image', 'file', '必填', '输入图;多张参考图重复 image[] 字段(multipart 文件上传)'],
  ['prompt', 'string', '必填', '编辑/参考描述'],
  ['model', 'string', '必填', '模型 id(需支持图生图)'],
  ['size', 'string', '可选', '同图像:决定比例 + 分辨率档(见下方对照表)'],
]
const videoParams = [
  ['model', 'string', '必填', '模型 id,见上表(视频)'],
  ['prompt', 'string', '必填', '文字描述'],
  ['seconds', 'string|int', '必填', '时长秒数,如 "5" "8"(取决于模型支持)'],
  ['size', 'string', '可选', '如 "1280x720" / "720x1280" → 决定比例与分辨率'],
  ['input_reference', 'file', '可选', '首帧/参考图(multipart 文件;runway 图生视频必填 1 张)'],
]

// ---- size → 比例 × 分辨率档 对照表(用 size 该传的值)----
// size 的长边映射档位:<1800→1K · 1800–3499→2K · ≥3500→4K;宽高比映射比例。
const sizeTable = [
  { ratio: '1:1 · 方',   k1: '1024x1024', k2: '2048x2048', k4: '4096x4096' },
  { ratio: '5:4 · 横',   k1: '1280x1024', k2: '2560x2048', k4: '3840x3072' },
  { ratio: '4:3 · 横',   k1: '1024x768',  k2: '2048x1536', k4: '4096x3072' },
  { ratio: '3:2 · 横',   k1: '1200x800',  k2: '2400x1600', k4: '3600x2400' },
  { ratio: '16:9 · 横',  k1: '1280x720',  k2: '2048x1152', k4: '4096x2304' },
  { ratio: '2:1 · 横',   k1: '1440x720',  k2: '2880x1440', k4: '4096x2048' },
  { ratio: '21:9 · 超宽', k1: '1680x720',  k2: '2520x1080', k4: '5040x2160' },
  { ratio: '3:1 · 超宽',  k1: '1536x512',  k2: '2304x768',  k4: '3840x1280' },
  { ratio: '4:5 · 竖',   k1: '1024x1280', k2: '2048x2560', k4: '3072x3840' },
  { ratio: '3:4 · 竖',   k1: '768x1024',  k2: '1536x2048', k4: '3072x4096' },
  { ratio: '2:3 · 竖',   k1: '800x1200',  k2: '1600x2400', k4: '2400x3600' },
  { ratio: '9:16 · 竖',  k1: '720x1280',  k2: '1152x2048', k4: '2304x4096' },
  { ratio: '9:21 · 竖超宽', k1: '720x1680', k2: '1080x2520', k4: '2160x5040' },
  { ratio: '1:3 · 竖',   k1: '512x1536',  k2: '768x2304',  k4: '1280x3840' },
]

// ---- examples (built in script so refs resolve correctly) ----
const examples = computed(() => [
  {
    title: '文生图 · curl',
    code:
`curl ${base.value}/v1/images/generations \\
  -H "Authorization: Bearer ${keyHint.value}" \\
  -H "Content-Type: application/json" \\
  -d '{
    "model": "${sampleImage.value}",
    "prompt": "a corgi running in a golden wheat field, cinematic",
    "size": "2048x2048"
  }'`,
  },
  {
    title: '文生图 · Python (openai SDK)',
    code:
`import base64
from openai import OpenAI

client = OpenAI(api_key="${keyHint.value}", base_url="${base.value}/v1")

resp = client.images.generate(
    model="${sampleImage.value}",
    prompt="a corgi running in a golden wheat field, cinematic",
    size="2048x2048",   # 2K · 1:1,见下方对照表
)
# 结果是 base64(无 URL)
with open("out.png", "wb") as f:
    f.write(base64.b64decode(resp.data[0].b64_json))`,
  },
  {
    title: '图生图 / 参考图 · curl (multipart)',
    code:
`curl ${base.value}/v1/images/edits \\
  -H "Authorization: Bearer ${keyHint.value}" \\
  -F model="${sampleImage.value}" \\
  -F prompt="把这张图改成赛博朋克风格" \\
  -F size="2048x2048" \\
  -F image=@input.png
# 多张参考图:重复 -F image=@a.png -F image=@b.png`,
  },
  {
    title: '图生图 · Python (openai SDK)',
    code:
`import base64
from openai import OpenAI

client = OpenAI(api_key="${keyHint.value}", base_url="${base.value}/v1")

resp = client.images.edit(
    model="${sampleImage.value}",
    image=open("input.png", "rb"),     # 多张:image=[open("a.png","rb"), open("b.png","rb")]
    prompt="把这张图改成赛博朋克风格",
)
with open("out.png", "wb") as f:
    f.write(base64.b64decode(resp.data[0].b64_json))`,
  },
  {
    title: '视频 · curl(创建 → 轮询 → 下载)',
    code:
`# 1) 创建任务 → 立即返回 {"id": "...", "status": "queued"}
curl ${base.value}/v1/videos \\
  -H "Authorization: Bearer ${keyHint.value}" \\
  -H "Content-Type: application/json" \\
  -d '{
    "model": "${sampleVideo.value}",
    "prompt": "a paper boat sailing down a rainy street, cinematic",
    "seconds": "${sampleSeconds.value}",
    "size": "1280x720"
  }'

# 2) 轮询状态,直到 status=completed
curl ${base.value}/v1/videos/<VIDEO_ID> \\
  -H "Authorization: Bearer ${keyHint.value}"

# 3) 下载 mp4(完成后)
curl ${base.value}/v1/videos/<VIDEO_ID>/content \\
  -H "Authorization: Bearer ${keyHint.value}" -o out.mp4`,
  },
  {
    title: '视频 · Python (requests, 轮询)',
    code:
`import time, requests

base = "${base.value}/v1"
h = {"Authorization": "Bearer ${keyHint.value}"}

# 1) 创建
job = requests.post(f"{base}/videos", headers=h, json={
    "model": "${sampleVideo.value}",
    "prompt": "a paper boat sailing down a rainy street",
    "seconds": "${sampleSeconds.value}",
    "size": "1280x720",
}).json()
vid = job["id"]

# 2) 轮询
while True:
    s = requests.get(f"{base}/videos/{vid}", headers=h).json()
    if s["status"] in ("completed", "failed"):
        break
    time.sleep(5)

# 3) 下载
if s["status"] == "completed":
    mp4 = requests.get(f"{base}/videos/{vid}/content", headers=h).content
    open("out.mp4", "wb").write(mp4)`,
  },
  {
    title: '列出模型 · curl',
    code:
`curl ${base.value}/v1/models \\
  -H "Authorization: Bearer ${keyHint.value}"`,
  },
])

// ---- copy + toast ----
const toastMsg = ref('')
let t = null
function toast(m) { toastMsg.value = m; clearTimeout(t); t = setTimeout(() => (toastMsg.value = ''), 1800) }
async function copy(text) {
  try { await navigator.clipboard.writeText(text); toast('已复制') } catch { toast('复制失败') }
}
</script>

<template>
  <div class="theme-text space-y-10">
    <header>
      <div class="text-[10px] uppercase tracking-[0.3em] text-sky-300/70 font-medium">开发者</div>
      <h1 class="mt-2 text-4xl md:text-5xl font-bold tracking-tight">接口文档</h1>
      <p class="text-white/45 mt-2">完全兼容 OpenAI 接口规范 — 改个 <code class="text-white/70">base_url</code> 和 <code class="text-white/70">api_key</code> 即可直接调用。图像 / 视频 / 图生图全支持。</p>
    </header>

    <!-- quickstart -->
    <section class="grid md:grid-cols-2 gap-4">
      <div class="card p-6">
        <h2 class="text-sm font-semibold text-white/80">基础信息</h2>
        <dl class="mt-4 space-y-3 text-sm">
          <div class="flex items-center justify-between gap-3">
            <dt class="text-white/45">Base URL</dt><dd class="font-mono text-white/90">{{ base }}/v1</dd>
          </div>
          <div class="flex items-center justify-between gap-3">
            <dt class="text-white/45">鉴权</dt><dd class="font-mono text-white/90">Authorization: Bearer &lt;key&gt;</dd>
          </div>
          <div class="flex items-center justify-between gap-3">
            <dt class="text-white/45">你的 Key</dt><dd class="font-mono text-white/70">{{ keyHint }}</dd>
          </div>
        </dl>
        <p class="text-[11px] text-white/40 mt-4">还没有 Key?去 <router-link to="/settings" class="text-violet-300 underline">设置 → API Key</router-link> 生成。</p>
      </div>

      <div class="card p-6">
        <h2 class="text-sm font-semibold text-white/80">端点</h2>
        <ul class="mt-4 space-y-2.5 text-sm font-mono">
          <li class="flex items-center gap-2"><span class="badge-get">GET</span><span class="text-white/80">/v1/models</span></li>
          <li class="flex items-center gap-2"><span class="badge-post">POST</span><span class="text-white/80">/v1/images/generations</span><span class="text-white/35 font-sans text-xs">文生图</span></li>
          <li class="flex items-center gap-2"><span class="badge-post">POST</span><span class="text-white/80">/v1/images/edits</span><span class="text-white/35 font-sans text-xs">图生图(multipart)</span></li>
          <li class="flex items-center gap-2"><span class="badge-post">POST</span><span class="text-white/80">/v1/videos</span><span class="text-white/35 font-sans text-xs">建视频任务</span></li>
          <li class="flex items-center gap-2"><span class="badge-get">GET</span><span class="text-white/80">/v1/videos/{id}</span><span class="text-white/35 font-sans text-xs">查状态</span></li>
          <li class="flex items-center gap-2"><span class="badge-get">GET</span><span class="text-white/80">/v1/videos/{id}/content</span><span class="text-white/35 font-sans text-xs">下载 mp4</span></li>
        </ul>
      </div>
    </section>

    <!-- models -->
    <section>
      <h2 class="text-lg font-semibold mb-3">可用模型</h2>
      <div class="card overflow-hidden">
        <table class="w-full text-sm">
          <thead>
            <tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
              <th class="px-4 py-3 font-medium">model</th>
              <th class="px-4 py-3 font-medium">类型</th>
              <th class="px-4 py-3 font-medium">分辨率 / 时长</th>
              <th class="px-4 py-3 font-medium text-right">价格</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="m in models" :key="m.id" class="border-b border-white/[0.04] last:border-0">
              <td class="px-4 py-3 font-mono text-white/90">{{ m.id }}</td>
              <td class="px-4 py-3 text-white/60">{{ m.type === 'video' ? '视频' : '图像' }}</td>
              <td class="px-4 py-3 text-white/60">{{ (m.type === 'video' ? m.durations : m.resolutions || [])?.join(' · ') || '—' }}</td>
              <td class="px-4 py-3 text-right tabular-nums text-white/80">{{ priceOf(m) }}</td>
            </tr>
            <tr v-if="!models.length"><td colspan="4" class="px-4 py-10 text-center text-white/35">暂无可用模型</td></tr>
          </tbody>
        </table>
      </div>
    </section>

    <!-- parameters -->
    <section class="grid lg:grid-cols-2 gap-6">
      <div>
        <h2 class="text-lg font-semibold mb-3">文生图参数 <span class="text-xs font-normal text-white/40">/v1/images/generations</span></h2>
        <div class="card overflow-hidden">
          <table class="w-full text-sm">
            <thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
              <th class="px-4 py-2.5 font-medium">参数</th><th class="px-4 py-2.5 font-medium">类型</th><th class="px-4 py-2.5 font-medium">必填</th><th class="px-4 py-2.5 font-medium">说明</th>
            </tr></thead>
            <tbody>
              <tr v-for="p in imageParams" :key="p[0]" class="border-b border-white/[0.04] last:border-0">
                <td class="px-4 py-2.5 font-mono text-white/85">{{ p[0] }}</td>
                <td class="px-4 py-2.5 text-white/50 font-mono text-xs">{{ p[1] }}</td>
                <td class="px-4 py-2.5 text-white/55">{{ p[2] }}</td>
                <td class="px-4 py-2.5 text-white/60 text-xs">{{ p[3] }}</td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>

      <div>
        <h2 class="text-lg font-semibold mb-3">图生图参数 <span class="text-xs font-normal text-white/40">/v1/images/edits · multipart</span></h2>
        <div class="card overflow-hidden">
          <table class="w-full text-sm">
            <thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
              <th class="px-4 py-2.5 font-medium">参数</th><th class="px-4 py-2.5 font-medium">类型</th><th class="px-4 py-2.5 font-medium">必填</th><th class="px-4 py-2.5 font-medium">说明</th>
            </tr></thead>
            <tbody>
              <tr v-for="p in editParams" :key="p[0]" class="border-b border-white/[0.04] last:border-0">
                <td class="px-4 py-2.5 font-mono text-white/85">{{ p[0] }}</td>
                <td class="px-4 py-2.5 text-white/50 font-mono text-xs">{{ p[1] }}</td>
                <td class="px-4 py-2.5 text-white/55">{{ p[2] }}</td>
                <td class="px-4 py-2.5 text-white/60 text-xs">{{ p[3] }}</td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>

      <div class="lg:col-span-2">
        <h2 class="text-lg font-semibold mb-3">视频参数 <span class="text-xs font-normal text-white/40">/v1/videos · 异步</span></h2>
        <div class="card overflow-hidden">
          <table class="w-full text-sm">
            <thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
              <th class="px-4 py-2.5 font-medium">参数</th><th class="px-4 py-2.5 font-medium">类型</th><th class="px-4 py-2.5 font-medium">必填</th><th class="px-4 py-2.5 font-medium">说明</th>
            </tr></thead>
            <tbody>
              <tr v-for="p in videoParams" :key="p[0]" class="border-b border-white/[0.04] last:border-0">
                <td class="px-4 py-2.5 font-mono text-white/85">{{ p[0] }}</td>
                <td class="px-4 py-2.5 text-white/50 font-mono text-xs">{{ p[1] }}</td>
                <td class="px-4 py-2.5 text-white/55">{{ p[2] }}</td>
                <td class="px-4 py-2.5 text-white/60 text-xs">{{ p[3] }}</td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </section>

    <!-- size 对照表(课时表)—— 解决"传错分辨率" -->
    <section>
      <h2 class="text-lg font-semibold mb-1">分辨率对照表 · <code class="text-white/70 text-sm">size</code> 该传什么</h2>
      <p class="text-xs text-white/45 mb-3">
        左边选比例,上面选分辨率档,交叉格里就是 <code class="text-white/70">size</code> 要传的值(直接复制)。
        没有 <code class="text-white/70">quality</code> 参数,分辨率只看 <code class="text-white/70">size</code> 的长边。
        档位必须是该模型支持的(见上方「可用模型」的分辨率列),不支持会自动回退到该模型最低档。
      </p>
      <div class="card overflow-hidden">
        <table class="w-full text-sm">
          <thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
            <th class="px-4 py-2.5 font-medium">比例</th>
            <th class="px-4 py-2.5 font-medium">1K</th>
            <th class="px-4 py-2.5 font-medium">2K</th>
            <th class="px-4 py-2.5 font-medium">4K</th>
          </tr></thead>
          <tbody>
            <tr v-for="row in sizeTable" :key="row.ratio" class="border-b border-white/[0.04] last:border-0">
              <td class="px-4 py-2.5 text-white/75">{{ row.ratio }}</td>
              <td class="px-4 py-2.5 font-mono text-white/85">{{ row.k1 }}</td>
              <td class="px-4 py-2.5 font-mono text-white/85">{{ row.k2 }}</td>
              <td class="px-4 py-2.5 font-mono text-white/85">{{ row.k4 }}</td>
            </tr>
          </tbody>
        </table>
      </div>
      <p class="text-xs text-white/40 mt-2">
        例:想要 <strong class="text-white/70">2K 的 16:9 横图</strong> → <code class="text-white/70">"size": "2048x1152"</code>。
        留空 size = 默认 <strong class="text-white/70">1:1 · 2K</strong>。
      </p>
    </section>

    <!-- examples -->
    <section class="space-y-4">
      <h2 class="text-lg font-semibold">调用示例</h2>
      <div v-for="ex in examples" :key="ex.title" class="card overflow-hidden">
        <div class="flex items-center justify-between px-4 py-2.5 border-b border-white/[0.06]">
          <span class="text-xs text-white/55">{{ ex.title }}</span>
          <button @click="copy(ex.code)" class="text-xs text-white/50 hover:text-white inline-flex items-center gap-1.5 transition-colors">
            <Icon name="copy" class="w-3.5 h-3.5" /> 复制
          </button>
        </div>
        <pre class="p-4 text-[12px] leading-relaxed text-white/80 overflow-auto"><code>{{ ex.code }}</code></pre>
      </div>
    </section>

    <!-- responses -->
    <section>
      <h2 class="text-lg font-semibold mb-3">响应 & 计费</h2>
      <div class="card p-6 space-y-3 text-sm text-white/70">
        <p><strong class="text-white/90">图像</strong>(generations / edits)返回 OpenAI 图片格式:<code class="text-white/85 font-mono">{{ '{ "created": ..., "data": [{ "b64_json": "..." }] }' }}</code> —— 产物以 <strong class="text-white/90">base64</strong> 直接放在 <code class="text-white/85 font-mono">data[0].b64_json</code>(原始 base64、无 <code class="text-white/70">data:</code> 前缀),自行解码保存为图片。<strong class="text-white/90">不返回 URL、服务端不留存</strong>。</p>
        <p><strong class="text-white/90">视频</strong>(异步,Sora 风格三步):</p>
        <ol class="list-decimal list-inside space-y-1 text-white/65 pl-1">
          <li><code class="text-white/85 font-mono">POST /v1/videos</code> 立即返回任务对象 <code class="text-white/85 font-mono">{{ '{ "id": "...", "object": "video", "status": "queued", ... }' }}</code></li>
          <li>轮询 <code class="text-white/85 font-mono">GET /v1/videos/{id}</code>,<code class="text-white/70">status</code> 从 <code class="text-white/70">queued → in_progress → completed</code>(或 <code class="text-white/70">failed</code>)</li>
          <li>完成后 <code class="text-white/85 font-mono">GET /v1/videos/{id}/content</code> 返回 <strong class="text-white/90">mp4 原始二进制</strong>(非 base64、非 URL)</li>
        </ol>
        <p><strong class="text-white/90">计费(预扣)</strong>:生成<strong class="text-white/90">前</strong>按上表价格从你的 Key 账号预扣积分;图像或视频上游失败会自动退回 —— 失败不扣费。</p>
        <p><strong class="text-white/90">参数映射</strong>:<code class="text-white/70">size</code>(宽x高)同时决定<strong class="text-white/90">比例 + 分辨率档</strong>(长边:&lt;1800→1K · 1800–3499→2K · ≥3500→4K),<code class="text-white/70">seconds</code>→视频时长。<strong class="text-white/90">没有 quality 参数</strong>,分辨率只看 size。档位须是该模型支持的(不支持会回退到该模型最低档);参数须落在定价表内否则 400,余额不足 402。</p>
        <div class="pt-2 grid sm:grid-cols-2 gap-2 text-xs">
          <div class="flex items-center gap-2"><span class="badge-err">401</span> Key 无效 / 上游需重新授权</div>
          <div class="flex items-center gap-2"><span class="badge-err">404</span> 未知 model / 视频任务不存在</div>
          <div class="flex items-center gap-2"><span class="badge-err">400</span> 参数缺失 / 不支持或未定价</div>
          <div class="flex items-center gap-2"><span class="badge-err">402</span> 积分不足</div>
          <div class="flex items-center gap-2"><span class="badge-err">409</span> 视频尚未完成(content 未就绪)</div>
          <div class="flex items-center gap-2"><span class="badge-err">429</span> 账号并发已满,请重试</div>
          <div class="flex items-center gap-2"><span class="badge-err">503</span> 上游繁忙,请重试</div>
        </div>
      </div>
    </section>

    <transition name="fade">
      <div v-if="toastMsg" class="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 bg-white text-black text-sm font-medium px-5 py-2.5 rounded-full shadow-2xl">{{ toastMsg }}</div>
    </transition>
  </div>
</template>

<style scoped>
.badge-get, .badge-post, .badge-err {
  border-radius: 4px; padding: 2px 6px; font-size: 10px; line-height: 1;
}
.badge-get { background: rgb(16 185 129 / 0.14); color: rgb(4 120 87); box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.35); }
.badge-post { background: rgb(14 165 233 / 0.14); color: rgb(3 105 161); box-shadow: inset 0 0 0 1px rgb(14 165 233 / 0.35); }
.badge-err { background: rgb(244 63 94 / 0.12); color: rgb(190 18 60); box-shadow: inset 0 0 0 1px rgb(244 63 94 / 0.3); font-family: ui-monospace, monospace; }
html.dark .badge-get { background: rgb(16 185 129 / 0.15); color: rgb(110 231 183); box-shadow: inset 0 0 0 1px rgb(52 211 153 / 0.3); }
html.dark .badge-post { background: rgb(14 165 233 / 0.15); color: rgb(125 211 252); box-shadow: inset 0 0 0 1px rgb(56 189 248 / 0.3); }
html.dark .badge-err { background: rgb(244 63 94 / 0.15); color: rgb(253 164 175); box-shadow: inset 0 0 0 1px rgb(251 113 133 / 0.3); }
</style>
