import { useEffect, useMemo, useRef, useState, type FormEvent, type ReactNode } from 'react';
import { Popover as AntPopover } from 'antd';
import { ChevronLeft, ChevronRight, Copy, CreditCard, Eye, KeyRound, ListChecks, Plus, ReceiptText, RotateCcw, Search, ShieldCheck, SlidersHorizontal, Trash2, UserRound } from 'lucide-react';
import type { GatewayAccessRuleBatchRequest, GatewayApiKey, GatewayApiKeyScopeUpdateRequest, GatewayTask, GatewayTaskParamPreprocessingLog, GatewayWalletAccount, GatewayWalletTransaction, IntegrationPlatform, PlatformModel } from '@easyai-ai-gateway/contracts';
import type { ConsoleData } from '../app-state';
import { EntityTable } from '../components/EntityTable';
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Checkbox, ConfirmDialog, DateTimePicker, DateTimeRangePicker, FormDialog, Input, Label, Select, Table, TableCell, TableFooter, TableHead, TablePageActions, TableRow, TableToolbar, TableViewportLayout, Tabs } from '../components/ui';
import { AccessPermissionEditor, countAccessPermissionRules } from './admin/AccessPermissionEditor';
import type { ApiKeyForm, LoadState, WorkspaceSection, WorkspaceTaskQuery, WorkspaceTransactionQuery } from '../types';
import { listTaskParamPreprocessing } from '../api';

const tabs = [
  { value: 'overview', label: '个人总览', icon: <UserRound size={15} /> },
  { value: 'billing', label: '余额充值', icon: <CreditCard size={15} /> },
  { value: 'apiKeys', label: 'API Key', icon: <KeyRound size={15} /> },
  { value: 'tasks', label: '任务记录', icon: <ListChecks size={15} /> },
  { value: 'transactions', label: '消费记录', icon: <ReceiptText size={15} /> },
] satisfies Array<{ value: WorkspaceSection; label: string; icon: ReactNode }>;

const taskPageSizeOptions = [10, 20, 50];

const apiKeyScopeOptions = [
  { value: 'chat', label: '对话', description: 'Chat Completions / Responses' },
  { value: 'embedding', label: '向量', description: 'Embeddings' },
  { value: 'rerank', label: '重排', description: 'Reranks' },
  { value: 'image', label: '图像', description: 'Images' },
  { value: 'video', label: '视频', description: 'Videos' },
  { value: 'music', label: '音乐生成', description: 'Song / Music' },
  { value: 'audio', label: '语音合成', description: 'Speech / TTS' },
  { value: 'all', label: '全部能力', description: '不按接口能力限制' },
] as const;

const knownApiKeyScopes = new Set(apiKeyScopeOptions.map((item) => item.value));

export function WorkspacePage(props: {
  apiKeyForm: ApiKeyForm;
  apiKeySecret: string;
  apiKeySecretsById: Record<string, string>;
  apiKeyPolicyModels: PlatformModel[];
  data: ConsoleData;
  message: string;
  section: WorkspaceSection;
  state: LoadState;
  token: string;
  taskQuery: WorkspaceTaskQuery;
  taskTotal: number;
  transactionQuery: WorkspaceTransactionQuery;
  transactionTotal: number;
  onBatchAccessRules: (input: GatewayAccessRuleBatchRequest) => Promise<void>;
  onDeleteApiKey: (apiKeyId: string) => Promise<void>;
  onApiKeyFormChange: (value: ApiKeyForm) => void;
  onSectionChange: (value: WorkspaceSection) => void;
  onSaveApiKeyScopes: (apiKeyId: string, input: GatewayApiKeyScopeUpdateRequest) => Promise<void>;
  onSubmitApiKey: (event: FormEvent<HTMLFormElement>) => void | Promise<void>;
  onTaskQueryChange: (value: WorkspaceTaskQuery) => void;
  onTransactionQueryChange: (value: WorkspaceTransactionQuery) => void;
  onUseApiKeyForPlayground: (apiKeyId?: string) => void;
}) {
  return (
    <div className="pageStack">
      <div className="subPageLayout">
        <Tabs className="sideTabs" value={props.section} tabs={tabs} onValueChange={props.onSectionChange} />
        <div className="subPageContent">
          {props.section === 'overview' && <WorkspaceOverview data={props.data} />}
          {props.section === 'billing' && <BillingPanel walletAccounts={props.data.walletAccounts} />}
          {props.section === 'apiKeys' && <ApiKeyPanel {...props} />}
          {props.section === 'tasks' && <TaskPanel data={props.data} query={props.taskQuery} token={props.token} total={props.taskTotal} onQueryChange={props.onTaskQueryChange} />}
          {props.section === 'transactions' && <ConsumptionPanel data={props.data} query={props.transactionQuery} total={props.transactionTotal} onQueryChange={props.onTransactionQueryChange} />}
        </div>
      </div>
    </div>
  );
}

function WorkspaceOverview(props: { data: ConsoleData }) {
  const owner = props.data.currentUser;
  const groupRows = currentUserGroupRows(owner, props.data.currentUserGroups);
  return (
    <section className="contentGrid two">
      <Card>
        <CardHeader>
          <CardTitle>个人中心总览</CardTitle>
        </CardHeader>
        <CardContent className="profileGrid">
          <InfoItem label="账号" value={owner?.username ?? '-'} />
          <InfoItem label="租户" value={owner?.tenantKey || owner?.tenantId || 'default'} />
          <InfoItem label="身份源" value={owner?.source ?? 'gateway'} />
          <InfoItem label="API Key" value={String(props.data.apiKeys.length)} />
        </CardContent>
      </Card>
      <Card>
        <CardHeader>
          <CardTitle>用户组策略</CardTitle>
        </CardHeader>
        <CardContent>
          <EntityTable
            columns={['用户组', '优先级', '状态', '来源']}
            empty="暂无用户组"
            rows={groupRows}
          />
        </CardContent>
      </Card>
    </section>
  );
}

function currentUserGroupRows(owner: ConsoleData['currentUser'], groups: ConsoleData['userGroups']) {
  if (!owner) return [];
  const ownerGroupKeys = Array.from(new Set([
    owner.userGroupKey,
    ...(owner.userGroupKeys ?? []),
  ].filter((value): value is string => Boolean(value?.trim()))));
  const ownerGroupIds = new Set([
    owner.userGroupId,
    ...(owner.userGroupKeys ?? []),
  ].filter((value): value is string => Boolean(value?.trim())));
  const matchedGroups = groups.filter((group) => ownerGroupIds.has(group.id) || ownerGroupKeys.includes(group.groupKey));
  if (matchedGroups.length) {
    return matchedGroups.map((item) => [item.groupKey, item.priority, item.status, item.source]);
  }
  return ownerGroupKeys.map((groupKey) => [groupKey, '-', '-', owner.source ?? 'gateway']);
}

function BillingPanel(props: { walletAccounts: GatewayWalletAccount[] }) {
  const primaryWallet = primaryWalletAccount(props.walletAccounts);
  const availableBalance = primaryWallet ? primaryWallet.balance - primaryWallet.frozenBalance : 0;
  return (
    <section className="contentGrid two">
      <Card>
        <CardHeader>
          <CardTitle>余额</CardTitle>
        </CardHeader>
        <CardContent className="balanceCard">
          <span>{primaryWallet?.currency ?? 'resource'}</span>
          <strong>{formatMoney(primaryWallet?.balance ?? 0)}</strong>
          <Badge variant={primaryWallet?.status === 'active' || !primaryWallet ? 'success' : 'secondary'}>{primaryWallet?.status ?? 'active'}</Badge>
          <div className="walletMetricGrid">
            <InfoItem label="可用余额" value={formatMoney(availableBalance)} />
            <InfoItem label="冻结余额" value={formatMoney(primaryWallet?.frozenBalance ?? 0)} />
            <InfoItem label="累计充值" value={formatMoney(primaryWallet?.totalRecharged ?? 0)} />
            <InfoItem label="累计消费" value={formatMoney(primaryWallet?.totalSpent ?? 0)} />
          </div>
        </CardContent>
      </Card>
      <Card>
        <CardHeader>
          <CardTitle>充值</CardTitle>
        </CardHeader>
        <CardContent className="formGrid">
          <Label>
            金额
            <Input value="100" readOnly />
          </Label>
          <Button type="button" variant="secondary">创建充值订单</Button>
        </CardContent>
      </Card>
    </section>
  );
}

function ConsumptionPanel(props: {
  data: ConsoleData;
  query: WorkspaceTransactionQuery;
  total: number;
  onQueryChange: (value: WorkspaceTransactionQuery) => void;
}) {
  const transactions = props.data.walletTransactions;
  const transactionQuery = props.query;
  const [pageJump, setPageJump] = useState(String(transactionQuery.page));
  const pageSizeOptions = useMemo(() => {
    return Array.from(new Set([...taskPageSizeOptions, transactionQuery.pageSize])).sort((a, b) => a - b);
  }, [transactionQuery.pageSize]);
  const totalPages = Math.max(1, Math.ceil(props.total / transactionQuery.pageSize));
  const currentPage = Math.min(transactionQuery.page, totalPages);
  const pageStart = props.total ? Math.min((currentPage - 1) * transactionQuery.pageSize + 1, props.total) : 0;
  const pageEnd = Math.min(currentPage * transactionQuery.pageSize, props.total);
  const hasActiveFilters = Boolean(transactionQuery.query || transactionQuery.createdFrom || transactionQuery.createdTo);

  useEffect(() => {
    if (transactionQuery.page > totalPages) {
      props.onQueryChange({ ...transactionQuery, page: totalPages });
    }
  }, [props.onQueryChange, transactionQuery, totalPages]);

  useEffect(() => {
    setPageJump(String(currentPage));
  }, [currentPage]);

  function updateQuery(value: string) {
    props.onQueryChange({ ...transactionQuery, query: value, page: 1 });
  }

  function updateCreatedRange(value: { from: string; to: string }) {
    props.onQueryChange({ ...transactionQuery, createdFrom: value.from, createdTo: value.to, page: 1 });
  }

  function updatePageSize(value: string) {
    const nextPageSize = Number(value);
    props.onQueryChange({ ...transactionQuery, page: 1, pageSize: Number.isFinite(nextPageSize) ? nextPageSize : 10 });
  }

  function updatePage(page: number) {
    props.onQueryChange({ ...transactionQuery, page });
  }

  function submitPageJump() {
    const parsed = Number(pageJump);
    if (!Number.isFinite(parsed) || parsed <= 0) {
      setPageJump(String(currentPage));
      return;
    }
    updatePage(Math.min(totalPages, Math.max(1, Math.floor(parsed))));
  }

  function resetFilters() {
    props.onQueryChange({
      query: '',
      createdFrom: '',
      createdTo: '',
      page: 1,
      pageSize: transactionQuery.pageSize,
    });
  }

  return (
    <Card className="shTableViewportCard walletTransactionViewport">
      <CardContent className="shTableViewportPanel">
        <TableViewportLayout>
          <TableToolbar className="walletTransactionFilters">
            <Label className="taskRecordSearchLabel">
              搜索
              <span className="taskRecordSearchBox">
                <Search size={15} />
                <Input
                  value={transactionQuery.query}
                  placeholder="搜索任务 / 模型 / 平台 / 类型 / API Key / RequestID"
                  onChange={(event) => updateQuery(event.target.value)}
                />
              </span>
            </Label>
            <Label className="taskRecordRangeLabel">
              消费时间
              <DateTimeRangePicker
                from={transactionQuery.createdFrom}
                fromPlaceholder="开始日期"
                to={transactionQuery.createdTo}
                toPlaceholder="结束日期"
                onChange={updateCreatedRange}
              />
            </Label>
            <Button type="button" variant="outline" size="sm" disabled={!hasActiveFilters} onClick={resetFilters}>
              <RotateCcw size={14} />
              重置
            </Button>
          </TableToolbar>

          {transactions.length ? (
            <Table className="shTableViewport walletTransactionTable" density="compact">
              <TableRow className="shTableHeader">
                <TableHead>消费时间</TableHead>
                <TableHead>模型</TableHead>
                <TableHead>平台</TableHead>
                <TableHead>类型</TableHead>
                <TableHead>API Key</TableHead>
                <TableHead>Token 消耗</TableHead>
                <TableHead>扣费</TableHead>
                <TableHead>余额变化</TableHead>
                <TableHead>任务</TableHead>
              </TableRow>
              {transactions.map((transaction) => (
                <WalletTransactionRecord key={transaction.id} transaction={transaction} />
              ))}
            </Table>
          ) : (
            <div className="emptyState">
              <strong>暂无消费记录</strong>
              <span>完成可计费任务后会生成消费流水。</span>
            </div>
          )}
          <TableFooter>
            <div className="shTableFooterGroup">
              <Label>
                每页
                <Select size="sm" value={String(transactionQuery.pageSize)} onChange={(event) => updatePageSize(event.target.value)}>
                  {pageSizeOptions.map((option) => (
                    <option key={option} value={option}>{option} 条</option>
                  ))}
                </Select>
              </Label>
              <span>共 {props.total} 条 · {pageStart}-{pageEnd}</span>
            </div>
            <form
              className="shTablePageJump"
              onSubmit={(event) => {
                event.preventDefault();
                submitPageJump();
              }}
            >
              <span>第 {currentPage} / {totalPages} 页</span>
              <span>跳至</span>
              <Input
                aria-label="跳转页码"
                inputMode="numeric"
                min={1}
                max={totalPages}
                size="xs"
                type="number"
                value={pageJump}
                onChange={(event) => setPageJump(event.target.value)}
              />
              <span>页</span>
              <Button type="submit" variant="outline" size="xs" disabled={totalPages <= 1}>跳转</Button>
            </form>
            <TablePageActions>
              <Button type="button" variant="outline" size="sm" disabled={currentPage <= 1} onClick={() => updatePage(Math.max(1, currentPage - 1))}>
                <ChevronLeft size={14} />
                上一页
              </Button>
              <Button type="button" variant="outline" size="sm" disabled={currentPage >= totalPages} onClick={() => updatePage(Math.min(totalPages, currentPage + 1))}>
                下一页
                <ChevronRight size={14} />
              </Button>
            </TablePageActions>
          </TableFooter>
        </TableViewportLayout>
      </CardContent>
    </Card>
  );
}

function WalletTransactionRecord(props: { transaction: GatewayWalletTransaction }) {
  const transaction = props.transaction;
  const metadata = transaction.metadata;
  const referenceId = transaction.referenceId || metadataString(transaction.metadata, 'taskId');
  const requestId = metadataString(metadata, 'requestId');
  const model = walletTransactionCalledModel(metadata);
  const resolvedModel = walletTransactionResolvedModel(metadata);
  const platform = metadataString(metadata, 'platformName') || metadataString(metadata, 'platformKey') || metadataString(metadata, 'provider') || metadataString(metadata, 'clientId');
  const provider = metadataString(metadata, 'provider');
  const taskType = metadataString(metadata, 'modelType') || metadataString(metadata, 'kind');
  const apiKey = metadataString(metadata, 'apiKeyName') || metadataString(metadata, 'apiKeyPrefix') || metadataString(metadata, 'apiKeyId');
  const chargeAmount = transactionChargeAmount(transaction);
  const chargeCurrency = transactionChargeCurrency(transaction);
  return (
    <TableRow>
      <TableCell>{formatDateTime(transaction.createdAt)}</TableCell>
      <TableCell className="walletTransactionModelCell">
        <span className="walletTransactionPrimaryCell">
          <strong>{model || '-'}</strong>
          {resolvedModel && resolvedModel !== model && <small>{resolvedModel}</small>}
        </span>
      </TableCell>
      <TableCell className="walletTransactionPlatformCell">
        <span className="walletTransactionPrimaryCell">
          <strong>{platform || '-'}</strong>
          {provider && provider !== platform && <small>{provider}</small>}
        </span>
      </TableCell>
      <TableCell>
        <span className="walletTransactionPrimaryCell">
          <strong>{taskType || '-'}</strong>
          <small>{transactionTypeLabel(transaction.transactionType)}</small>
        </span>
      </TableCell>
      <TableCell>{apiKey || '-'}</TableCell>
      <TableCell className="walletTransactionTokenCell">{formatTokenUsage(metadataObject(metadata, 'usage'))}</TableCell>
      <TableCell>
        <span className="walletTransactionAmount" data-direction={transaction.direction}>
          {transaction.direction === 'debit' ? '-' : '+'}{formatMoney(chargeAmount)} {chargeCurrency}
        </span>
      </TableCell>
      <TableCell>
        <span className="walletBalanceChange">
          <span>{formatMoney(transaction.balanceBefore)}</span>
          <span>{formatMoney(transaction.balanceAfter)}</span>
        </span>
      </TableCell>
      <TableCell>
        <span className="walletTransactionRef">
          {referenceId ? <code title={referenceId}>{referenceId}</code> : '-'}
          {requestId && <small title={requestId}>RequestID {requestId}</small>}
        </span>
      </TableCell>
    </TableRow>
  );
}

function ApiKeyPanel(props: {
  apiKeyForm: ApiKeyForm;
  apiKeySecret: string;
  apiKeySecretsById: Record<string, string>;
  apiKeyPolicyModels: PlatformModel[];
  data: ConsoleData;
  message: string;
  state: LoadState;
  onBatchAccessRules: (input: GatewayAccessRuleBatchRequest) => Promise<void>;
  onDeleteApiKey: (apiKeyId: string) => Promise<void>;
  onApiKeyFormChange: (value: ApiKeyForm) => void;
  onSaveApiKeyScopes: (apiKeyId: string, input: GatewayApiKeyScopeUpdateRequest) => Promise<void>;
  onSubmitApiKey: (event: FormEvent<HTMLFormElement>) => void | Promise<void>;
  onUseApiKeyForPlayground: (apiKeyId?: string) => void;
}) {
  const [createOpen, setCreateOpen] = useState(false);
  const [policyApiKeyId, setPolicyApiKeyId] = useState('');
  const [scopeApiKeyId, setScopeApiKeyId] = useState('');
  const [scopeDraft, setScopeDraft] = useState<string[]>([]);
  const [scopeError, setScopeError] = useState('');
  const [pendingDelete, setPendingDelete] = useState<GatewayApiKey | null>(null);
  const [localMessage, setLocalMessage] = useState('');
  const selectedPolicyKey = useMemo(
    () => props.data.apiKeys.find((item) => item.id === policyApiKeyId),
    [policyApiKeyId, props.data.apiKeys],
  );
  const selectedScopeKey = useMemo(
    () => props.data.apiKeys.find((item) => item.id === scopeApiKeyId),
    [scopeApiKeyId, props.data.apiKeys],
  );
  const permissionPlatforms = useMemo(() => platformsForPermissionTree(props.apiKeyPolicyModels), [props.apiKeyPolicyModels]);

  async function copyApiKey(item: GatewayApiKey) {
    const secret = apiKeySecretFor(item, props.apiKeySecretsById);
    if (!secret) return;
    await navigator.clipboard.writeText(secret);
    setLocalMessage(`已复制 ${item.name || item.keyPrefix}`);
  }

  async function submitCreate(event: FormEvent<HTMLFormElement>) {
    try {
      await props.onSubmitApiKey(event);
      setCreateOpen(false);
    } catch {
      return;
    }
  }

  async function confirmDeleteApiKey() {
    if (!pendingDelete) return;
    await props.onDeleteApiKey(pendingDelete.id);
    setPendingDelete(null);
  }

  function openScopeDialog(item: GatewayApiKey) {
    setScopeApiKeyId(item.id);
    setScopeDraft(normalizeApiKeyScopes(item.scopes));
    setScopeError('');
  }

  function closeScopeDialog() {
    setScopeApiKeyId('');
    setScopeDraft([]);
    setScopeError('');
  }

  async function submitScopes(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!selectedScopeKey) return;
    const scopes = normalizeApiKeyScopes(scopeDraft);
    if (!scopes.length) {
      setScopeError('至少保留一个能力范围。');
      return;
    }
    try {
      await props.onSaveApiKeyScopes(selectedScopeKey.id, { scopes });
      closeScopeDialog();
    } catch {
      return;
    }
  }

  return (
    <>
      <Card>
        <CardHeader>
          <div>
            <CardTitle>API Key</CardTitle>
            <p className="mutedText">按 Key 维护调用凭证、最近使用时间和平台/模型权限策略。</p>
          </div>
          <Button type="button" size="sm" onClick={() => setCreateOpen(true)}>
            <Plus size={15} />
            创建 API Key
          </Button>
        </CardHeader>
        <CardContent>
          {(localMessage || props.message) && <p className="formMessage">{localMessage || props.message}</p>}
          <Table className="apiKeyTable">
            <TableRow className="shTableHeader">
              <TableHead>名称</TableHead>
              <TableHead>API Key</TableHead>
              <TableHead>能力范围</TableHead>
              <TableHead>权限策略</TableHead>
              <TableHead>最近使用</TableHead>
              <TableHead>有效期</TableHead>
              <TableHead>状态</TableHead>
              <TableHead>创建时间</TableHead>
              <TableHead>操作</TableHead>
            </TableRow>
            {props.data.apiKeys.length ? props.data.apiKeys.map((item) => {
              const secret = apiKeySecretFor(item, props.apiKeySecretsById);
              const summary = countAccessPermissionRules(props.data.accessRules, 'api_key', item.id);
              return (
                <TableRow key={item.id}>
                  <TableCell>
                    <span className="apiKeyNameCell">
                      <strong>{item.name}</strong>
                      <small>{item.keyPrefix}</small>
                    </span>
                  </TableCell>
                  <TableCell>
                    <span className="apiKeySecretCell">
                      <code>{secret ? maskApiKey(secret) : item.keyPrefix}</code>
                      <Button
                        type="button"
                        variant="ghost"
                        size="icon"
                        title={secret ? '复制 API Key' : '暂无可复制的完整 Key'}
                        disabled={!secret}
                        onClick={() => void copyApiKey(item)}
                      >
                        <Copy size={14} />
                      </Button>
                    </span>
                  </TableCell>
                  <TableCell>
                    <span className="apiKeyScopeCell">
                      <ApiKeyScopeBadges scopes={item.scopes} />
                      <Button type="button" variant="ghost" size="icon" title="维护能力范围" onClick={() => openScopeDialog(item)}>
                        <SlidersHorizontal size={14} />
                      </Button>
                    </span>
                  </TableCell>
                  <TableCell>
                    <button type="button" className="apiKeyPolicyButton" onClick={() => setPolicyApiKeyId(item.id)}>
                      <ShieldCheck size={14} />
                      <span>{permissionSummaryText(summary)}</span>
                    </button>
                  </TableCell>
                  <TableCell>{formatDateTime(item.lastUsedAt)}</TableCell>
                  <TableCell>{formatDateTime(item.expiresAt)}</TableCell>
                  <TableCell><Badge variant={item.status === 'active' ? 'success' : 'secondary'}>{item.status}</Badge></TableCell>
                  <TableCell>{formatDateTime(item.createdAt)}</TableCell>
                  <TableCell>
                    <Button type="button" variant="ghost" size="icon" title="删除 API Key" onClick={() => setPendingDelete(item)}>
                      <Trash2 size={14} />
                    </Button>
                  </TableCell>
                </TableRow>
              );
            }) : (
              <div className="emptyState">
                <strong>暂无 API Key</strong>
                <span>点击右上角创建调用凭证。</span>
              </div>
            )}
          </Table>
        </CardContent>
      </Card>

      <FormDialog
        ariaLabel="创建 API Key"
        bodyClassName="apiKeyCreateDialogBody"
        footer={(
          <>
            <Button type="button" variant="outline" size="sm" disabled={props.state === 'loading'} onClick={() => setCreateOpen(false)}>取消</Button>
            <Button type="submit" size="sm" disabled={props.state === 'loading'}>
              <KeyRound size={15} />
              确认创建
            </Button>
          </>
        )}
        open={createOpen}
        title="创建 API Key"
        onClose={() => setCreateOpen(false)}
        onSubmit={(event) => void submitCreate(event)}
      >
        <Label>
          名称
          <Input value={props.apiKeyForm.name} placeholder="例如：生产调用 Key" onChange={(event) => props.onApiKeyFormChange({ ...props.apiKeyForm, name: event.target.value })} />
        </Label>
        <Label>
          有效期
          <DateTimePicker
            value={props.apiKeyForm.expiresAt}
            placeholder="不设置则长期有效"
            onChange={(expiresAt) => props.onApiKeyFormChange({ ...props.apiKeyForm, expiresAt })}
          />
        </Label>
      </FormDialog>

      <FormDialog
        ariaLabel="维护 API Key 能力范围"
        bodyClassName="apiKeyScopeDialogBody"
        footer={(
          <>
            <Button type="button" variant="outline" size="sm" disabled={props.state === 'loading'} onClick={closeScopeDialog}>取消</Button>
            <Button type="submit" size="sm" disabled={props.state === 'loading' || normalizeApiKeyScopes(scopeDraft).length === 0}>
              <SlidersHorizontal size={15} />
              保存范围
            </Button>
          </>
        )}
        open={Boolean(selectedScopeKey)}
        title={selectedScopeKey ? `能力范围：${selectedScopeKey.name}` : '能力范围'}
        onClose={closeScopeDialog}
        onSubmit={(event) => void submitScopes(event)}
      >
        {scopeError && <p className="formMessage error">{scopeError}</p>}
        <APIKeyScopeEditor value={scopeDraft} onChange={(scopes) => {
          setScopeDraft(scopes);
          if (scopeError) setScopeError('');
        }} />
      </FormDialog>

      <FormDialog
        ariaLabel="维护 API Key 权限策略"
        bodyClassName="apiKeyPolicyDialogBody"
        className="apiKeyPolicyDialog"
        footer={<Button type="button" size="sm" onClick={() => setPolicyApiKeyId('')}>关闭</Button>}
        open={Boolean(selectedPolicyKey)}
        title={selectedPolicyKey ? `权限策略：${selectedPolicyKey.name}` : '权限策略'}
        onClose={() => setPolicyApiKeyId('')}
        onSubmit={(event) => event.preventDefault()}
      >
        <AccessPermissionEditor
          accessRules={props.data.accessRules}
          metadataMode="api_key_permission_tree"
          platformModels={props.apiKeyPolicyModels}
          platforms={permissionPlatforms}
          state={props.state}
          subjectId={selectedPolicyKey?.id ?? ''}
          subjectType="api_key"
          onBatchAccessRules={props.onBatchAccessRules}
        />
      </FormDialog>

      <ConfirmDialog
        confirmLabel="删除 API Key"
        description="删除后该 Key 将无法继续调用，关联的平台/模型权限策略会同步删除。"
        loading={props.state === 'loading'}
        open={Boolean(pendingDelete)}
        title={`确认删除 ${pendingDelete?.name ?? ''}？`}
        onCancel={() => setPendingDelete(null)}
        onConfirm={confirmDeleteApiKey}
      />
    </>
  );
}

function APIKeyScopeEditor(props: {
  value: string[];
  onChange: (value: string[]) => void;
}) {
  const scopes = normalizeApiKeyScopes(props.value);
  const customScopes = scopes.filter((scope) => !knownApiKeyScopes.has(scope as (typeof apiKeyScopeOptions)[number]['value']));
  const customValue = customScopes.join(', ');

  function setKnownScope(scope: string, checked: boolean) {
    const next = new Set(scopes);
    if (checked) {
      if (scope === 'all') {
        for (const option of apiKeyScopeOptions) {
          next.delete(option.value);
        }
      } else {
        next.delete('all');
      }
      next.add(scope);
    } else {
      next.delete(scope);
    }
    props.onChange(Array.from(next));
  }

  function setCustomScopes(value: string) {
    const known = scopes.filter((scope) => knownApiKeyScopes.has(scope as (typeof apiKeyScopeOptions)[number]['value']));
    props.onChange(normalizeApiKeyScopes([...known, ...value.split(',')]));
  }

  return (
    <div className="apiKeyScopeEditor">
      <div className="apiKeyScopeOptionGrid">
        {apiKeyScopeOptions.map((option) => (
          <label key={option.value} className="apiKeyScopeOption">
            <Checkbox
              checked={scopes.includes(option.value)}
              onCheckedChange={(checked) => setKnownScope(option.value, checked === true)}
            />
            <span>
              <strong>{option.label}</strong>
              <small>{option.description}</small>
            </span>
          </label>
        ))}
      </div>
      <Label>
        自定义 scope
        <Input
          value={customValue}
          placeholder="例如：text_to_speech, audio_generate"
          onChange={(event) => setCustomScopes(event.target.value)}
        />
      </Label>
    </div>
  );
}

function TaskPanel(props: {
  data: ConsoleData;
  query: WorkspaceTaskQuery;
  token: string;
  total: number;
  onQueryChange: (value: WorkspaceTaskQuery) => void;
}) {
  const [localMessage, setLocalMessage] = useState('');
  const [jsonTask, setJsonTask] = useState<GatewayTask | null>(null);
  const [pageJump, setPageJump] = useState(String(props.query.page));
  const taskQuery = props.query;
  const tasks = props.data.tasks;
  const taskTypes = useMemo(() => {
    const knownTypes = ['text_generate', 'image_generate', 'image_edit', 'video_generate', 'image_to_video'];
    const values = [...knownTypes, taskQuery.modelType, ...tasks.map((task) => task.modelType)];
    return Array.from(new Set(values.filter((value): value is string => Boolean(value)))).sort((a, b) => a.localeCompare(b));
  }, [taskQuery.modelType, tasks]);
  const pageSizeOptions = useMemo(() => {
    return Array.from(new Set([...taskPageSizeOptions, taskQuery.pageSize])).sort((a, b) => a - b);
  }, [taskQuery.pageSize]);
  const totalPages = Math.max(1, Math.ceil(props.total / taskQuery.pageSize));
  const currentPage = Math.min(taskQuery.page, totalPages);
  const pageStart = props.total ? Math.min((currentPage - 1) * taskQuery.pageSize + 1, props.total) : 0;
  const pageEnd = Math.min(currentPage * taskQuery.pageSize, props.total);
  const hasActiveFilters = Boolean(taskQuery.query || taskQuery.createdFrom || taskQuery.createdTo || taskQuery.modelType);

  useEffect(() => {
    if (taskQuery.page > totalPages) {
      props.onQueryChange({ ...taskQuery, page: totalPages });
    }
  }, [props.onQueryChange, taskQuery, totalPages]);

  useEffect(() => {
    setPageJump(String(currentPage));
  }, [currentPage]);

  async function copyTaskRequestId(task: GatewayTask) {
    if (!task.requestId) return;
    await navigator.clipboard.writeText(task.requestId);
    setLocalMessage(`已复制 RequestID：${task.requestId}`);
  }

  function resetFilters() {
    props.onQueryChange({
      query: '',
      modelType: '',
      createdFrom: '',
      createdTo: '',
      page: 1,
      pageSize: taskQuery.pageSize,
    });
  }

  function updateQuery(value: string) {
    props.onQueryChange({ ...taskQuery, query: value, page: 1 });
  }

  function updateTypeFilter(value: string) {
    props.onQueryChange({ ...taskQuery, modelType: value === 'all' ? '' : value, page: 1 });
  }

  function updateCreatedRange(value: { from: string; to: string }) {
    props.onQueryChange({ ...taskQuery, createdFrom: value.from, createdTo: value.to, page: 1 });
  }

  function updatePageSize(value: string) {
    const nextPageSize = Number(value);
    props.onQueryChange({ ...taskQuery, page: 1, pageSize: Number.isFinite(nextPageSize) ? nextPageSize : 10 });
  }

  function updatePage(page: number) {
    props.onQueryChange({ ...taskQuery, page });
  }

  function submitPageJump() {
    const parsed = Number(pageJump);
    if (!Number.isFinite(parsed) || parsed <= 0) {
      setPageJump(String(currentPage));
      return;
    }
    updatePage(Math.min(totalPages, Math.max(1, Math.floor(parsed))));
  }

  return (
    <>
      <Card className="shTableViewportCard taskRecordViewport">
        <CardContent className="shTableViewportPanel">
          {localMessage && <p className="formMessage">{localMessage}</p>}
          <TableViewportLayout>
            <TableToolbar className="taskRecordFilters">
              <Label className="taskRecordSearchLabel">
                搜索
                <span className="taskRecordSearchBox">
                  <Search size={15} />
                  <Input
                    value={taskQuery.query}
                    placeholder="搜索 ID / RequestID / 模型 / API Key"
                    onChange={(event) => updateQuery(event.target.value)}
                  />
                </span>
              </Label>
              <Label>
                类型
                <Select value={taskQuery.modelType || 'all'} onChange={(event) => updateTypeFilter(event.target.value)}>
                  <option value="all">全部类型</option>
                  {taskTypes.map((type) => (
                    <option key={type} value={type}>{type}</option>
                  ))}
                </Select>
              </Label>
              <Label className="taskRecordRangeLabel">
                创建时间
                <DateTimeRangePicker
                  from={taskQuery.createdFrom}
                  fromPlaceholder="开始日期"
                  to={taskQuery.createdTo}
                  toPlaceholder="结束日期"
                  onChange={updateCreatedRange}
                />
              </Label>
              <Button type="button" variant="outline" size="sm" disabled={!hasActiveFilters} onClick={resetFilters}>
                <RotateCcw size={14} />
                重置
              </Button>
            </TableToolbar>

            {tasks.length ? (
              <Table className="shTableViewport taskRecordTable" density="compact">
                <TableRow className="shTableHeader">
                  <TableHead>任务</TableHead>
                  <TableHead>RequestID</TableHead>
                  <TableHead>状态</TableHead>
                  <TableHead>模型</TableHead>
                  <TableHead>尝试链路</TableHead>
                  <TableHead>参数转换</TableHead>
                  <TableHead>类型</TableHead>
                  <TableHead>API Key</TableHead>
                  <TableHead>Token</TableHead>
                  <TableHead>扣费</TableHead>
                  <TableHead>耗时</TableHead>
                  <TableHead>创建时间</TableHead>
                  <TableHead>原始 JSON</TableHead>
                </TableRow>
                {tasks.map((task) => (
                  <TaskRecord key={task.id} task={task} token={props.token} onCopyRequestId={copyTaskRequestId} onOpenJson={setJsonTask} />
                ))}
              </Table>
            ) : (
              <div className="emptyState">
                <strong>{hasActiveFilters ? '没有匹配的任务' : '暂无任务'}</strong>
                {hasActiveFilters && <span>调整关键词、类型或创建时间后再试。</span>}
              </div>
            )}

            <TableFooter>
              <div className="shTableFooterGroup">
                <Label>
                  每页
                  <Select size="sm" value={String(taskQuery.pageSize)} onChange={(event) => updatePageSize(event.target.value)}>
                    {pageSizeOptions.map((option) => (
                      <option key={option} value={option}>{option} 条</option>
                    ))}
                  </Select>
                </Label>
                <span>共 {props.total} 条 · {pageStart}-{pageEnd}</span>
              </div>
              <form
                className="shTablePageJump"
                onSubmit={(event) => {
                  event.preventDefault();
                  submitPageJump();
                }}
              >
                <span>第 {currentPage} / {totalPages} 页</span>
                <span>跳至</span>
                <Input
                  aria-label="跳转页码"
                  inputMode="numeric"
                  min={1}
                  max={totalPages}
                  size="xs"
                  type="number"
                  value={pageJump}
                  onChange={(event) => setPageJump(event.target.value)}
                />
                <span>页</span>
                <Button type="submit" variant="outline" size="xs" disabled={totalPages <= 1}>跳转</Button>
              </form>
              <TablePageActions>
                <Button type="button" variant="outline" size="sm" disabled={currentPage <= 1} onClick={() => updatePage(Math.max(1, currentPage - 1))}>
                  <ChevronLeft size={14} />
                  上一页
                </Button>
                <Button type="button" variant="outline" size="sm" disabled={currentPage >= totalPages} onClick={() => updatePage(Math.min(totalPages, currentPage + 1))}>
                  下一页
                  <ChevronRight size={14} />
                </Button>
              </TablePageActions>
            </TableFooter>
          </TableViewportLayout>
        </CardContent>
      </Card>

      <FormDialog
        ariaLabel="查看任务原始 JSON"
        bodyClassName="taskJsonDialogBody"
        className="taskJsonDialog"
        footer={<Button type="button" size="sm" onClick={() => setJsonTask(null)}>关闭</Button>}
        open={Boolean(jsonTask)}
        title="任务原始 JSON"
        onClose={() => setJsonTask(null)}
        onSubmit={(event) => event.preventDefault()}
      >
        <pre className="taskJsonPreview">{JSON.stringify(jsonTask, null, 2)}</pre>
      </FormDialog>
    </>
  );
}

function TaskRecord(props: { task: GatewayTask; token: string; onCopyRequestId: (task: GatewayTask) => Promise<void>; onOpenJson: (task: GatewayTask) => void }) {
  const usage = props.task.usage ?? {};
  const tokenUsage = formatTokenUsage(usage);
  const chargeText = props.task.finalChargeAmount !== undefined ? formatCellValue(props.task.finalChargeAmount) : '-';
  const calledModel = taskCalledModelLabel(props.task);
  const resolvedModel = taskResolvedModelLabel(props.task);
  const badgeVariant = props.task.status === 'succeeded' ? 'success' : props.task.status === 'failed' ? 'destructive' : 'secondary';
  return (
    <TableRow>
      <TableCell>
        <span className="taskRecordPrimaryCell taskRecordIdentityCell">
          <strong>{props.task.kind}</strong>
          <span className="taskRecordIdLine">
            <span>ID</span>
            <code title={props.task.id}>{props.task.id}</code>
          </span>
        </span>
      </TableCell>
      <TableCell>
        <span className="taskRecordRequestLine">
          <code title={props.task.requestId || '-'}>{props.task.requestId || '-'}</code>
          <Button
            type="button"
            variant="ghost"
            size="icon"
            title={props.task.requestId ? '复制 RequestID' : '暂无 RequestID'}
            disabled={!props.task.requestId}
            onClick={() => void props.onCopyRequestId(props.task)}
          >
            <Copy size={14} />
          </Button>
        </span>
      </TableCell>
      <TableCell><Badge variant={badgeVariant}>{props.task.status}</Badge></TableCell>
      <TableCell className="taskRecordModelCell">
        <span className="taskRecordPrimaryCell">
          <strong>{calledModel || '-'}</strong>
          {resolvedModel && resolvedModel !== calledModel && <small>{resolvedModel}</small>}
        </span>
      </TableCell>
      <TableCell className="taskRecordAttemptCell">
        <TaskAttemptChain task={props.task} />
      </TableCell>
      <TableCell className="taskRecordParamCell">
        <TaskParamConversionCell task={props.task} token={props.token} />
      </TableCell>
      <TableCell>{props.task.modelType || '-'}</TableCell>
      <TableCell>{props.task.apiKeyName || props.task.apiKeyPrefix || props.task.apiKeyId || '-'}</TableCell>
      <TableCell className="taskRecordTokenCell">{tokenUsage}</TableCell>
      <TableCell>{chargeText}</TableCell>
      <TableCell>{formatDuration(taskDurationMs(props.task))}</TableCell>
      <TableCell>{formatDateTime(props.task.createdAt)}</TableCell>
      <TableCell>
        <Button type="button" variant="ghost" size="sm" className="taskRecordJsonButton" title={taskErrorText(props.task) || '查看原始 JSON'} onClick={() => props.onOpenJson(props.task)}>
          <Eye size={14} />
          原始 JSON
        </Button>
      </TableCell>
    </TableRow>
  );
}

type TaskParamConversionSummary = {
  changed: boolean;
  changeCount: number;
  actions: string[];
  paths: string[];
  capabilityPaths: string[];
};

function TaskParamConversionCell(props: { task: GatewayTask; token: string }) {
  const summary = taskParamConversionSummary(props.task);
  const [open, setOpen] = useState(false);
  const [logs, setLogs] = useState<GatewayTaskParamPreprocessingLog[] | null>(null);
  const [loadState, setLoadState] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle');
  const [error, setError] = useState('');
  const logsRef = useRef<GatewayTaskParamPreprocessingLog[] | null>(null);
  const loadingRef = useRef(false);

  useEffect(() => {
    logsRef.current = null;
    loadingRef.current = false;
    setLogs(null);
    setLoadState('idle');
    setError('');
  }, [props.task.id]);

  useEffect(() => {
    if (!open || !summary.changed || logsRef.current || loadingRef.current || !props.token) return;
    let cancelled = false;
    loadingRef.current = true;
    setLoadState('loading');
    setError('');
    listTaskParamPreprocessing(props.token, props.task.id)
      .then((response) => {
        if (cancelled) return;
        const items = response.items ?? [];
        logsRef.current = items;
        setLogs(items);
        setLoadState('ready');
      })
      .catch((err) => {
        if (cancelled) return;
        setError(err instanceof Error ? err.message : '参数转换明细加载失败');
        setLoadState('error');
      })
      .finally(() => {
        if (!cancelled) {
          loadingRef.current = false;
        }
      });
    return () => {
      cancelled = true;
      loadingRef.current = false;
    };
  }, [open, props.task.id, props.token, summary.changed]);

  if (!summary.changed) {
    return <span className="taskParamConversionEmpty">无转换</span>;
  }

  return (
    <AntPopover
      align={{ offset: [0, 8] }}
      content={<TaskParamConversionPopover error={error} loadState={loadState} logs={logs} summary={summary} />}
      overlayClassName="taskParamConversionAntPopover"
      placement="bottomLeft"
      trigger={['hover', 'focus']}
      onOpenChange={setOpen}
    >
      <button className="taskParamConversionTrigger" type="button" aria-label={`参数转换 ${summary.changeCount} 项`}>
        <SlidersHorizontal size={14} />
        <span>{summary.changeCount || '有'} 项</span>
      </button>
    </AntPopover>
  );
}

function TaskParamConversionPopover(props: {
  error: string;
  loadState: 'idle' | 'loading' | 'ready' | 'error';
  logs: GatewayTaskParamPreprocessingLog[] | null;
  summary: TaskParamConversionSummary;
}) {
  const logs = props.logs ?? [];
  return (
    <span className="taskParamConversionPopover" role="tooltip">
      <span className="taskParamConversionPopoverHeader">
        <strong>参数转换汇总</strong>
        <small>{taskParamSummaryText(props.summary)}</small>
      </span>
      {props.loadState === 'loading' && <span className="taskParamConversionState">正在加载转换明细...</span>}
      {props.loadState === 'error' && <span className="taskParamConversionState error">{props.error || '参数转换明细加载失败'}</span>}
      {props.loadState === 'ready' && logs.length === 0 && <span className="taskParamConversionState">暂无转换明细。</span>}
      {logs.map((log) => (
        <span key={log.id} className="taskParamConversionLog">
          <span className="taskParamConversionLogHeader">
            <span className="taskParamConversionLogTitleBlock">
              <strong>{taskParamLogTitle(log)}</strong>
              <small title={log.clientId || undefined}>{taskParamLogSubtitle(log)}</small>
            </span>
            <Badge className="taskParamConversionCountBadge" variant={log.changed ? 'secondary' : 'outline'}>{log.changeCount} 项</Badge>
          </span>
          {(log.changes ?? []).slice(0, 8).map((change, index) => (
            <span key={`${log.id}-change-${index}`} className="taskParamConversionChange">
              <span className="taskParamConversionChangeTop">
                <Badge variant="outline">{taskParamActionLabel(objectString(change, 'action'))}</Badge>
                <strong>{objectString(change, 'path') || '-'}</strong>
              </span>
              <span>{objectString(change, 'reason') || '按模型能力配置调整参数。'}</span>
              {objectString(change, 'capabilityPath') && <small>能力配置：{objectString(change, 'capabilityPath')}</small>}
              <code>{taskParamChangePreview(change)}</code>
            </span>
          ))}
          {(log.changes?.length ?? 0) > 8 && <small>还有 {(log.changes?.length ?? 0) - 8} 项转换未展开。</small>}
        </span>
      ))}
      {props.loadState !== 'ready' && props.summary.capabilityPaths.length > 0 && (
        <span className="taskParamConversionSummaryPaths">
          {props.summary.capabilityPaths.slice(0, 4).map((path) => <code key={path}>{path}</code>)}
        </span>
      )}
    </span>
  );
}

function TaskAttemptChain(props: { task: GatewayTask }) {
  const attempts = props.task.attempts ?? [];
  if (!attempts.length) return <span>-</span>;

  return (
    <AntPopover
      align={{ offset: [0, 8] }}
      content={<TaskAttemptPopoverContent task={props.task} />}
      overlayClassName="taskRecordAttemptAntPopover"
      placement="bottomLeft"
      trigger={['hover', 'focus']}
    >
      <button className="taskRecordAttemptCount" type="button" aria-label={attempts.map(taskAttemptTitle).join('\n')}>
        {attempts.length} 次尝试
      </button>
    </AntPopover>
  );
}

function TaskAttemptPopoverContent(props: { task: GatewayTask }) {
  const attempts = props.task.attempts ?? [];
  return (
    <span className="taskRecordAttemptPopover" role="tooltip">
      {attempts.map((attempt) => {
        const trace = taskAttemptTrace(attempt);
        const rateLimitText = taskAttemptRateLimitText(attempt);
        return (
          <span
            key={attempt.id || `${props.task.id}-${attempt.attemptNo}`}
            className={`taskRecordAttemptDetail ${attempt.status === 'failed' ? 'failed' : attempt.status === 'succeeded' ? 'succeeded' : ''}`}
          >
            <span className="taskRecordAttemptDetailHeader">
              <strong>#{attempt.attemptNo} {taskAttemptTarget(attempt)}</strong>
              <Badge variant={attempt.status === 'succeeded' ? 'success' : attempt.status === 'failed' ? 'destructive' : 'secondary'}>{taskAttemptStatusText(attempt.status)}</Badge>
            </span>
            <small>{taskAttemptMeta(attempt)}</small>
            {attempt.status === 'failed' && <span className="taskRecordAttemptError">{taskAttemptFailureReason(attempt)}</span>}
            {(rateLimitText || trace.length > 0) && (
              <span className="taskRecordAttemptTrace">
                {rateLimitText && <span className="taskRecordAttemptTraceItem">{rateLimitText}</span>}
                {trace.map((entry, index) => (
                  <span key={`${attempt.id || attempt.attemptNo}-trace-${index}`} className="taskRecordAttemptTraceItem">
                    {taskAttemptTraceText(entry)}
                  </span>
                ))}
              </span>
            )}
          </span>
        );
      })}
    </span>
  );
}

function taskAttemptTitle(attempt: NonNullable<GatewayTask['attempts']>[number]) {
  const parts = [
    `#${attempt.attemptNo}`,
    attempt.platformName || attempt.provider || attempt.clientId || '',
    attempt.status,
    attempt.errorMessage || attempt.errorCode || metadataString(attempt.metrics, 'error') || '',
  ].filter(Boolean);
  return parts.join(' · ');
}

function taskAttemptTarget(attempt: NonNullable<GatewayTask['attempts']>[number]) {
  return attempt.platformName || attempt.provider || attempt.clientId || `尝试 ${attempt.attemptNo}`;
}

function taskAttemptStatusText(status: string) {
  if (status === 'succeeded') return '成功';
  if (status === 'failed') return '失败';
  if (status === 'running') return '运行中';
  return status || '-';
}

function taskAttemptMeta(attempt: NonNullable<GatewayTask['attempts']>[number]) {
  const statusCode = taskAttemptStatusCode(attempt);
  const values = [
    attempt.providerModelName || attempt.modelName || attempt.modelAlias,
    taskAttemptPriorityText(attempt),
    attempt.requestId ? `RequestID ${attempt.requestId}` : '',
    statusCode ? `状态码 ${statusCode}` : '',
    formatDuration(attemptDurationMs(attempt)),
  ].filter(Boolean);
  return values.join(' · ') || attempt.clientId || '-';
}

function taskAttemptFailureReason(attempt: NonNullable<GatewayTask['attempts']>[number]) {
  const detail = firstText(
    attempt.errorMessage,
    metadataString(attempt.metrics, 'error'),
    metadataString(attempt.metrics, 'message'),
  );
  const code = firstText(attempt.errorCode, metadataString(attempt.metrics, 'errorCode'));
  const statusCode = taskAttemptStatusCode(attempt);
  const statusText = statusCode ? `状态码 ${statusCode}` : '';
  const category = taskAttemptFailureCategory(attempt);
  const categoryText = category ? `错误分类 ${category}` : '';
  if (detail && code && detail !== code) return [detail, code, statusText, categoryText].filter(Boolean).join(' · ');
  return [detail || code || '失败', statusText, categoryText].filter(Boolean).join(' · ');
}

function taskAttemptStatusCode(attempt: NonNullable<GatewayTask['attempts']>[number]) {
  const value = attempt.statusCode ?? metadataNumber(attempt.metrics, 'statusCode');
  return value && Number.isFinite(value) ? Math.trunc(value) : null;
}

function taskAttemptTrace(attempt: NonNullable<GatewayTask['attempts']>[number]) {
  const raw = attempt.metrics?.trace;
  if (!Array.isArray(raw)) return [];
  return raw.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === 'object' && !Array.isArray(item));
}

function taskAttemptRateLimitText(attempt: NonNullable<GatewayTask['attempts']>[number]) {
  const detail = metadataObject(attempt.metrics, 'rateLimit');
  if (!Object.keys(detail).length) return '';
  const scopeName = objectString(detail, 'scopeName') || objectString(detail, 'scopeKey') || '限流对象';
  const metric = objectString(detail, 'metric') || 'rate_limit';
  const current = metadataNumber(detail, 'current');
  const amount = metadataNumber(detail, 'amount');
  const projected = metadataNumber(detail, 'projected');
  const limit = metadataNumber(detail, 'limit');
  const windowSeconds = metadataNumber(detail, 'windowSeconds');
  const retryAfterMs = metadataNumber(detail, 'retryAfterMs');
  const values = [
    `限流 ${scopeName} · ${metric}`,
    current !== null ? `当前 ${formatCellValue(current)}` : '',
    amount !== null ? `本次 ${formatCellValue(amount)}` : '',
    projected !== null ? `预计 ${formatCellValue(projected)}` : '',
    limit !== null ? `限制 ${formatCellValue(limit)}` : '',
    windowSeconds !== null ? `窗口 ${Math.trunc(windowSeconds)} 秒` : '',
    retryAfterMs !== null ? `约 ${formatDuration(Math.trunc(retryAfterMs))} 后可重试` : '',
  ].filter(Boolean);
  return values.join(' · ');
}

function taskAttemptTraceText(entry: Record<string, unknown>) {
  const event = objectString(entry, 'event');
  const action = objectString(entry, 'action');
  const reason = objectString(entry, 'reason');
  const statusCode = metadataNumber(entry, 'statusCode');
  const errorCode = objectString(entry, 'errorCode');
  const category = objectString(entry, 'category');
  const policy = taskAttemptTracePolicy(entry);
  const priority = metadataPriorityText(entry);
  const values = [
    taskAttemptTraceEventLabel(event, action),
    priority,
    statusCode ? `状态码 ${Math.trunc(statusCode)}` : '',
    errorCode ? `错误 ${errorCode}` : '',
    category ? `错误分类 ${category}` : '',
    policy,
    reason ? `原因 ${taskAttemptTraceReasonLabel(reason)}` : '',
  ].filter(Boolean);
  return values.join(' · ');
}

function taskAttemptPriorityText(attempt: NonNullable<GatewayTask['attempts']>[number]) {
  return metadataPriorityText(attempt.metrics);
}

function metadataPriorityText(metadata: Record<string, unknown> | undefined) {
  const priority = metadataNumber(metadata, 'currentPriority') ?? metadataNumber(metadata, 'platformPriority');
  return priority !== null ? `优先级 ${formatCellValue(priority)}` : '';
}

function taskAttemptFailureCategory(attempt: NonNullable<GatewayTask['attempts']>[number]) {
  const category = firstText(
    metadataString(attempt.metrics, 'errorCategory'),
    metadataString(attempt.metrics, 'category'),
  );
  if (category) return category;
  for (const entry of taskAttemptTrace(attempt)) {
    const traceCategory = objectString(entry, 'category');
    if (traceCategory) return traceCategory;
  }
  return '';
}

function taskAttemptTracePolicy(entry: Record<string, unknown>) {
  const source = objectString(entry, 'policySource');
  const policy = objectString(entry, 'policy');
  const rule = objectString(entry, 'policyRule');
  const matchedValue = objectString(entry, 'matchedValue');
  const sourceLabel = source || policy;
  const policyPath = [sourceLabel, rule].filter(Boolean).join('.');
  if (!policyPath) return '';
  return matchedValue ? `策略 ${policyPath}=${matchedValue}` : `策略 ${policyPath}`;
}

function taskAttemptTraceEventLabel(event: string, action: string) {
  if (event === 'failure') return '失败';
  if (event === 'same_client_retry') return action === 'retry' ? '本平台重试' : '本平台停止重试';
  if (event === 'failover_next') return '切换下个平台';
  if (event === 'failover_stop') return '停止切换平台';
  if (event === 'priority_demoted') return '优先级降级';
  return event || action || '链路事件';
}

function taskAttemptTraceReasonLabel(reason: string) {
  const labels: Record<string, string> = {
    client_call_failed: '客户端调用失败',
    retry_disabled: '模型重试关闭',
    retry_deny_policy: '命中模型拒绝重试规则',
    retry_allow_policy: '命中模型允许重试规则',
    client_retryable: '客户端标记可重试',
    client_non_retryable: '客户端标记不可重试',
    same_client_max_attempts: '达到本平台最大尝试次数',
    request_validation_failed: '请求校验失败',
    candidate_selection_failed: '候选模型选择失败',
    parameter_preprocessing_failed: '参数预处理失败',
    wallet_balance_check_failed: '余额校验失败',
    local_rate_limit_blocked: '本地限流拦截',
    pre_provider_failed: '调用上游前失败',
    local_rate_limit_wait_queue: '本地限流排队等待',
    failover_time_budget_exceeded: '超过全局切换时间预算',
    runner_policy_disabled: '全局调度策略停用',
    hard_stop_policy: '命中硬拒绝规则',
    failover_disabled: '平台切换关闭',
    failover_deny_policy: '命中拒绝切换规则',
    failover_allow_policy: '命中允许切换规则',
    max_platforms_reached: '达到最大平台数',
    no_next_platform: '没有更多候选平台',
    priority_demote_policy: '命中优先级降级规则',
  };
  return labels[reason] ?? reason;
}

function taskParamConversionSummary(task: GatewayTask): TaskParamConversionSummary {
  const summary: TaskParamConversionSummary = {
    changed: false,
    changeCount: 0,
    actions: [],
    paths: [],
    capabilityPaths: [],
  };
  let mergedAttemptSummary = false;
  for (const attempt of task.attempts ?? []) {
    const attemptSummary = metadataObject(attempt.metrics, 'parameterPreprocessingSummary');
    if (Object.keys(attemptSummary).length) {
      mergedAttemptSummary = true;
      mergeTaskParamSummary(summary, attemptSummary);
    }
  }
  if (!mergedAttemptSummary) {
    mergeTaskParamSummary(summary, metadataObject(task.metrics, 'parameterPreprocessingSummary'));
  }
  return summary;
}

function mergeTaskParamSummary(target: TaskParamConversionSummary, raw: Record<string, unknown>) {
  if (!Object.keys(raw).length) return;
  target.changed = target.changed || raw.changed === true;
  const changeCount = metadataNumber(raw, 'changeCount');
  if (changeCount) target.changeCount += Math.max(0, Math.trunc(changeCount));
  for (const action of metadataStringList(raw, 'actions')) appendUniqueText(target.actions, action);
  for (const path of metadataStringList(raw, 'paths')) appendUniqueText(target.paths, path);
  for (const path of metadataStringList(raw, 'capabilityPaths')) appendUniqueText(target.capabilityPaths, path);
}

function taskParamSummaryText(summary: TaskParamConversionSummary) {
  const actionText = summary.actions.map(taskParamActionLabel).join('、');
  const parts = [
    `${summary.changeCount || summary.paths.length || 1} 项转换`,
    actionText ? `动作 ${actionText}` : '',
    summary.capabilityPaths.length ? `涉及 ${summary.capabilityPaths.length} 项能力配置` : '',
  ].filter(Boolean);
  return parts.join(' · ');
}

function taskParamLogTitle(log: GatewayTaskParamPreprocessingLog) {
  const parts = [
    log.attemptNo ? `#${log.attemptNo}` : '',
    log.modelType || '',
  ].filter(Boolean);
  return parts.join(' · ') || '预处理记录';
}

function taskParamLogSubtitle(log: GatewayTaskParamPreprocessingLog) {
  const model = log.model ?? {};
  const modelLabel = firstNonEmptyText(
    objectString(model, 'modelAlias'),
    objectString(model, 'providerModelName'),
    objectString(model, 'modelName'),
  );
  const provider = objectString(model, 'provider');
  const parts = [
    provider,
    modelLabel,
  ].filter(Boolean);
  if (parts.length) return parts.join(' · ');
  return log.clientId || '候选模型';
}

function firstNonEmptyText(...values: string[]) {
  return values.find((value) => value.trim()) ?? '';
}

function taskParamActionLabel(action: string) {
  if (action === 'remove') return '移除';
  if (action === 'adjust') return '调整';
  if (action === 'set') return '补齐';
  return action || '转换';
}

function taskParamChangePreview(change: Record<string, unknown>) {
  const before = previewCompactValue(change.before);
  const after = previewCompactValue(change.after);
  const action = objectString(change, 'action');
  if (action === 'remove') return `原值 ${before}`;
  if (action === 'set') return `新值 ${after}`;
  return `原值 ${before} -> 新值 ${after}`;
}

function previewCompactValue(value: unknown) {
  if (value === undefined || value === null || value === '') return '-';
  const text = typeof value === 'string' ? value : JSON.stringify(value);
  if (!text) return '-';
  return text.length > 150 ? `${text.slice(0, 150)}...` : text;
}

function formatCellValue(value: unknown) {
  if (value === undefined || value === null || value === '') return '-';
  return String(value);
}

function primaryWalletAccount(accounts: GatewayWalletAccount[]) {
  return accounts.find((account) => account.currency === 'resource') ?? accounts[0];
}

function formatMoney(value: unknown) {
  const numericValue = typeof value === 'number' ? value : Number(value);
  if (!Number.isFinite(numericValue)) return '0.00';
  return new Intl.NumberFormat(undefined, {
    maximumFractionDigits: 2,
    minimumFractionDigits: 2,
  }).format(numericValue);
}

function transactionTypeLabel(value: string) {
  if (value === 'task_billing') return '任务扣费';
  if (value === 'admin_adjust') return '余额调整';
  if (value === 'recharge') return '充值';
  if (value === 'refund') return '退款';
  if (value === 'reserve') return '冻结';
  if (value === 'release') return '释放';
  return value || '-';
}

function firstText(...values: Array<unknown>) {
  for (const value of values) {
    if (typeof value === 'string' && value.trim()) return value.trim();
  }
  return '';
}

function walletTransactionCalledModel(metadata: Record<string, unknown> | undefined) {
  return firstText(
    metadataModelReference(metadata, 'requestedModel'),
    metadataModelReference(metadata, 'model'),
    metadataModelReference(metadata, 'modelId'),
  );
}

function walletTransactionResolvedModel(metadata: Record<string, unknown> | undefined) {
  return firstText(
    metadataString(metadata, 'resolvedModel'),
    metadataString(metadata, 'platformModelAlias'),
    metadataString(metadata, 'providerModel'),
    metadataString(metadata, 'modelName'),
  );
}

function taskCalledModelLabel(task: GatewayTask) {
  return firstText(
    task.requestedModel,
    modelReferenceLabel(task.request?.model),
    task.model,
  );
}

function taskResolvedModelLabel(task: GatewayTask) {
  return firstText(
    task.resolvedModel,
    metadataString(task.metrics, 'resolvedModel'),
    metadataString(task.metrics, 'providerModel'),
    metadataString(task.metrics, 'modelName'),
  );
}

function metadataModelReference(metadata: Record<string, unknown> | undefined, key: string) {
  return modelReferenceLabel(metadata?.[key]);
}

function modelReferenceLabel(value: unknown) {
  if (typeof value === 'string' && value.trim()) return value.trim();
  if (!value || typeof value !== 'object' || Array.isArray(value)) return '';
  const record = value as Record<string, unknown>;
  return firstText(
    objectString(record, 'modelId'),
    objectString(record, 'model_id'),
    objectString(record, 'modelName'),
    objectString(record, 'model_name'),
    objectString(record, 'id'),
    objectString(record, 'name'),
  );
}

function metadataString(metadata: Record<string, unknown> | undefined, key: string) {
  const value = metadata?.[key];
  return typeof value === 'string' && value.trim() ? value.trim() : '';
}

function metadataStringList(metadata: Record<string, unknown> | undefined, key: string) {
  const value = metadata?.[key];
  if (!Array.isArray(value)) return [];
  return value.filter((item): item is string => typeof item === 'string' && item.trim() !== '').map((item) => item.trim());
}

function metadataNumber(metadata: Record<string, unknown> | undefined, key: string) {
  const value = metadata?.[key];
  if (value === undefined || value === null || value === '') return null;
  const numericValue = typeof value === 'number' ? value : Number(value);
  return Number.isFinite(numericValue) ? numericValue : null;
}

function metadataObject(metadata: Record<string, unknown> | undefined, key: string): Record<string, unknown> {
  const value = metadata?.[key];
  if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
  return value as Record<string, unknown>;
}

function objectString(value: Record<string, unknown>, key: string) {
  const next = value[key];
  return typeof next === 'string' && next.trim() ? next.trim() : '';
}

function appendUniqueText(values: string[], value: string) {
  if (!value || values.includes(value)) return;
  values.push(value);
}

function transactionChargeAmount(transaction: GatewayWalletTransaction) {
  return metadataNumber(transaction.metadata, 'finalChargeAmount') ?? transaction.amount;
}

function transactionChargeCurrency(transaction: GatewayWalletTransaction) {
  const summary = metadataObject(transaction.metadata, 'billingSummary');
  const finalCharge = metadataObject(summary, 'finalCharge');
  return objectString(finalCharge, 'currency') || objectString(summary, 'currency') || transaction.currency || 'resource';
}

function formatTokenUsage(usage: Record<string, unknown>) {
  const input = tokenValue(
    usage.inputTokens ??
      usage.promptTokens ??
      usage.input_tokens ??
      usage.prompt_tokens,
  );
  const promptDetails = usageObject(
    usage.prompt_tokens_details ?? usage.promptTokensDetails,
  );
  const inputDetails = usageObject(
    usage.input_tokens_details ?? usage.inputTokensDetails,
  );
  const cachedInput = tokenValue(
    usage.cachedInputTokens ??
      usage.cachedPromptTokens ??
      usage.cached_input_tokens ??
      usage.cached_tokens ??
      promptDetails.cached_tokens ??
      promptDetails.cachedTokens ??
      inputDetails.cached_tokens ??
      inputDetails.cachedTokens,
  );
  const output = tokenValue(
    usage.outputTokens ??
      usage.completionTokens ??
      usage.output_tokens ??
      usage.completion_tokens,
  );
  const total = tokenValue(
    usage.totalTokens ??
      usage.total_tokens ??
      (input !== null && output !== null ? input + output : null),
  );
  if (input === null && cachedInput === null && output === null && total === null) {
    return '-';
  }
  return (
    <span className="taskRecordTokenUsage">
      <span>输入：{formatCellValue(input)}/输出：{formatCellValue(output)}</span>
      {cachedInput !== null ? <span>缓存输入：{formatCellValue(cachedInput)}</span> : null}
      <span>总计：{formatCellValue(total)}</span>
    </span>
  );
}

function usageObject(value: unknown): Record<string, unknown> {
  return value && typeof value === 'object' && !Array.isArray(value)
    ? (value as Record<string, unknown>)
    : {};
}

function tokenValue(value: unknown) {
  if (value === undefined || value === null || value === '') return null;
  const numericValue = typeof value === 'number' ? value : Number(value);
  return Number.isFinite(numericValue) ? numericValue : null;
}

function taskDurationMs(task: GatewayTask) {
  return (
    positiveDurationMs(task.responseDurationMs) ??
    elapsedDurationMs(task.responseStartedAt, task.responseFinishedAt) ??
    elapsedDurationMs(task.createdAt, task.finishedAt)
  );
}

function attemptDurationMs(attempt: NonNullable<GatewayTask['attempts']>[number]) {
  return (
    positiveDurationMs(attempt.responseDurationMs) ??
    elapsedDurationMs(attempt.responseStartedAt, attempt.responseFinishedAt) ??
    elapsedDurationMs(attempt.startedAt, attempt.finishedAt)
  );
}

function positiveDurationMs(value?: number) {
  if (value === undefined || value === null) return undefined;
  const numericValue = Number(value);
  return Number.isFinite(numericValue) && numericValue > 0 ? numericValue : undefined;
}

function elapsedDurationMs(start?: string, end?: string) {
  if (!start || !end) return undefined;
  const startedAt = new Date(start).getTime();
  const finishedAt = new Date(end).getTime();
  if (!Number.isFinite(startedAt) || !Number.isFinite(finishedAt)) return undefined;
  const elapsed = finishedAt - startedAt;
  return elapsed > 0 ? Math.max(1, Math.round(elapsed)) : undefined;
}

function formatDuration(value?: number) {
  if (value === undefined || value === null) return '-';
  const milliseconds = Math.max(0, Math.round(value));
  if (milliseconds === 0) return '-';
  if (milliseconds < 1000) return `${milliseconds}毫秒`;
  const totalSeconds = Math.round(milliseconds / 1000);
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  const seconds = totalSeconds % 60;
  if (hours > 0) return `${hours}小时${minutes}分${seconds}秒`;
  if (minutes > 0) return `${minutes}分${seconds}秒`;
  return `${seconds}秒`;
}

function taskErrorText(task: GatewayTask) {
  return task.errorCode || task.errorMessage || task.error || '';
}

function InfoItem(props: { label: string; value: string }) {
  return (
    <div className="infoItem">
      <span>{props.label}</span>
      <strong>{props.value}</strong>
    </div>
  );
}

function apiKeySecretFor(item: GatewayApiKey, secretsById: Record<string, string>) {
  return secretsById[item.id] || item.secret || '';
}

function ApiKeyScopeBadges(props: { scopes?: string[] }) {
  const scopes = normalizeApiKeyScopes(props.scopes);
  if (!scopes.length) {
    return <Badge variant="warning">未配置</Badge>;
  }
  const visibleScopes = scopes.includes('all') || scopes.includes('*') ? ['all'] : scopes.slice(0, 3);
  const hiddenCount = scopes.length - visibleScopes.length;
  return (
    <span className="apiKeyScopeBadges">
      {visibleScopes.map((scope) => (
        <Badge key={scope} variant={scope === 'all' ? 'success' : 'outline'}>{apiKeyScopeLabel(scope)}</Badge>
      ))}
      {hiddenCount > 0 && <Badge variant="secondary">+{hiddenCount}</Badge>}
    </span>
  );
}

function normalizeApiKeyScopes(scopes?: string[]) {
  const out: string[] = [];
  const seen = new Set<string>();
  for (const value of scopes ?? []) {
    let scope = value.trim().toLowerCase();
    if (scope === '*') scope = 'all';
    if (!scope || seen.has(scope)) continue;
    seen.add(scope);
    out.push(scope);
  }
  return out;
}

function apiKeyScopeLabel(scope: string) {
  return apiKeyScopeOptions.find((item) => item.value === scope)?.label ?? scope;
}

function maskApiKey(secret: string) {
  if (secret.length <= 18) return secret;
  return `${secret.slice(0, 12)}...${secret.slice(-4)}`;
}

function permissionSummaryText(summary: ReturnType<typeof countAccessPermissionRules>) {
  const allow = summary.allow.platforms + summary.allow.models;
  const deny = summary.deny.platforms + summary.deny.models;
  if (allow === 0 && deny === 0) return '未配置';
  return `允许 ${allow} / 拒绝 ${deny}`;
}

function formatDateTime(value?: string) {
  if (!value) return '-';
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return '-';
  return date.toLocaleString();
}

function platformsForPermissionTree(models: PlatformModel[]): IntegrationPlatform[] {
  const byPlatform = new Map<string, IntegrationPlatform>();
  for (const model of models) {
    if (byPlatform.has(model.platformId)) continue;
    const name = model.platformName || model.provider || model.platformId;
    byPlatform.set(model.platformId, {
      id: model.platformId,
      provider: model.provider || '',
      platformKey: model.platformId,
      name,
      authType: '',
      status: 'enabled',
      priority: 100,
      defaultPricingMode: 'inherit',
      defaultDiscountFactor: 1,
      createdAt: '',
      updatedAt: '',
    });
  }
  return Array.from(byPlatform.values()).sort((a, b) => a.name.localeCompare(b.name));
}
