<!-- SPEC: PJ2026-010405 云端控制台 / Access. -->
<!-- Implementation reference: draft-2026-07-13-p0-cloud-console. -->
<script setup lang="ts">
import { computed, onMounted, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import AsyncBoundary from "@/components/common/AsyncBoundary.vue";
import EmptyState from "@/components/common/EmptyState.vue";
import OperationsWorkspace from "@/components/console/OperationsWorkspace.vue";
import StatusStrip from "@/components/layout/StatusStrip.vue";
import AccessPermissionWorkspace from "./AccessPermissionWorkspace.vue";
import AccessUserIndex from "./AccessUserIndex.vue";
import AccessUserInspector from "./AccessUserInspector.vue";
import { useConsoleViewState } from "@/composables/useConsoleViewState";
import { useAccessStore } from "@/stores/access";
import {
  accessUserLabel,
  type AccessUserRow,
} from "@/stores/admin-access-view";
import type { AccessToolId, AccessUserRole, AccessUserStatus } from "@/types";

const access = useAccessStore();
const route = useRoute();
const router = useRouter();
const viewState = useConsoleViewState<"matrix" | "list">({
  view: "matrix",
  viewValues: ["matrix", "list"],
});
const openfga = computed(() => access.summaryView.openfga);
const query = computed({
  get: () => viewState.q.value,
  set: (value: string) => void viewState.update({ q: value.trim() }),
});
const viewMode = computed({
  get: () => viewState.view.value,
  set: (value: "matrix" | "list") =>
    void viewState.update(
      { view: value === "matrix" ? undefined : value },
      false,
    ),
});
const filteredRows = computed(() => {
  const needle = query.value.toLowerCase();
  return access.userRows.filter(
    (row) =>
      !needle ||
      [
        row.id,
        row.username,
        row.displayName,
        row.role,
        row.status,
        ...Object.keys(row.tools),
      ]
        .filter(Boolean)
        .join(" ")
        .toLowerCase()
        .includes(needle),
  );
});
const asyncState = computed(() =>
  access.loading
    ? access.userRows.length
      ? "refreshing"
      : "initial-loading"
    : access.error
      ? access.userRows.length
        ? "partial"
        : "error"
      : access.users
        ? "ready"
        : "empty",
);
const statusItems = computed(() => [
  { id: "users", label: "Users", value: access.summaryView.users },
  { id: "tuples", label: "Tuples", value: access.summaryView.tuples },
  { id: "grants", label: "Tool grants", value: access.summaryView.toolTuples },
  {
    id: "openfga",
    label: "OpenFGA",
    value: valueText(
      openfga.value.status || (openfga.value.ready ? "ready" : "not ready"),
    ),
    tone: openfga.value.ready ? ("ok" as const) : ("warn" as const),
  },
]);
const factSteps = computed(() => [
  {
    id: "identity",
    label: "Identity",
    value: access.selectedUser
      ? accessUserLabel(access.selectedUser)
      : "选择用户",
  },
  { id: "role", label: "Role", value: access.selectedUser?.role || "-" },
  {
    id: "tools",
    label: "Tool grants",
    value: access.selectedUser
      ? `${access.selectedUser.toolCount}/${Object.keys(access.selectedUser.tools).length}`
      : "-",
  },
  {
    id: "tuples",
    label: "OpenFGA tuples",
    value: String(access.detailView.tuples.length),
    tone: openfga.value.ready ? ("ok" as const) : ("warn" as const),
  },
]);

onMounted(async () => {
  await access.refresh();
  const userId =
    typeof route.params.userId === "string" ? route.params.userId : "";
  if (userId) await access.selectUser(userId);
});
watch(
  () => route.params.userId,
  (userId) => {
    const value = typeof userId === "string" ? userId : "";
    if (value && value !== access.selectedUserId) void access.selectUser(value);
    if (!value) access.clearSelection();
  },
);

function selectRow(row: AccessUserRow): void {
  if (route.params.userId !== row.id)
    void router.push({
      name: "AccessUserDetail",
      params: { userId: row.id },
      query: route.query,
    });
  void access.selectUser(row.id);
}
function closeInspector(): void {
  access.clearSelection();
  void router.push({ name: "Access", query: route.query });
}
function onRoleChange(role: AccessUserRole): void {
  void access.updateSelectedUser({ role });
}
function onStatusChange(status: AccessUserStatus): void {
  void access.updateSelectedUser({ status });
}
function onToolChange(toolId: AccessToolId, allowed: boolean): void {
  void access.setTool(toolId, allowed);
}
function valueText(value: unknown): string {
  return typeof value === "string" && value.trim()
    ? value
    : typeof value === "number"
      ? String(value)
      : typeof value === "boolean"
        ? value
          ? "yes"
          : "no"
        : "-";
}
</script>

<template>
  <AsyncBoundary
    :state="asyncState"
    :has-content="Boolean(access.userRows.length)"
    label="加载授权工作区"
    empty-title="暂无授权用户"
    empty-description="同源 Access API 当前未返回用户。"
    @retry="access.refresh"
  >
    <template #error
      ><EmptyState
        title="授权工作区加载失败"
        :description="access.error || '请求未完成'"
        action-label="重试"
        @action="access.refresh"
    /></template>
    <StatusStrip :items="statusItems" live="polite" />
    <OperationsWorkspace
      class="access-operations"
      label="授权运营工作区"
      :inspector-open="Boolean(access.selectedUser)"
    >
      <template #index>
        <AccessUserIndex v-model:query="query" :rows="filteredRows" :selected-id="access.selectedUserId" :refreshing="access.loading" @select="selectRow" @refresh="access.refresh" />
      </template>
      <AccessPermissionWorkspace v-model:view="viewMode" :rows="filteredRows" :tools="access.toolRows" :selected-id="access.selectedUserId" :fact-steps="factSteps" :openfga="openfga" @select="selectRow" />

      <template #inspector>
        <AccessUserInspector v-if="access.selectedUser" :user="access.selectedUser" :tools="access.toolRows" :tuples="access.detailView.tuples" :mutation-error="access.mutationError" :saving="access.savingKey !== null" @close="closeInspector" @role="onRoleChange" @status="onStatusChange" @tool="onToolChange" />
      </template>
    </OperationsWorkspace>
  </AsyncBoundary>
</template>

<style>
.access-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 {
  min-width: 0;
  height: 34px;
  border: 1px solid #b8c5cc;
  border-radius: 5px;
  padding: 0 9px;
  background: white;
}
.entity-index-row small {
  color: #64757d;
}
.entity-index-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 4px;
}
.entity-index-meta code {
  color: #64757d;
  font-size: 10px;
}
.operations-main-stack {
  display: grid;
  gap: 14px;
  min-width: 0;
}
.permission-matrix {
  overflow: auto;
}
.permission-matrix-head,
.permission-matrix > [role="row"] {
  display: grid;
  grid-template-columns: minmax(180px, 1.6fr) repeat(
      var(--permission-tool-count),
      minmax(86px, 1fr)
    );
  min-width: 100%;
  width: max-content;
}
.permission-matrix-head {
  position: sticky;
  top: 0;
  z-index: 1;
  background: #eef3f4;
}
.permission-matrix-head span,
.permission-matrix > [role="row"] > [role="gridcell"] {
  padding: 9px;
  border-right: 1px solid #d8e1e4;
  border-bottom: 1px solid #d8e1e4;
  text-align: center;
}
.permission-matrix-head span:first-child,
.permission-matrix > [role="row"] > [role="gridcell"]:first-child {
  text-align: left;
}
.permission-matrix > [role="row"]:not(.permission-matrix-head) {
  width: 100%;
  background: white;
  color: inherit;
}
.permission-matrix > [role="row"]:not(.permission-matrix-head):hover,
.permission-matrix > [role="row"][data-selected="true"] {
  background: #e8f6f5;
}
.permission-matrix > [role="row"] > [role="gridcell"]:first-child button {
  width: 100%;
  border: 0;
  background: transparent;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.permission-matrix strong,
.permission-matrix small {
  display: block;
}
.permission-matrix small {
  color: #64757d;
}
.permission-matrix [data-allowed="true"] {
  color: #08756f;
  font-weight: 800;
}
.access-compact-list {
  display: grid;
}
.access-compact-list button {
  display: grid;
  grid-template-columns: minmax(180px, 1fr) 120px 90px 90px;
  gap: 10px;
  align-items: center;
  border: 0;
  border-bottom: 1px solid #d8e1e4;
  padding: 9px 11px;
  background: white;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.access-compact-list button:hover,
.access-compact-list button[data-selected="true"] {
  background: #e8f6f5;
}
.access-compact-list strong,
.access-compact-list small {
  display: block;
}
.technical-disclosure {
  border: 1px solid #d8e1e4;
  background: #f8fafb;
}
.technical-disclosure summary {
  cursor: pointer;
  padding: 11px 13px;
  font-weight: 800;
}
.technical-disclosure .field-grid {
  padding: 0 13px 13px;
}
.tool-toggle-grid {
  display: grid;
  gap: 7px;
}
.tuple-list {
  min-width: 0;
  overflow: auto;
}
.tuple-list li {
  min-width: 520px;
}
</style>
