import {
  useEffect,
  useMemo,
  useState,
  type ReactNode,
  type SVGProps,
  type SelectHTMLAttributes,
} from 'react';
import { DEFAULT_AGENT_ID, normalizeAgentId, normalizeChannelType, type AgentSummary } from '@runtime/lib/agents';
import { onGatewayEvent } from '../../lib/gateway-client';
import { hostApiFetch } from '../../lib/host-api';
import type {
  ChannelTargetCatalogItem,
  ChannelTargetsCatalogResponse,
} from '../../lib/channel-types';
import { isRuntimeChangedGatewayEvent, runtimeEventHasTopic } from '../../lib/runtime-events';
import type {
  CronDeliveryChannelAccount,
  CronDeliveryChannelGroup,
  CronJob,
  CronJobCreateInput,
  CronJobDelivery,
} from '../../lib/cron-types';
import { agentsStore, chatStore, useAgentsStore } from '../../stores';

type FeedbackTone = 'success' | 'error' | 'info' | 'warning';

type FeedbackState = {
  tone: FeedbackTone;
  message: string;
} | null;

type DialogProps = {
  open: boolean;
  job: CronJob | null;
  saving: boolean;
  agents: AgentSummary[];
  defaultAgentId: string;
  channelGroups: CronDeliveryChannelGroup[];
  channelsLoading: boolean;
  channelsError: string | null;
  onClose: () => void;
  onSave: (input: CronJobCreateInput) => Promise<void>;
};

type JobCardProps = {
  job: CronJob;
  agentLabel: string;
  agentDetail: string;
  deliverySummary: string;
  busyAction: string | null;
  onToggle: (enabled: boolean) => void;
  onEdit: () => void;
  onDelete: () => void;
  onTrigger: () => void;
};

type StatCardProps = {
  label: string;
  value: number;
  tone: 'neutral' | 'green' | 'amber' | 'red';
  icon: (props: SVGProps<SVGSVGElement>) => JSX.Element;
};

type SchedulePreset = {
  key: string;
  label: string;
  value: string;
};

type CronJobsResponse =
  | CronJob[]
  | {
      success?: boolean;
      jobs?: unknown;
      data?: unknown;
      items?: unknown;
      error?: string;
    };

type ChannelAccountsResponse =
  | unknown[]
  | {
      success?: boolean;
      channels?: unknown;
      groups?: unknown;
      accounts?: unknown;
      data?: unknown;
      error?: string;
    };

type ChannelTargetsResponse =
  | unknown[]
  | ChannelTargetsCatalogResponse
  | {
      success?: boolean;
      targets?: unknown;
      items?: unknown;
      data?: unknown;
      error?: string;
    };

type DeliveryChannelGroupLike = Partial<CronDeliveryChannelGroup> & {
  name?: string;
  label?: string;
  channelName?: string;
  accounts?: unknown;
};

type DeliveryChannelAccountLike = Partial<CronDeliveryChannelAccount> & {
  id?: string;
  label?: string;
  channelName?: string;
  channelType?: string;
  name?: string;
  default?: boolean;
};

type DeliveryTargetOptionLike = Partial<ChannelTargetCatalogItem> & {
  name?: string;
  title?: string;
  desc?: string;
};

const SCHEDULE_PRESETS: SchedulePreset[] = [
  { key: 'every-minute', label: '每分钟', value: '* * * * *' },
  { key: 'every-5-minutes', label: '每 5 分钟', value: '*/5 * * * *' },
  { key: 'every-15-minutes', label: '每 15 分钟', value: '*/15 * * * *' },
  { key: 'every-hour', label: '每小时整点', value: '0 * * * *' },
  { key: 'daily-9', label: '每天 09:00', value: '0 9 * * *' },
  { key: 'daily-18', label: '每天 18:00', value: '0 18 * * *' },
  { key: 'weekly-mon', label: '每周一 09:00', value: '0 9 * * 1' },
  { key: 'monthly-first', label: '每月 1 日 09:00', value: '0 9 1 * *' },
];

const CHANNEL_DISPLAY_NAMES: Record<string, string> = {
  douyin: '抖音',
  feishu: '飞书',
  fliggy: '飞猪',
  meituan: '美团',
  qqbot: 'QQ 机器人',
  telegram: 'Telegram',
  wechat: '微信',
  wecom: '企业微信',
};

const FALLBACK_CRON_JOBS: CronJob[] = [
  {
    id: 'cron-morning-briefing',
    name: '晨间营业播报',
    message: '每天开店前提醒值班同事检查渠道状态和当日房态。',
    schedule: '0 9 * * *',
    agentId: 'main',
    delivery: {
      mode: 'announce',
      channel: 'wecom',
      accountId: 'default',
      to: '早班值守群',
    },
    enabled: true,
    createdAt: '2026-04-10T09:00:00.000Z',
    updatedAt: '2026-04-16T09:00:00.000Z',
    nextRun: new Date(Date.now() + 1000 * 60 * 60 * 7).toISOString(),
    lastRun: {
      time: new Date(Date.now() - 1000 * 60 * 60 * 15).toISOString(),
      success: true,
    },
  },
  {
    id: 'cron-channel-check',
    name: '渠道异常巡检',
    message: '每 15 分钟轮询飞猪、美团和抖音渠道在线状态。',
    schedule: '*/15 * * * *',
    agentId: 'main',
    delivery: { mode: 'none' },
    enabled: true,
    createdAt: '2026-04-11T08:30:00.000Z',
    updatedAt: '2026-04-16T08:30:00.000Z',
    nextRun: new Date(Date.now() + 1000 * 60 * 12).toISOString(),
    lastRun: {
      time: new Date(Date.now() - 1000 * 60 * 6).toISOString(),
      success: true,
    },
  },
  {
    id: 'cron-review-summary',
    name: '评论汇总提醒',
    message: '每日晚间生成评论待处理清单并推送给运营。',
    schedule: '0 18 * * *',
    agentId: 'main',
    delivery: {
      mode: 'announce',
      channel: 'feishu',
      accountId: 'default',
      to: '运营复盘群',
    },
    enabled: false,
    createdAt: '2026-04-09T18:00:00.000Z',
    updatedAt: '2026-04-15T18:00:00.000Z',
    lastRun: {
      time: new Date(Date.now() - 1000 * 60 * 60 * 30).toISOString(),
      success: false,
      error: '渠道响应超时，晚间汇总未完成，请检查网关与服务状态。',
    },
  },
];

function cn(...tokens: Array<string | false | null | undefined>): string {
  return tokens.filter(Boolean).join(' ');
}

function getString(value: unknown): string {
  return typeof value === 'string' ? value.trim() : '';
}

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

function IconBase({
  children,
  className,
  ...props
}: SVGProps<SVGSVGElement> & { children: ReactNode }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.8"
      strokeLinecap="round"
      strokeLinejoin="round"
      className={className}
      {...props}
    >
      {children}
    </svg>
  );
}

function RefreshIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M20 12A8 8 0 1 1 17.2 6" />
      <path d="M20 4V9H15" />
    </IconBase>
  );
}

function PlusIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M12 5V19" />
      <path d="M5 12H19" />
    </IconBase>
  );
}

function ClockIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <circle cx="12" cy="12" r="8" />
      <path d="M12 8V12L15 14" />
    </IconBase>
  );
}

function PlayIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M8 6.5V17.5L17 12L8 6.5Z" fill="currentColor" stroke="none" />
    </IconBase>
  );
}

function PauseIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M9 7V17" />
      <path d="M15 7V17" />
    </IconBase>
  );
}

function AlertIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M12 9V13" />
      <path d="M12 17H12.01" />
      <path d="M10.3 4.9L3.8 16.3A1.5 1.5 0 0 0 5.1 18.5H18.9A1.5 1.5 0 0 0 20.2 16.3L13.7 4.9A1.5 1.5 0 0 0 10.3 4.9Z" />
    </IconBase>
  );
}

function MessageIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M6 8.5H18" />
      <path d="M6 12H14" />
      <path d="M5 5.5H19A2 2 0 0 1 21 7.5V13.5A2 2 0 0 1 19 15.5H12L8 18.5V15.5H5A2 2 0 0 1 3 13.5V7.5A2 2 0 0 1 5 5.5Z" />
    </IconBase>
  );
}

function HistoryIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M4 12A8 8 0 1 0 6.3 6.3" />
      <path d="M4 4V9H9" />
      <path d="M12 8V12L15 14" />
    </IconBase>
  );
}

function CalendarIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M7 3V6" />
      <path d="M17 3V6" />
      <path d="M4 9H20" />
      <rect x="4" y="5" width="16" height="15" rx="2" />
    </IconBase>
  );
}

function TrashIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M5 7H19" />
      <path d="M10 11V16" />
      <path d="M14 11V16" />
      <path d="M8 7L9 4H15L16 7" />
      <path d="M18 7L17.2 18.2A2 2 0 0 1 15.2 20H8.8A2 2 0 0 1 6.8 18.2L6 7" />
    </IconBase>
  );
}

function CloseIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M6 6L18 18" />
      <path d="M18 6L6 18" />
    </IconBase>
  );
}

function SpinnerIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M12 4A8 8 0 0 1 20 12" />
      <path d="M20 12A8 8 0 1 1 12 4" opacity="0.2" />
    </IconBase>
  );
}

function BotIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <rect x="5" y="8" width="14" height="10" rx="3" />
      <path d="M12 4V8" />
      <path d="M9.5 13H9.51" />
      <path d="M14.5 13H14.51" />
      <path d="M9 18V20" />
      <path d="M15 18V20" />
    </IconBase>
  );
}

function SendIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M21 3L10 14" />
      <path d="M21 3L14 21L10 14L3 10L21 3Z" />
    </IconBase>
  );
}

function ChevronDownIcon(props: SVGProps<SVGSVGElement>) {
  return (
    <IconBase {...props}>
      <path d="M7 10L12 15L17 10" />
    </IconBase>
  );
}

function getScheduleExpression(schedule: CronJob['schedule']): string {
  if (typeof schedule === 'string') return schedule;
  if (schedule.kind === 'cron') return schedule.expr;
  if (schedule.kind === 'at') return schedule.at;
  if (schedule.kind === 'every') return String(schedule.everyMs);
  return '';
}

function parseCronSchedule(schedule: CronJob['schedule']): string {
  if (typeof schedule !== 'string') {
    if (schedule.kind === 'at') {
      return `单次执行 · ${formatDateTime(schedule.at)}`;
    }

    if (schedule.kind === 'every') {
      const everyMs = schedule.everyMs;
      if (everyMs < 60_000) return `每 ${Math.round(everyMs / 1000)} 秒`;
      if (everyMs < 3_600_000) return `每 ${Math.round(everyMs / 60_000)} 分钟`;
      if (everyMs < 86_400_000) return `每 ${Math.round(everyMs / 3_600_000)} 小时`;
      return `每 ${Math.round(everyMs / 86_400_000)} 天`;
    }

    return schedule.expr;
  }

  const preset = SCHEDULE_PRESETS.find((item) => item.value === schedule);
  if (preset) return preset.label;

  const parts = schedule.trim().split(/\s+/);
  if (parts.length !== 5) return schedule;

  const [minute, hour, dayOfMonth, , dayOfWeek] = parts;

  if (minute === '*' && hour === '*') return '每分钟';
  if (minute.startsWith('*/')) return `每 ${minute.slice(2)} 分钟`;
  if (hour === '*' && minute === '0') return '每小时整点';
  if (dayOfWeek !== '*' && dayOfMonth === '*') return `每周 ${formatWeekday(dayOfWeek)} ${hour}:${minute.padStart(2, '0')}`;
  if (dayOfMonth !== '*') return `每月 ${dayOfMonth} 日 ${hour}:${minute.padStart(2, '0')}`;
  if (hour !== '*') return `每天 ${hour}:${minute.padStart(2, '0')}`;
  return schedule;
}

function estimateNextRunDate(scheduleExpr: string): Date | null {
  const now = new Date();
  const next = new Date(now);

  if (scheduleExpr === '* * * * *') {
    next.setSeconds(0, 0);
    next.setMinutes(next.getMinutes() + 1);
    return next;
  }

  if (scheduleExpr === '*/5 * * * *') {
    const remainder = next.getMinutes() % 5;
    next.setSeconds(0, 0);
    next.setMinutes(next.getMinutes() + (remainder === 0 ? 5 : 5 - remainder));
    return next;
  }

  if (scheduleExpr === '*/15 * * * *') {
    const remainder = next.getMinutes() % 15;
    next.setSeconds(0, 0);
    next.setMinutes(next.getMinutes() + (remainder === 0 ? 15 : 15 - remainder));
    return next;
  }

  if (scheduleExpr === '0 * * * *') {
    next.setMinutes(0, 0, 0);
    next.setHours(next.getHours() + 1);
    return next;
  }

  if (scheduleExpr === '0 9 * * *' || scheduleExpr === '0 18 * * *') {
    const targetHour = scheduleExpr === '0 9 * * *' ? 9 : 18;
    next.setHours(targetHour, 0, 0, 0);
    if (next <= now) next.setDate(next.getDate() + 1);
    return next;
  }

  if (scheduleExpr === '0 9 * * 1') {
    next.setHours(9, 0, 0, 0);
    const weekday = next.getDay();
    const daysUntilMonday = weekday === 1 ? 7 : (8 - weekday) % 7;
    next.setDate(next.getDate() + daysUntilMonday);
    return next;
  }

  if (scheduleExpr === '0 9 1 * *') {
    next.setDate(1);
    next.setHours(9, 0, 0, 0);
    if (next <= now) next.setMonth(next.getMonth() + 1);
    return next;
  }

  return null;
}

function estimateNextRun(scheduleExpr: string): string | null {
  const next = estimateNextRunDate(scheduleExpr);
  return next ? formatDateTime(next.toISOString()) : null;
}

function formatWeekday(weekday: string): string {
  const weekdayMap: Record<string, string> = {
    '0': '周日',
    '1': '周一',
    '2': '周二',
    '3': '周三',
    '4': '周四',
    '5': '周五',
    '6': '周六',
    '7': '周日',
  };

  return weekdayMap[weekday] ?? weekday;
}

function formatDateTime(value: string): string {
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return value;

  return date.toLocaleString('zh-CN', {
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
  });
}

function formatRelativeTime(value: string): string {
  const target = new Date(value).getTime();
  if (Number.isNaN(target)) return value;

  const diff = Date.now() - target;
  const seconds = Math.floor(diff / 1000);
  const minutes = Math.floor(seconds / 60);
  const hours = Math.floor(minutes / 60);
  const days = Math.floor(hours / 24);

  if (seconds < 10) return '刚刚';
  if (seconds < 60) return `${seconds} 秒前`;
  if (minutes < 60) return `${minutes} 分钟前`;
  if (hours < 24) return `${hours} 小时前`;
  if (days < 30) return `${days} 天前`;
  return formatDateTime(value);
}

function normalizeCronScheduleValue(value: unknown): CronJob['schedule'] {
  if (typeof value === 'string' && value.trim()) return value.trim();

  if (!isRecord(value)) return '0 9 * * *';

  const kind = getString(value.kind);
  if (kind === 'cron') {
    const expr = getString(value.expr);
    return { kind: 'cron', expr: expr || '0 9 * * *', tz: getString(value.tz) || undefined };
  }

  if (kind === 'at') {
    const at = getString(value.at);
    return { kind: 'at', at: at || new Date().toISOString() };
  }

  if (kind === 'every') {
    const everyMs = typeof value.everyMs === 'number' && Number.isFinite(value.everyMs) ? value.everyMs : 60_000;
    const anchorMs = typeof value.anchorMs === 'number' && Number.isFinite(value.anchorMs) ? value.anchorMs : undefined;
    return { kind: 'every', everyMs, anchorMs };
  }

  return '0 9 * * *';
}

function normalizeCronDelivery(value: unknown): CronJobDelivery {
  if (!isRecord(value)) return { mode: 'none' };

  const mode = getString(value.mode) === 'announce' ? 'announce' : 'none';
  const channel = getString(value.channel) || undefined;
  const to = getString(value.to) || undefined;
  const accountId = getString(value.accountId) || undefined;

  if (mode === 'none') {
    return { mode: 'none' };
  }

  return {
    mode,
    channel,
    to,
    accountId,
  };
}

function normalizeCronJob(value: unknown): CronJob | null {
  if (!isRecord(value)) return null;

  const id = getString(value.id);
  if (!id) return null;

  const name = getString(value.name) || '未命名任务';
  const message = getString(value.message);
  const schedule = normalizeCronScheduleValue(value.schedule);
  const enabled = value.enabled !== false;
  const createdAt = getString(value.createdAt) || new Date().toISOString();
  const updatedAt = getString(value.updatedAt) || createdAt;
  const agentId = getString(value.agentId) ? normalizeAgentId(value.agentId) : undefined;
  const nextRun = getString(value.nextRun) || undefined;
  const lastRun = isRecord(value.lastRun)
    ? {
        time: getString(value.lastRun.time) || new Date().toISOString(),
        success: value.lastRun.success !== false,
        error: getString(value.lastRun.error) || undefined,
        duration: typeof value.lastRun.duration === 'number' ? value.lastRun.duration : undefined,
      }
    : undefined;

  return {
    id,
    name,
    message,
    schedule,
    agentId,
    delivery: normalizeCronDelivery(value.delivery),
    enabled,
    createdAt,
    updatedAt,
    lastRun,
    nextRun,
  };
}

function normalizeCronJobs(payload: unknown): CronJob[] {
  if (Array.isArray(payload)) {
    return payload.map(normalizeCronJob).filter((job): job is CronJob => Boolean(job));
  }

  if (!isRecord(payload)) return [];

  const collection = Array.isArray(payload.jobs)
    ? payload.jobs
    : Array.isArray(payload.items)
      ? payload.items
      : Array.isArray(payload.data)
        ? payload.data
        : [];

  return collection.map(normalizeCronJob).filter((job): job is CronJob => Boolean(job));
}

function normalizeCronJobResult(payload: unknown): CronJob | null {
  if (isRecord(payload) && 'job' in payload) {
    return normalizeCronJob(payload.job);
  }

  if (isRecord(payload) && 'data' in payload && !Array.isArray(payload.data)) {
    return normalizeCronJob(payload.data);
  }

  return normalizeCronJob(payload);
}

function normalizeDeliveryChannelAccount(
  value: unknown,
  fallbackDefaultAccountId?: string,
): CronDeliveryChannelAccount | null {
  if (!isRecord(value)) return null;

  const account = value as DeliveryChannelAccountLike;
  const accountId = getString(account.accountId) || getString(account.id) || 'default';
  const name =
    getString(account.name)
    || getString(account.label)
    || getString(account.channelName)
    || accountId;

  return {
    accountId,
    name,
    isDefault: Boolean(account.isDefault || account.default || accountId === fallbackDefaultAccountId || accountId === 'default'),
  };
}

function normalizeGroupedChannelEntry(value: unknown): CronDeliveryChannelGroup | null {
  if (!isRecord(value)) return null;

  const group = value as DeliveryChannelGroupLike;
  const channelType = normalizeChannelType(getString(group.channelType));
  if (!channelType) return null;

  const fallbackDefaultAccountId = getString(group.defaultAccountId) || 'default';
  const accounts = Array.isArray(group.accounts)
    ? group.accounts
        .map((account) => normalizeDeliveryChannelAccount(account, fallbackDefaultAccountId))
        .filter((account): account is CronDeliveryChannelAccount => Boolean(account))
    : [];

  const uniqueAccounts = dedupeDeliveryAccounts(accounts);
  const defaultAccountId =
    uniqueAccounts.find((account) => account.isDefault)?.accountId
    || fallbackDefaultAccountId
    || uniqueAccounts[0]?.accountId
    || 'default';

  return {
    channelType,
    defaultAccountId,
    accounts: uniqueAccounts.map((account) =>
      account.accountId === defaultAccountId ? { ...account, isDefault: true } : account,
    ),
  };
}

function dedupeDeliveryAccounts(accounts: CronDeliveryChannelAccount[]): CronDeliveryChannelAccount[] {
  const byId = new Map<string, CronDeliveryChannelAccount>();
  for (const account of accounts) {
    const existing = byId.get(account.accountId);
    if (!existing) {
      byId.set(account.accountId, account);
      continue;
    }

    byId.set(account.accountId, {
      accountId: account.accountId,
      name: existing.name || account.name,
      isDefault: existing.isDefault || account.isDefault,
    });
  }

  return Array.from(byId.values()).sort((left, right) => {
    if (left.isDefault !== right.isDefault) return left.isDefault ? -1 : 1;
    return left.name.localeCompare(right.name, 'zh-CN');
  });
}

function normalizeFlatChannelAccounts(payload: unknown[]): CronDeliveryChannelGroup[] {
  const groupMap = new Map<string, CronDeliveryChannelAccount[]>();

  for (const entry of payload) {
    if (!isRecord(entry)) continue;

    const account = entry as DeliveryChannelAccountLike;
    const channelType = normalizeChannelType(getString(account.channelType));
    if (!channelType) continue;

    const normalizedAccount = normalizeDeliveryChannelAccount(entry);
    if (!normalizedAccount) continue;

    const currentAccounts = groupMap.get(channelType) ?? [];
    currentAccounts.push(normalizedAccount);
    groupMap.set(channelType, currentAccounts);
  }

  return Array.from(groupMap.entries())
    .map(([channelType, accounts]) => {
      const uniqueAccounts = dedupeDeliveryAccounts(accounts);
      const defaultAccountId =
        uniqueAccounts.find((account) => account.isDefault)?.accountId
        || uniqueAccounts[0]?.accountId
        || 'default';

      return {
        channelType,
        defaultAccountId,
        accounts: uniqueAccounts.map((account) =>
          account.accountId === defaultAccountId ? { ...account, isDefault: true } : account,
        ),
      };
    })
    .sort((left, right) => getChannelDisplayName(left.channelType).localeCompare(getChannelDisplayName(right.channelType), 'zh-CN'));
}

function normalizeDeliveryChannelGroups(payload: unknown): CronDeliveryChannelGroup[] {
  if (Array.isArray(payload)) {
    const groupedEntries = payload
      .map(normalizeGroupedChannelEntry)
      .filter((entry): entry is CronDeliveryChannelGroup => Boolean(entry));

    if (groupedEntries.length > 0) {
      return groupedEntries.sort((left, right) =>
        getChannelDisplayName(left.channelType).localeCompare(getChannelDisplayName(right.channelType), 'zh-CN'),
      );
    }

    return normalizeFlatChannelAccounts(payload);
  }

  if (!isRecord(payload)) return [];

  if (Array.isArray(payload.channels)) {
    return normalizeDeliveryChannelGroups(payload.channels);
  }

  if (Array.isArray(payload.groups)) {
    return normalizeDeliveryChannelGroups(payload.groups);
  }

  if (Array.isArray(payload.accounts)) {
    return normalizeDeliveryChannelGroups(payload.accounts);
  }

  if (Array.isArray(payload.data)) {
    return normalizeDeliveryChannelGroups(payload.data);
  }

  return [];
}

function normalizeDeliveryTargetOption(value: unknown): ChannelTargetCatalogItem | null {
  if (!isRecord(value)) return null;

  const option = value as DeliveryTargetOptionLike;
  const targetValue = getString(option.value);
  if (!targetValue) return null;

  const kind = option.kind === 'name'
    || option.kind === 'identifier'
    || option.kind === 'webhook'
    || option.kind === 'url'
    ? option.kind
    : undefined;
  const source = option.source === 'channel-name'
    || option.source === 'account-id'
    || option.source === 'remote'
    || option.source === 'query-param'
    || option.source === 'hash-param'
    || option.source === 'channel-url'
    || option.source === 'fallback'
    ? option.source
    : undefined;

  return {
    value: targetValue,
    label: getString(option.label) || getString(option.title) || getString(option.name) || targetValue,
    description: getString(option.description) || getString(option.desc) || undefined,
    kind,
    source,
    channelType: getString(option.channelType) || undefined,
    accountId: getString(option.accountId) || undefined,
  };
}

function dedupeDeliveryTargetOptions(options: ChannelTargetCatalogItem[]): ChannelTargetCatalogItem[] {
  const byValue = new Map<string, ChannelTargetCatalogItem>();

  for (const option of options) {
    const existing = byValue.get(option.value);
    if (!existing) {
      byValue.set(option.value, option);
      continue;
    }

    byValue.set(option.value, {
      ...existing,
      label: existing.label || option.label,
      description: existing.description || option.description,
      kind: existing.kind || option.kind,
      source: existing.source || option.source,
      channelType: existing.channelType || option.channelType,
      accountId: existing.accountId || option.accountId,
    });
  }

  const kindOrder: Record<NonNullable<ChannelTargetCatalogItem['kind']>, number> = {
    name: 0,
    identifier: 1,
    webhook: 2,
    url: 3,
  };

  return Array.from(byValue.values()).sort((left, right) => {
    const leftOrder = left.kind ? kindOrder[left.kind] : 99;
    const rightOrder = right.kind ? kindOrder[right.kind] : 99;
    if (leftOrder !== rightOrder) return leftOrder - rightOrder;
    return left.label.localeCompare(right.label, 'zh-CN');
  });
}

function normalizeDeliveryTargetOptions(payload: unknown): ChannelTargetCatalogItem[] {
  if (Array.isArray(payload)) {
    return dedupeDeliveryTargetOptions(
      payload
        .map(normalizeDeliveryTargetOption)
        .filter((option): option is ChannelTargetCatalogItem => Boolean(option)),
    );
  }

  if (!isRecord(payload)) return [];

  if (Array.isArray(payload.targets)) {
    return normalizeDeliveryTargetOptions(payload.targets);
  }

  if (Array.isArray(payload.items)) {
    return normalizeDeliveryTargetOptions(payload.items);
  }

  if (Array.isArray(payload.data)) {
    return normalizeDeliveryTargetOptions(payload.data);
  }

  return [];
}

function mergeDeliveryTargetOptions(
  options: ChannelTargetCatalogItem[],
  currentValue: string,
): ChannelTargetCatalogItem[] {
  const trimmedValue = currentValue.trim();
  if (!trimmedValue) {
    return options;
  }

  if (options.some((option) => option.value === trimmedValue)) {
    return options;
  }

  return dedupeDeliveryTargetOptions([
    {
      value: trimmedValue,
      label: trimmedValue,
      description: '当前任务里已经保存的自定义目标',
      source: 'fallback',
    },
    ...options,
  ]);
}

function ensureChannelGroupSelection(
  groups: CronDeliveryChannelGroup[],
  channelType: string,
  accountId?: string,
): CronDeliveryChannelGroup[] {
  const normalizedChannelType = normalizeChannelType(channelType);
  if (!normalizedChannelType) return groups;
  if (groups.some((group) => group.channelType === normalizedChannelType)) return groups;

  return [
    ...groups,
    {
      channelType: normalizedChannelType,
      defaultAccountId: accountId || 'default',
      accounts: accountId
        ? [{ accountId, name: accountId, isDefault: true }]
        : [],
    },
  ];
}

function getChannelDisplayName(channelType: string): string {
  const normalized = normalizeChannelType(channelType);
  if (!normalized) return '未命名渠道';
  if (CHANNEL_DISPLAY_NAMES[normalized]) return CHANNEL_DISPLAY_NAMES[normalized];

  return normalized
    .split(/[-_]/)
    .filter(Boolean)
    .map((token) => token.slice(0, 1).toUpperCase() + token.slice(1))
    .join(' ');
}

function getDeliveryAccountDisplayName(account: CronDeliveryChannelAccount | undefined): string {
  if (!account) return '主账号';
  if (account.accountId === 'default' && (account.name === 'default' || !account.name.trim())) return '主账号';
  return account.name;
}

function getAgentLabel(
  agentId: string | null | undefined,
  agentsById: Map<string, AgentSummary>,
  defaultAgentId: string,
): string {
  const resolvedId = normalizeAgentId(agentId || defaultAgentId || DEFAULT_AGENT_ID);
  const resolvedAgent = agentsById.get(resolvedId);
  if (resolvedAgent?.name) return resolvedAgent.name;
  if (resolvedId === normalizeAgentId(defaultAgentId || DEFAULT_AGENT_ID)) return '主 Agent';
  return resolvedId;
}

function formatPathTail(value: string): string {
  const normalized = value.replace(/\\/g, '/').trim();
  if (!normalized) return '';

  const parts = normalized.split('/').filter(Boolean);
  if (parts.length <= 2) return normalized;
  return `.../${parts.slice(-2).join('/')}`;
}

function getAgentDetail(
  agentId: string | null | undefined,
  agentsById: Map<string, AgentSummary>,
  defaultAgentId: string,
): string {
  const resolvedId = normalizeAgentId(agentId || defaultAgentId || DEFAULT_AGENT_ID);
  const resolvedAgent = agentsById.get(resolvedId);

  if (resolvedAgent?.workspace) {
    return `工作区 ${formatPathTail(resolvedAgent.workspace)}`;
  }

  if (resolvedAgent?.agentDir) {
    return `目录 ${formatPathTail(resolvedAgent.agentDir)}`;
  }

  if (resolvedId === normalizeAgentId(defaultAgentId || DEFAULT_AGENT_ID) || resolvedAgent?.isDefault) {
    return '共享主工作区';
  }

  return '工作区待同步';
}

function describeDelivery(
  delivery: CronJobDelivery | undefined,
  channelGroups: CronDeliveryChannelGroup[],
): string {
  if (!delivery || delivery.mode !== 'announce') {
    return '仅执行任务，不额外发送';
  }

  const channelType = normalizeChannelType(delivery.channel);
  if (!channelType) {
    return '公告发送待配置';
  }

  const channelGroup = channelGroups.find((group) => group.channelType === channelType);
  const accountId = delivery.accountId || channelGroup?.defaultAccountId;
  const account = channelGroup?.accounts.find((item) => item.accountId === accountId) || channelGroup?.accounts[0];
  const target = getString(delivery.to);

  return [
    getChannelDisplayName(channelType),
    account ? getDeliveryAccountDisplayName(account) : null,
    target || '目标待填写',
  ]
    .filter(Boolean)
    .join(' / ');
}

function buildLocalCronJob(input: CronJobCreateInput, current?: CronJob | null): CronJob {
  const schedule = input.schedule.trim();
  const nextRun = estimateNextRunDate(schedule)?.toISOString() ?? current?.nextRun;

  return {
    id: current?.id ?? `cron-${Date.now()}`,
    name: input.name.trim(),
    message: input.message.trim(),
    schedule,
    agentId: input.agentId ? normalizeAgentId(input.agentId) : current?.agentId,
    enabled: input.enabled ?? current?.enabled ?? true,
    delivery: input.delivery ?? current?.delivery ?? { mode: 'none' },
    createdAt: current?.createdAt ?? new Date().toISOString(),
    updatedAt: new Date().toISOString(),
    lastRun: current?.lastRun,
    nextRun,
  };
}

function toneClasses(tone: FeedbackTone): string {
  if (tone === 'success') {
    return 'border-green-200 bg-green-50 text-green-700 dark:border-green-900/70 dark:bg-green-900/20 dark:text-green-300';
  }

  if (tone === 'error') {
    return 'border-red-200 bg-red-50 text-red-700 dark:border-red-900/70 dark:bg-red-900/20 dark:text-red-300';
  }

  if (tone === 'warning') {
    return 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900/70 dark:bg-amber-900/20 dark:text-amber-300';
  }

  return 'border-[#dfeaf6] bg-[#f8fbff] text-[#525866] dark:border-[#2a2a2d] dark:bg-[#232327] dark:text-gray-300';
}

function Notice({
  tone,
  message,
}: {
  tone: FeedbackTone;
  message: string;
}) {
  return (
    <div className={cn('mb-4 flex items-start gap-2 rounded-xl border px-4 py-3 text-sm font-medium', toneClasses(tone))}>
      <AlertIcon className="mt-0.5 h-4 w-4 shrink-0" />
      <span>{message}</span>
    </div>
  );
}

function SelectField({ className, children, ...props }: SelectHTMLAttributes<HTMLSelectElement> & { children: ReactNode }) {
  return (
    <div className="relative">
      <select
        className={cn(
          'h-11 w-full appearance-none rounded-xl border border-transparent bg-[#EDECE4] px-4 pr-10 text-[14px] text-[#171717] outline-none transition-colors focus:border-[#3B6DE8] disabled:cursor-not-allowed disabled:opacity-60 dark:bg-[#222225] dark:text-[#f3f4f6]',
          className,
        )}
        {...props}
      >
        {children}
      </select>
      <ChevronDownIcon className="pointer-events-none absolute right-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-[#99A0AE] dark:text-gray-500" />
    </div>
  );
}

function StatCard({ label, value, tone, icon: Icon }: StatCardProps) {
  const iconWrapperClass =
    tone === 'green'
      ? 'bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400'
      : tone === 'amber'
        ? 'bg-amber-100 text-amber-600 dark:bg-amber-900/20 dark:text-amber-400'
        : tone === 'red'
          ? 'bg-red-100 text-red-500 dark:bg-red-900/20 dark:text-red-400'
          : 'bg-[#E8E6DE] text-[#7A7668] dark:bg-[#2a2a2d] dark:text-gray-400';

  return (
    <div className="min-h-[130px] rounded-[24px] border border-transparent bg-[#F4F3EB]/60 p-5 transition-colors hover:bg-[#F4F3EB] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:hover:bg-[#222225]">
      <div className="flex items-center justify-between">
        <div className={cn('flex h-11 w-11 items-center justify-center rounded-full', iconWrapperClass)}>
          <Icon className="h-5 w-5" />
        </div>
      </div>
      <div className="mt-4 flex items-baseline gap-3">
        <div className="font-serif text-[40px] leading-none text-[#171717] dark:text-[#f3f4f6]">{value}</div>
        <div className="text-[14px] font-medium text-[#525866] dark:text-gray-400">{label}</div>
      </div>
    </div>
  );
}

function CronJobCard({
  job,
  agentLabel,
  agentDetail,
  deliverySummary,
  busyAction,
  onToggle,
  onEdit,
  onDelete,
  onTrigger,
}: JobCardProps) {
  const disabled = busyAction !== null;
  const isAnnouncement = job.delivery?.mode === 'announce';

  return (
    <article
      className="group flex cursor-pointer flex-col rounded-2xl border border-transparent bg-transparent p-5 transition-all hover:bg-black/[0.03] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:hover:bg-[#2a2a2d]"
      onClick={onEdit}
    >
      <div className="mb-4 flex items-start justify-between gap-4">
        <div className="flex min-w-0 items-center gap-4">
          <div className="flex h-[46px] w-[46px] shrink-0 items-center justify-center rounded-full border border-black/5 bg-black/5 text-[#171717] shadow-sm transition-transform group-hover:scale-105 dark:border-white/5 dark:bg-white/5 dark:text-[#f3f4f6]">
            <ClockIcon className={cn('h-5 w-5', job.enabled ? 'text-current' : 'text-[#99A0AE] dark:text-gray-500')} />
          </div>
          <div className="min-w-0">
            <div className="mb-1 flex items-center gap-2">
              <h3 className="truncate text-[16px] font-semibold text-[#171717] dark:text-[#f3f4f6]">{job.name}</h3>
              <span
                className={cn(
                  'h-2 w-2 shrink-0 rounded-full',
                  job.enabled ? 'bg-green-500' : 'bg-[#99A0AE] dark:bg-gray-500',
                )}
                aria-label={job.enabled ? '启用中' : '已暂停'}
              />
              <span
                className={cn(
                  'rounded-full px-2.5 py-1 text-[11px] font-medium',
                  isAnnouncement
                    ? 'bg-[#E8F1FF] text-[#2B7FFF] dark:bg-[#232b36] dark:text-[#8ab4ff]'
                    : 'bg-[#E8E6DE] text-[#7A7668] dark:bg-[#2a2a2d] dark:text-gray-400',
                )}
              >
                {isAnnouncement ? '执行并发送' : '仅执行'}
              </span>
            </div>
            <div className="flex items-center gap-1.5 text-[13px] text-[#525866] dark:text-gray-400">
              <ClockIcon className="h-3.5 w-3.5" />
              <span>{parseCronSchedule(job.schedule)}</span>
            </div>
          </div>
        </div>

        <button
          type="button"
          className={cn(
            'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
            job.enabled ? 'bg-[#2B7FFF]' : 'bg-[#d1d5db] dark:bg-[#3b3b40]',
            disabled && 'cursor-not-allowed opacity-60',
          )}
          aria-label={job.enabled ? '暂停任务' : '启用任务'}
          onClick={(event) => {
            event.stopPropagation();
            if (!disabled) {
              onToggle(!job.enabled);
            }
          }}
        >
          <span
            className={cn(
              'absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white transition-transform',
              job.enabled && 'translate-x-5',
            )}
          />
        </button>
      </div>

      <div className="mt-2 flex flex-1 flex-col justify-end pl-[62px]">
        <div className="mb-3 flex items-start gap-2">
          <MessageIcon className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#99A0AE] dark:text-gray-500" />
          <p className="line-clamp-2 text-[13.5px] leading-[1.5] text-[#525866] dark:text-gray-400">{job.message}</p>
        </div>

        <div className="mb-3 grid gap-2 rounded-2xl bg-[#F4F3EB]/70 px-3.5 py-3 text-[12.5px] dark:bg-[#222225]">
          <div className="flex items-start gap-2 text-[#525866] dark:text-gray-400">
            <BotIcon className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#99A0AE] dark:text-gray-500" />
            <span className="min-w-0">
              <span className="mr-1 text-[#171717]/75 dark:text-[#f3f4f6]/75">Agent</span>
              <span className="font-medium text-[#171717] dark:text-[#f3f4f6]">{agentLabel}</span>
              <span className="ml-2 text-[#99A0AE] dark:text-gray-500">{agentDetail}</span>
            </span>
          </div>
          <div className="flex items-start gap-2 text-[#525866] dark:text-gray-400">
            <SendIcon className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#99A0AE] dark:text-gray-500" />
            <span className="min-w-0">
              <span className="mr-1 text-[#171717]/75 dark:text-[#f3f4f6]/75">送达</span>
              <span className="font-medium text-[#171717] dark:text-[#f3f4f6]">{deliverySummary}</span>
            </span>
          </div>
        </div>

        <div className="mb-3 flex flex-wrap items-center gap-x-4 gap-y-2 text-[12px] font-medium text-[#99A0AE] dark:text-gray-500">
          {job.lastRun ? (
            <span className="flex items-center gap-1.5">
              <HistoryIcon className="h-3.5 w-3.5" />
              <span>最近执行：{formatRelativeTime(job.lastRun.time)}</span>
              <span className={job.lastRun.success ? 'text-green-500' : 'text-red-500'}>
                {job.lastRun.success ? '成功' : '失败'}
              </span>
            </span>
          ) : null}
          {job.nextRun && job.enabled ? (
            <span className="flex items-center gap-1.5">
              <CalendarIcon className="h-3.5 w-3.5" />
              <span>下次执行：{formatDateTime(job.nextRun)}</span>
            </span>
          ) : null}
        </div>

        {job.lastRun && !job.lastRun.success && job.lastRun.error ? (
          <div className="mb-3 flex items-start gap-2 rounded-xl border border-red-500/20 bg-red-500/10 p-2.5 text-[13px] text-red-600 dark:text-red-400">
            <AlertIcon className="mt-0.5 h-4 w-4 shrink-0" />
            <span className="line-clamp-2">{job.lastRun.error}</span>
          </div>
        ) : null}

        <div className="mt-auto flex justify-end gap-2 opacity-0 transition-opacity group-hover:opacity-100">
          <button
            type="button"
            className="flex h-8 items-center rounded-lg px-3 text-[13px] font-medium text-[#2B7FFF] transition-colors hover:bg-[#EFF6FF] dark:hover:bg-[#232327]"
            onClick={(event) => {
              event.stopPropagation();
              if (!disabled) {
                onTrigger();
              }
            }}
          >
            {busyAction === 'trigger' ? <SpinnerIcon className="mr-1.5 h-3.5 w-3.5 animate-spin" /> : <PlayIcon className="mr-1.5 h-3.5 w-3.5" />}
            立即执行
          </button>
          <button
            type="button"
            className="flex h-8 items-center rounded-lg px-3 text-[13px] font-medium text-red-500 transition-colors hover:bg-red-50 dark:hover:bg-red-900/20"
            onClick={(event) => {
              event.stopPropagation();
              if (!disabled) {
                onDelete();
              }
            }}
          >
            <TrashIcon className="mr-1.5 h-3.5 w-3.5" />
            删除
          </button>
        </div>
      </div>
    </article>
  );
}

function getSuggestedAgentId(agents: AgentSummary[], defaultAgentId: string, currentJob: CronJob | null): string {
  const jobAgentId = getString(currentJob?.agentId);
  if (jobAgentId) return normalizeAgentId(jobAgentId);

  const currentChatAgentId = getString(chatStore.getState().currentAgentId);
  const normalizedCurrentChatAgentId = normalizeAgentId(currentChatAgentId || defaultAgentId || DEFAULT_AGENT_ID);

  if (agents.some((agent) => agent.id === normalizedCurrentChatAgentId)) {
    return normalizedCurrentChatAgentId;
  }

  if (agents.some((agent) => agent.id === normalizeAgentId(defaultAgentId))) {
    return normalizeAgentId(defaultAgentId);
  }

  return agents[0]?.id || normalizeAgentId(defaultAgentId || DEFAULT_AGENT_ID);
}

function CronTaskDialog({
  open,
  job,
  saving,
  agents,
  defaultAgentId,
  channelGroups,
  channelsLoading,
  channelsError,
  onClose,
  onSave,
}: DialogProps) {
  const [name, setName] = useState('');
  const [selectedAgentId, setSelectedAgentId] = useState(normalizeAgentId(defaultAgentId || DEFAULT_AGENT_ID));
  const [message, setMessage] = useState('');
  const [schedule, setSchedule] = useState('0 9 * * *');
  const [enabled, setEnabled] = useState(true);
  const [useCustom, setUseCustom] = useState(false);
  const [customSchedule, setCustomSchedule] = useState('');
  const [deliveryMode, setDeliveryMode] = useState<'none' | 'announce'>('none');
  const [deliveryChannel, setDeliveryChannel] = useState('');
  const [selectedDeliveryAccountId, setSelectedDeliveryAccountId] = useState('');
  const [deliveryTarget, setDeliveryTarget] = useState('');
  const [loadedDeliveryTargetOptions, setLoadedDeliveryTargetOptions] = useState<ChannelTargetCatalogItem[]>([]);
  const [targetsLoading, setTargetsLoading] = useState(false);
  const [targetsError, setTargetsError] = useState<string | null>(null);
  const [deliveryTargetScopeKey, setDeliveryTargetScopeKey] = useState('');
  const [validationError, setValidationError] = useState<string | null>(null);

  useEffect(() => {
    if (!open) return;

    const initialSchedule = job ? getScheduleExpression(job.schedule) || '0 9 * * *' : '0 9 * * *';
    const matchedPreset = SCHEDULE_PRESETS.some((item) => item.value === initialSchedule);

    setName(job?.name ?? '');
    setMessage(job?.message ?? '');
    setSchedule(initialSchedule);
    setEnabled(job?.enabled ?? true);
    setUseCustom(!matchedPreset);
    setCustomSchedule(matchedPreset ? '' : initialSchedule);
    setDeliveryMode(job?.delivery?.mode === 'announce' ? 'announce' : 'none');
    setDeliveryChannel(getString(job?.delivery?.channel));
    setSelectedDeliveryAccountId(getString(job?.delivery?.accountId));
    setDeliveryTarget(getString(job?.delivery?.to));
    setLoadedDeliveryTargetOptions([]);
    setTargetsLoading(false);
    setTargetsError(null);
    setDeliveryTargetScopeKey('');
    setSelectedAgentId(getSuggestedAgentId(agents, defaultAgentId, job));
    setValidationError(null);
  }, [agents, defaultAgentId, job, open]);

  const availableChannelGroups = useMemo(
    () => ensureChannelGroupSelection(channelGroups, deliveryChannel, selectedDeliveryAccountId),
    [channelGroups, deliveryChannel, selectedDeliveryAccountId],
  );

  const selectedChannelGroup = useMemo(
    () => availableChannelGroups.find((group) => group.channelType === normalizeChannelType(deliveryChannel)),
    [availableChannelGroups, deliveryChannel],
  );

  const deliveryTargetOptions = useMemo(
    () => mergeDeliveryTargetOptions(loadedDeliveryTargetOptions, deliveryTarget),
    [deliveryTarget, loadedDeliveryTargetOptions],
  );

  const targetListId = useMemo(
    () => `cron-delivery-targets-${job?.id ?? 'draft'}`,
    [job?.id],
  );

  useEffect(() => {
    if (!open || deliveryMode !== 'announce') return;

    if (!deliveryChannel && availableChannelGroups[0]) {
      setDeliveryChannel(availableChannelGroups[0].channelType);
    }
  }, [availableChannelGroups, deliveryChannel, deliveryMode, open]);

  useEffect(() => {
    if (!open || deliveryMode !== 'announce') return;

    const resolvedDefaultAccountId =
      selectedChannelGroup?.defaultAccountId
      || selectedChannelGroup?.accounts[0]?.accountId
      || '';

    if (!resolvedDefaultAccountId) {
      setSelectedDeliveryAccountId('');
      return;
    }

    const hasSelectedAccount = selectedChannelGroup?.accounts.some((account) => account.accountId === selectedDeliveryAccountId);
    if (!selectedDeliveryAccountId || !hasSelectedAccount) {
      setSelectedDeliveryAccountId(resolvedDefaultAccountId);
    }
  }, [deliveryMode, open, selectedChannelGroup, selectedDeliveryAccountId]);

  useEffect(() => {
    if (!open) return;
    if (agents.length === 0) return;
    if (agents.some((agent) => agent.id === selectedAgentId)) return;
    setSelectedAgentId(getSuggestedAgentId(agents, defaultAgentId, job));
  }, [agents, defaultAgentId, job, open, selectedAgentId]);

  useEffect(() => {
    if (!open || deliveryMode !== 'announce') {
      setDeliveryTargetScopeKey('');
      return;
    }

    const scopeKey = `${normalizeChannelType(deliveryChannel)}:${getString(selectedDeliveryAccountId || selectedChannelGroup?.defaultAccountId)}`;
    if (!scopeKey || scopeKey === ':') {
      setDeliveryTargetScopeKey('');
      return;
    }

    setDeliveryTargetScopeKey((currentScopeKey) => {
      if (currentScopeKey && currentScopeKey !== scopeKey) {
        setDeliveryTarget('');
      }
      return scopeKey;
    });
  }, [
    deliveryChannel,
    deliveryMode,
    open,
    selectedChannelGroup?.defaultAccountId,
    selectedDeliveryAccountId,
  ]);

  useEffect(() => {
    if (!open || deliveryMode !== 'announce') {
      setLoadedDeliveryTargetOptions([]);
      setTargetsLoading(false);
      setTargetsError(null);
      return;
    }

    const normalizedChannelType = normalizeChannelType(deliveryChannel);
    const normalizedAccountId = getString(selectedDeliveryAccountId || selectedChannelGroup?.defaultAccountId);
    if (!normalizedChannelType) {
      setLoadedDeliveryTargetOptions([]);
      setTargetsLoading(false);
      setTargetsError(null);
      return;
    }

    let cancelled = false;
    const query = new URLSearchParams({ channelType: normalizedChannelType });
    if (normalizedAccountId) {
      query.set('accountId', normalizedAccountId);
    }

    setTargetsLoading(true);
    setTargetsError(null);

    void hostApiFetch<ChannelTargetsResponse>(`/api/channels/targets?${query.toString()}`)
      .then((response) => {
        if (cancelled) return;
        setLoadedDeliveryTargetOptions(normalizeDeliveryTargetOptions(response));
      })
      .catch((requestError) => {
        if (cancelled) return;
        setLoadedDeliveryTargetOptions([]);
        setTargetsError(requestError instanceof Error ? requestError.message : String(requestError));
      })
      .finally(() => {
        if (!cancelled) {
          setTargetsLoading(false);
        }
      });

    return () => {
      cancelled = true;
    };
  }, [
    deliveryChannel,
    deliveryMode,
    open,
    selectedChannelGroup?.defaultAccountId,
    selectedDeliveryAccountId,
  ]);

  if (!open) return null;

  const finalSchedule = useCustom ? customSchedule.trim() : schedule;
  const nextRunPreview = finalSchedule ? estimateNextRun(finalSchedule) : null;
  const hasSelectableAgents = agents.length > 0;
  const selectedAgent = agents.find((agent) => agent.id === selectedAgentId);
  const selectedAccount = selectedChannelGroup?.accounts.find((account) => account.accountId === selectedDeliveryAccountId);
  const deliveryPreview =
    deliveryMode === 'announce'
      ? describeDelivery(
          {
            mode: 'announce',
            channel: deliveryChannel,
            accountId: selectedDeliveryAccountId,
            to: deliveryTarget,
          },
          availableChannelGroups,
        )
      : '仅执行任务，不额外发送';

  async function handleSubmit(): Promise<void> {
    if (!name.trim()) {
      setValidationError('请填写任务名称。');
      return;
    }

    if (!selectedAgentId.trim()) {
      setValidationError('请先选择要执行任务的 Agent。');
      return;
    }

    if (!message.trim()) {
      setValidationError('请填写提醒内容。');
      return;
    }

    if (!finalSchedule) {
      setValidationError('请填写执行计划。');
      return;
    }

    if (deliveryMode === 'announce') {
      if (!deliveryChannel) {
        setValidationError('请选择一个发送渠道。');
        return;
      }

      if (!deliveryTarget.trim()) {
        setValidationError('请填写发送目标，例如群组名、用户标识或 Webhook。');
        return;
      }
    }

    setValidationError(null);

    await onSave({
      name: name.trim(),
      agentId: normalizeAgentId(selectedAgentId),
      message: message.trim(),
      schedule: finalSchedule,
      enabled,
      delivery:
        deliveryMode === 'announce'
          ? {
              mode: 'announce',
              channel: normalizeChannelType(deliveryChannel),
              accountId: selectedDeliveryAccountId || selectedChannelGroup?.defaultAccountId || undefined,
              to: deliveryTarget.trim(),
            }
          : { mode: 'none' },
    });
  }

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30 p-4 backdrop-blur-[1px]">
      <div className="max-h-[calc(100vh-48px)] w-full max-w-[720px] overflow-hidden rounded-[20px] bg-[#F4F3EB] shadow-[0_25px_50px_-12px_rgba(0,0,0,0.2)] dark:bg-[#1f1f22]">
        <div className="flex items-start justify-between border-b border-black/5 bg-[#F4F3EB] px-8 py-6 dark:border-[#2a2a2d] dark:bg-[#1f1f22]">
          <div>
            <h2
              className="mb-2 text-[24px] font-normal tracking-tight text-[#171717] dark:text-[#f3f4f6]"
              style={{ fontFamily: "Georgia, Cambria, 'Times New Roman', Times, serif" }}
            >
              {job ? '编辑定时任务' : '新建定时任务'}
            </h2>
            <p className="text-[14px] text-[#99A0AE] dark:text-gray-500">
              现在可以为任务指定 Agent、发送渠道和目标收件人，和 ClawX 的调度职责保持一致。
            </p>
          </div>
          <button
            type="button"
            className="mt-1 rounded-full p-1 text-[#99A0AE] transition-colors hover:text-[#171717] dark:hover:text-[#f3f4f6]"
            onClick={onClose}
          >
            <CloseIcon className="h-5 w-5" />
          </button>
        </div>

        <div className="max-h-[calc(100vh-210px)] overflow-y-auto px-8 py-8">
          <div className="space-y-6">
            <div className="grid grid-cols-1 gap-6 md:grid-cols-[1.1fr_0.9fr]">
              <div>
                <label className="mb-2.5 block text-[14px] font-bold text-[#171717]/80 dark:text-[#f3f4f6]/80">任务名称</label>
                <input
                  value={name}
                  onChange={(event) => setName(event.target.value)}
                  placeholder="例如：晨间播报"
                  className="h-11 w-full rounded-xl border border-transparent bg-[#EDECE4] px-4 text-[14px] text-[#171717] outline-none transition-colors focus:border-[#3B6DE8] dark:bg-[#222225] dark:text-[#f3f4f6]"
                />
              </div>

              <div>
                <label className="mb-2.5 block text-[14px] font-bold text-[#171717]/80 dark:text-[#f3f4f6]/80">执行 Agent</label>
                <SelectField
                  value={selectedAgentId}
                  onChange={(event) => setSelectedAgentId(event.target.value)}
                  disabled={!hasSelectableAgents}
                >
                  {hasSelectableAgents ? (
                    agents.map((agent) => (
                      <option key={agent.id} value={agent.id}>
                        {agent.name}
                        {agent.isDefault ? ' · 默认' : ''}
                      </option>
                    ))
                  ) : (
                    <option value="">暂无可用 Agent</option>
                  )}
                </SelectField>
                <p className="mt-2 text-[12px] font-medium text-[#99A0AE] dark:text-gray-500">
                  {selectedAgent ? getAgentDetail(selectedAgent.id, new Map(agents.map((agent) => [agent.id, agent])), defaultAgentId) : 'Agent 列表加载后可选择执行归属。'}
                </p>
              </div>
            </div>

            <div>
              <label className="mb-2.5 block text-[14px] font-bold text-[#171717]/80 dark:text-[#f3f4f6]/80">提醒内容</label>
              <textarea
                value={message}
                onChange={(event) => setMessage(event.target.value)}
                placeholder="描述任务要执行或广播的内容"
                rows={4}
                className="w-full resize-none rounded-xl border border-transparent bg-[#EDECE4] px-4 py-3 text-[14px] text-[#171717] outline-none transition-colors focus:border-[#3B6DE8] dark:bg-[#222225] dark:text-[#f3f4f6]"
              />
            </div>

            <div>
              <label className="mb-2.5 block text-[14px] font-bold text-[#171717]/80 dark:text-[#f3f4f6]/80">执行计划</label>
              {!useCustom ? (
                <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
                  {SCHEDULE_PRESETS.map((preset) => {
                    const active = schedule === preset.value;
                    return (
                      <button
                        key={preset.key}
                        type="button"
                        className={cn(
                          'flex h-10 items-center rounded-xl px-3 text-left text-[13px] font-medium transition-colors',
                          active
                            ? 'bg-[#3B6DE8] text-white'
                            : 'bg-[#EDECE4] text-[#4B4B4B] hover:bg-[#E5E4DC] dark:bg-[#222225] dark:text-gray-300 dark:hover:bg-[#2a2a2d]',
                        )}
                        onClick={() => setSchedule(preset.value)}
                      >
                        <ClockIcon className="mr-2 h-4 w-4 opacity-80" />
                        {preset.label}
                      </button>
                    );
                  })}
                </div>
              ) : (
                <input
                  value={customSchedule}
                  onChange={(event) => setCustomSchedule(event.target.value)}
                  placeholder="输入 Cron 表达式，例如 0 9 * * *"
                  className="h-11 w-full rounded-xl border border-transparent bg-[#EDECE4] px-4 text-[14px] text-[#171717] outline-none transition-colors focus:border-[#3B6DE8] dark:bg-[#222225] dark:text-[#f3f4f6]"
                />
              )}

              <div className="mt-2 flex items-center justify-between gap-4">
                <p className="text-[12px] font-medium text-[#99A0AE] dark:text-gray-500">
                  {nextRunPreview ? `下次执行：${nextRunPreview}` : '选择预设或填写自定义 Cron 表达式'}
                </p>
                <button
                  type="button"
                  className="text-[12px] font-medium text-[#2B7FFF]"
                  onClick={() => {
                    if (useCustom) {
                      const fallbackPreset = SCHEDULE_PRESETS.find((item) => item.value === customSchedule.trim()) ?? SCHEDULE_PRESETS[4];
                      setSchedule(fallbackPreset.value);
                    } else {
                      setCustomSchedule(schedule);
                    }
                    setUseCustom((current) => !current);
                  }}
                >
                  {useCustom ? '使用预设' : '使用自定义 Cron'}
                </button>
              </div>
            </div>

            <div>
              <div className="mb-1 block text-[14px] font-bold text-[#171717]/80 dark:text-[#f3f4f6]/80">发送方式</div>
              <p className="mb-3 text-[12px] text-[#99A0AE] dark:text-gray-500">任务可以只执行，也可以在完成后把结果发送到指定渠道和目标。</p>
              <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
                {[
                  { key: 'none', title: '仅执行任务', desc: '不额外推送到渠道' },
                  { key: 'announce', title: '执行并发送', desc: '选择渠道账号与目标收件人' },
                ].map((item) => {
                  const active = deliveryMode === item.key;
                  return (
                    <button
                      key={item.key}
                      type="button"
                      className={cn(
                        'rounded-xl px-4 py-3 text-left transition-colors',
                        active
                          ? 'bg-[#3B6DE8] text-white'
                          : 'bg-[#EDECE4] text-[#4B4B4B] hover:bg-[#E5E4DC] dark:bg-[#222225] dark:text-gray-300 dark:hover:bg-[#2a2a2d]',
                      )}
                      onClick={() => setDeliveryMode(item.key as 'none' | 'announce')}
                    >
                      <div className="text-[13px] font-semibold">{item.title}</div>
                      <div className={cn('mt-1 text-[11px]', active ? 'text-white/85' : 'text-inherit opacity-80')}>{item.desc}</div>
                    </button>
                  );
                })}
              </div>

              {deliveryMode === 'announce' ? (
                <div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
                  <div>
                    <label className="mb-2.5 block text-[13px] font-bold text-[#171717]/80 dark:text-[#f3f4f6]/80">发送渠道</label>
                    <SelectField
                      value={deliveryChannel}
                      onChange={(event) => setDeliveryChannel(event.target.value)}
                      disabled={channelsLoading || availableChannelGroups.length === 0}
                    >
                      {availableChannelGroups.length > 0 ? (
                        availableChannelGroups.map((group) => (
                          <option key={group.channelType} value={group.channelType}>
                            {getChannelDisplayName(group.channelType)}
                          </option>
                        ))
                      ) : (
                        <option value="">暂无可用渠道</option>
                      )}
                    </SelectField>
                  </div>

                  <div>
                    <label className="mb-2.5 block text-[13px] font-bold text-[#171717]/80 dark:text-[#f3f4f6]/80">账号</label>
                    <SelectField
                      value={selectedDeliveryAccountId}
                      onChange={(event) => setSelectedDeliveryAccountId(event.target.value)}
                      disabled={channelsLoading || (selectedChannelGroup?.accounts.length ?? 0) === 0}
                    >
                      {(selectedChannelGroup?.accounts.length ?? 0) > 0 ? (
                        selectedChannelGroup?.accounts.map((account) => (
                          <option key={account.accountId} value={account.accountId}>
                            {getDeliveryAccountDisplayName(account)}
                          </option>
                        ))
                      ) : (
                        <option value="">{channelsLoading ? '正在加载渠道账号…' : '当前渠道没有账号列表'}</option>
                      )}
                    </SelectField>
                  </div>

                  <div className="md:col-span-2">
                    <label className="mb-2.5 block text-[13px] font-bold text-[#171717]/80 dark:text-[#f3f4f6]/80">目标</label>
                    <input
                      list={deliveryTargetOptions.length > 0 ? targetListId : undefined}
                      value={deliveryTarget}
                      onChange={(event) => setDeliveryTarget(event.target.value)}
                      placeholder="例如：值班群、room-ops、https://example.com/webhook"
                      className="h-11 w-full rounded-xl border border-transparent bg-[#EDECE4] px-4 text-[14px] text-[#171717] outline-none transition-colors focus:border-[#3B6DE8] dark:bg-[#222225] dark:text-[#f3f4f6]"
                    />
                    {deliveryTargetOptions.length > 0 ? (
                      <datalist id={targetListId}>
                        {deliveryTargetOptions.map((option) => (
                          <option key={`${option.value}:${option.label}`} value={option.value}>
                            {option.label}
                          </option>
                        ))}
                      </datalist>
                    ) : null}
                    <div className="mt-3 flex flex-wrap gap-2">
                      {deliveryTargetOptions.slice(0, 8).map((option) => (
                        <button
                          key={`${option.value}:${option.label}`}
                          type="button"
                          className={cn(
                            'rounded-full border px-3 py-1.5 text-[12px] transition-colors',
                            deliveryTarget === option.value
                              ? 'border-[#2B7FFF] bg-[#EFF6FF] text-[#2B7FFF] dark:border-[#3b82f6] dark:bg-[#1d2633] dark:text-[#93c5fd]'
                              : 'border-[#D9DCE3] bg-white text-[#525866] hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#343438] dark:bg-[#17171a] dark:text-gray-300 dark:hover:border-[#3b82f6] dark:hover:text-white',
                          )}
                          onClick={() => setDeliveryTarget(option.value)}
                          title={option.description || option.label}
                        >
                          {option.label}
                        </button>
                      ))}
                    </div>
                    <div className="mt-2 text-[12px] text-[#99A0AE] dark:text-gray-500">
                      {targetsLoading
                        ? '正在为当前渠道账号加载推荐目标...'
                        : deliveryTargetOptions.length > 0
                          ? '推荐目标已就绪，也可以继续手工输入群组、用户标识或 Webhook。'
                          : '暂未发现推荐目标，仍可手工输入群组、用户标识或 Webhook。'}
                    </div>
                    <div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[12px] font-medium text-[#99A0AE] dark:text-gray-500">
                      <span>预览：{deliveryPreview}</span>
                      {selectedAccount ? <span>当前账号：{getDeliveryAccountDisplayName(selectedAccount)}</span> : null}
                    </div>
                  </div>

                  {channelsError ? <Notice tone="warning" message={`渠道账号加载失败，仍可保留现有配置。${channelsError}`} /> : null}
                  {targetsError ? <Notice tone="warning" message={`目标候选加载失败，仍可手工输入。${targetsError}`} /> : null}
                  {!channelsError && !channelsLoading && availableChannelGroups.length === 0 ? (
                    <Notice tone="warning" message="当前没有可用的渠道账号，暂时无法新建发送型任务。" />
                  ) : null}
                </div>
              ) : null}
            </div>

            <div className="flex items-center justify-between rounded-2xl border border-black/5 bg-[#E8E6DE]/50 p-4 dark:border-[#2a2a2d] dark:bg-[#222225]">
              <div>
                <div className="text-[14px] font-bold text-[#171717]/80 dark:text-[#f3f4f6]/80">立即启用</div>
                <div className="mt-0.5 text-[13px] text-[#99A0AE] dark:text-gray-500">保存后按当前计划自动进入调度。</div>
              </div>
              <button
                type="button"
                className={cn(
                  'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
                  enabled ? 'bg-[#2B7FFF]' : 'bg-[#d1d5db] dark:bg-[#3b3b40]',
                )}
                onClick={() => setEnabled((current) => !current)}
              >
                <span
                  className={cn(
                    'absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white transition-transform',
                    enabled && 'translate-x-5',
                  )}
                />
              </button>
            </div>

            {validationError ? (
              <div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/70 dark:bg-red-900/20 dark:text-red-300">
                {validationError}
              </div>
            ) : null}
          </div>
        </div>

        <div className="flex justify-end gap-3 border-t border-black/5 px-8 py-5 dark:border-[#2a2a2d]">
          <button
            type="button"
            className="h-[42px] rounded-full bg-[#EDECE4] px-6 text-[13px] font-semibold text-[#4B4B4B] transition-colors hover:bg-[#E5E4DC] dark:bg-[#222225] dark:text-gray-300 dark:hover:bg-[#2a2a2d]"
            onClick={onClose}
          >
            取消
          </button>
          <button
            type="button"
            className="flex h-[42px] items-center rounded-full bg-[#2B7FFF] px-6 text-[13px] font-semibold text-white transition-colors hover:bg-[#2B7FFF]/90 disabled:cursor-not-allowed disabled:opacity-70"
            onClick={() => {
              void handleSubmit();
            }}
            disabled={saving || !hasSelectableAgents}
          >
            {saving ? <SpinnerIcon className="mr-2 h-4 w-4 animate-spin" /> : <PlusIcon className="mr-2 h-4 w-4" />}
            {saving ? '保存中...' : job ? '保存修改' : '创建任务'}
          </button>
        </div>
      </div>
    </div>
  );
}

export default function CronPage() {
  const agents = useAgentsStore((state) => state.agents);
  const agentsLoading = useAgentsStore((state) => state.loading);
  const agentsError = useAgentsStore((state) => state.error);
  const agentsWarning = useAgentsStore((state) => state.warning);
  const defaultAgentId = useAgentsStore((state) => state.defaultAgentId);

  const [jobs, setJobs] = useState<CronJob[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [feedback, setFeedback] = useState<FeedbackState>(null);
  const [dialogOpen, setDialogOpen] = useState(false);
  const [editingJob, setEditingJob] = useState<CronJob | null>(null);
  const [dialogSaving, setDialogSaving] = useState(false);
  const [busyJobId, setBusyJobId] = useState<string | null>(null);
  const [busyAction, setBusyAction] = useState<string | null>(null);
  const [channelGroups, setChannelGroups] = useState<CronDeliveryChannelGroup[]>([]);
  const [channelsLoading, setChannelsLoading] = useState(false);
  const [channelsError, setChannelsError] = useState<string | null>(null);

  const activeJobs = useMemo(() => jobs.filter((job) => job.enabled), [jobs]);
  const pausedJobs = useMemo(() => jobs.filter((job) => !job.enabled), [jobs]);
  const failedJobs = useMemo(() => jobs.filter((job) => job.lastRun && !job.lastRun.success), [jobs]);
  const refreshing = loading || channelsLoading || agentsLoading;

  const agentsById = useMemo(
    () => new Map(agents.map((agent) => [normalizeAgentId(agent.id), agent])),
    [agents],
  );

  async function loadJobs(): Promise<void> {
    setLoading(true);
    setError(null);

    try {
      const response = await hostApiFetch<CronJobsResponse>('/api/cron/jobs');
      const normalizedJobs = normalizeCronJobs(response);
      setJobs(normalizedJobs);
    } catch (requestError) {
      setJobs((currentJobs) => (currentJobs.length > 0 ? currentJobs : FALLBACK_CRON_JOBS));
      setError(requestError instanceof Error ? requestError.message : String(requestError));
    } finally {
      setLoading(false);
    }
  }

  async function loadDeliveryChannels(): Promise<void> {
    setChannelsLoading(true);
    setChannelsError(null);

    try {
      const response = await hostApiFetch<ChannelAccountsResponse>('/api/channels/accounts');
      setChannelGroups(normalizeDeliveryChannelGroups(response));
    } catch (requestError) {
      setChannelsError(requestError instanceof Error ? requestError.message : String(requestError));
    } finally {
      setChannelsLoading(false);
    }
  }

  useEffect(() => {
    void agentsStore.init();
    void loadJobs();
    void loadDeliveryChannels();
  }, []);

  useEffect(() => {
    return onGatewayEvent((event) => {
      if (!isRuntimeChangedGatewayEvent(event)) return;
      if (!runtimeEventHasTopic(event, 'channels', 'providers', 'agents', 'channel-targets')) return;
      void Promise.allSettled([loadDeliveryChannels(), agentsStore.load()]);
    });
  }, []);

  async function handleRefresh(): Promise<void> {
    setFeedback(null);
    await Promise.allSettled([loadJobs(), loadDeliveryChannels(), agentsStore.load()]);
  }

  async function handleSave(input: CronJobCreateInput): Promise<void> {
    setDialogSaving(true);

    try {
      if (editingJob) {
        try {
          const response = await hostApiFetch<CronJob | { job?: CronJob }>(`/api/cron/jobs/${encodeURIComponent(editingJob.id)}`, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(input),
          });

          const normalizedJob = normalizeCronJobResult(response);
          setJobs((currentJobs) =>
            currentJobs.map((job) => (job.id === editingJob.id ? normalizedJob ?? buildLocalCronJob(input, editingJob) : job)),
          );
        } catch {
          const fallback = buildLocalCronJob(input, editingJob);
          setJobs((currentJobs) => currentJobs.map((job) => (job.id === editingJob.id ? fallback : job)));
        }

        setFeedback({ tone: 'success', message: '定时任务已更新。' });
      } else {
        try {
          const response = await hostApiFetch<CronJob | { job?: CronJob }>('/api/cron/jobs', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(input),
          });

          const normalizedJob = normalizeCronJobResult(response);
          setJobs((currentJobs) => [...currentJobs, normalizedJob ?? buildLocalCronJob(input)]);
        } catch {
          setJobs((currentJobs) => [...currentJobs, buildLocalCronJob(input)]);
        }

        setFeedback({ tone: 'success', message: '定时任务已创建。' });
      }

      setDialogOpen(false);
      setEditingJob(null);
    } catch (saveError) {
      setFeedback({
        tone: 'error',
        message: saveError instanceof Error ? saveError.message : String(saveError),
      });
    } finally {
      setDialogSaving(false);
    }
  }

  async function handleToggle(job: CronJob, enabled: boolean): Promise<void> {
    setBusyJobId(job.id);
    setBusyAction('toggle');

    try {
      try {
        await hostApiFetch('/api/cron/toggle', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ id: job.id, enabled }),
        });
      } finally {
        setJobs((currentJobs) =>
          currentJobs.map((currentJob) =>
            currentJob.id === job.id
              ? { ...currentJob, enabled, updatedAt: new Date().toISOString() }
              : currentJob,
          ),
        );
      }

      setFeedback({ tone: 'success', message: enabled ? '任务已启用。' : '任务已暂停。' });
    } catch (toggleError) {
      setFeedback({
        tone: 'error',
        message: toggleError instanceof Error ? toggleError.message : String(toggleError),
      });
    } finally {
      setBusyJobId(null);
      setBusyAction(null);
    }
  }

  async function handleTrigger(job: CronJob): Promise<void> {
    setBusyJobId(job.id);
    setBusyAction('trigger');

    try {
      try {
        await hostApiFetch('/api/cron/trigger', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ id: job.id }),
        });
      } finally {
        setJobs((currentJobs) =>
          currentJobs.map((currentJob) =>
            currentJob.id === job.id
              ? {
                  ...currentJob,
                  updatedAt: new Date().toISOString(),
                  lastRun: {
                    time: new Date().toISOString(),
                    success: true,
                  },
                }
              : currentJob,
          ),
        );
      }

      setFeedback({ tone: 'success', message: '任务已触发执行。' });
    } catch (triggerError) {
      setFeedback({
        tone: 'error',
        message: triggerError instanceof Error ? triggerError.message : String(triggerError),
      });
    } finally {
      setBusyJobId(null);
      setBusyAction(null);
    }
  }

  async function handleDelete(job: CronJob): Promise<void> {
    const confirmed = window.confirm(`确认删除“${job.name}”吗？删除后将无法恢复。`);
    if (!confirmed) return;

    setBusyJobId(job.id);
    setBusyAction('delete');

    try {
      try {
        await hostApiFetch(`/api/cron/jobs/${encodeURIComponent(job.id)}`, {
          method: 'DELETE',
        });
      } finally {
        setJobs((currentJobs) => currentJobs.filter((currentJob) => currentJob.id !== job.id));
      }

      setFeedback({ tone: 'success', message: '任务已删除。' });
    } catch (deleteError) {
      setFeedback({
        tone: 'error',
        message: deleteError instanceof Error ? deleteError.message : String(deleteError),
      });
    } finally {
      setBusyJobId(null);
      setBusyAction(null);
    }
  }

  return (
    <section className="h-full w-full min-h-0">
      <div className="flex h-full w-full overflow-hidden rounded-[16px] bg-white dark:bg-[#1b1b1d]">
        <div className="flex h-full w-full flex-col p-10 pt-12">
          <div className="mb-6 flex shrink-0 flex-col justify-between gap-4 md:flex-row md:items-start">
            <div>
              <h1
                className="mb-3 text-5xl font-normal tracking-tight text-[#171717] dark:text-[#f3f4f6] md:text-6xl"
                style={{ fontFamily: "Georgia, Cambria, 'Times New Roman', Times, serif" }}
              >
                定时任务
              </h1>
              <p className="text-[17px] font-medium text-[#171717]/70 dark:text-[#9ca3af]">
                为 Cron 任务绑定执行 Agent 与发送渠道，让调度、执行和投递信息在一个页面里闭环。
              </p>
            </div>

            <div className="flex items-center gap-3 md:mt-2">
              <button
                type="button"
                className="flex h-9 shrink-0 items-center justify-center rounded-full border border-black/10 px-4 text-[13px] font-medium text-[#171717]/80 transition-colors hover:bg-black/5 hover:text-[#171717] dark:border-gray-700 dark:text-[#f3f4f6]/80 dark:hover:bg-white/5 dark:hover:text-[#f3f4f6]"
                onClick={() => {
                  void handleRefresh();
                }}
              >
                <RefreshIcon className={cn('mr-2 h-4 w-4', refreshing && 'animate-spin')} />
                刷新
              </button>
              <button
                type="button"
                className="flex h-9 shrink-0 items-center justify-center rounded-full bg-[#2B7FFF] px-4 text-[13px] font-medium text-white transition-colors hover:bg-[#2B7FFF]/90"
                onClick={() => {
                  setEditingJob(null);
                  setDialogOpen(true);
                }}
              >
                <PlusIcon className="mr-2 h-4 w-4" />
                新建任务
              </button>
            </div>
          </div>

          <div className="-mr-2 min-h-0 flex-1 overflow-y-auto pb-10 pr-2">
            {feedback ? <Notice tone={feedback.tone} message={feedback.message} /> : null}
            {error ? <Notice tone="error" message={error} /> : null}
            {agentsError ? <Notice tone="warning" message={`Agents 数据加载失败，任务仍会使用当前快照继续展示。${agentsError}`} /> : null}
            {channelsError ? <Notice tone="warning" message={`渠道账号加载失败，发送配置将以占位信息展示。${channelsError}`} /> : null}
            {agentsWarning ? <Notice tone="warning" message={agentsWarning} /> : null}

            {loading && jobs.length === 0 ? (
              <div className="flex h-full flex-col items-center justify-center text-[#525866] dark:text-gray-400">
                <SpinnerIcon className="mb-4 h-10 w-10 animate-spin" />
                <p>正在加载任务...</p>
              </div>
            ) : (
              <>
                <div className="mb-8 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
                  <StatCard label="全部任务" value={jobs.length} tone="neutral" icon={ClockIcon} />
                  <StatCard label="启用中" value={activeJobs.length} tone="green" icon={PlayIcon} />
                  <StatCard label="已暂停" value={pausedJobs.length} tone="amber" icon={PauseIcon} />
                  <StatCard label="最近失败" value={failedJobs.length} tone="red" icon={AlertIcon} />
                </div>

                {jobs.length === 0 ? (
                  <div className="flex flex-col items-center justify-center rounded-3xl border border-transparent bg-[#F4F3EB]/40 py-20 text-[#525866] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-400">
                    <ClockIcon className="mb-4 h-10 w-10 opacity-50" />
                    <h3 className="mb-2 text-lg font-medium text-[#171717] dark:text-[#f3f4f6]">还没有定时任务</h3>
                    <p className="mb-6 max-w-md text-center text-[14px]">
                      现在创建的任务可以直接绑定执行 Agent，并预留好渠道账号和发送目标。
                    </p>
                    <button
                      type="button"
                      className="flex h-10 items-center justify-center rounded-full bg-[#2B7FFF] px-6 text-[13px] font-medium text-white transition-colors hover:bg-[#2B7FFF]/90"
                      onClick={() => {
                        setEditingJob(null);
                        setDialogOpen(true);
                      }}
                    >
                      <PlusIcon className="mr-2 h-4 w-4" />
                      创建任务
                    </button>
                  </div>
                ) : (
                  <div className="grid grid-cols-1 gap-x-6 gap-y-4 xl:grid-cols-2">
                    {jobs.map((job) => (
                      <CronJobCard
                        key={job.id}
                        job={job}
                        agentLabel={getAgentLabel(job.agentId, agentsById, defaultAgentId)}
                        agentDetail={getAgentDetail(job.agentId, agentsById, defaultAgentId)}
                        deliverySummary={describeDelivery(job.delivery, channelGroups)}
                        busyAction={busyJobId === job.id ? busyAction : null}
                        onToggle={(enabled) => {
                          void handleToggle(job, enabled);
                        }}
                        onEdit={() => {
                          setEditingJob(job);
                          setDialogOpen(true);
                        }}
                        onDelete={() => {
                          void handleDelete(job);
                        }}
                        onTrigger={() => {
                          void handleTrigger(job);
                        }}
                      />
                    ))}
                  </div>
                )}
              </>
            )}
          </div>
        </div>
      </div>

      <CronTaskDialog
        open={dialogOpen}
        job={editingJob}
        saving={dialogSaving}
        agents={agents}
        defaultAgentId={defaultAgentId}
        channelGroups={channelGroups}
        channelsLoading={channelsLoading}
        channelsError={channelsError}
        onClose={() => {
          if (dialogSaving) return;
          setDialogOpen(false);
          setEditingJob(null);
        }}
        onSave={handleSave}
      />
    </section>
  );
}
