<!-- SPEC: PJ2026-010405 云端控制台. -->
<!-- Implementation reference: draft-2026-07-13-p0-cloud-console. -->
<script setup lang="ts">
import { Grid2X2, List, Rows3, Rows4 } from "lucide-vue-next";
import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { apiKeysAPI } from "@/api";
import AsyncBoundary from "@/components/common/AsyncBoundary.vue";
import BaseDialog from "@/components/common/BaseDialog.vue";
import ConfirmDialog from "@/components/common/ConfirmDialog.vue";
import EmptyState from "@/components/common/EmptyState.vue";
import ResourceCollection from "@/components/common/ResourceCollection.vue";
import InspectorPanel from "@/components/console/InspectorPanel.vue";
import ViewModeSwitch from "@/components/console/ViewModeSwitch.vue";
import ApiKeyResourceCard from "@/components/api-keys/ApiKeyResourceCard.vue";
import BoundedWorkspace from "@/components/layout/BoundedWorkspace.vue";
import PageCommandBar from "@/components/layout/PageCommandBar.vue";
import StatusStrip from "@/components/layout/StatusStrip.vue";
import { useConsoleViewState } from "@/composables/useConsoleViewState";
import type { ApiKeyRecord } from "@/types";

const keys = ref<ApiKeyRecord[]>([]);
const route = useRoute();
const router = useRouter();
const viewState = useConsoleViewState({ filter: "all" });
const loading = ref(true);
const loaded = ref(false);
const error = ref<string | null>(null);
const mutationError = ref<string | null>(null);
const newKeyName = ref("HWLAB CLI key");
const createdSecret = ref<string | null>(null);
const pending = ref<string | null>(null);
const editingId = ref<string | null>(null);
const editingName = ref("");
const createDialogOpen = ref(false);
const revokeTarget = ref<ApiKeyRecord | null>(null);

const activeKeys = computed(() => keys.value.filter((key) => !key.revokedAt));
const query = computed({
  get: () => viewState.q.value,
  set: (value: string) => void viewState.update({ q: value.trim() })
});
const statusFilter = computed({
  get: () => viewState.filter.value === "revoked" ? "revoked" : viewState.filter.value === "active" ? "active" : "all",
  set: (value: string) => void viewState.update({ filter: value === "all" ? "" : value })
});
const viewMode = computed<"cards" | "list">({
  get: () => viewState.view.value,
  set: (value) => void viewState.update({ view: value === "cards" ? undefined : value })
});
const density = computed<"compact" | "comfortable">({
  get: () => viewState.density.value,
  set: (value) => void viewState.update({ density: value === "comfortable" ? undefined : value })
});
const filteredKeys = computed(() => {
  const needle = query.value.trim().toLowerCase();
  return keys.value.filter((key) => {
    const status = key.revokedAt ? "revoked" : "active";
    if (statusFilter.value !== "all" && statusFilter.value !== status) return false;
    return !needle || [key.name, key.id, key.prefix].filter(Boolean).join(" ").toLowerCase().includes(needle);
  });
});
const selectedKey = computed(() => {
  const keyId = typeof route.params.keyId === "string" ? route.params.keyId : "";
  return keys.value.find((key) => key.id === keyId) ?? null;
});
const keyStatusItems = computed(() => [
  { id: "active", label: "Active", value: activeKeys.value.length },
  { id: "total", label: "Total", value: keys.value.length },
  { id: "visible", label: "Visible", value: filteredKeys.value.length },
  { id: "disclosure", label: "Secret", value: "one-time", detail: "列表只保留 prefix、状态和使用时间" }
]);
const asyncState = computed(() => {
  if (loading.value) return keys.value.length ? "refreshing" : "initial-loading";
  if (error.value) return keys.value.length ? "partial" : "error";
  return loaded.value ? "ready" : "empty";
});

onMounted(refresh);

async function refresh(): Promise<void> {
  loading.value = true;
  const response = await apiKeysAPI.list();
  if (response.ok) {
    keys.value = response.data?.keys ?? [];
    error.value = null;
  } else {
    error.value = response.error ?? `HTTP ${response.status}`;
  }
  loading.value = false;
  loaded.value = true;
}

async function createKey(): Promise<void> {
  pending.value = "create";
  mutationError.value = null;
  createdSecret.value = null;
  const response = await apiKeysAPI.create(newKeyName.value.trim() || "HWLAB API key");
  pending.value = null;
  if (!response.ok) {
    mutationError.value = response.error ?? `HTTP ${response.status}`;
    return;
  }
  const payload = response.data ?? {};
  createdSecret.value = payload.secret ?? payload.key?.displaySecret ?? null;
  await refresh();
}

function openCreateDialog(): void {
  mutationError.value = null;
  createdSecret.value = null;
  createDialogOpen.value = true;
}

function closeCreateDialog(): void {
  createdSecret.value = null;
  createDialogOpen.value = false;
}

function keyId(key: ApiKeyRecord): string {
  return key.id;
}

function keyTitle(key: ApiKeyRecord): string {
  return key.name || key.id;
}

function keySubtitle(key: ApiKeyRecord): string {
  return `${key.prefix || "-"} · ${formatDate(key.lastUsedAt)}`;
}

function keyStatus(key: ApiKeyRecord): string {
  return key.revokedAt ? "revoked" : "active";
}

function selectKey(key: ApiKeyRecord): void {
  void router.push({ name: "ApiKeyDetail", params: { keyId: key.id }, query: route.query });
}

function closeInspector(): void {
  editingId.value = null;
  editingName.value = "";
  void router.push({ name: "ApiKeys", query: route.query });
}

function startRename(key: ApiKeyRecord): void {
  editingId.value = key.id;
  editingName.value = key.name || "";
}

async function saveRename(key: ApiKeyRecord): Promise<void> {
  if (!editingId.value) return;
  pending.value = `rename:${key.id}`;
  mutationError.value = null;
  const response = await apiKeysAPI.rename(key.id, editingName.value.trim() || key.name || key.id);
  pending.value = null;
  if (!response.ok) {
    mutationError.value = response.error ?? `HTTP ${response.status}`;
    return;
  }
  editingId.value = null;
  editingName.value = "";
  await refresh();
}

async function revokeKey(key: ApiKeyRecord): Promise<void> {
  pending.value = `revoke:${key.id}`;
  mutationError.value = null;
  const response = await apiKeysAPI.revoke(key.id);
  pending.value = null;
  if (!response.ok) {
    mutationError.value = response.error ?? `HTTP ${response.status}`;
    return;
  }
  revokeTarget.value = null;
  await refresh();
}

function formatDate(value: unknown): string {
  const text = String(value ?? "").trim();
  if (!text) return "-";
  const date = new Date(text);
  if (Number.isNaN(date.getTime())) return text;
  return date.toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
}

</script>

<template>
  <section class="route-stack api-keys-page">
    <PageCommandBar eyebrow="User" title="API Keys" description="API key 供 CLI、AgentRun/HWPOD 调用使用；浏览器请求仍使用 HttpOnly Web session。" />
    <AsyncBoundary :state="asyncState" :has-content="Boolean(keys.length)" label="加载 API keys" error-title="API key 接口不可用" :error="error || '请求未完成'" @retry="refresh">
      <section class="api-key-command-bar" aria-label="API key 集合控制">
        <label><span>搜索</span><input v-model="query" type="search" placeholder="名称、prefix 或 id" /></label>
        <label><span>状态</span><select v-model="statusFilter"><option value="all">全部</option><option value="active">active</option><option value="revoked">revoked</option></select></label>
        <ViewModeSwitch :model-value="viewMode" :options="[{ value: 'cards', label: '卡片', icon: Grid2X2 }, { value: 'list', label: '列表', icon: List }]" label="API Key 显示模式" @update:model-value="viewMode = $event as 'cards' | 'list'" />
        <ViewModeSwitch :model-value="density" :options="[{ value: 'comfortable', label: '舒适密度', icon: Rows4 }, { value: 'compact', label: '紧凑密度', icon: Rows3 }]" label="API Key 显示密度" @update:model-value="density = $event as 'compact' | 'comfortable'" />
        <button class="api-key-primary" type="button" @click="openCreateDialog">+ 创建 Key</button>
      </section>

      <StatusStrip :items="keyStatusItems" label="API key 摘要" />

      <BoundedWorkspace class="api-key-workspace" :inspector-open="Boolean(selectedKey)" inspector-width="360px" label="API Key 集合与详情">
        <section class="data-panel api-key-collection-panel">
          <header><div><span>USER BILLING AUTHORITY</span><h2>当前 API Keys</h2></div><small>prefix / lifecycle / timestamps</small></header>
          <EmptyState v-if="!filteredKeys.length" title="没有匹配 API key" description="调整搜索或状态筛选；当前不会虚构 scope、轮换或额外权限。" />
          <ResourceCollection v-else :items="filteredKeys" :item-key="keyId" :item-title="keyTitle" :item-subtitle="keySubtitle" :item-status="keyStatus" :view="viewMode" :density="density" :selected-id="selectedKey?.id" :show-controls="false" aria-label="API Key 集合" @select="selectKey">
            <template #card="{ item: key }"><ApiKeyResourceCard :api-key="key" :selected="selectedKey?.id === key.id" :to="{ name: 'ApiKeyDetail', params: { keyId: key.id }, query: route.query }" /></template>
            <template #list="{ item: key }"><ApiKeyResourceCard :api-key="key" :selected="selectedKey?.id === key.id" :to="{ name: 'ApiKeyDetail', params: { keyId: key.id }, query: route.query }" /></template>
          </ResourceCollection>
        </section>

        <template #inspector>
        <InspectorPanel v-if="selectedKey" :title="selectedKey.name || selectedKey.id" :subtitle="selectedKey.id" @close="closeInspector">
          <dl class="api-key-inspector-facts">
            <div><dt>ID</dt><dd><code>{{ selectedKey.id }}</code></dd></div>
            <div><dt>Prefix</dt><dd><code>{{ selectedKey.prefix || '-' }}</code></dd></div>
            <div><dt>状态</dt><dd>{{ selectedKey.revokedAt ? 'revoked' : 'active' }}</dd></div>
            <div><dt>创建时间</dt><dd>{{ formatDate(selectedKey.createdAt) }}</dd></div>
            <div><dt>最后使用</dt><dd>{{ formatDate(selectedKey.lastUsedAt) }}</dd></div>
            <div v-if="selectedKey.revokedAt"><dt>撤销时间</dt><dd>{{ formatDate(selectedKey.revokedAt) }}</dd></div>
          </dl>
          <section v-if="!selectedKey.revokedAt" class="api-key-inspector-actions">
            <div v-if="editingId === selectedKey.id" class="api-key-rename"><input v-model="editingName" class="input" /><button type="button" :disabled="pending === `rename:${selectedKey.id}`" @click="saveRename(selectedKey)">保存</button></div>
            <button v-else type="button" @click="startRename(selectedKey)">重命名</button>
            <button class="danger" type="button" :disabled="pending === `revoke:${selectedKey.id}`" @click="revokeTarget = selectedKey">撤销 Key</button>
          </section>
          <p class="api-key-fact-note">当前 API 未声明 scope 或 rotation 合同，因此详情不会展示或模拟这些能力。</p>
        </InspectorPanel>
        </template>
      </BoundedWorkspace>

      <BaseDialog :open="createDialogOpen" title="创建 API key" description="Key secret 仅在成功后显示一次。" @close="closeCreateDialog">
        <form class="api-key-create-form" @submit.prevent="createKey">
          <label><span>名称</span><input v-model="newKeyName" class="input" placeholder="API key 名称" autofocus /></label>
          <p v-if="mutationError" class="form-error">{{ mutationError }}</p>
          <div v-if="createdSecret" class="secret-once-box"><strong>立即保存此 secret</strong><code>{{ createdSecret }}</code><small>关闭后无法再次查看。</small></div>
        </form>
        <template #footer><button class="table-action" type="button" @click="closeCreateDialog">{{ createdSecret ? '已保存并关闭' : '取消' }}</button><button v-if="!createdSecret" class="table-action" type="button" :disabled="pending === 'create'" @click="createKey">{{ pending === 'create' ? '创建中' : '创建' }}</button></template>
      </BaseDialog>
      <ConfirmDialog :open="revokeTarget !== null" title="撤销 API key" :message="`撤销 ${revokeTarget?.name || revokeTarget?.id || '当前 key'}；现有服务端 lifecycle 与审计语义保持不变。`" confirm-label="撤销" :pending="Boolean(revokeTarget && pending === `revoke:${revokeTarget.id}`)" @close="revokeTarget = null" @confirm="revokeTarget && revokeKey(revokeTarget)" />
    </AsyncBoundary>
  </section>
</template>

<style scoped>
.api-keys-page { min-width: 0; }
.api-key-command-bar { display: grid; grid-template-columns: minmax(220px, 1fr) 150px auto auto auto; gap: 8px; align-items: end; padding: 10px; border: 1px solid #cbd5dc; border-radius: 8px; background: #f4f7f8; box-shadow: inset 3px 0 #16a3a0; }
.api-key-command-bar label { display: grid; gap: 4px; color: #60717a; font-size: 9px; font-weight: 850; letter-spacing: 0; text-transform: uppercase; }
.api-key-command-bar input,
.api-key-command-bar select { width: 100%; height: 34px; border: 1px solid #b9c6ce; border-radius: 6px; background: #fff; padding: 0 9px; color: #192b33; }
.api-key-secondary,
.api-key-primary { height: 34px; border: 0; background: #fff; padding: 0 11px; color: #344750; font-weight: 750; cursor: pointer; }
.api-key-secondary,
.api-key-primary { border: 1px solid #aebbc3; border-radius: 6px; }
.api-key-primary { border-color: #17343b; background: #17343b; color: #ecffff; }
.api-key-summary-strip { display: grid; grid-template-columns: 120px 120px 120px minmax(0, 1fr); border: 1px solid #cbd5dc; border-radius: 8px; overflow: hidden; background: #fff; }
.api-key-summary-strip div { display: grid; gap: 3px; padding: 10px 13px; border-right: 1px solid #dbe3e7; }
.api-key-summary-strip span,
.api-key-collection-panel > header span { color: #16807d; font: 850 9px/1 ui-monospace, monospace; letter-spacing: 0; }
.api-key-summary-strip strong { color: #19303a; font-size: 20px; }
.api-key-summary-strip p { align-self: center; margin: 0; padding: 0 14px; color: #64757e; font-size: 11px; }
.api-key-workspace { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; min-height: 0; }
.api-key-workspace[data-inspector-open="true"] { grid-template-columns: minmax(0, 1fr) minmax(290px, 360px); }
.api-key-collection-panel { display: grid; align-content: start; gap: 11px; padding: 13px; }
.api-key-collection-panel > header { display: flex; align-items: start; justify-content: space-between; gap: 12px; border-bottom: 1px solid #e0e7ea; padding-bottom: 10px; }
.api-key-collection-panel h2 { margin: 5px 0 0; color: #172b34; font-size: 17px; }
.api-key-collection-panel > header small { color: #697a83; font-size: 10px; }
.api-key-collection { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 9px; }
.api-key-collection[data-view="list"] { grid-template-columns: 1fr; }
.api-key-collection[data-density="compact"] { gap: 5px; }
.api-key-inspector-facts { display: grid; gap: 0; margin: 0; }
.api-key-inspector-facts div { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 10px; border-bottom: 1px solid #e2e8eb; padding: 9px 0; }
.api-key-inspector-facts dt { color: #74828a; font-size: 10px; }
.api-key-inspector-facts dd { min-width: 0; margin: 0; color: #263b44; font-size: 11px; overflow-wrap: anywhere; }
.api-key-inspector-actions { display: grid; gap: 7px; }
.api-key-inspector-actions button,
.api-key-rename button { min-height: 34px; border: 1px solid #aebdc5; border-radius: 6px; background: #fff; color: #30434c; font-weight: 750; cursor: pointer; }
.api-key-inspector-actions button.danger { border-color: #e0a29d; color: #9b332c; }
.api-key-rename { display: grid; grid-template-columns: 1fr auto; gap: 6px; }
.api-key-fact-note { margin: 0; border: 1px solid #dfc984; border-radius: 6px; background: #fff9e8; padding: 9px; color: #745d27; font-size: 10px; line-height: 1.5; }
.api-key-create-form { display: grid; gap: 12px; }
.api-key-create-form label { display: grid; gap: 6px; color: #53656e; font-size: 11px; font-weight: 750; }
.secret-once-box { display: grid; gap: 8px; border: 1px solid #d2ae58; border-radius: 7px; background: #fff8de; padding: 12px; }
.secret-once-box code { overflow-wrap: anywhere; color: #563e07; }
.secret-once-box small { color: #7c672f; }
@media (max-width: 1120px) {
  .api-key-workspace[data-inspector-open="true"] { grid-template-columns: 1fr; }
}
@media (max-width: 760px) {
  .api-key-command-bar { grid-template-columns: 1fr 1fr; }
  .api-key-command-bar label:first-child { grid-column: 1 / -1; }
  .api-key-summary-strip { grid-template-columns: repeat(3, 1fr); }
  .api-key-summary-strip p { grid-column: 1 / -1; border-top: 1px solid #dbe3e7; padding: 10px 13px; }
  .api-key-collection,
  .api-key-collection[data-view="list"] { grid-template-columns: 1fr; }
  .api-key-collection[data-view="list"] .api-key-prefix,
  .api-key-collection[data-view="list"] .api-key-time { display: none; }
}
</style>
