import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
  Button, Card, Input, InputNumber, Modal, QRCode, Select, Space, Table, Tag, Typography,
} from 'antd';
import type { TableProps } from 'antd';
import {
  CheckCircleOutlined, CreditCardOutlined, FieldTimeOutlined, GiftOutlined,
  ImportOutlined, QrcodeOutlined, ReloadOutlined,
  SearchOutlined, SettingOutlined, ShoppingOutlined, StopOutlined,
} from '@ant-design/icons';
import {
  douyuApi,
  type DouyuConfig,
  type DouyuGoodsItem,
  type DouyuTaskAccountItem,
  type DouyuTaskItem,
} from '../api/modules';
import { usePermissions } from '../hooks/usePermissions';
import { useWebSocketLogs } from '../hooks/useWebSocketLogs';
import { formatTime } from '../utils/time';
import { getErrorMessage } from '../utils/error';
import { message } from '../utils/antdMessage';

const { Text } = Typography;

type HandbookKind = 'elite' | 'esports';

const ELITE_QUICK_ACTIONS = [
  { key: 'get_bind_qr', icon: <QrcodeOutlined /> },
  { key: 'confirm_bind', icon: <CheckCircleOutlined /> },
  { key: 'create_elite_qr', icon: <CreditCardOutlined /> },
  { key: 'query_points', icon: <SearchOutlined /> },
  { key: 'query_game_name', icon: <SearchOutlined /> },
  { key: 'query_change_bind_time', icon: <FieldTimeOutlined /> },
  { key: 'query_limited_goods', icon: <SearchOutlined /> },
  { key: 'query_gold_balance', icon: <SearchOutlined /> },
  { key: 'exchange_goods', icon: <ShoppingOutlined /> },
  { key: 'create_gold_qr', icon: <CreditCardOutlined /> },
  { key: 'donate_elite_gift', icon: <GiftOutlined /> },
];

const ESPORTS_QUICK_ACTIONS = [
  { key: 'prepare_esports_bind', icon: <QrcodeOutlined /> },
  { key: 'query_esports_game_name', icon: <SearchOutlined /> },
  { key: 'create_esports_qr', icon: <CreditCardOutlined /> },
  { key: 'query_esports_points', icon: <SearchOutlined /> },
  { key: 'query_gold_balance', icon: <SearchOutlined /> },
  { key: 'refresh_esports_goods', icon: <ReloadOutlined /> },
  { key: 'exchange_esports_goods', icon: <ShoppingOutlined /> },
  { key: 'create_gold_qr', icon: <CreditCardOutlined /> },
  { key: 'donate_esports_chicken_gift', icon: <GiftOutlined /> },
  { key: 'donate_esports_firework_gift', icon: <GiftOutlined /> },
];

const ELITE_TASK_TYPES = new Set([
  'get_bind_qr',
  'confirm_bind',
  'create_elite_qr',
  'create_gold_qr',
  'donate_elite_gift',
  'query_points',
  'exchange_goods',
  'query_game_name',
  'query_change_bind_time',
  'query_limited_goods',
  'query_gold_balance',
  'refresh_goods',
  'query_exchange_records',
  'prefetch_csrf_token',
]);
const ESPORTS_TASK_TYPES = new Set([
  'prepare_esports_bind',
  'get_esports_bind_qr',
  'query_esports_game_name',
  'confirm_esports_bind',
  'create_esports_qr',
  'query_esports_points',
  'query_gold_balance',
  'refresh_esports_goods',
  'exchange_esports_goods',
  'create_gold_qr',
  'donate_esports_chicken_gift',
  'donate_esports_firework_gift',
]);
const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount';
const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count';

const STATUS_COLORS: Record<string, string> = {
  planned: 'default', pending: 'default', running: 'processing',
  success: 'success', failed: 'error', error: 'error', stopped: 'warning',
};

const STATUS_LABELS: Record<string, string> = {
  planned: '已计划', pending: '等待中', running: '执行中',
  success: '成功', failed: '失败', error: '异常', stopped: '已停止',
};

function resultText(result: Record<string, unknown> | null | undefined, key: string): string {
  const value = result?.[key];
  return typeof value === 'string' ? value : '';
}

function resultFlag(result: Record<string, unknown> | null | undefined, key: string): boolean {
  const value = result?.[key];
  if (value === true) return true;
  if (value === false || value == null) return false;
  const text = String(value).trim().toLowerCase();
  return text === '1' || text === 'true' || text === 'yes';
}

function taskPayUrl(task: DouyuTaskItem | null | undefined): string {
  return resultText(task?.result, 'pay_url') || resultText(task?.result, 'url');
}

function bindReadyForConfirm(task: DouyuTaskItem | null | undefined): boolean {
  if (!task || task.task_type !== 'get_bind_qr') return false;
  if (task.result?.bind_ready_for_confirm === true) return true;
  const roleName = resultText(task.result, 'role_name');
  if (!roleName) return false;
  // 已确认绑定的角色不能再次直接确认
  if (resultFlag(task.result, 'bind_confirmed') || resultFlag(task.result, 'is_bound_act')) {
    return false;
  }
  return true;
}

function hasBindQrcode(task: DouyuTaskItem | null | undefined): boolean {
  if (!task || task.task_type !== 'get_bind_qr') return false;
  // 有二维码，或已有待确认角色（无需再扫码）都应可展示绑定面板
  return Boolean(resultText(task.result, 'url'))
    || bindReadyForConfirm(task)
    || resultFlag(task.result, 'esports_bound');
}

function hasPaymentQrcode(task: DouyuTaskItem | null | undefined): boolean {
  return Boolean(
    task
    && ['create_elite_qr', 'create_esports_qr', 'create_gold_qr'].includes(task.task_type)
    && resultText(task.result, 'pay_url'),
  );
}

function paymentArrived(task: DouyuTaskItem | null | undefined): boolean {
  if (!task || !['create_elite_qr', 'create_esports_qr', 'create_gold_qr'].includes(task.task_type)) return false;
  if (task.status !== 'success') return false;
  return task.result?.gold_recharged === true
    || task.result?.elite_opened === true
    || task.result?.esports_opened === true;
}

function bindPhaseText(phase: string, polling: boolean): string {
  if (phase === 'role_ready') return '已识别角色，待确认';
  if (phase === 'change_waiting') return '换绑冷却中';
  if (phase === 'role_timeout') return '未检测到角色';
  if (phase === 'stopped') return '任务已停止';
  if (phase === 'confirmed') return '已绑定';
  if (phase === 'confirm_failed') return '确认未生效';
  if (polling || phase === 'waiting_scan') return '等待扫码绑定';
  return phase || '等待绑定';
}

function goodsLabel(item: DouyuGoodsItem): string {
  const stock = item.raw?.count;
  const stockText =
    stock != null && Number.isFinite(Number(stock)) ? ` / 库存${stock}` : '';
  return `${item.name || item.commodity_id}${item.score ? ` / ${item.score}积分` : ''}${stockText}`;
}

function formatWaitSeconds(seconds: number | null | undefined): string {
  if (seconds == null || Number.isNaN(Number(seconds))) return '';
  const total = Math.max(0, Math.floor(Number(seconds)));
  if (total <= 0) return '可换绑';
  const days = Math.floor(total / 86400);
  const hours = Math.floor((total % 86400) / 3600);
  const minutes = Math.floor((total % 3600) / 60);
  const secs = total % 60;
  if (days > 0) return `${days}天${hours}小时${minutes}分`;
  if (hours > 0) return `${hours}小时${minutes}分${secs}秒`;
  return `${minutes}分${secs}秒`;
}

function esportsRebindStatus(canChangeTime: number | null | undefined): {
  available: boolean;
  text: string;
} | null {
  if (canChangeTime == null || Number.isNaN(Number(canChangeTime))) return null;
  const timestamp = Math.floor(Number(canChangeTime));
  if (timestamp <= 0 || timestamp <= Math.floor(Date.now() / 1000)) {
    return { available: true, text: '可换绑' };
  }
  return {
    available: false,
    text: new Date(timestamp * 1000).toLocaleString('zh-CN', { hour12: false }),
  };
}

function resultNumber(result: Record<string, unknown> | null | undefined, key: string): number | null {
  const value = result?.[key];
  if (typeof value === 'number' && Number.isFinite(value)) return value;
  if (typeof value === 'string' && value.trim()) {
    const n = Number(value);
    return Number.isFinite(n) ? n : null;
  }
  return null;
}

function limitedGoodsName(item: unknown): string {
  if (!item || typeof item !== 'object') return '';
  const record = item as Record<string, unknown>;
  const value = record.commodityName ?? record.name;
  return typeof value === 'string' ? value : '';
}

function limitedGoodsTaskText(task: DouyuTaskItem | null | undefined): string {
  if (!task) return '';
  if (task.message?.trim()) return task.message.trim();
  const count = resultNumber(task.result, 'limited_count');
  if (count == null) return '';
  if (count <= 0) return '无限制商品';
  const goods = task.result?.limited_goods;
  const names = Array.isArray(goods)
    ? goods.map(limitedGoodsName).filter(Boolean).slice(0, 5)
    : [];
  return names.length ? `限兑 ${count} 个: ${names.join(', ')}` : `限兑 ${count} 个`;
}

function savedPositiveInteger(key: string, fallback = 1): number {
  const value = Number(localStorage.getItem(key));
  return Number.isInteger(value) && value >= 1 ? value : fallback;
}

export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) {
  const isEsportsHandbook = handbook === 'esports';
  const handbookTitle = isEsportsHandbook ? '电竞手册工作台' : '精英宝典工作台';
  const handbookDescription = isEsportsHandbook
    ? '电竞手册角色绑定与开通任务'
    : '精英宝典绑定、开通、积分与兑换任务';
  const activeTaskTypes = isEsportsHandbook ? ESPORTS_TASK_TYPES : ELITE_TASK_TYPES;
  const quickActions = isEsportsHandbook ? ESPORTS_QUICK_ACTIONS : ELITE_QUICK_ACTIONS;
  const { can } = usePermissions();
  const [accounts, setAccounts] = useState<DouyuTaskAccountItem[]>([]);
  const [selectedIds, setSelectedIds] = useState<number[]>([]);
  const [goods, setGoods] = useState<DouyuGoodsItem[]>([]);
  const [tasks, setTasks] = useState<DouyuTaskItem[]>([]);
  const [taskTypes, setTaskTypes] = useState<Record<string, string>>({});
  const [config, setConfig] = useState<DouyuConfig | null>(null);
  const [loading, setLoading] = useState(false);
  const [runningBatchId, setRunningBatchId] = useState('');
  const [configOpen, setConfigOpen] = useState(false);
  const [concurrency, setConcurrency] = useState(3);
  const [goldAmount, setGoldAmount] = useState(() => savedPositiveInteger(DOUYU_GOLD_AMOUNT_STORAGE_KEY));
  const [giftCount, setGiftCount] = useState(() => savedPositiveInteger(DOUYU_GIFT_COUNT_STORAGE_KEY));
  const [selectedGoodsId, setSelectedGoodsId] = useState('');
  const [accountSearch, setAccountSearch] = useState('');
  const taskRecordAreaRef = useRef<HTMLDivElement | null>(null);
  const [taskRecordAreaHeight, setTaskRecordAreaHeight] = useState(360);
  const accountTableAreaRef = useRef<HTMLDivElement | null>(null);
  const [accountTableAreaHeight, setAccountTableAreaHeight] = useState(360);

  const [importOpen, setImportOpen] = useState(false);
  const [importPool, setImportPool] = useState<DouyuTaskAccountItem[]>([]);
  const [importSearch, setImportSearch] = useState('');
  const [importSelectedIds, setImportSelectedIds] = useState<number[]>([]);
  const [importLoading, setImportLoading] = useState(false);

  // 绑定/支付弹窗以 task 为唯一数据源
  const [qrTask, setQrTask] = useState<DouyuTaskItem | null>(null);
  const [esportsBindTask, setEsportsBindTask] = useState<DouyuTaskItem | null>(null);
  const [payTask, setPayTask] = useState<DouyuTaskItem | null>(null);
  const autoOpenedQrTaskIds = useRef<Set<number>>(new Set());
  const autoOpenedEsportsBindTaskIds = useRef<Set<number>>(new Set());
  const autoOpenedPayTaskIds = useRef<Set<number>>(new Set());
  const autoOpenQrReady = useRef(false);
  const autoOpenEsportsBindReady = useRef(false);
  const autoOpenPayReady = useRef(false);

  const logs = useWebSocketLogs();
  const canConfig = can('douyu:config');
  const latestQueryGameTaskByAccount = useMemo(() => {
    const map = new Map<number, DouyuTaskItem>();
    for (const task of tasks) {
      if (task.task_type !== 'query_game_name') continue;
      const previous = map.get(task.account_id);
      if (!previous || task.id > previous.id) map.set(task.account_id, task);
    }
    return map;
  }, [tasks]);

  const latestEsportsStateTaskByAccount = useMemo(() => {
    const map = new Map<number, DouyuTaskItem>();
    for (const task of tasks) {
      if (!['prepare_esports_bind', 'get_esports_bind_qr', 'query_esports_game_name', 'confirm_esports_bind']
        .includes(task.task_type) || !task.result) continue;
      const previous = map.get(task.account_id);
      if (!previous || task.id > previous.id) map.set(task.account_id, task);
    }
    return map;
  }, [tasks]);

  const visibleTasks = useMemo(
    () => tasks.filter((task) => activeTaskTypes.has(task.task_type)),
    [activeTaskTypes, tasks],
  );

  // 换绑时间：优先最近一次查询/拦截任务结果，回退账号落库字段
  const latestChangeWaitByAccount = useMemo(() => {
    const map = new Map<number, {
      wait: number | null; canChangeTime: number | null; text: string; taskId: number;
    }>();
    for (const task of tasks) {
      const taskTypes = isEsportsHandbook
        ? ['prepare_esports_bind', 'get_esports_bind_qr', 'query_esports_game_name', 'confirm_esports_bind']
        : ['query_change_bind_time', 'get_bind_qr'];
      if (!taskTypes.includes(task.task_type)) continue;
      if (!['success', 'failed'].includes(task.status)) continue;
      const wait = resultNumber(task.result, 'change_role_wait_time');
      const canChangeTime = resultNumber(task.result, 'can_change_time');
      // get_bind_qr 只有冷却拦截时才有 wait；无 wait 时跳过，避免覆盖
      if (!isEsportsHandbook && task.task_type === 'get_bind_qr' && wait == null) continue;
      const existing = map.get(task.account_id);
      if (existing && existing.taskId > task.id) continue;
      const esportsStatus = isEsportsHandbook ? esportsRebindStatus(canChangeTime) : null;
      const text = esportsStatus?.text || resultText(task.result, 'change_role_wait_text') || formatWaitSeconds(wait);
      map.set(task.account_id, { wait, canChangeTime, text, taskId: task.id });
    }
    return map;
  }, [isEsportsHandbook, tasks]);

  const latestLimitedGoodsTaskByAccount = useMemo(() => {
    const map = new Map<number, DouyuTaskItem>();
    for (const task of tasks) {
      if (task.task_type !== 'query_limited_goods') continue;
      if (!['success', 'failed', 'error'].includes(task.status)) continue;
      const previous = map.get(task.account_id);
      if (!previous || task.id > previous.id) map.set(task.account_id, task);
    }
    return map;
  }, [tasks]);

  const loadData = useCallback(async () => {
    setLoading(true);
    try {
      const [accResult, goodsResult, taskResult, cfgResult, typeResult] = await Promise.allSettled([
        douyuApi.listAccounts(),
        isEsportsHandbook ? douyuApi.listEsportsGoods() : douyuApi.listGoods(),
        douyuApi.listTasks(),
        canConfig ? douyuApi.getConfig() : Promise.resolve(null),
        douyuApi.taskTypes(),
      ]);
      if (accResult.status === 'fulfilled') setAccounts(accResult.value);
      if (goodsResult.status === 'fulfilled') setGoods(goodsResult.value);
      if (taskResult.status === 'fulfilled') {
        setTasks(taskResult.value);
        // 仅首次加载时屏蔽历史二维码自动弹窗；后续刷新不能把新任务一并标记掉
        if (!autoOpenQrReady.current) {
          for (const task of taskResult.value) {
            if (hasBindQrcode(task)) autoOpenedQrTaskIds.current.add(task.id);
            if (task.task_type === 'prepare_esports_bind' && task.result?.esports_bind_dialog === true) {
              autoOpenedEsportsBindTaskIds.current.add(task.id);
            }
            if (hasPaymentQrcode(task)) autoOpenedPayTaskIds.current.add(task.id);
          }
          autoOpenQrReady.current = true;
          autoOpenEsportsBindReady.current = true;
          autoOpenPayReady.current = true;
        }
      }
      if (cfgResult.status === 'fulfilled' && cfgResult.value) setConfig(cfgResult.value);
      if (typeResult.status === 'fulfilled') setTaskTypes(typeResult.value);
    } finally {
      setLoading(false);
    }
  }, [canConfig, isEsportsHandbook]);

  const loadTasks = useCallback(async () => {
    try {
      const data = await douyuApi.listTasks();
      setTasks(data);
      if (!data.some((task) => ['pending', 'running', 'planned'].includes(task.status))) {
        // 批次已结束时清理 busy 标记，避免 onResult 丢失导致永久锁住
        setRunningBatchId((current) => (current ? '' : current));
      }
    } catch {
      // 轮询失败不打扰操作
    }
  }, []);

  useEffect(() => { loadData(); }, [loadData]);

  // 监听任务记录/账号表格区域高度变化，动态计算 scroll.y 实现表体内部滚动
  useEffect(() => {
    const updateNode = (
      node: HTMLDivElement | null,
      setter: (v: number) => void,
    ) => {
      if (!node) return;
      setter(Math.max(200, Math.floor(node.getBoundingClientRect().height)));
    };
    const taskNode = taskRecordAreaRef.current;
    const accountNode = accountTableAreaRef.current;
    const updateHeights = () => {
      updateNode(taskNode, setTaskRecordAreaHeight);
      updateNode(accountNode, setAccountTableAreaHeight);
    };
    updateHeights();
    if (typeof ResizeObserver === 'undefined') {
      window.addEventListener('resize', updateHeights);
      return () => window.removeEventListener('resize', updateHeights);
    }
    const observer = new ResizeObserver(updateHeights);
    if (taskNode) observer.observe(taskNode);
    if (accountNode) observer.observe(accountNode);
    return () => observer.disconnect();
  }, []);

  useEffect(() => {
    localStorage.setItem(DOUYU_GOLD_AMOUNT_STORAGE_KEY, String(goldAmount));
  }, [goldAmount]);

  useEffect(() => {
    localStorage.setItem(DOUYU_GIFT_COUNT_STORAGE_KEY, String(giftCount));
  }, [giftCount]);

  const hasActiveTasks = useMemo(
    () => visibleTasks.some((task) => ['pending', 'running', 'planned'].includes(task.status)),
    [visibleTasks],
  );

  // 有活跃任务或 WS 连接时 3 秒刷 tasks；空闲 15 秒轻量刷新
  useEffect(() => {
    const intervalMs = hasActiveTasks || logs.connected || runningBatchId ? 3000 : 15000;
    const timer = setInterval(loadTasks, intervalMs);
    return () => clearInterval(timer);
  }, [hasActiveTasks, loadTasks, logs.connected, runningBatchId]);

  const openQrTask = useCallback((task: DouyuTaskItem) => {
    autoOpenedQrTaskIds.current.add(task.id);
    setEsportsBindTask(null);
    setPayTask(null);
    setQrTask(task);
  }, []);

  const openEsportsBindTask = useCallback((task: DouyuTaskItem) => {
    autoOpenedEsportsBindTaskIds.current.add(task.id);
    setQrTask(null);
    setPayTask(null);
    setEsportsBindTask(task);
  }, []);

  const openPayTask = useCallback((task: DouyuTaskItem) => {
    autoOpenedPayTaskIds.current.add(task.id);
    setQrTask(null);
    setEsportsBindTask(null);
    setPayTask(task);
  }, []);

  // 自动弹出最新绑定二维码（running 有 url 即可）
  useEffect(() => {
    if (!autoOpenQrReady.current || qrTask) return;
    const nextQrTask = visibleTasks
      .filter((task) => hasBindQrcode(task) && !autoOpenedQrTaskIds.current.has(task.id))
      .sort((a, b) => b.id - a.id)[0];
    if (nextQrTask) openQrTask(nextQrTask);
  }, [openQrTask, qrTask, visibleTasks]);

  // 电竞手册先查状态再打开独立绑定面板，不把切换角色二维码当作主流程。
  useEffect(() => {
    if (!autoOpenEsportsBindReady.current || esportsBindTask) return;
    const nextTask = visibleTasks
      .filter((task) => (
        task.task_type === 'prepare_esports_bind'
        && task.result?.esports_bind_dialog === true
        && !autoOpenedEsportsBindTaskIds.current.has(task.id)
      ))
      .sort((a, b) => b.id - a.id)[0];
    if (nextTask) openEsportsBindTask(nextTask);
  }, [esportsBindTask, openEsportsBindTask, visibleTasks]);

  // 自动弹出支付二维码
  useEffect(() => {
    if (!autoOpenPayReady.current || payTask) return;
    const nextPayTask = visibleTasks
      .filter((task) => hasPaymentQrcode(task) && !paymentArrived(task) && !autoOpenedPayTaskIds.current.has(task.id))
      .sort((a, b) => b.id - a.id)[0];
    if (nextPayTask) openPayTask(nextPayTask);
  }, [openPayTask, payTask, visibleTasks]);

  // 弹窗跟随最新 task 状态（后端 progress 写入后 UI 自动更新）
  useEffect(() => {
    if (!qrTask) return;
    const latest = tasks.find((task) => task.id === qrTask.id);
    if (latest && latest !== qrTask) setQrTask(latest);
  }, [qrTask, tasks]);

  useEffect(() => {
    if (!esportsBindTask) return;
    const latest = tasks.find((task) => task.id === esportsBindTask.id);
    if (latest && latest !== esportsBindTask) setEsportsBindTask(latest);
  }, [esportsBindTask, tasks]);

  useEffect(() => {
    if (!payTask) return;
    const latest = tasks.find((task) => task.id === payTask.id);
    if (!latest) return;
    if (paymentArrived(latest)) {
      setPayTask(null);
      return;
    }
    if (latest !== payTask) setPayTask(latest);
  }, [payTask, tasks]);

  const batchBusy = Boolean(runningBatchId || logs.connected || hasActiveTasks);

  const startTask = async (
    taskType: string,
    accountIds?: number[],
    extraPayload: Record<string, unknown> = {},
  ) => {
    const ids = accountIds || selectedIds;
    if (ids.length === 0) {
      message.warning('请先选择账号');
      return;
    }
    // 绑定确认/查询允许在其他账号仍轮询时继续，避免卡在多账号批次里
    const allowDuringBatch = [
      'confirm_bind',
      'confirm_esports_bind',
      'query_game_name',
      'query_esports_game_name',
      'prepare_esports_bind',
      'get_esports_bind_qr',
    ].includes(taskType);
    if (batchBusy && !allowDuringBatch) {
      message.warning('当前已有批次在运行，请先停止或等待结束');
      return;
    }
    if (['exchange_goods', 'exchange_esports_goods'].includes(taskType) && !selectedGoodsId) {
      message.warning('请先选择兑换商品');
      return;
    }
    const payload: Record<string, unknown> = { ...extraPayload };
    if (['exchange_goods', 'exchange_esports_goods'].includes(taskType)) payload.commodity_id = selectedGoodsId;
    if (taskType === 'create_gold_qr') payload.amount = goldAmount;
    if (['donate_elite_gift', 'donate_esports_chicken_gift', 'donate_esports_firework_gift'].includes(taskType)) {
      payload.gift_count = giftCount;
    }
    try {
      const result = await douyuApi.createTasks({
        account_ids: ids,
        task_type: taskType,
        concurrency,
        payload,
      });
      setRunningBatchId(result.batch_id);
      logs.connect(`/api/douyu/ws/${result.batch_id}`, {
        clear: true,
        onResult: () => {
          setRunningBatchId('');
          void loadData();
        },
      });
      message.success(`已创建 ${result.count} 个任务`);
      setTimeout(() => { void loadTasks(); }, 400);
    } catch (error) {
      message.error(getErrorMessage(error));
    }
  };

  const stopBatch = async () => {
    if (!runningBatchId) return;
    try {
      await douyuApi.stopBatch(runningBatchId);
      message.success('已停止');
    } catch (error) {
      message.error(getErrorMessage(error));
    }
  };

  const saveConfig = async (values: DouyuConfig) => {
    try {
      const saved = await douyuApi.updateConfig(values);
      setConfig(saved);
      setConfigOpen(false);
      message.success('配置已保存');
    } catch (error) {
      message.error(getErrorMessage(error));
    }
  };

  // ---- Import ----
  const loadImportPool = useCallback(async () => {
    setImportLoading(true);
    try {
      const result = await douyuApi.listAccounts({ search: importSearch });
      setImportPool(result.filter((a) => !accounts.find((x) => x.id === a.id)));
    } finally {
      setImportLoading(false);
    }
  }, [importSearch, accounts]);
  useEffect(() => { if (importOpen) void loadImportPool(); }, [importOpen, loadImportPool]);

  const importAccounts = (ids: number[]) => {
    const toImport = importPool.filter((a) => ids.includes(a.id));
    setAccounts((prev) => {
      const existing = new Set(prev.map((a) => a.id));
      return [...prev, ...toImport.filter((a) => !existing.has(a.id))];
    });
    setImportSelectedIds([]);
    setImportOpen(false);
    message.success(`已导入 ${toImport.length} 个账号`);
  };

  const removeSelected = () => {
    const sel = new Set(selectedIds);
    setAccounts((prev) => prev.filter((a) => !sel.has(a.id)));
    setSelectedIds([]);
  };

  // ---- Context menu ----
  const [contextMenu, setContextMenu] = useState<{
    accountIds: number[]; x: number; y: number;
  } | null>(null);

  const handleRowContextMenu = (record: DouyuTaskAccountItem, event: React.MouseEvent) => {
    event.preventDefault();
    const ids = selectedIds.includes(record.id) ? selectedIds : [record.id];
    setContextMenu({ accountIds: ids, x: event.clientX, y: event.clientY });
    if (!selectedIds.includes(record.id)) setSelectedIds([record.id]);
  };

  useEffect(() => {
    if (!contextMenu) return;
    const close = () => setContextMenu(null);
    const closeOnEscape = (event: KeyboardEvent) => {
      if (event.key === 'Escape') close();
    };
    window.addEventListener('click', close);
    window.addEventListener('scroll', close, true);
    window.addEventListener('resize', close);
    window.addEventListener('keydown', closeOnEscape);
    return () => {
      window.removeEventListener('click', close);
      window.removeEventListener('scroll', close, true);
      window.removeEventListener('resize', close);
      window.removeEventListener('keydown', closeOnEscape);
    };
  }, [contextMenu]);

  const contextMenuStyle = contextMenu ? {
    position: 'fixed' as const,
    left: Math.max(8, Math.min(contextMenu.x, window.innerWidth - 220)),
    top: Math.max(8, Math.min(contextMenu.y, window.innerHeight - 390)),
    zIndex: 1050,
  } : { display: 'none' };

  const runContextAction = (taskType: string) => {
    if (!contextMenu) return;
    setContextMenu(null);
    void startTask(taskType, contextMenu.accountIds);
  };

  // ---- 电竞手册绑定面板 ----
  const esportsRelatedTasks = esportsBindTask
    ? tasks
      .filter((task) => (
        task.account_id === esportsBindTask.account_id
        && task.id >= esportsBindTask.id
        && ['prepare_esports_bind', 'get_esports_bind_qr', 'query_esports_game_name', 'confirm_esports_bind']
          .includes(task.task_type)
      ))
      .sort((a, b) => b.id - a.id)
    : [];
  const esportsLatestStateTask = esportsRelatedTasks.find((task) => task.result) || esportsBindTask;
  const esportsLatestRoleTask = esportsRelatedTasks.find(
    (task) => task.task_type === 'query_esports_game_name',
  ) || null;
  const esportsLatestQrTask = esportsRelatedTasks.find(
    (task) => Boolean(resultText(task.result, 'url')),
  ) || null;
  const esportsConfirmRunning = esportsRelatedTasks.some(
    (task) => task.task_type === 'confirm_esports_bind' && ['planned', 'pending', 'running'].includes(task.status),
  );
  const esportsRoleQueryRunning = Boolean(
    esportsLatestRoleTask && ['planned', 'pending', 'running'].includes(esportsLatestRoleTask.status),
  );
  const esportsResult = esportsLatestStateTask?.result || esportsBindTask?.result || null;
  const esportsBound = resultFlag(esportsResult, 'esports_bound') || resultFlag(esportsResult, 'bind_confirmed');
  const esportsRoleName = resultText(esportsResult, 'role_name');
  const esportsAreaName = resultText(esportsResult, 'area_name');
  const esportsPlatName = resultText(esportsResult, 'plat_name');
  const esportsRoleLine = [esportsPlatName, esportsAreaName].filter(Boolean).join(' / ');
  const esportsCanChangeRole = Boolean(esportsBindTask && !esportsConfirmRunning);
  const esportsCanChangeTime = resultNumber(esportsResult, 'can_change_time');
  const esportsRebind = esportsRebindStatus(esportsCanChangeTime);
  const esportsQrUrl = resultText(esportsLatestQrTask?.result, 'url');
  const esportsAccountName = esportsBindTask
    ? (
      esportsBindTask.account_nickname
      || esportsBindTask.account_username
      || esportsBindTask.account_uid
      || `#${esportsBindTask.account_id}`
    )
    : '';
  const esportsStatusText = esportsBound
    ? '电竞手册已绑定'
    : esportsConfirmRunning
      ? '正在完成电竞手册绑定'
      : esportsRoleQueryRunning
        ? '正在查询最新角色'
        : esportsLatestStateTask?.message || '加载电竞手册绑定状态';
  const esportsCanConfirm = Boolean(
    esportsBindTask
    && !esportsBound
    && esportsRoleName
    && !esportsConfirmRunning,
  );

  const queryEsportsRole = () => {
    if (!esportsBindTask || esportsRoleQueryRunning) return;
    void startTask('query_esports_game_name', [esportsBindTask.account_id]);
  };

  const switchEsportsRole = () => {
    if (!esportsBindTask || !esportsCanChangeRole) return;
    void startTask('get_esports_bind_qr', [esportsBindTask.account_id]);
  };

  const confirmEsportsBindFromDialog = () => {
    if (!esportsBindTask || !esportsCanConfirm) {
      message.warning('请先确认当前角色');
      return;
    }
    void startTask('confirm_esports_bind', [esportsBindTask.account_id]);
  };

  // ---- QR panel derived state ----
  const qrResult = qrTask?.result || null;
  const qrUrl = resultText(qrResult, 'url');
  const qrBindPhase = resultText(qrResult, 'bind_phase');
  const qrAutoPolling = qrTask?.status === 'running' && qrResult?.bind_polling === true;
  const qrQueryTask = qrTask
    ? latestQueryGameTaskByAccount.get(qrTask.account_id) || null
    : null;
  const qrQueryResult = qrTask && qrQueryTask && qrQueryTask.id > qrTask.id ? qrQueryTask.result : null;
  const qrQueryRunning = Boolean(qrQueryTask && qrTask && qrQueryTask.id > qrTask.id && qrQueryTask.status === 'running');
  const qrQueryRoleName = resultText(qrQueryResult, 'role_name');
  const qrQueryIsBoundAct = resultFlag(qrQueryResult, 'is_bound_act') || resultFlag(qrQueryResult, 'bind_confirmed');
  const qrQueryPending = Boolean(
    qrQueryResult
    && qrQueryRoleName
    && (
      qrQueryResult.bind_ready_for_confirm === true
      || (!qrQueryIsBoundAct)
    ),
  );
  const qrBindReady = bindReadyForConfirm(qrTask) || qrQueryPending;
  const qrRoleSourceResult = qrQueryPending
    ? qrQueryResult
    : (bindReadyForConfirm(qrTask) ? qrResult : null);
  const qrRoleName = resultText(qrRoleSourceResult, 'role_name');
  const qrAreaName = resultText(qrRoleSourceResult, 'area_name');
  const qrPlatName = resultText(qrRoleSourceResult, 'plat_name');
  const qrCurrentRoleName = qrQueryRoleName
    || resultText(qrResult, 'current_role_name')
    || (
      resultFlag(qrResult, 'is_bound_act')
        ? resultText(qrResult, 'role_name')
        : ''
    )
    || (qrQueryIsBoundAct ? qrQueryRoleName : '');
  const qrCurrentAreaName = resultText(qrQueryResult, 'area_name')
    || resultText(qrResult, 'current_area_name')
    || (
      resultFlag(qrResult, 'is_bound_act')
        ? resultText(qrResult, 'area_name')
        : ''
    )
    || (qrQueryIsBoundAct ? resultText(qrQueryResult, 'area_name') : '');
  const qrCurrentPlatName = resultText(qrQueryResult, 'plat_name')
    || resultText(qrResult, 'current_plat_name')
    || (
      resultFlag(qrResult, 'is_bound_act')
        ? resultText(qrResult, 'plat_name')
        : ''
    )
    || (qrQueryIsBoundAct ? resultText(qrQueryResult, 'plat_name') : '');
  const qrRoleLine = qrBindReady
    ? [qrPlatName, qrAreaName, qrRoleName].filter(Boolean).join(' - ')
    : '';
  const qrBindSummary = resultText(qrResult, 'bind_summary') || resultText(qrQueryResult, 'bind_summary');
  const qrStatusText = qrBindReady
    ? '已识别角色，待确认'
    : qrQueryRunning
      ? '查询角色中'
      : qrQueryTask && qrQueryTask.id > (qrTask?.id || 0) && qrQueryTask.status === 'success' && qrQueryIsBoundAct
        ? '查询结果：仍是当前绑定角色'
        : bindPhaseText(qrBindPhase, qrAutoPolling);
  const qrAccountName = qrTask
    ? (qrTask.account_nickname || qrTask.account_username || qrTask.account_uid || `#${qrTask.account_id}`)
    : '';

  const confirmQrBind = () => {
    if (!qrTask || !qrBindReady) {
      message.warning('请先扫码识别待确认角色');
      return;
    }
    const accountId = qrTask.account_id;
    setQrTask(null);
    void startTask('confirm_bind', [accountId]);
  };

  const queryQrRole = () => {
    if (!qrTask) return;
    if (qrAutoPolling) {
      message.info('正在自动识别角色，请稍候');
      return;
    }
    void startTask('query_game_name', [qrTask.account_id]);
  };

  // Account table
  const accountColumns: TableProps<DouyuTaskAccountItem>['columns'] = [
    {
      title: '#', width: 45,
      render: (_, __, index) => <Text type="secondary">{index + 1}</Text>,
    },
    {
      title: '账号', dataIndex: 'username', width: 150,
      render: (_, record) => (
        <Space direction="vertical" size={0}>
          <Text strong>{record.nickname || record.username}</Text>
          <Text type="secondary" style={{ fontSize: 12 }}>UID {record.uid || '-'}</Text>
        </Space>
      ),
    },
    {
      title: '游戏名', dataIndex: 'game_name', width: 130,
      render: (_, record) => {
        const queryResult = (
          isEsportsHandbook
            ? latestEsportsStateTaskByAccount.get(record.id)
            : latestQueryGameTaskByAccount.get(record.id)
        )?.result || null;
        const roleName = resultText(queryResult, 'role_name')
          || (isEsportsHandbook ? record.esports_game_name : record.game_name);
        const channel = (
          [resultText(queryResult, 'area_name'), resultText(queryResult, 'plat_name')]
            .filter(Boolean)
            .join(' / ')
          || (isEsportsHandbook ? record.esports_game_channel : record.game_channel)
        );
        return roleName ? (
          <Space direction="vertical" size={0}>
            <Text>{roleName}</Text>
            {channel ? <Text type="secondary" style={{ fontSize: 12 }}>{channel}</Text> : null}
          </Space>
        ) : <Text type="secondary">未查</Text>;
      },
    },
    {
      title: isEsportsHandbook ? '电竞积分' : '积分',
      dataIndex: isEsportsHandbook ? 'esports_points' : 'points',
      width: 80,
      render: (v) => v ?? <Text type="secondary">-</Text>,
    },
    {
      title: '鱼翅', dataIndex: 'gold_balance', width: 70,
      render: (v: number | null) => v ?? <Text type="secondary">-</Text>,
    },
    {
      title: '限制兑换', dataIndex: 'exchange_balance', width: 150, ellipsis: true,
      render: (_, record) => {
        const latestLimitedTask = latestLimitedGoodsTaskByAccount.get(record.id);
        const text = limitedGoodsTaskText(latestLimitedTask);
        if (!text) return <Text type="secondary">未查</Text>;
        if (latestLimitedTask?.status !== 'success') {
          return <Text type="secondary">{text}</Text>;
        }
        const count = resultNumber(latestLimitedTask.result, 'limited_count');
        if (count === 0 || text === '无限制商品') return <Tag color="green">无限制商品</Tag>;
        return <Text title={text}>{text}</Text>;
      },
    },
    {
      title: '换绑时间', dataIndex: 'change_role_wait_time', width: 130,
      render: (_, record) => {
        const fromTask = latestChangeWaitByAccount.get(record.id);
        const canChangeTime = fromTask?.canChangeTime ?? (
          isEsportsHandbook ? record.esports_can_change_time : null
        );
        const esportsStatus = isEsportsHandbook ? esportsRebindStatus(canChangeTime) : null;
        if (esportsStatus) {
          return esportsStatus.available
            ? <Tag color="success">可换绑</Tag>
            : (
              <Space direction="vertical" size={0}>
                <Tag color="orange">暂不可换绑</Tag>
                <Text type="secondary" style={{ fontSize: 12 }}>{esportsStatus.text}</Text>
              </Space>
            );
        }
        const wait = fromTask?.wait ?? (
          isEsportsHandbook ? record.esports_change_role_wait_time : record.change_role_wait_time
        );
        const text = fromTask?.text || formatWaitSeconds(wait);
        if (wait == null && !text) {
          return <Text type="secondary">未查</Text>;
        }
        if (wait != null && wait <= 0) {
          return <Tag color="success">可换绑</Tag>;
        }
        if (wait != null && wait > 0) {
          return (
            <Space direction="vertical" size={0}>
              <Tag color="orange">冷却中</Tag>
              <Text type="secondary" style={{ fontSize: 12 }}>{text || formatWaitSeconds(wait)}</Text>
            </Space>
          );
        }
        // 接口没返回数字但有文案
        if (text === '可换绑') return <Tag color="success">可换绑</Tag>;
        return <Text type="secondary">{text || '-'}</Text>;
      },
      sorter: (a, b) => {
        const aw = isEsportsHandbook
          ? latestChangeWaitByAccount.get(a.id)?.canChangeTime ?? a.esports_can_change_time ?? -1
          : latestChangeWaitByAccount.get(a.id)?.wait
          ?? (isEsportsHandbook ? a.esports_change_role_wait_time : a.change_role_wait_time) ?? -1;
        const bw = isEsportsHandbook
          ? latestChangeWaitByAccount.get(b.id)?.canChangeTime ?? b.esports_can_change_time ?? -1
          : latestChangeWaitByAccount.get(b.id)?.wait
          ?? (isEsportsHandbook ? b.esports_change_role_wait_time : b.change_role_wait_time) ?? -1;
        return aw - bw;
      },
    },
  ];

  // 任务记录表
  const taskColumns: TableProps<DouyuTaskItem>['columns'] = [
    {
      title: '账号', dataIndex: 'account_username', width: 130,
      render: (_, r) => r.account_nickname || r.account_username || r.account_uid,
    },
    {
      title: '结果', dataIndex: 'message', width: 300, ellipsis: true,
      render: (_, r) => {
        if (['prepare_esports_bind', 'get_esports_bind_qr'].includes(r.task_type)) {
          const roleName = resultText(r.result, 'role_name');
          const esportsBound = resultFlag(r.result, 'esports_bound') || resultFlag(r.result, 'bind_confirmed');
          const bindUrl = resultText(r.result, 'url');
          return (
            <Space wrap size={4}>
              <Text>{r.message || '-'}</Text>
              {roleName ? <Tag color={esportsBound ? 'green' : 'blue'}>{roleName}</Tag> : null}
              {bindUrl || r.result?.esports_bind_dialog === true ? (
                <Button size="small" icon={<QrcodeOutlined />} onClick={() => openEsportsBindTask(r)}>
                  查看
                </Button>
              ) : null}
            </Space>
          );
        }
        if (r.task_type === 'get_bind_qr') {
          const roleName = resultText(r.result, 'role_name');
          const phase = resultText(r.result, 'bind_phase');
          const ready = bindReadyForConfirm(r);
          const polling = r.status === 'running' && r.result?.bind_polling === true;
          const bindUrl = resultText(r.result, 'url');
          return (
            <Space wrap size={4}>
              <Text>{r.message || '-'}</Text>
              {ready ? <Tag color="gold">{roleName || '待确认'}</Tag> : null}
              {!ready && (polling || phase) ? (
                <Tag color="processing">{bindPhaseText(phase, polling)}</Tag>
              ) : null}
              {bindUrl && (
                <Button size="small" icon={<QrcodeOutlined />} onClick={() => openQrTask(r)}>
                  查看
                </Button>
              )}
            </Space>
          );
        }
        if (['query_game_name', 'query_esports_game_name'].includes(r.task_type)) {
          const roleName = resultText(r.result, 'role_name');
          const areaName = resultText(r.result, 'area_name');
          const platName = resultText(r.result, 'plat_name');
          return (
            <Space wrap size={4}>
              <Text>{r.message || '-'}</Text>
              {roleName ? <Tag color="blue">{roleName}</Tag> : null}
              {(areaName || platName) ? (
                <Text type="secondary">{[areaName, platName].filter(Boolean).join(' / ')}</Text>
              ) : null}
            </Space>
          );
        }
        if (r.task_type === 'confirm_bind') {
          const roleName = resultText(r.result, 'role_name');
          const confirmed = resultFlag(r.result, 'bind_confirmed');
          return (
            <Space wrap size={4}>
              <Text>{r.message || '-'}</Text>
              {roleName ? <Tag color={confirmed ? 'green' : 'orange'}>{roleName}</Tag> : null}
            </Space>
          );
        }
        if (r.task_type === 'confirm_esports_bind') {
          const roleName = resultText(r.result, 'role_name');
          const confirmed = resultFlag(r.result, 'bind_confirmed');
          return (
            <Space wrap size={4}>
              <Text>{r.message || '-'}</Text>
              {roleName ? <Tag color={confirmed ? 'green' : 'orange'}>{roleName}</Tag> : null}
            </Space>
          );
        }
        const url = taskPayUrl(r);
        return (
          <Space wrap size={4}>
            <Text>{r.message || '-'}</Text>
            {url && (
              <Button
                size="small"
                icon={<QrcodeOutlined />}
                onClick={() => {
                  if (hasPaymentQrcode(r)) openPayTask(r);
                  else if (hasBindQrcode(r)) openQrTask(r);
                }}
              >
                查看
              </Button>
            )}
          </Space>
        );
      },
    },
    { title: '任务', dataIndex: 'task_type', width: 170, render: (v) => taskTypes[v] || v },
    {
      title: '状态', dataIndex: 'status', width: 80,
      render: (v) => <Tag color={STATUS_COLORS[v]}>{STATUS_LABELS[v] || v}</Tag>,
    },
    {
      title: '时间', dataIndex: 'finished_at', width: 170,
      render: (_, r) => formatTime(r.finished_at || r.created_at),
    },
  ];

  const filteredAccounts = useMemo(() => {
    if (!accountSearch.trim()) return accounts;
    const kw = accountSearch.toLowerCase();
    return accounts.filter((a) =>
      (a.nickname || '').toLowerCase().includes(kw)
      || (a.username || '').toLowerCase().includes(kw)
      || (a.uid || '').toLowerCase().includes(kw)
      || (a.game_name || '').toLowerCase().includes(kw)
      || (a.esports_game_name || '').toLowerCase().includes(kw),
    );
  }, [accounts, accountSearch]);

  const [configFormValues, setConfigFormValues] = useState<DouyuConfig | null>(null);
  useEffect(() => {
    if (configOpen && config) setConfigFormValues({ ...config });
  }, [configOpen, config]);

  const payUrl = resultText(payTask?.result, 'pay_url');
  const payAccountName = payTask
    ? (payTask.account_nickname || payTask.account_username || payTask.account_uid || `#${payTask.account_id}`)
    : '';
  const selectedText = selectedIds.length > 0 ? `已选 ${selectedIds.length} 个账号` : '请先勾选账号';
  const actionByKey = useMemo(() => new Map(quickActions.map((item) => [item.key, item])), [quickActions]);
  const actionDisabled = (taskType: string) => {
    if (batchBusy || selectedIds.length === 0) return true;
    if (['exchange_goods', 'exchange_esports_goods'].includes(taskType)) return !selectedGoodsId;
    return false;
  };
  const renderActionButton = (taskType: string, type: 'default' | 'primary' = 'default') => {
    const item = actionByKey.get(taskType);
    return (
      <Button
        key={taskType}
        size="small"
        block
        type={type}
        icon={item?.icon}
        onClick={() => startTask(taskType)}
        disabled={actionDisabled(taskType)}
        style={{ justifyContent: 'flex-start' }}
      >
        {taskTypes[taskType] || taskType}
      </Button>
    );
  };
  const sectionStyle = {
    border: '1px solid #f0f0f0',
    borderRadius: 6,
    padding: 8,
    background: '#fff',
  };
  const sectionTitleStyle = {
    display: 'flex',
    alignItems: 'center',
    gap: 6,
    marginBottom: 8,
    fontWeight: 600,
  };
  const actionGridStyle = {
    display: 'grid',
    gridTemplateColumns: '1fr 1fr',
    gap: 6,
  };
  const compactOperationGridStyle = {
    display: 'grid',
    gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
    gap: 8,
  };
  // 任务记录表:表头固定、表体内部滚动、分页器固定显示在底部
  const taskRecordTableStyle = `
    .douyu-task-record-table {
      flex: 1;
      min-height: 0;
      display: flex;
      flex-direction: column;
    }
    .douyu-task-record-table .ant-spin-nested-loading,
    .douyu-task-record-table .ant-spin-container {
      flex: 1;
      min-height: 0;
      display: flex;
      flex-direction: column;
    }
    .douyu-task-record-table .ant-table {
      flex: 1;
      min-height: 0;
      font-size: 12px;
      display: flex;
      flex-direction: column;
    }
    .douyu-task-record-table .ant-table-container {
      flex: 1;
      min-height: 0;
      display: flex;
      flex-direction: column;
    }
    .douyu-task-record-table .ant-table-header {
      flex-shrink: 0;
    }
    .douyu-task-record-table .ant-table-body {
      flex: 1;
      min-height: 0;
      overflow: auto !important;
    }
    .douyu-task-record-table .ant-table-thead > tr > th,
    .douyu-task-record-table .ant-table-tbody > tr > td {
      padding: 4px 8px;
      line-height: 20px;
    }
    .douyu-task-record-table .ant-typography {
      font-size: 12px;
    }
    .douyu-task-record-table .ant-tag {
      font-size: 11px;
      line-height: 18px;
      margin-inline-end: 0;
      padding-inline: 5px;
    }
    .douyu-task-record-table .ant-pagination {
      flex-shrink: 0;
      margin: 0;
      padding: 6px 4px 2px;
      border-top: 1px solid #f0f0f0;
    }
  `;

  return (
    <div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
      <style>{taskRecordTableStyle}</style>
      {/* 头部 */}
      <Space style={{ justifyContent: 'space-between', width: '100%', marginBottom: 8, flexShrink: 0 }}>
        <div>
          <h2 style={{ margin: 0 }}>{handbookTitle}</h2>
          <Text type="secondary">{handbookDescription}</Text>
        </div>
        <Space>
          <Button icon={<ReloadOutlined />} onClick={loadData} loading={loading}>刷新</Button>
          {canConfig && <Button icon={<SettingOutlined />} onClick={() => setConfigOpen(true)}>配置</Button>}
          {runningBatchId && <Button danger icon={<StopOutlined />} onClick={stopBatch}>停止</Button>}
        </Space>
      </Space>

      {/* 主布局 */}
      <div style={{
        flex: 1, minHeight: 0,
        display: 'grid',
        gridTemplateColumns: 'minmax(0, 2fr) minmax(0, 1fr)',
        gap: 12, overflow: 'hidden',
      }}>
        {/* 左侧账号 */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12, minHeight: 0, overflow: 'hidden' }}>
          <Card
            size="small" title="账号"
            extra={<Tag color="blue">已选 {selectedIds.length}/{accounts.length}</Tag>}
            style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
            styles={{ body: { flex: 1, minHeight: 0, overflow: 'hidden', padding: 8, display: 'flex', flexDirection: 'column' } }}
          >
            <Space style={{ marginBottom: 8, width: '100%', justifyContent: 'space-between', flexShrink: 0 }} wrap>
              <Space>
                <Input
                  size="small" placeholder="搜索账号/昵称/游戏名" prefix={<SearchOutlined />}
                  value={accountSearch} onChange={(e) => setAccountSearch(e.target.value)}
                  style={{ width: 200 }} allowClear
                />
              </Space>
              <Space>
                <Button size="small" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>导入账号</Button>
                {selectedIds.length > 0 && (
                  <Button size="small" danger onClick={removeSelected}>移出选中</Button>
                )}
              </Space>
            </Space>
            <div ref={accountTableAreaRef} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
              <Table
                rowKey="id"
                size="small"
                className="douyu-task-record-table"
                loading={loading}
                rowSelection={{
                  selectedRowKeys: selectedIds,
                  onChange: (keys) => setSelectedIds(keys.map(Number)),
                }}
                columns={accountColumns}
                dataSource={filteredAccounts}
                pagination={{ pageSize: 15, showSizeChanger: false, size: 'small', showLessItems: true }}
                tableLayout="fixed"
                scroll={{ y: Math.max(120, accountTableAreaHeight - 80) }}
                onRow={(record) => ({
                  onContextMenu: (e) => handleRowContextMenu(record, e),
                  style: { cursor: 'context-menu' },
                  title: '右键打开账号动作',
                })}
              />
            </div>
          </Card>
        </div>

        {/* 右侧:任务记录(弹性高度) + 操作区(自适应) */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12, minHeight: 0, overflow: 'hidden' }}>
          <Card
            size="small" title="任务记录"
            extra={
              <Space size={12}>
                <Tag color="success">成功 {visibleTasks.filter((t) => t.status === 'success').length}</Tag>
                <Tag color="error">失败 {visibleTasks.filter((t) => ['failed', 'error'].includes(t.status)).length}</Tag>
              </Space>
            }
            style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
            styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}
          >
            <div ref={taskRecordAreaRef} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
              <Table
                rowKey="id" size="small" columns={taskColumns} dataSource={visibleTasks}
                className="douyu-task-record-table"
                loading={loading}
                pagination={{
                  pageSize: 15,
                  showSizeChanger: false,
                  size: 'small',
                  showLessItems: true,
                  showTotal: (total) => `共 ${total} 条`,
                }}
                tableLayout="fixed"
                scroll={{ x: 850, y: Math.max(120, taskRecordAreaHeight - 80) }}
              />
            </div>
          </Card>

          <Card
            size="small"
            title={isEsportsHandbook ? '电竞手册操作' : '精英宝典操作'}
            extra={<Tag color={selectedIds.length ? 'blue' : 'default'}>{selectedText}</Tag>}
            style={{ flexShrink: 0, display: 'flex', flexDirection: 'column' }}
            styles={{ body: { padding: 8, maxHeight: 260, overflow: 'auto' } }}
          >
            <Space direction="vertical" style={{ width: '100%' }} size={8}>
              <Space.Compact style={{ width: '100%' }}>
                <Button size="small" disabled>并发</Button>
                <InputNumber
                  size="small"
                  min={1}
                  max={10}
                  value={concurrency}
                  onChange={(v) => setConcurrency(v || 1)}
                  style={{ width: '100%' }}
                />
              </Space.Compact>

              {isEsportsHandbook ? (
                <div style={compactOperationGridStyle}>
                  <div style={sectionStyle}>
                    <div style={sectionTitleStyle}>
                      <CreditCardOutlined />
                      <span>开通手册</span>
                    </div>
                    {renderActionButton('create_esports_qr', 'primary')}
                  </div>
                  <div style={sectionStyle}>
                    <div style={sectionTitleStyle}>
                      <ShoppingOutlined />
                      <span>皮肤兑换</span>
                    </div>
                    <Space direction="vertical" style={{ width: '100%' }} size={6}>
                      <Select
                        size="small"
                        value={selectedGoodsId || undefined}
                        onChange={setSelectedGoodsId}
                        options={goods.map((item) => ({ value: item.commodity_id, label: goodsLabel(item) }))}
                        placeholder="选择电竞皮肤"
                        showSearch
                        optionFilterProp="label"
                        style={{ width: '100%' }}
                      />
                      <div style={actionGridStyle}>
                        {renderActionButton('refresh_esports_goods')}
                        {renderActionButton('exchange_esports_goods', 'primary')}
                      </div>
                    </Space>
                  </div>
                  <div style={sectionStyle}>
                    <div style={sectionTitleStyle}>
                      <CreditCardOutlined />
                      <span>充值与送礼</span>
                    </div>
                    <Space direction="vertical" style={{ width: '100%' }} size={6}>
                      <Space.Compact style={{ width: '100%' }}>
                        <InputNumber
                          size="small"
                          min={1}
                          value={goldAmount}
                          onChange={(v) => setGoldAmount(v || 1)}
                          style={{ width: '100%' }}
                        />
                        <Button
                          size="small"
                          icon={<CreditCardOutlined />}
                          onClick={() => startTask('create_gold_qr')}
                          disabled={actionDisabled('create_gold_qr')}
                        >
                          充值鱼翅
                        </Button>
                      </Space.Compact>
                      <InputNumber
                        size="small"
                        min={1}
                        value={giftCount}
                        onChange={(v) => setGiftCount(v || 1)}
                        addonAfter="赠送数量"
                        style={{ width: '100%' }}
                      />
                      <div style={actionGridStyle}>
                        {renderActionButton('donate_esports_chicken_gift', 'primary')}
                        {renderActionButton('donate_esports_firework_gift', 'primary')}
                      </div>
                    </Space>
                  </div>
                </div>
              ) : (
                <div style={compactOperationGridStyle}>
                  <div style={sectionStyle}>
                    <div style={sectionTitleStyle}>
                      <ShoppingOutlined />
                      <span>兑换</span>
                    </div>
                    <Space direction="vertical" style={{ width: '100%' }} size={6}>
                      <Select
                        size="small"
                        value={selectedGoodsId || undefined}
                        onChange={setSelectedGoodsId}
                        options={goods.map((item) => ({ value: item.commodity_id, label: goodsLabel(item) }))}
                        placeholder="选择兑换商品"
                        showSearch
                        optionFilterProp="label"
                        style={{ width: '100%' }}
                      />
                      <div style={actionGridStyle}>
                        {renderActionButton('refresh_goods')}
                        {renderActionButton('exchange_goods', 'primary')}
                      </div>
                    </Space>
                  </div>

                  <div style={sectionStyle}>
                    <div style={sectionTitleStyle}>
                      <CreditCardOutlined />
                      <span>开通、充值与送礼</span>
                    </div>
                    <Space direction="vertical" style={{ width: '100%' }} size={6}>
                      {renderActionButton('create_elite_qr', 'primary')}
                      <Space.Compact style={{ width: '100%' }}>
                        <InputNumber
                          size="small"
                          min={1}
                          value={goldAmount}
                          onChange={(v) => setGoldAmount(v || 1)}
                          style={{ width: '100%' }}
                        />
                        <Button
                          size="small"
                          icon={<CreditCardOutlined />}
                          onClick={() => startTask('create_gold_qr')}
                          disabled={actionDisabled('create_gold_qr')}
                        >
                          充值鱼翅
                        </Button>
                      </Space.Compact>
                      <Space.Compact style={{ width: '100%' }}>
                        <InputNumber
                          size="small"
                          min={1}
                          value={giftCount}
                          onChange={(v) => setGiftCount(v || 1)}
                          style={{ width: '100%' }}
                        />
                        <Button
                          size="small"
                          icon={<GiftOutlined />}
                          onClick={() => startTask('donate_elite_gift')}
                          disabled={actionDisabled('donate_elite_gift')}
                        >
                          赠送精英令
                        </Button>
                      </Space.Compact>
                    </Space>
                  </div>
                </div>
              )}
              <Text type="secondary" style={{ fontSize: 12 }}>
                勾选账号后可批量执行；账号行右键可执行绑定与查询动作。
              </Text>
            </Space>
          </Card>
        </div>
      </div>

      {/* 右键菜单 */}
      {contextMenu && (
        <div style={contextMenuStyle} onClick={(e) => e.stopPropagation()}>
          <Card
            size="small"
            title={contextMenu.accountIds.length > 1 ? '批量动作' : '账号动作'}
            styles={{ body: { padding: 4 } }}
            style={{ width: 200, boxShadow: '0 4px 12px rgba(0,0,0,0.12)' }}
          >
            <Space direction="vertical" style={{ width: '100%' }}>
              {quickActions.map((item) => (
                <Button
                  key={item.key} block size="small" type="text"
                  icon={item.icon}
                  onClick={() => runContextAction(item.key)}
                  disabled={batchBusy}
                  style={{ justifyContent: 'flex-start' }}
                >
                  {taskTypes[item.key] || item.key}
                </Button>
              ))}
            </Space>
          </Card>
        </div>
      )}

      {/* Import modal */}
      <Modal
        title="导入账号到操作台"
        open={importOpen}
        onCancel={() => setImportOpen(false)}
        footer={null}
        width={700}
      >
        <Space style={{ marginBottom: 12, width: '100%', justifyContent: 'space-between' }}>
          <Input
            placeholder="搜索账号/昵称" prefix={<SearchOutlined />}
            value={importSearch} onChange={(e) => setImportSearch(e.target.value)}
            style={{ width: 250 }}
          />
          <Space>
            <Button onClick={() => importAccounts(importPool.map((a) => a.id))}>
              导入全部 ({importPool.length})
            </Button>
            <Button type="primary" onClick={() => importAccounts(importSelectedIds)} disabled={importSelectedIds.length === 0}>
              导入选中 ({importSelectedIds.length})
            </Button>
          </Space>
        </Space>
        <Table
          rowKey="id" size="small" loading={importLoading}
          rowSelection={{ selectedRowKeys: importSelectedIds, onChange: (keys) => setImportSelectedIds(keys.map(Number)) }}
          dataSource={importPool}
          columns={[
            { title: '昵称', dataIndex: 'nickname', render: (_, r) => r.nickname || r.username },
            { title: 'UID', dataIndex: 'uid' },
            {
              title: '游戏名',
              dataIndex: isEsportsHandbook ? 'esports_game_name' : 'game_name',
              render: (v) => v || '-',
            },
            {
              title: isEsportsHandbook ? '电竞积分' : '积分',
              dataIndex: isEsportsHandbook ? 'esports_points' : 'points',
              render: (v) => v ?? '-',
            },
          ]}
          pagination={{ pageSize: 10 }}
          locale={{ emptyText: '没有可导入的账号（操作台已包含所有账号）' }}
        />
      </Modal>

      {/* Config modal */}
      <Modal
        title={isEsportsHandbook ? '电竞手册配置' : '精英宝典配置'}
        open={configOpen}
        onCancel={() => setConfigOpen(false)}
        onOk={() => configFormValues && saveConfig(configFormValues)}
        okText="保存" cancelText="取消"
        width={600}
      >
        {configFormValues && (
          <Space direction="vertical" style={{ width: '100%' }} size={8}>
            {(isEsportsHandbook ? [
              { label: '电竞手册 manualID', key: 'esports_manual_id' },
              { label: '电竞手册活动', key: 'esports_act_alias' },
              { label: '房间 ID', key: 'room_id' },
              { label: '冠军鸡腿礼物 ID', key: 'esports_chicken_gift_id' },
              { label: '冠军鸡腿皮肤 ID', key: 'esports_chicken_skin_id' },
              { label: '冠军烟花礼物 ID', key: 'esports_firework_gift_id' },
              { label: '冠军烟花皮肤 ID', key: 'esports_firework_skin_id' },
            ] : [
              { label: 'manualID', key: 'manual_id' },
              { label: 'RID', key: 'rid' },
              { label: '绑定二维码活动', key: 'bind_act_alias' },
              { label: '确认绑定活动', key: 'confirm_act_alias' },
              { label: '查询绑定活动(最新)', key: 'legacy_act_alias' },
              { label: '房间 ID', key: 'room_id' },
              { label: '精英令礼物 ID', key: 'gift_id' },
              { label: '皮肤 ID', key: 'skin_id' },
            ]).map(({ label, key }) => (
              <Space key={key} style={{ width: '100%', justifyContent: 'space-between' }}>
                <Text>{label}</Text>
                <Input
                  style={{ width: 300 }}
                  value={String(configFormValues[key as keyof DouyuConfig] ?? '')}
                  onChange={(e) => setConfigFormValues((prev) => prev ? { ...prev, [key]: e.target.value } : prev)}
                />
              </Space>
            ))}
            {(isEsportsHandbook ? [
              { label: '电竞手册金额(分)', key: 'esports_amount' },
            ] : [
              { label: '宝典金额(分)', key: 'elite_amount' },
              { label: '鱼翅支付方式', key: 'gold_pay_type' },
            ]).map(({ label, key }) => (
              <Space key={key} style={{ width: '100%', justifyContent: 'space-between' }}>
                <Text>{label}</Text>
                <InputNumber
                  style={{ width: 300 }}
                  value={Number(configFormValues[key as keyof DouyuConfig]) || 0}
                  onChange={(v) => setConfigFormValues((prev) => prev ? { ...prev, [key]: v ?? 0 } : prev)}
                />
              </Space>
            ))}
          </Space>
        )}
      </Modal>

      {/* 电竞手册绑定弹窗：活动绑定与角色选择是独立流程。 */}
      <Modal
        title={
          <Space>
            <QrcodeOutlined />
            <span>绑定电竞手册角色</span>
          </Space>
        }
        open={!!esportsBindTask}
        onCancel={() => {
          setEsportsBindTask(null);
        }}
        footer={null}
        centered
        width={440}
      >
        {esportsBindTask && (
          <Space direction="vertical" style={{ width: '100%' }} size={14}>
            {esportsAccountName ? (
              <Text type="secondary">{esportsAccountName}</Text>
            ) : null}
            <Tag color={esportsBound ? 'green' : esportsConfirmRunning ? 'processing' : 'blue'}>
              {esportsStatusText}
            </Tag>

            {esportsRoleName ? (
              <div>
                <Text type="secondary" style={{ display: 'block', marginBottom: 4 }}>当前游戏角色</Text>
                <Text strong style={{ fontSize: 16 }}>{esportsRoleName}</Text>
                {esportsRoleLine ? (
                  <Text type="secondary" style={{ display: 'block', marginTop: 4 }}>{esportsRoleLine}</Text>
                ) : null}
              </div>
            ) : (
              <Text type="secondary">当前未检测到游戏角色，请先切换角色。</Text>
            )}

            {esportsRebind && esportsRoleName ? (
              <Text type="secondary">
                {esportsRebind.available ? '当前可换绑' : `可换绑时间：${esportsRebind.text}`}
              </Text>
            ) : null}

            {esportsQrUrl ? (
              <div style={{ textAlign: 'center' }}>
                <QRCode value={esportsQrUrl} size={220} bordered={false} />
                <Text type="secondary" style={{ display: 'block', marginTop: 8 }}>
                  请在腾讯页面选择角色，完成后查询最新角色。
                </Text>
              </div>
            ) : null}

            <Space wrap>
              <Button
                icon={<SearchOutlined />}
                onClick={queryEsportsRole}
                loading={esportsRoleQueryRunning}
                disabled={esportsConfirmRunning}
              >
                查询最新角色
              </Button>
              <Button
                icon={<QrcodeOutlined />}
                onClick={switchEsportsRole}
                disabled={!esportsCanChangeRole || esportsConfirmRunning}
              >
                切换角色
              </Button>
              <Button
                type="primary"
                icon={<CheckCircleOutlined />}
                onClick={confirmEsportsBindFromDialog}
                disabled={!esportsCanConfirm}
                loading={esportsConfirmRunning}
              >
                完成绑定
              </Button>
            </Space>
          </Space>
        )}
      </Modal>

      {/* 精英宝典绑定二维码弹窗。 */}
      <Modal
        title={
          <Space>
            <QrcodeOutlined />
            <span>{qrBindReady ? '已识别新角色' : qrStatusText}</span>
          </Space>
        }
        open={!!qrTask}
        onCancel={() => setQrTask(null)}
        footer={null}
        centered
        width={420}
      >
        {qrTask && (
          <div style={{ textAlign: 'center' }}>
            {qrAccountName && (
              <Text type="secondary" style={{ marginBottom: 12, display: 'block' }}>{qrAccountName}</Text>
            )}
            <Space direction="vertical" align="center" size={12}>
              <Tag color={qrBindReady ? 'gold' : qrBindPhase === 'role_timeout' ? 'orange' : 'processing'}>
                {qrStatusText}
              </Tag>
              {qrUrl ? (
                <QRCode value={qrUrl} size={220} bordered={false} />
              ) : qrBindReady ? (
                <Text type="secondary">绑定二维码暂未返回</Text>
              ) : qrTask?.status === 'running' ? (
                <Text type="secondary">二维码生成中...</Text>
              ) : (
                <Text type="secondary">本次未生成二维码</Text>
              )}
              {qrBindReady ? (
                <>
                  <Tag color="gold" style={{ fontSize: 16, padding: '6px 16px' }}>
                    {qrRoleName}
                  </Tag>
                  {qrRoleLine && <Text type="secondary">{qrRoleLine}</Text>}
                </>
              ) : qrCurrentRoleName ? (
                <>
                  <Tag color="blue" style={{ fontSize: 14, padding: '4px 12px' }}>
                    当前绑定: {qrCurrentRoleName}
                  </Tag>
                  {(qrCurrentAreaName || qrCurrentPlatName) && (
                    <Text type="secondary">
                      {[qrCurrentPlatName, qrCurrentAreaName].filter(Boolean).join(' - ')}
                    </Text>
                  )}
                </>
              ) : null}
              {qrBindSummary ? (
                <Text type="secondary" style={{ fontSize: 12, wordBreak: 'break-all' }}>
                  {qrBindSummary}
                </Text>
              ) : null}
              {qrQueryTask && qrQueryTask.id > (qrTask?.id || 0) && qrQueryTask.message ? (
                <Text type="secondary" style={{ fontSize: 12 }}>
                  最近查询: {qrQueryTask.message}
                </Text>
              ) : null}
              <Space>
                <Button
                  icon={<SearchOutlined />}
                  onClick={queryQrRole}
                  loading={qrQueryRunning}
                  disabled={qrAutoPolling}
                >
                  {qrAutoPolling ? '自动识别中' : '查询角色'}
                </Button>
                <Button
                  type="primary"
                  icon={<CheckCircleOutlined />}
                  onClick={confirmQrBind}
                  disabled={!qrBindReady}
                >
                  确认绑定
                </Button>
              </Space>
            </Space>
          </div>
        )}
      </Modal>

      {/* Pay QR Modal */}
      <Modal
        title={
          <Space>
            <CreditCardOutlined />
            <span>支付二维码</span>
          </Space>
        }
        open={!!payTask}
        onCancel={() => setPayTask(null)}
        footer={null}
        centered
        width={400}
      >
        {payTask && (
          <div style={{ textAlign: 'center' }}>
            {payAccountName && (
              <Text type="secondary" style={{ marginBottom: 12, display: 'block' }}>{payAccountName}</Text>
            )}
            <Space direction="vertical" align="center" size={12}>
              {payUrl ? (
                <QRCode value={payUrl} size={220} bordered={false} />
              ) : (
                <Text type="secondary">支付码生成中...</Text>
              )}
              <Text>{payTask.message || '请扫码支付'}</Text>
            </Space>
          </div>
        )}
      </Modal>
    </div>
  );
}
