<script setup lang="ts">
import { NDataTable, NModal, NTooltip, type DataTableColumns } from 'naive-ui'
import { computed, h, onMounted, reactive, ref } from 'vue'

import { listAiCallLogs, listAiContentLogs } from '@/api/aigateway'
import DetailFieldList, { type DetailRow } from '@/components/DetailFieldList.vue'
import DetailHero, { type HeroChip } from '@/components/DetailHero.vue'
import StatusBadge from '@/components/StatusBadge.vue'
import { useAutoRefresh } from '@/composables/useAutoRefresh'
import { fmtTime } from '@/composables/useFormat'
import type { AiCallLog, AiContentLog } from '@/types/api'
import { useToast } from '@/composables/useToast'

const message = useToast()
const rows = ref<AiCallLog[]>([])
const loading = ref(false)

const pagination = reactive({
  page: 1,
  pageSize: 20,
  itemCount: 0,
  showSizePicker: true,
  pageSizes: [20, 50, 100],
  onUpdatePage: (p: number) => {
    pagination.page = p
    void load()
  },
  onUpdatePageSize: (ps: number) => {
    pagination.pageSize = ps
    pagination.page = 1
    void load()
  },
})

async function load(silent = false) {
  if (!silent) loading.value = true
  try {
    const data = await listAiCallLogs({ page: pagination.page, size: pagination.pageSize })
    rows.value = data.items
    pagination.itemCount = data.total
  } catch {
    // 自动刷新失败静默,手动路径由全局兜底
  } finally {
    if (!silent) loading.value = false
  }
}

onMounted(() => void load())
useAutoRefresh(() => load(true))

function fmtLatency(ms: number) {
  return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`
}

function statusCell(row: AiCallLog) {
  const ok = row.status >= 200 && row.status < 300
  const badge = h(StatusBadge, { kind: ok ? 'run' : 'term', label: String(row.status) })
  if (ok || !row.errMsg) return badge
  return h(
    NTooltip,
    { style: { maxWidth: '380px' } },
    {
      trigger: () => h('span', { class: 'inline-flex cursor-help' }, [badge]),
      default: () => h('span', { class: 'text-xs leading-relaxed whitespace-pre-wrap break-all' }, row.errMsg),
    },
  )
}

function tokensCell(row: AiCallLog) {
  if (row.errMsg && !row.totalTokens) return h('span', { class: 'text-[13px] text-ink-3' }, '—')
  const flow = row.stream ? ' · 流' : ''
  return h('span', { class: 'mono text-[13px]' }, `${row.promptTokens} / ${row.completionTokens}${flow}`)
}

const columns: DataTableColumns<AiCallLog> = [
  { title: '时间', key: 'createdAt', width: 150, render: (r) => h('span', { class: 'text-[13px] whitespace-nowrap text-ink-2' }, fmtTime(r.createdAt)) },
  { title: '密钥', key: 'keyName', width: 140, ellipsis: { tooltip: true }, render: (r) => h('span', { class: 'text-[13px]' }, r.keyName || '—') },
  { title: '请求 IP', key: 'clientIp', width: 130, render: (r) => (r.clientIp ? h('span', { class: 'mono text-[12.5px] text-ink-2' }, r.clientIp) : h('span', { class: 'text-[13px] text-ink-3' }, '—')) },
  { title: '模型', key: 'model', minWidth: 220, ellipsis: { tooltip: true }, render: (r) => h('span', { class: 'mono text-xs' }, r.model) },
  { title: '渠道', key: 'channelName', minWidth: 180, ellipsis: { tooltip: true }, render: (r) => h('span', { class: 'text-[13px]' }, r.channelName || '—') },
  { title: '状态', key: 'status', width: 80, render: statusCell },
  { title: 'Tokens(入/出)', key: 'tokens', width: 150, render: tokensCell },
  { title: '耗时', key: 'latencyMs', width: 80, render: (r) => h('span', { class: 'mono text-[13px]' }, fmtLatency(r.latencyMs)) },
  { title: '重试', key: 'retries', width: 60, render: (r) => h('span', { class: 'mono text-[13px]' }, String(r.retries)) },
]

// ---- 行点击详情 ----

const EP_PATH: Record<string, string> = {
  openai: '/ai/v1/chat/completions',
  anthropic: '/ai/v1/messages',
  responses: '/ai/v1/responses',
}

const detail = ref<AiCallLog | null>(null)
const showDetail = ref(false)

// ---- 内容日志正文(仅密钥内容日志窗口期内的调用存在) ----
const contentLog = ref<AiContentLog | null>(null)

async function loadContent(callLogId: number) {
  contentLog.value = null
  try {
    const { items } = await listAiContentLogs({ page: 1, size: 1, callLogId })
    contentLog.value = items[0] ?? null
  } catch {
    // 正文加载失败不阻塞元数据详情
  }
}

/** 展示层深截断:base64 data URI 只留协议头与少量前缀,多轮对话里的
 *  长文本(历史回答/长 prompt)留前 500 字符;存储仍是完整快照,全文走「复制完整」 */
function truncateText(s: string): string {
  const b64 = /^(data:[\w/+.-]+;base64,)/.exec(s)
  if (b64) {
    const keep = b64[1].length + 24
    return s.length > keep ? `${s.slice(0, keep)}…(已截断,共 ${s.length} 字符)` : s
  }
  if (s.length > 600) return `${s.slice(0, 500)}…(已截断,共 ${s.length} 字符)`
  return s
}

function truncateDeep(v: unknown): unknown {
  if (typeof v === 'string') return truncateText(v)
  if (Array.isArray(v)) return v.map(truncateDeep)
  if (v && typeof v === 'object')
    return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, truncateDeep(x)]))
  return v
}

/** 正文按 JSON 美化并深截断超长值;非 JSON(如 64KB 尾部截断)按文本截断展示 */
function fmtBody(s: string) {
  try {
    return JSON.stringify(truncateDeep(JSON.parse(s)), null, 2)
  } catch {
    return s.length > 2000 ? `${s.slice(0, 2000)}…(内容过长,完整正文请复制查看)` : s
  }
}

async function copyBody(text: string) {
  try {
    await navigator.clipboard.writeText(text)
    message.success('已复制完整正文')
  } catch {
    message.error('复制失败,请手动选择复制')
  }
}

const rowProps = (row: AiCallLog) => ({
  style: 'cursor: pointer',
  onClick: () => {
    detail.value = row
    showDetail.value = true
    void loadContent(row.id)
  },
})

const detailOk = computed(() => !!detail.value && detail.value.status >= 200 && detail.value.status < 300)

const heroSub = computed(() => {
  const d = detail.value
  if (!d) return ''
  const path = EP_PATH[d.endpoint] ?? d.endpoint
  return `POST ${path} · ${d.stream ? '流式(SSE)' : '非流式'} · ${fmtLatency(d.latencyMs)}`
})

const heroChips = computed<HeroChip[]>(() => {
  const d = detail.value
  if (!d) return []
  const chips: HeroChip[] = [{ label: '密钥', value: d.keyName || '—' }]
  if (d.clientIp) chips.push({ label: '来源 IP', value: d.clientIp, mono: true })
  chips.push({ label: '时间', value: fmtTime(d.createdAt) })
  return chips
})

const detailRows = computed<DetailRow[]>(() => {
  const d = detail.value
  if (!d) return []
  const list: DetailRow[] = []
  if (d.errMsg) list.push({ label: '失败原因', value: d.errMsg, tone: 'err', span: 2 })
  list.push({ label: '渠道', value: d.channelName || '—(未命中渠道)' })
  list.push({ label: '重试次数', value: String(d.retries), mono: true })
  return list
})

/** 失败且无用量时隐藏 Token 区(无意义的 0/0) */
const showTokens = computed(() => detailOk.value || (detail.value?.totalTokens ?? 0) > 0)

const footerText = computed(() => {
  if (contentLog.value) return '该调用命中密钥内容日志窗口,正文为截断快照(上限 64KB,保留 7 天 / 1 万行)'
  return detailOk.value ? '无计费信息,OCI 成本见总览成本卡' : '仅记录元数据与 token 用量,不含任何对话内容'
})
</script>

<template>
  <div class="panel">
    <div class="border-b border-line-soft px-4.5 py-3">
      <div class="text-sm font-semibold">AI 调用日志</div>
      <div class="mt-0.5 text-xs text-ink-3">仅元数据与 token 用量,不记录对话内容;保留 90 天 / 5 万行 · 每 5 秒自动刷新</div>
    </div>
    <NDataTable
      remote
      :columns="columns"
      :data="rows"
      :loading="loading"
      :pagination="pagination"
      :bordered="false"
      :row-props="rowProps"
      size="small"
    />

    <NModal v-model:show="showDetail" preset="card" closable :style="{ width: '600px', maxWidth: 'calc(100vw - 24px)' }">
      <template #header>
        <DetailHero
          v-if="detail"
          :kind="`AI 调用 · ${detail.endpoint.toUpperCase()} 协议`"
          :title="detail.model"
          title-mono
          :sub="heroSub"
          :chips="heroChips"
        >
          <StatusBadge
            :kind="detailOk ? 'run' : 'term'"
            :label="`${detailOk ? '成功' : '失败'} ${detail.status}`"
            :dot="false"
          />
        </DetailHero>
      </template>
      <div v-if="detail" class="flex flex-col gap-3.5">
        <DetailFieldList :rows="detailRows" :cols="2" />
        <div v-if="showTokens" class="overflow-hidden rounded-[10px] border border-line-soft">
          <div class="flex items-baseline gap-2 border-b border-line-soft bg-wash px-3.5 py-2">
            <span class="text-xs font-semibold text-ink-2">Token 明细</span>
            <span class="text-[11px] text-ink-3">仅用量计数 · 不含对话内容</span>
          </div>
          <div class="grid" :class="detail.cachedTokens > 0 ? 'grid-cols-4' : 'grid-cols-3'">
            <div class="px-3.5 py-3 text-center">
              <div class="mono text-[21px] font-semibold">{{ detail.promptTokens }}</div>
              <div class="mt-0.5 text-[11px] text-ink-3">输入 Token</div>
            </div>
            <div v-if="detail.cachedTokens > 0" class="border-l border-line-soft px-3.5 py-3 text-center">
              <div class="mono text-[21px] font-semibold text-ok">{{ detail.cachedTokens }}</div>
              <div class="mt-0.5 text-[11px] text-ink-3">缓存读取</div>
            </div>
            <div class="border-l border-line-soft px-3.5 py-3 text-center">
              <div class="mono text-[21px] font-semibold">{{ detail.completionTokens }}</div>
              <div class="mt-0.5 text-[11px] text-ink-3">输出 Token</div>
            </div>
            <div class="border-l border-line-soft px-3.5 py-3 text-center">
              <div class="mono text-[21px] font-semibold">{{ detail.totalTokens }}</div>
              <div class="mt-0.5 text-[11px] text-ink-3">合计</div>
            </div>
          </div>
        </div>
        <template v-if="contentLog">
          <div class="overflow-hidden rounded-[10px] border border-line-soft">
            <div class="flex items-baseline gap-2 border-b border-line-soft bg-wash px-3.5 py-2">
              <span class="text-xs font-semibold text-ink-2">请求正文</span>
              <span class="text-[11px] text-ink-3">内容日志 · 超长值已截断展示</span>
              <span class="flex-1"></span>
              <button
                type="button"
                class="cursor-pointer text-[11px] font-medium text-accent hover:text-accent-hover"
                @click="copyBody(contentLog.requestBody)"
              >
                复制完整
              </button>
            </div>
            <pre class="mono max-h-56 overflow-auto px-3.5 py-2.5 text-[11.5px] leading-relaxed break-all whitespace-pre-wrap">{{ fmtBody(contentLog.requestBody) }}</pre>
          </div>
          <div v-if="contentLog.responseBody" class="overflow-hidden rounded-[10px] border border-line-soft">
            <div class="flex items-baseline gap-2 border-b border-line-soft bg-wash px-3.5 py-2">
              <span class="text-xs font-semibold text-ink-2">响应正文</span>
              <span class="text-[11px] text-ink-3">内容日志 · 超长值已截断展示</span>
              <span class="flex-1"></span>
              <button
                type="button"
                class="cursor-pointer text-[11px] font-medium text-accent hover:text-accent-hover"
                @click="copyBody(contentLog.responseBody)"
              >
                复制完整
              </button>
            </div>
            <pre class="mono max-h-56 overflow-auto px-3.5 py-2.5 text-[11.5px] leading-relaxed break-all whitespace-pre-wrap">{{ fmtBody(contentLog.responseBody) }}</pre>
          </div>
          <div v-else class="text-[11px] text-ink-3">无响应正文:流式响应、向量结果与失败调用只记请求</div>
        </template>
        <div class="text-xs leading-relaxed text-ink-3">
          {{ footerText }}
        </div>
      </div>
    </NModal>
  </div>
</template>
