<!-- SPEC: PJ2026-010405 云端控制台 / Secrets. -->
<!-- Implementation reference: draft-2026-07-13-p0-cloud-console. -->
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { adminSecretsAPI } from "@/api";
import ApiErrorDiagnostic from "@/components/common/ApiErrorDiagnostic.vue";
import AsyncBoundary from "@/components/common/AsyncBoundary.vue";
import EmptyState from "@/components/common/EmptyState.vue";
import StatusBadge from "@/components/common/StatusBadge.vue";
import OperationsWorkspace from "@/components/console/OperationsWorkspace.vue";
import PageCommandBar from "@/components/layout/PageCommandBar.vue";
import StatusStrip from "@/components/layout/StatusStrip.vue";
import SecretInventoryIndex from "@/components/secrets/SecretInventoryIndex.vue";
import SecretInventoryWorkspace from "@/components/secrets/SecretInventoryWorkspace.vue";
import { useConsoleViewState } from "@/composables/useConsoleViewState";
import {
  normalizeAdminSecretsRows,
  normalizeAdminSecretsSummary,
  type AdminSecretsRow,
  type AdminSecretsSummaryView,
} from "@/stores/admin-secrets-view";
import type { ApiError, ErrorDiagnostic } from "@/types";
import AdminSecretsPlanPanel from "./AdminSecretsPlanPanel.vue";

const loading = ref(true);
const refreshing = ref(false);
const error = ref<string | null>(null);
const apiError = ref<ApiError | null>(null);
const diagnostic = ref<ErrorDiagnostic | null>(null);
const summary = ref<AdminSecretsSummaryView | null>(null);
const rows = ref<AdminSecretsRow[]>([]);
const planPanelOpen = ref(false);
const route = useRoute();
const router = useRouter();
let reloadSequence = 0;
const viewState = useConsoleViewState<"flow" | "list">({
  view: "flow",
  viewValues: ["flow", "list"],
  filter: "all",
});
const canPlan = computed(() => summary.value?.permissions.plan === true);
const query = computed({
  get: () => viewState.q.value,
  set: (value: string) => void viewState.update({ q: value.trim() }),
});
const statusFilter = computed({
  get: () => viewState.filter.value || "all",
  set: (value: string) =>
    void viewState.update({ filter: value === "all" ? "" : value }),
});
const viewMode = computed({
  get: () => viewState.view.value,
  set: (value: "flow" | "list") =>
    void viewState.update(
      { view: value === "flow" ? undefined : value },
      false,
    ),
});
const filteredRows = computed(() =>
  rows.value.filter((row) => {
    if (
      statusFilter.value !== "all" &&
      row.status.toLowerCase() !== statusFilter.value.toLowerCase()
    )
      return false;
    const needle = query.value.toLowerCase();
    return (
      !needle ||
      [
        row.name,
        row.namespace,
        row.cluster,
        row.targetSecretName,
        row.storeName,
        row.status,
        row.reason,
      ]
        .join(" ")
        .toLowerCase()
        .includes(needle)
    );
  }),
);
const selected = computed(() => {
  const namespace =
    typeof route.params.namespace === "string" ? route.params.namespace : "";
  const name = typeof route.params.name === "string" ? route.params.name : "";
  return (
    rows.value.find(
      (row) => row.namespace === namespace && row.name === name,
    ) ??
    filteredRows.value[0] ??
    null
  );
});
const asyncState = computed(() =>
  loading.value || refreshing.value
    ? summary.value
      ? "refreshing"
      : "initial-loading"
    : error.value
      ? summary.value
        ? "partial"
        : "error"
      : summary.value
        ? "ready"
        : "empty",
);
const statusItems = computed(() => [
  { id: "objects", label: "ExternalSecrets", value: rows.value.length },
  {
    id: "ready",
    label: "Ready",
    value: rows.value.filter((row) => row.status.toLowerCase() === "ready")
      .length,
    tone: "ok" as const,
  },
  {
    id: "attention",
    label: "Attention",
    value: rows.value.filter(
      (row) => !["ready", "unknown"].includes(row.status.toLowerCase()),
    ).length,
    tone: "warn" as const,
  },
  {
    id: "authority",
    label: "Values",
    value: "redacted",
    tone: "info" as const,
  },
]);
const factSteps = computed(() =>
  selected.value
    ? [
        {
          id: "source",
          label: "sourceRef / store",
          value: `${selected.value.storeKind}/${selected.value.storeName}`,
        },
        {
          id: "external",
          label: "ExternalSecret",
          value: `${selected.value.namespace}/${selected.value.name}`,
          tone:
            selected.value.status.toLowerCase() === "ready"
              ? ("ok" as const)
              : ("warn" as const),
        },
        {
          id: "target",
          label: "target Secret",
          value: selected.value.targetSecretName || "-",
        },
        {
          id: "consumer",
          label: "consumer",
          value: selected.value.cluster || "cluster workload",
        },
      ]
    : [],
);

onMounted(() => void reload());
async function reload(): Promise<void> {
  const requestSequence = ++reloadSequence;
  refreshing.value = true;
  error.value = null;
  apiError.value = null;
  diagnostic.value = null;
  const [summaryResult, listResult] = await Promise.all([
    adminSecretsAPI.summary(),
    adminSecretsAPI.externalSecrets(),
  ]);
  if (requestSequence !== reloadSequence) return;
  if (!summaryResult.ok) {
    error.value = summaryResult.error ?? `HTTP ${summaryResult.status}`;
    apiError.value = summaryResult.apiError ?? null;
    diagnostic.value = summaryResult.diagnostic ?? null;
  } else {
    summary.value = normalizeAdminSecretsSummary(summaryResult.data);
    rows.value = normalizeAdminSecretsRows(
      listResult.ok
        ? listResult.data?.items
        : summaryResult.data?.externalSecrets,
    );
    if (!listResult.ok) {
      error.value = listResult.error ?? `HTTP ${listResult.status}`;
      apiError.value = listResult.apiError ?? null;
      diagnostic.value = listResult.diagnostic ?? null;
    }
  }
  loading.value = false;
  refreshing.value = false;
}
function rowKey(row: AdminSecretsRow): string {
  return `${row.namespace}/${row.name}`;
}
function openSecret(row: AdminSecretsRow): void {
  void router.push({
    name: "AdminExternalSecretDetail",
    params: { namespace: row.namespace, name: row.name },
    query: route.query,
  });
}
function displayDate(value: string): string {
  if (!value) return "-";
  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? value : date.toLocaleString("zh-CN");
}
</script>

<template>
  <section class="route-stack secrets-admin-page">
    <PageCommandBar
      eyebrow="Admin"
      title="密钥库存与流向"
      description="只展示 ExternalSecret 脱敏 read model；Secret 值永不进入浏览器。"
    >
      <template #actions
        ><button
          class="table-action"
          type="button"
          :disabled="refreshing"
          @click="reload"
        >
          {{ refreshing ? "刷新中" : "刷新" }}</button
        ><button
          v-if="canPlan"
          class="table-action"
          type="button"
          @click="planPanelOpen = !planPanelOpen"
        >
          {{ planPanelOpen ? "关闭 Plan" : "Plan" }}
        </button></template
      >
    </PageCommandBar>
    <AsyncBoundary
      :state="asyncState"
      :has-content="Boolean(summary)"
      label="加载密钥库存"
      empty-title="暂无密钥库存"
      empty-description="当前 admin secrets read model 为空。"
      @retry="reload"
    >
      <template #error
        ><EmptyState
          title="密钥库存加载失败"
          :description="error || '请求未完成'"
          action-label="重试"
          @action="reload"
      /></template>
      <ApiErrorDiagnostic
        v-if="error && summary"
        :api-error="apiError"
        :diagnostic="diagnostic"
      />
      <StatusStrip :items="statusItems" live="polite" />
      <AdminSecretsPlanPanel
        v-if="canPlan && planPanelOpen && summary"
        :summary="summary"
      />
      <OperationsWorkspace
        class="secrets-operations"
        label="ExternalSecret 库存工作区"
      >
        <template #index>
          <SecretInventoryIndex v-model:query="query" v-model:status="statusFilter" :rows="filteredRows" :total="rows.length" :selected-id="selected ? rowKey(selected) : null" @select="openSecret" />
        </template>
        <SecretInventoryWorkspace v-model:view="viewMode" :selected="selected" :rows="filteredRows" :fact-steps="factSteps" :generated-at="summary?.generatedAt || ''" @select="openSecret" @inspect="openSecret" />
      </OperationsWorkspace>
    </AsyncBoundary>
  </section>
</template>

<style>
.secrets-operations {
  min-height: 610px;
}
.operations-search {
  display: grid;
  gap: 5px;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0;
  text-transform: uppercase;
}
.operations-search input {
  height: 34px;
  min-width: 0;
  border: 1px solid #b8c5cc;
  border-radius: 5px;
  padding: 0 9px;
}
.entity-index-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 4px;
}
.entity-index-meta code {
  max-width: 150px;
  overflow: hidden;
  text-overflow: ellipsis;
  color: #64757d;
  font-size: 10px;
}
.operations-main-stack {
  display: grid;
  gap: 14px;
}
.view-mode-row {
  display: flex;
  justify-content: flex-end;
}
.secret-compact-list {
  overflow: auto;
}
.secret-compact-list button {
  display: grid;
  grid-template-columns: minmax(190px, 1.4fr) 90px minmax(160px, 1fr) minmax(
      140px,
      1fr
    ) 70px;
  gap: 9px;
  width: 100%;
  min-width: 720px;
  align-items: center;
  border: 0;
  border-bottom: 1px solid #d8e1e4;
  padding: 9px 11px;
  background: white;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.secret-compact-list button:hover,
.secret-compact-list button[data-selected="true"] {
  background: #e8f6f5;
}
.fact-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  margin: 0;
  background: #d8e1e4;
}
.fact-grid div {
  min-width: 0;
  padding: 11px;
  background: white;
}
.fact-grid dt {
  color: #64757d;
  font-size: 10px;
  text-transform: uppercase;
}
.fact-grid dd {
  margin: 5px 0 0;
  overflow-wrap: anywhere;
}
@media (max-width: 640px) {
  .fact-grid {
    grid-template-columns: 1fr;
  }
}
</style>
