const {
  App: AntApp,
  Button,
  Card,
  Col,
  Collapse,
  ConfigProvider,
  DatePicker,
  Drawer,
  Form,
  Input,
  InputNumber,
  Layout,
  Modal,
  Row,
  Select,
  Space,
  Statistic,
  Switch,
  Table,
  Tabs,
  Tag,
  TimePicker,
  Typography,
  Empty,
  Divider,
} = antd;

const { Content } = Layout;
const { Title, Text, Paragraph } = Typography;
const { RangePicker } = DatePicker;
const { RangePicker: TimeRangePicker } = TimePicker;

if (dayjs.locale() !== "zh-cn") {
  dayjs.locale("zh-cn");
}

const DATE_PICKER_ZH_CN = {
  lang: {
    locale: "zh_CN",
    today: "今天",
    now: "此刻",
    backToToday: "返回今天",
    ok: "确定",
    clear: "清除",
    month: "月",
    year: "年",
    timeSelect: "选择时间",
    dateSelect: "选择日期",
    monthSelect: "选择月份",
    yearSelect: "选择年份",
    decadeSelect: "选择年代",
    previousMonth: "上个月",
    nextMonth: "下个月",
    previousYear: "上一年",
    nextYear: "下一年",
    previousDecade: "上一年代",
    nextDecade: "下一年代",
    previousCentury: "上一世纪",
    nextCentury: "下一世纪",
    yearFormat: "YYYY年",
    cellDateFormat: "D",
    monthBeforeYear: false,
    placeholder: "请选择日期",
    rangePlaceholder: ["开始日期", "结束日期"],
  },
  timePickerLocale: {
    placeholder: "请选择时间",
    rangePlaceholder: ["开始时间", "结束时间"],
  },
};

const DATE_RANGE_PRESETS = [
  { label: "今天", value: [dayjs(), dayjs()] },
  { label: "近7天", value: [dayjs().subtract(6, "day"), dayjs()] },
  { label: "近30天", value: [dayjs().subtract(29, "day"), dayjs()] },
];

const DEFAULT_SOURCE_CHAT_ID = 2420906261;
const DEFAULT_LINK = "https://t.me/maid_union";
const ROUTE_ROW_FIELD_BY_METRIC = {
  distanceMeters: "routeDistanceMeters",
  distanceKm: "routeDistanceKm",
  durationSeconds: "routeDurationSeconds",
  durationMinutes: "routeDurationMinutes",
};

function formatDateTime(value) {
  if (!value) return "-";
  return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
}

function formatDate(value) {
  if (!value) return "-";
  return dayjs(value).format("YYYY-MM-DD");
}

function formatDutyStateLabel(row) {
  if (row?.isOnDuty === true) return "在店";
  if (row?.isOnDuty === false) return "休息";
  return row?.dutyState === "off" ? "休息" : "在店";
}

function looksLikeCoordinateText(value) {
  return /^\s*-?\d+(?:\.\d+)?\s*,\s*-?\d+(?:\.\d+)?\s*$/.test(value || "");
}

function clearRouteFields(row) {
  return {
    ...row,
    routeOriginLocationId: null,
    routeDistanceMeters: null,
    routeDistanceKm: null,
    routeDurationSeconds: null,
    routeDurationMinutes: null,
    routeQueriedAt: null,
  };
}

function RefreshIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
      <path
        d="M13.5 2.8v3.7H9.8M2.5 13.2V9.5h3.7M3.1 7.2A5 5 0 0 1 12.6 5M12.9 8.8A5 5 0 0 1 3.4 11"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.6"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function buildQuery(params) {
  const query = new URLSearchParams();
  Object.entries(params || {}).forEach(([key, value]) => {
    if (value === undefined || value === null || value === "") return;
    query.set(key, String(value));
  });
  return query.toString();
}

function buildWsUrl(path, params) {
  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
  const query = buildQuery(params);
  return `${protocol}//${window.location.host}${path}${query ? `?${query}` : ""}`;
}

async function requestJson(url, options) {
  const response = await fetch(url, {
    headers: { "Content-Type": "application/json" },
    ...options,
  });
  const text = await response.text();
  const data = text ? JSON.parse(text) : null;
  if (!response.ok) {
    throw new Error(data?.error || data?.message || `请求失败: ${response.status}`);
  }
  return data;
}

function statusTag(status) {
  const mapping = {
    open: "success",
    connecting: "processing",
    closed: "warning",
    error: "error",
    idle: "default",
    running: "processing",
    starting: "processing",
    stopping: "warning",
    completed: "success",
    stopped: "default",
    interrupted: "warning",
    failed: "error",
    pending: "default",
    resolved: "success",
    schedule: "success",
    possible_schedule: "processing",
    mixed_chat: "warning",
    off: "default",
    working: "success",
  };

  return mapping[status] || "default";
}

function SummaryCards({ summary, loading }) {
  const cards = [
    { title: "原始消息", value: summary?.totalRawMessages || 0, suffix: "条" },
    { title: "已识别频道", value: summary?.channelCatalogRows || 0, suffix: "个" },
    { title: "发现链接", value: summary?.discoveredLinks || 0, suffix: "条" },
    { title: "排班消息", value: summary?.scheduleMessages || 0, suffix: "条" },
    { title: "店员记录", value: summary?.latestStaffEntries || 0, suffix: "条" },
    { title: "排班频道", value: summary?.scheduleChannels || 0, suffix: "个" },
  ];

  return (
    <Row gutter={[16, 16]} className="stat-grid">
      {cards.map((card) => (
        <Col xs={24} sm={12} lg={8} xl={4} key={card.title}>
          <Card className="stat-card" loading={loading}>
            <Statistic title={card.title} value={card.value} suffix={card.suffix} />
          </Card>
        </Col>
      ))}
    </Row>
  );
}

function App() {
  const [messageApi, contextHolder] = antd.message.useMessage();
  const [summary, setSummary] = React.useState(null);
  const [messages, setMessages] = React.useState([]);
  const [channels, setChannels] = React.useState([]);
  const [collectChannelOptions, setCollectChannelOptions] = React.useState([]);
  const [scheduleResult, setScheduleResult] = React.useState({
    items: [],
    total: 0,
    page: 1,
    pageSize: 20,
  });
  const [scheduleChannelResult, setScheduleChannelResult] = React.useState({
    items: [],
    total: 0,
    page: 1,
    pageSize: 20,
  });
  const [channelStaffDetails, setChannelStaffDetails] = React.useState({});
  const [channelRouteDetails, setChannelRouteDetails] = React.useState({});
  const [mapSavedLocations, setMapSavedLocations] = React.useState([]);
  const [selectedMapLocationId, setSelectedMapLocationId] = React.useState(null);
  const [routeRefreshing, setRouteRefreshing] = React.useState(false);
  const [channelAddressModal, setChannelAddressModal] = React.useState({
    open: false,
    row: null,
    addressName: "",
    addressText: "",
    location: "",
    city: "杭州",
  });
  const [mapLocationModal, setMapLocationModal] = React.useState({
    open: false,
    id: null,
    locationName: "",
    addressText: "",
    location: "",
    city: "杭州",
  });
  const [tasks, setTasks] = React.useState([]);
  const [loading, setLoading] = React.useState({
    summary: false,
    messages: false,
    channels: false,
    schedules: false,
    scheduleChannels: false,
    tasks: false,
  });
  const [liveConnectionState, setLiveConnectionState] = React.useState("connecting");
  const [logConnectionState, setLogConnectionState] = React.useState("idle");
  const [logAutoScroll, setLogAutoScroll] = React.useState(true);
  const [logState, setLogState] = React.useState({
    open: false,
    taskId: null,
    title: "",
    lines: [],
    loading: false,
  });
  const liveSocketRef = React.useRef(null);
  const liveRetryTimerRef = React.useRef(null);
  const logSocketRef = React.useRef(null);
  const logContainerRef = React.useRef(null);
  const routeOriginReloadedRef = React.useRef(null);

  const [channelFilter, setChannelFilter] = React.useState({
    sourceChatId: DEFAULT_SOURCE_CHAT_ID,
    category: "",
    keyword: "",
    limit: 100,
  });
  const [messageFilter, setMessageFilter] = React.useState({
    chatId: DEFAULT_SOURCE_CHAT_ID,
    keyword: "",
    limit: 100,
  });
  const [scheduleFilter, setScheduleFilter] = React.useState({
    dateRange: [dayjs().subtract(7, "day"), dayjs()],
    chatId: "",
    storeKeyword: "",
    staffKeyword: "",
    locationKeyword: "",
    descriptionKeyword: "",
    isOnDuty: "all",
    availableRange: [null, null],
    page: 1,
    pageSize: 20,
  });
  const [scheduleChannelFilter, setScheduleChannelFilter] = React.useState({
    dateRange: [dayjs().subtract(7, "day"), dayjs()],
    storeKeyword: "",
    staffKeyword: "",
    locationKeyword: "",
    descriptionKeyword: "",
    schedulePresence: "all",
    isOnDuty: "all",
    availableRange: [null, null],
    page: 1,
    pageSize: 20,
  });

  const [watchForm] = Form.useForm();
  const [backfillForm] = Form.useForm();
  const [discoverForm] = Form.useForm();
  const [probeForm] = Form.useForm();
  const [collectForm] = Form.useForm();
  const [workflowForm] = Form.useForm();
  const [scheduleFilterForm] = Form.useForm();
  const [scheduleChannelFilterForm] = Form.useForm();
  const collectMode = Form.useWatch("mode", collectForm) || "all";

  const withLoading = React.useCallback(async (key, task) => {
    setLoading((current) => ({ ...current, [key]: true }));
    try {
      return await task();
    } finally {
      setLoading((current) => ({ ...current, [key]: false }));
    }
  }, []);

  const loadSummary = React.useCallback(async () => {
    const query = buildQuery({ sourceChatId: DEFAULT_SOURCE_CHAT_ID });
    const data = await withLoading("summary", () => requestJson(`/api/summary?${query}`));
    setSummary(data);
  }, [withLoading]);

  const loadMessages = React.useCallback(async (override) => {
    const next = { ...messageFilter, ...(override || {}) };
    setMessageFilter(next);
    const query = buildQuery(next);
    const data = await withLoading("messages", () => requestJson(`/api/messages?${query}`));
    setMessages(data);
  }, [messageFilter, withLoading]);

  const loadChannels = React.useCallback(async (override) => {
    const next = { ...channelFilter, ...(override || {}) };
    setChannelFilter(next);
    const query = buildQuery(next);
    const data = await withLoading("channels", () => requestJson(`/api/channels?${query}`));
    setChannels(data);
  }, [channelFilter, withLoading]);

  const loadCollectChannelOptions = React.useCallback(async () => {
    const data = await requestJson("/api/collect-channel-options?limit=500");
    setCollectChannelOptions(data || []);
  }, []);

  const buildScheduleFilter = React.useCallback((override) => {
    const formValues = scheduleFilterForm.getFieldsValue();
    return {
      ...scheduleFilter,
      ...formValues,
      ...(override || {}),
    };
  }, [scheduleFilter, scheduleFilterForm]);

  const buildScheduleChannelFilter = React.useCallback((override) => {
    const formValues = scheduleChannelFilterForm.getFieldsValue();
    return {
      ...scheduleChannelFilter,
      ...formValues,
      ...(override || {}),
    };
  }, [scheduleChannelFilter, scheduleChannelFilterForm]);

  const buildScheduleQueryParams = React.useCallback((filter) => ({
    since: filter.dateRange?.[0]?.format("YYYY-MM-DD"),
    until: filter.dateRange?.[1]?.format("YYYY-MM-DD"),
    chatId: filter.chatId || null,
    storeKeyword: filter.storeKeyword,
    staffKeyword: filter.staffKeyword,
    locationKeyword: filter.locationKeyword,
    descriptionKeyword: filter.descriptionKeyword,
    schedulePresence: filter.schedulePresence === "all" ? null : filter.schedulePresence,
    isOnDuty: filter.isOnDuty === "all" ? null : filter.isOnDuty === "true",
    availableFrom: filter.availableRange?.[0]?.format("HH:mm"),
    availableUntil: filter.availableRange?.[1]?.format("HH:mm"),
    page: filter.page,
    pageSize: filter.pageSize,
  }), []);

  const loadSchedules = React.useCallback(async (override) => {
    const next = buildScheduleFilter(override);
    setScheduleFilter(next);
    const query = buildQuery(buildScheduleQueryParams(next));
    const data = await withLoading("schedules", () => requestJson(`/api/schedules?${query}`));
    setScheduleResult(data);
  }, [buildScheduleFilter, buildScheduleQueryParams, withLoading]);

  const loadScheduleChannelGroups = React.useCallback(async (override) => {
    const next = buildScheduleChannelFilter(override);
    setScheduleChannelFilter(next);
    setChannelStaffDetails({});
    setChannelRouteDetails({});
    const queryParams = {
      ...buildScheduleQueryParams(next),
      originLocationId: selectedMapLocationId,
      routeStrategy: "33",
    };
    const query = buildQuery(queryParams);
    const data = await withLoading("scheduleChannels", () => requestJson(`/api/schedule-channel-groups?${query}`));
    setScheduleChannelResult(data);
  }, [buildScheduleChannelFilter, buildScheduleQueryParams, selectedMapLocationId, withLoading]);

  const loadScheduleChannelStaff = React.useCallback(async (chatId) => {
    const current = buildScheduleChannelFilter({ page: 1, pageSize: 200 });
    setChannelStaffDetails((details) => ({
      ...details,
      [chatId]: { loading: true, items: details[chatId]?.items || [] },
    }));

    try {
      const query = buildQuery(buildScheduleQueryParams({ ...current, chatId }));
      const data = await requestJson(`/api/schedule-channel-groups/${chatId}/staff?${query}`);
      setChannelStaffDetails((details) => ({
        ...details,
        [chatId]: { loading: false, items: data?.items || [], total: data?.total || 0 },
      }));
    } catch (error) {
      setChannelStaffDetails((details) => ({
        ...details,
        [chatId]: { loading: false, items: details[chatId]?.items || [], error: error.message },
      }));
      messageApi.error(error.message);
    }
  }, [buildScheduleChannelFilter, buildScheduleQueryParams, messageApi]);

  const loadMapSavedLocations = React.useCallback(async () => {
    const data = await requestJson("/api/map/saved-locations");
    const items = data || [];
    setMapSavedLocations(items);
    setSelectedMapLocationId((current) => (
      items.some((item) => item.id === current) ? current : items[0]?.id || null
    ));
  }, []);

  const openChannelAddressModal = React.useCallback((row) => {
    const addressName = row.boundAddressName
      || (looksLikeCoordinateText(row.boundAddressText) ? row.channelLabel : row.boundAddressText)
      || row.channelLabel
      || "";
    setChannelAddressModal({
      open: true,
      row,
      addressName,
      addressText: row.boundAddressText || "",
      location: row.boundLocation || "",
      city: "杭州",
    });
  }, []);

  const closeChannelAddressModal = React.useCallback(() => {
    setChannelAddressModal((current) => ({ ...current, open: false }));
  }, []);

  const saveChannelAddress = React.useCallback(async () => {
    const row = channelAddressModal.row;
    if (!row) return;

    const addressName = channelAddressModal.addressName.trim();
    const addressText = channelAddressModal.addressText.trim();
    const location = channelAddressModal.location.trim();
    if (!addressName) {
      messageApi.error("地址名称不能为空");
      return;
    }
    if (!addressText && !location) {
      messageApi.error("地址或坐标至少填一个");
      return;
    }

    try {
      const saved = await requestJson("/api/channel-locations", {
        method: "POST",
        body: JSON.stringify({
          chatId: row.chatId,
          channelLabel: row.channelLabel,
          channelUrl: row.channelUrl,
          addressName,
          addressText,
          location,
          city: channelAddressModal.city.trim(),
        }),
      });

      messageApi.success("频道地址已保存");
      const nextRow = {
        ...row,
        boundAddressName: saved.addressName,
        boundAddressText: saved.addressText,
        boundLocation: saved.amapLocation,
        boundLongitude: saved.longitude,
        boundLatitude: saved.latitude,
        boundGeocodeStatus: saved.geocodeStatus,
        boundGeocodeInfo: saved.geocodeInfo,
      };

      setScheduleChannelResult((current) => ({
        ...current,
        items: (current.items || []).map((item) => item.chatId === row.chatId ? clearRouteFields(nextRow) : item),
      }));
      setChannelRouteDetails((details) => {
        const next = { ...details };
        delete next[row.chatId];
        return next;
      });
      closeChannelAddressModal();
    } catch (error) {
      messageApi.error(error.message);
    }
  }, [channelAddressModal, closeChannelAddressModal, messageApi]);

  const openMapLocationModal = React.useCallback((location) => {
    setMapLocationModal({
      open: true,
      id: location?.id || null,
      locationName: location?.locationName || "",
      addressText: location?.addressText || "",
      location: location?.amapLocation || "",
      city: "杭州",
    });
  }, []);

  const closeMapLocationModal = React.useCallback(() => {
    setMapLocationModal((current) => ({ ...current, open: false }));
  }, []);

  const saveMapLocation = React.useCallback(async () => {
    const locationName = mapLocationModal.locationName.trim();
    const addressText = mapLocationModal.addressText.trim();
    const location = mapLocationModal.location.trim();
    if (!locationName) {
      messageApi.error("位置名称不能为空");
      return;
    }
    if (!addressText && !location) {
      messageApi.error("地址或坐标至少填一个");
      return;
    }

    try {
      const saved = await requestJson("/api/map/saved-locations", {
        method: "POST",
        body: JSON.stringify({
          id: mapLocationModal.id,
          locationName,
          addressText,
          location,
          city: mapLocationModal.city.trim(),
        }),
      });
      messageApi.success("当前位置已保存");
      await loadMapSavedLocations();
      setSelectedMapLocationId(saved.id);
      routeOriginReloadedRef.current = null;
      setChannelRouteDetails({});
      setScheduleChannelResult((current) => ({
        ...current,
        items: (current.items || []).map(clearRouteFields),
      }));
      closeMapLocationModal();
    } catch (error) {
      messageApi.error(error.message);
    }
  }, [closeMapLocationModal, loadMapSavedLocations, mapLocationModal, messageApi]);

  const refreshChannelRoutesForRows = React.useCallback(async (rows, successText) => {
    if (!selectedMapLocationId) {
      messageApi.error("请先选择当前位置");
      return;
    }

    const chatIds = (rows || [])
      .filter((item) => item.boundLocation)
      .map((item) => item.chatId);
    if (chatIds.length === 0) {
      messageApi.warning("没有已绑定坐标的频道");
      return;
    }

    setRouteRefreshing(true);
    setChannelRouteDetails((details) => {
      const next = { ...details };
      chatIds.forEach((chatId) => {
        next[chatId] = { ...(next[chatId] || {}), loading: true, error: null };
      });
      return next;
    });

    try {
      const results = await requestJson("/api/map/channel-routes", {
        method: "POST",
        body: JSON.stringify({
          originLocationId: selectedMapLocationId,
          strategy: "33",
          chatIds,
        }),
      });
      setChannelRouteDetails((details) => {
        const next = { ...details };
        chatIds.forEach((chatId) => {
          if (!next[chatId]) {
            next[chatId] = { loading: false, route: null, error: "未返回路线结果" };
          }
        });
        (results || []).forEach((item) => {
          next[item.chatId] = { loading: false, route: item.error ? null : item, error: item.error || null };
        });
        return next;
      });
      setScheduleChannelResult((current) => ({
        ...current,
        items: (current.items || []).map((row) => {
          const route = (results || []).find((item) => item.chatId === row.chatId && !item.error);
          if (!route) return row;
          return {
            ...row,
            routeOriginLocationId: route.originLocationId,
            routeDistanceMeters: route.distanceMeters,
            routeDistanceKm: route.distanceKm,
            routeDurationSeconds: route.durationSeconds,
            routeDurationMinutes: route.durationMinutes,
            routeQueriedAt: route.queriedAt,
          };
        }),
      }));
      messageApi.success(successText);
    } catch (error) {
      messageApi.error(error.message);
    } finally {
      setRouteRefreshing(false);
    }
  }, [messageApi, selectedMapLocationId]);

  const refreshChannelRoutes = React.useCallback(() => (
    refreshChannelRoutesForRows(scheduleChannelResult.items || [], "距离和时间已刷新")
  ), [refreshChannelRoutesForRows, scheduleChannelResult.items]);

  const refreshSingleChannelRoute = React.useCallback((row) => (
    refreshChannelRoutesForRows([row], "该频道距离和时间已刷新")
  ), [refreshChannelRoutesForRows]);

  const loadTasks = React.useCallback(async () => {
    const data = await withLoading("tasks", () => requestJson("/api/tasks"));
    setTasks(data);
  }, [withLoading]);

  const refreshAll = React.useCallback(async () => {
    await Promise.all([loadSummary(), loadMessages(), loadChannels(), loadSchedules(), loadScheduleChannelGroups(), loadTasks(), loadCollectChannelOptions(), loadMapSavedLocations()]);
  }, [loadSummary, loadMessages, loadChannels, loadSchedules, loadScheduleChannelGroups, loadTasks, loadCollectChannelOptions, loadMapSavedLocations]);

  const submitScheduleSearch = React.useCallback((values) => {
    const next = {
      ...scheduleFilter,
      ...values,
      page: 1,
    };
    loadSchedules(next);
  }, [loadSchedules, scheduleFilter]);

  const resetScheduleSearch = React.useCallback(() => {
    const next = {
      dateRange: [dayjs().subtract(7, "day"), dayjs()],
      chatId: "",
      storeKeyword: "",
      staffKeyword: "",
      locationKeyword: "",
      descriptionKeyword: "",
      isOnDuty: "all",
      availableRange: [null, null],
      page: 1,
      pageSize: scheduleFilter.pageSize || 20,
    };

    scheduleFilterForm.setFieldsValue({
      dateRange: next.dateRange,
      storeKeyword: next.storeKeyword,
      staffKeyword: next.staffKeyword,
      locationKeyword: next.locationKeyword,
      descriptionKeyword: next.descriptionKeyword,
      isOnDuty: next.isOnDuty,
      availableRange: next.availableRange,
    });

    loadSchedules(next);
  }, [loadSchedules, scheduleFilter.pageSize, scheduleFilterForm]);

  const handleScheduleTableChange = React.useCallback((pagination) => {
    loadSchedules({
      page: pagination?.current || 1,
      pageSize: pagination?.pageSize || scheduleFilter.pageSize || 20,
    });
  }, [loadSchedules, scheduleFilter.pageSize]);

  const submitScheduleChannelSearch = React.useCallback((values) => {
    const next = {
      ...scheduleChannelFilter,
      ...values,
      page: 1,
    };
    loadScheduleChannelGroups(next);
  }, [loadScheduleChannelGroups, scheduleChannelFilter]);

  const resetScheduleChannelSearch = React.useCallback(() => {
    const next = {
      dateRange: [dayjs().subtract(7, "day"), dayjs()],
      storeKeyword: "",
      staffKeyword: "",
      locationKeyword: "",
      descriptionKeyword: "",
      schedulePresence: "all",
      isOnDuty: "all",
      availableRange: [null, null],
      page: 1,
      pageSize: scheduleChannelFilter.pageSize || 20,
    };

    scheduleChannelFilterForm.setFieldsValue({
      dateRange: next.dateRange,
      storeKeyword: next.storeKeyword,
      staffKeyword: next.staffKeyword,
      locationKeyword: next.locationKeyword,
      descriptionKeyword: next.descriptionKeyword,
      schedulePresence: next.schedulePresence,
      isOnDuty: next.isOnDuty,
      availableRange: next.availableRange,
    });

    loadScheduleChannelGroups(next);
  }, [loadScheduleChannelGroups, scheduleChannelFilter.pageSize, scheduleChannelFilterForm]);

  const handleScheduleChannelTableChange = React.useCallback((pagination, _filters, _sorter, extra) => {
    if (extra?.action === "sort") {
      return;
    }

    const nextPage = pagination?.current || 1;
    const nextPageSize = pagination?.pageSize || scheduleChannelFilter.pageSize || 20;
    if (nextPage === scheduleChannelResult.page && nextPageSize === scheduleChannelResult.pageSize) {
      return;
    }

    loadScheduleChannelGroups({
      page: nextPage,
      pageSize: nextPageSize,
    });
  }, [loadScheduleChannelGroups, scheduleChannelFilter.pageSize, scheduleChannelResult.page, scheduleChannelResult.pageSize]);

  React.useEffect(() => {
    watchForm.setFieldsValue({ link: DEFAULT_LINK, backfill: 0, join: false });
    backfillForm.setFieldsValue({
      link: DEFAULT_LINK,
      pageSize: 200,
      range: [dayjs().subtract(30, "day"), dayjs()],
    });
    discoverForm.setFieldsValue({
      link: DEFAULT_LINK,
      maxMessages: 20000,
      resolve: true,
      range: [dayjs().subtract(30, "day"), dayjs()],
    });
    probeForm.setFieldsValue({
      link: DEFAULT_LINK,
      candidateLimit: 60,
      includeProbed: false,
      range: [dayjs().subtract(30, "day"), dayjs()],
    });
    collectForm.setFieldsValue({
      mode: "all",
      selectedLinks: [],
      linksText: "",
      fromDatabase: true,
      join: false,
      range: [dayjs().subtract(7, "day"), dayjs()],
    });
    workflowForm.setFieldsValue({
      link: DEFAULT_LINK,
      sourceRange: [dayjs().subtract(30, "day"), dayjs()],
      collectRange: [dayjs().subtract(7, "day"), dayjs()],
    });
    scheduleFilterForm.setFieldsValue({
      dateRange: [dayjs().subtract(7, "day"), dayjs()],
      storeKeyword: "",
      staffKeyword: "",
      locationKeyword: "",
      descriptionKeyword: "",
      isOnDuty: "all",
      availableRange: [null, null],
    });
    scheduleChannelFilterForm.setFieldsValue({
      dateRange: [dayjs().subtract(7, "day"), dayjs()],
      storeKeyword: "",
      staffKeyword: "",
      locationKeyword: "",
      descriptionKeyword: "",
      schedulePresence: "all",
      isOnDuty: "all",
      availableRange: [null, null],
    });
    refreshAll();
  }, []);

  React.useEffect(() => {
    if (!selectedMapLocationId || routeOriginReloadedRef.current === selectedMapLocationId) {
      return;
    }

    routeOriginReloadedRef.current = selectedMapLocationId;
    setChannelRouteDetails({});
    setScheduleChannelResult((current) => ({
      ...current,
      items: (current.items || []).map(clearRouteFields),
    }));
    loadScheduleChannelGroups();
  }, [loadScheduleChannelGroups, selectedMapLocationId]);

  React.useEffect(() => {
    let cancelled = false;

    const connect = () => {
      if (cancelled) return;

      setLiveConnectionState("connecting");
      const socket = new WebSocket(buildWsUrl("/ws/live", { sourceChatId: DEFAULT_SOURCE_CHAT_ID }));
      liveSocketRef.current = socket;

      socket.onopen = () => {
        if (!cancelled) setLiveConnectionState("open");
      };

      socket.onmessage = (event) => {
        const data = JSON.parse(event.data);
        if (data.type === "summary") {
          setSummary(data.payload);
          return;
        }

        if (data.type === "tasks") {
          setTasks(data.payload || []);
        }
      };

      socket.onerror = () => {
        if (!cancelled) setLiveConnectionState("error");
      };

      socket.onclose = () => {
        if (liveSocketRef.current === socket) {
          liveSocketRef.current = null;
        }

        if (cancelled) return;
        setLiveConnectionState("closed");
        liveRetryTimerRef.current = window.setTimeout(connect, 3000);
      };
    };

    connect();

    return () => {
      cancelled = true;
      if (liveRetryTimerRef.current) {
        window.clearTimeout(liveRetryTimerRef.current);
        liveRetryTimerRef.current = null;
      }
      if (liveSocketRef.current) {
        liveSocketRef.current.close();
        liveSocketRef.current = null;
      }
    };
  }, []);

  const submitTask = React.useCallback(async (url, payload, successText) => {
    const result = await requestJson(url, {
      method: "POST",
      body: JSON.stringify(payload),
    });
    messageApi.success(successText);
    setTasks((current) => [result, ...current.filter((item) => item.id !== result.id)]);
    return result;
  }, [messageApi]);

  const closeLogDrawer = React.useCallback(() => {
    if (logSocketRef.current) {
      logSocketRef.current.close();
      logSocketRef.current = null;
    }

    setLogConnectionState("idle");
    setLogAutoScroll(true);
    setLogState((current) => ({ ...current, open: false, loading: false }));
  }, []);

  const openLogDrawer = React.useCallback((task) => {
    if (logSocketRef.current) {
      logSocketRef.current.close();
      logSocketRef.current = null;
    }

    setLogState({ open: true, taskId: task.id, title: task.title, lines: [], loading: true });
    setLogConnectionState("connecting");
    setLogAutoScroll(true);

    const socket = new WebSocket(buildWsUrl("/ws/task-log", { id: task.id }));
    logSocketRef.current = socket;

    socket.onopen = () => setLogConnectionState("open");
    socket.onerror = () => setLogConnectionState("error");
    socket.onclose = () => {
      if (logSocketRef.current === socket) {
        logSocketRef.current = null;
      }

      setLogState((current) => ({ ...current, loading: false }));
      setLogConnectionState((current) => current === "completed" ? "completed" : "closed");
    };

    socket.onmessage = (event) => {
      const data = JSON.parse(event.data);

      if (data.type === "taskLogMissing") {
        setLogState((current) => ({
          ...current,
          loading: false,
          lines: ["任务不存在或日志文件不可用"],
        }));
        setLogConnectionState("closed");
        return;
      }

      if (data.type === "taskLogInit") {
        setLogState((current) => ({
          ...current,
          open: true,
          taskId: data.taskId,
          title: data.task?.title || current.title,
          lines: data.lines || [],
          loading: false,
        }));
        return;
      }

      if (data.type === "taskLogAppend") {
        setLogState((current) => ({
          ...current,
          lines: [...current.lines, ...(data.lines || [])].slice(-600),
          loading: false,
        }));
        return;
      }

      if (data.type === "taskLogComplete") {
        setLogConnectionState("completed");
        if (data.task) {
          setTasks((current) => current.map((item) => item.id === data.task.id ? data.task : item));
        }
      }
    };
  }, []);

  React.useEffect(() => {
    if (!logState.open || !logAutoScroll) return;
    const container = logContainerRef.current;
    if (!container) return;

    const frame = window.requestAnimationFrame(() => {
      container.scrollTop = container.scrollHeight;
    });

    return () => window.cancelAnimationFrame(frame);
  }, [logState.lines, logState.open, logAutoScroll]);

  const handleLogScroll = React.useCallback((event) => {
    const element = event.currentTarget;
    const remaining = element.scrollHeight - element.scrollTop - element.clientHeight;
    setLogAutoScroll(remaining <= 24);
  }, []);

  const stopTask = React.useCallback(async (taskId) => {
    await requestJson(`/api/tasks/${taskId}/stop`, { method: "POST" });
    messageApi.success("任务已停止");
  }, [messageApi]);

  const taskColumns = [
    {
      title: "任务",
      dataIndex: "title",
      key: "title",
      render: (_, row) => (
        <Space direction="vertical" size={2}>
          <Text strong>{row.title}</Text>
          <Text type="secondary">{row.kind}</Text>
        </Space>
      ),
    },
    {
      title: "状态",
      dataIndex: "status",
      key: "status",
      render: (value) => <Tag color={statusTag(value)}>{value}</Tag>,
    },
    {
      title: "创建时间",
      dataIndex: "createdAt",
      key: "createdAt",
      render: formatDateTime,
    },
    {
      title: "PID",
      dataIndex: "pid",
      key: "pid",
      render: (value) => value || "-",
    },
    {
      title: "操作",
      key: "actions",
      render: (_, row) => (
        <Space wrap>
          <Button size="small" onClick={() => openLogDrawer(row)}>日志</Button>
          {["starting", "running", "stopping"].includes(row.status) ? (
            <Button size="small" danger onClick={() => stopTask(row.id)}>停止</Button>
          ) : null}
        </Space>
      ),
    },
  ];

  const channelColumns = [
    {
      title: "频道",
      key: "name",
      render: (_, row) => (
        <Space direction="vertical" size={2}>
          <a href={row.normalizedUrl} target="_blank" rel="noreferrer">{row.targetChatLabel || row.normalizedUrl}</a>
          <Text type="secondary">{row.normalizedUrl}</Text>
        </Space>
      ),
    },
    {
      title: "分类",
      dataIndex: "channelCategory",
      key: "channelCategory",
      render: (value) => <Tag color={statusTag(value)}>{value}</Tag>,
    },
    {
      title: "状态",
      dataIndex: "resolveStatus",
      key: "resolveStatus",
      render: (value) => <Tag color={statusTag(value)}>{value}</Tag>,
    },
    {
      title: "命中",
      key: "hits",
      render: (_, row) => `${row.scheduleMessageHits} / ${row.scheduleStaffHits}`,
    },
    {
      title: "发现次数",
      dataIndex: "discoveredCount",
      key: "discoveredCount",
    },
    {
      title: "样本",
      key: "sampleText",
      render: (_, row) => <div className="sample-text">{row.sampleText || row.categoryReason || "-"}</div>,
    },
  ];

  const getRouteValue = React.useCallback((row, key) => {
    const value = channelRouteDetails[row.chatId]?.route?.[key];
    if (value !== undefined && value !== null) return value;

    const rowKey = ROUTE_ROW_FIELD_BY_METRIC[key];
    return rowKey ? row[rowKey] : null;
  }, [channelRouteDetails]);

  const getRouteMetric = React.useCallback((row, key) => {
    const value = getRouteValue(row, key);
    if (value === undefined || value === null) return Number.POSITIVE_INFINITY;

    const numberValue = Number(value);
    return Number.isFinite(numberValue) ? numberValue : Number.POSITIVE_INFINITY;
  }, [getRouteValue]);

  const renderChannelAddressInfo = React.useCallback((row) => {
    const addressName = row.boundAddressName
      || (looksLikeCoordinateText(row.boundAddressText) ? null : row.boundAddressText);
    const addressTextVisible = row.boundAddressText
      && row.boundAddressText !== row.boundAddressName
      && !looksLikeCoordinateText(row.boundAddressText);
    const hasLocation = Boolean(row.boundLocation);
    const detail = channelRouteDetails[row.chatId] || {};

    return (
      <Space direction="vertical" size={2}>
        <Space size={6} align="center">
          <Button
            type="link"
            onClick={() => openChannelAddressModal(row)}
            style={{ height: "auto", padding: 0, fontWeight: 600 }}
          >
            {addressName || "设置地址"}
          </Button>
          {hasLocation ? (
            <Button
              type="text"
              size="small"
              shape="circle"
              loading={detail.loading}
              disabled={!selectedMapLocationId}
              title="刷新该频道距离和驾车时间"
              aria-label="刷新该频道距离和驾车时间"
              icon={<RefreshIcon />}
              onClick={(event) => {
                event.stopPropagation();
                refreshSingleChannelRoute(row);
              }}
            />
          ) : null}
        </Space>
        {addressTextVisible ? (
          <Text type="secondary">{row.boundAddressText}</Text>
        ) : null}
      </Space>
    );
  }, [channelRouteDetails, openChannelAddressModal, refreshSingleChannelRoute, selectedMapLocationId]);

  const renderChannelRouteDistance = React.useCallback((row) => {
    const detail = channelRouteDetails[row.chatId] || {};
    const distanceKm = getRouteValue(row, "distanceKm");
    if (detail.loading) return <Tag color="processing">刷新中</Tag>;
    if (detail.error) return <Text type="danger">{detail.error}</Text>;
    if (distanceKm !== undefined && distanceKm !== null) {
      return <Text strong>{distanceKm} km</Text>;
    }
    if (!row.boundLocation) return <Text type="secondary">先设置地址</Text>;

    return <Text type="secondary">未刷新</Text>;
  }, [channelRouteDetails, getRouteValue]);

  const renderChannelRouteDuration = React.useCallback((row) => {
    const detail = channelRouteDetails[row.chatId] || {};
    const durationMinutes = getRouteValue(row, "durationMinutes");
    if (detail.loading) return <Tag color="processing">刷新中</Tag>;
    if (detail.error) return <Text type="secondary">-</Text>;
    if (durationMinutes !== undefined && durationMinutes !== null) {
      return <Text strong>{durationMinutes} 分钟</Text>;
    }
    if (!row.boundLocation) return <Text type="secondary">先设置地址</Text>;

    return <Text type="secondary">未刷新</Text>;
  }, [channelRouteDetails, getRouteValue]);

  const scheduleColumns = [
    {
      title: "日期",
      dataIndex: "scheduleDate",
      key: "scheduleDate",
      render: formatDate,
      width: 140,
    },
    {
      title: "店名",
      dataIndex: "storeName",
      key: "storeName",
      width: 220,
    },
    {
      title: "店员",
      dataIndex: "staffName",
      key: "staffName",
      width: 110,
    },
    {
      title: "状态",
      key: "state",
      width: 120,
      render: (_, row) => (
        <Tag
          color={row?.isOnDuty === false ? "default" : "success"}
          style={{ fontSize: 14, lineHeight: "20px", paddingInline: 10, marginInlineEnd: 0 }}
        >
          {formatDutyStateLabel(row)}
        </Tag>
      ),
    },
    {
      title: "班次",
      key: "shift",
      width: 140,
      render: (_, row) => `${row.shiftStartText || "--"} ~ ${row.shiftEndText || "--"}`,
    },
    {
      title: "地点",
      dataIndex: "locationText",
      key: "locationText",
      width: 180,
      render: (value) => value || "-",
    },
    {
      title: "说明",
      dataIndex: "descriptionText",
      key: "descriptionText",
      width: 220,
      render: (value) => value || "-",
    },
  ];

  const scheduleChannelColumns = [
    {
      title: "排班频道",
      dataIndex: "channelLabel",
      key: "channelLabel",
      width: 200,
      render: (value, row) => (
        <Space direction="vertical" size={2}>
          <Text strong>{value}</Text>
          {row.channelUrl ? (
            <Text
              type="secondary"
              copyable={{
                text: row.channelUrl,
                tooltips: ["复制频道URL", "已复制"],
              }}
            >
              {row.channelUrl}
            </Text>
          ) : (
            <Text type="secondary">未记录频道URL</Text>
          )}
        </Space>
      ),
    },
    {
      title: "地址",
      key: "address",
      width: 100,
      render: (_, row) => renderChannelAddressInfo(row),
    },
    {
      title: "距离",
      key: "distance",
      width: 100,
      sorter: (a, b) => {
        const left = getRouteMetric(a, "distanceMeters");
        const right = getRouteMetric(b, "distanceMeters");
        return left === right ? 0 : left - right;
      },
      render: (_, row) => renderChannelRouteDistance(row),
    },
    {
      title: "驾车时间",
      key: "duration",
      width: 100,
      sorter: (a, b) => {
        const left = getRouteMetric(a, "durationSeconds");
        const right = getRouteMetric(b, "durationSeconds");
        return left === right ? 0 : left - right;
      },
      render: (_, row) => renderChannelRouteDuration(row),
    },
    {
      title: "最新排班",
      dataIndex: "latestScheduleDate",
      key: "latestScheduleDate",
      width: 100,
      render: formatDate,
    },
    {
      title: "店员",
      key: "staff",
      width: 120,
      render: (_, row) => `${row.workingStaffEntries} 在店 / ${row.offStaffEntries} 休息`,
    },
    {
      title: "排班天数",
      dataIndex: "scheduleDays",
      key: "scheduleDays",
      width: 70,
    },
    {
      title: "地点汇总",
      dataIndex: "locationsSummary",
      key: "locationsSummary",
      width: 200,
      ellipsis: true,
      render: (value) => <div className="sample-text">{value || "-"}</div>,
    },
    {
      title: "最近消息",
      dataIndex: "lastSourceSentAt",
      key: "lastSourceSentAt",
      width: 180,
      render: formatDateTime,
    },
  ];

  const channelStaffColumns = [
    {
      title: "日期",
      dataIndex: "scheduleDate",
      key: "scheduleDate",
      width: 120,
      render: formatDate,
    },
    {
      title: "店员",
      dataIndex: "staffName",
      key: "staffName",
      width: 120,
    },
    {
      title: "状态",
      key: "state",
      width: 100,
      render: (_, row) => (
        <Tag
          color={row?.isOnDuty === false ? "default" : "success"}
          style={{ fontSize: 14, lineHeight: "20px", paddingInline: 10, marginInlineEnd: 0 }}
        >
          {formatDutyStateLabel(row)}
        </Tag>
      ),
    },
    {
      title: "班次",
      key: "shift",
      width: 140,
      render: (_, row) => `${row.shiftStartText || "--"} ~ ${row.shiftEndText || "--"}`,
    },
    {
      title: "地点",
      dataIndex: "locationText",
      key: "locationText",
      width: 180,
      render: (value) => value || "-",
    },
    {
      title: "说明",
      dataIndex: "descriptionText",
      key: "descriptionText",
      render: (value) => value || "-",
    },
    {
      title: "来源时间",
      dataIndex: "sourceSentAt",
      key: "sourceSentAt",
      width: 180,
      render: formatDateTime,
    },
  ];

  const messageColumns = [
    {
      title: "时间",
      dataIndex: "sentAt",
      key: "sentAt",
      render: formatDateTime,
      width: 180,
    },
    {
      title: "群",
      dataIndex: "chatLabel",
      key: "chatLabel",
      width: 160,
    },
    {
      title: "发送者",
      dataIndex: "senderLabel",
      key: "senderLabel",
      width: 150,
      render: (value) => value || "-",
    },
    {
      title: "内容",
      dataIndex: "messageText",
      key: "messageText",
      render: (value) => <div className="sample-text">{value}</div>,
    },
  ];

  const selectedMapLocation = mapSavedLocations.find((item) => item.id === selectedMapLocationId) || null;

  return (
    <ConfigProvider
      locale={antd.locale?.zh_CN}
      theme={{
        token: {
          colorPrimary: "#0f8a70",
          colorBgLayout: "transparent",
          colorText: "#35220e",
          borderRadius: 18,
          fontFamily: '"Space Grotesk", "Noto Sans SC", sans-serif',
        },
      }}
    >
      <AntApp>
        {contextHolder}
        <Layout className="shell">
          <section className="hero glass">
            <div className="hero-kicker">Telegram · Collector · Console</div>
            <h1 className="hero-title">TgBot 群消息与排班操作台</h1>
            <p className="hero-subtitle">
              用于持续监听 `maid_union`、回填历史、发现频道、试跑候选排班源，并直接查看当前数据库里的原始消息、频道目录和排班结构化结果。
            </p>
          </section>

          <Content>
            <div className="glass section-card">
              <div className="toolbar">
                <Space direction="vertical" size={2}>
                  <Title level={4} style={{ margin: 0 }}>概览</Title>
                  <Text type="secondary">默认聚焦 `maid_union` 发现链路，同时展示全库排班结果。</Text>
                </Space>
                <Space wrap>
                  <Tag color={statusTag(liveConnectionState)}>
                    {liveConnectionState === "open" ? "实时流已连接" : `实时流 ${liveConnectionState}`}
                  </Tag>
                  <Button onClick={refreshAll}>手动刷新</Button>
                </Space>
              </div>
              <SummaryCards summary={summary} loading={loading.summary} />
              <Row gutter={[16, 16]}>
                <Col xs={24} lg={12}>
                  <Card className="stat-card" loading={loading.summary}>
                    <Statistic title="最早消息" value={summary?.firstMessageAt ? formatDateTime(summary.firstMessageAt) : "-"} />
                  </Card>
                </Col>
                <Col xs={24} lg={12}>
                  <Card className="stat-card" loading={loading.summary}>
                    <Statistic title="最新消息" value={summary?.lastMessageAt ? formatDateTime(summary.lastMessageAt) : "-"} />
                  </Card>
                </Col>
              </Row>
            </div>

            <div className="glass section-card" style={{ marginTop: 20 }}>
              <Tabs
                defaultActiveKey="tasks"
                items={[
                  {
                    key: "tasks",
                    label: "任务中心",
                    children: (
                      <Space direction="vertical" size={18} style={{ width: "100%" }}>
                        <div className="task-form-grid">
                          <Card title="自动发现并采集" className="table-card">
                            <Form
                              form={workflowForm}
                              layout="vertical"
                              onFinish={async (values) => {
                                await submitTask("/api/tasks/workflow", {
                                  link: values.link,
                                  sourceSince: values.sourceRange[0].format("YYYY-MM-DD"),
                                  sourceUntil: values.sourceRange[1].format("YYYY-MM-DD"),
                                  collectSince: values.collectRange[0].format("YYYY-MM-DD"),
                                  collectUntil: values.collectRange[1].format("YYYY-MM-DD"),
                                }, "全流程任务已启动");
                              }}
                            >
                              <Form.Item label="来源群链接" name="link" rules={[{ required: true }]}>
                                <Input />
                              </Form.Item>
                              <Form.Item label="来源群消息日期范围" name="sourceRange" rules={[{ required: true }]}>
                                <RangePicker style={{ width: "100%" }} />
                              </Form.Item>
                              <Form.Item label="排班频道消息日期范围" name="collectRange" rules={[{ required: true }]}>
                                <RangePicker style={{ width: "100%" }} />
                              </Form.Item>
                              <Paragraph type="secondary" style={{ marginTop: -4 }}>
                                默认会自动串行执行：来源群历史回填、频道发现、候选试跑、命中排班频道采集。这里的日期表示排班频道消息回看范围，最终排班日期以消息正文解析结果为准。
                              </Paragraph>
                              <Button htmlType="submit" type="primary" block>启动全流程</Button>
                            </Form>
                          </Card>

                          <Card title="实时监听" className="table-card">
                            <Form
                              form={watchForm}
                              layout="vertical"
                              onFinish={async (values) => {
                                await submitTask("/api/tasks/watch", values, "监听任务已启动");
                              }}
                            >
                              <Form.Item label="群链接" name="link" rules={[{ required: true }]}>
                                <Input placeholder="https://t.me/maid_union" />
                              </Form.Item>
                              <Form.Item label="启动时回填条数" name="backfill">
                                <InputNumber min={0} max={1000} style={{ width: "100%" }} />
                              </Form.Item>
                              <Form.Item label="必要时自动加入" name="join" valuePropName="checked">
                                <Switch />
                              </Form.Item>
                              <Button htmlType="submit" type="primary" block>启动监听</Button>
                            </Form>
                          </Card>

                          <Card title="历史回填" className="table-card">
                            <Form
                              form={backfillForm}
                              layout="vertical"
                              onFinish={async (values) => {
                                await submitTask("/api/tasks/backfill", {
                                  link: values.link,
                                  since: values.range[0].format("YYYY-MM-DD"),
                                  until: values.range[1].format("YYYY-MM-DD"),
                                  pageSize: values.pageSize,
                                }, "历史回填任务已启动");
                              }}
                            >
                              <Form.Item label="群链接" name="link" rules={[{ required: true }]}>
                                <Input />
                              </Form.Item>
                              <Form.Item label="排班频道消息日期范围" name="range" rules={[{ required: true }]}>
                                <RangePicker style={{ width: "100%" }} />
                              </Form.Item>
                              <Paragraph type="secondary" style={{ marginTop: -4 }}>
                                这里的日期表示排班频道消息回看范围。只要这段时间内的消息里出现排班内容，都会采集下来；最终排班日期以消息正文解析结果为准。
                              </Paragraph>
                              <Form.Item label="分页大小" name="pageSize">
                                <InputNumber min={20} max={200} style={{ width: "100%" }} />
                              </Form.Item>
                              <Button htmlType="submit" type="primary" block>启动回填</Button>
                            </Form>
                          </Card>

                          <Card title="频道发现" className="table-card">
                            <Form
                              form={discoverForm}
                              layout="vertical"
                              onFinish={async (values) => {
                                await submitTask("/api/tasks/discover", {
                                  link: values.link,
                                  since: values.range[0].format("YYYY-MM-DD"),
                                  until: values.range[1].format("YYYY-MM-DD"),
                                  maxMessages: values.maxMessages,
                                  resolve: values.resolve,
                                }, "频道发现任务已启动");
                              }}
                            >
                              <Form.Item label="来源群" name="link" rules={[{ required: true }]}>
                                <Input />
                              </Form.Item>
                              <Form.Item label="日期范围" name="range" rules={[{ required: true }]}>
                                <RangePicker style={{ width: "100%" }} />
                              </Form.Item>
                              <Form.Item label="最大扫描消息数" name="maxMessages">
                                <InputNumber min={100} max={500000} style={{ width: "100%" }} />
                              </Form.Item>
                              <Form.Item label="解析时顺带 resolve" name="resolve" valuePropName="checked">
                                <Switch />
                              </Form.Item>
                              <Button htmlType="submit" type="primary" block>启动发现</Button>
                            </Form>
                          </Card>

                          <Card title="候选试跑" className="table-card">
                            <Form
                              form={probeForm}
                              layout="vertical"
                              onFinish={async (values) => {
                                await submitTask("/api/tasks/probe", {
                                  link: values.link,
                                  since: values.range[0].format("YYYY-MM-DD"),
                                  until: values.range[1].format("YYYY-MM-DD"),
                                  candidateLimit: values.candidateLimit,
                                  includeProbed: values.includeProbed,
                                }, "候选试跑任务已启动");
                              }}
                            >
                              <Form.Item label="来源群" name="link" rules={[{ required: true }]}>
                                <Input />
                              </Form.Item>
                              <Form.Item label="日期范围" name="range" rules={[{ required: true }]}>
                                <RangePicker style={{ width: "100%" }} />
                              </Form.Item>
                              <Form.Item label="候选数量" name="candidateLimit">
                                <InputNumber min={1} max={500} style={{ width: "100%" }} />
                              </Form.Item>
                              <Form.Item label="包含已试跑候选" name="includeProbed" valuePropName="checked">
                                <Switch />
                              </Form.Item>
                              <Button htmlType="submit" type="primary" block>启动试跑</Button>
                            </Form>
                          </Card>

                          <Card title="排班采集" className="table-card">
                            <Form
                              form={collectForm}
                              layout="vertical"
                              onFinish={async (values) => {
                                await submitTask("/api/tasks/collect", {
                                  mode: values.mode,
                                  selectedLinks: values.selectedLinks || [],
                                  linksText: values.linksText || "",
                                  since: values.range[0].format("YYYY-MM-DD"),
                                  until: values.range[1].format("YYYY-MM-DD"),
                                  join: values.join,
                                  fromDatabase: values.fromDatabase,
                                }, "排班采集任务已启动");
                              }}
                            >
                              <Form.Item label="采集方式" name="mode" rules={[{ required: true }]}>
                                <Select
                                  options={[
                                    { value: "all", label: "全部已知排班频道" },
                                    { value: "selected", label: "从已有频道里选择" },
                                    { value: "manual", label: "手动输入链接" },
                                  ]}
                                />
                              </Form.Item>
                              {collectMode === "all" ? (
                                <Paragraph type="secondary" style={{ marginTop: -4 }}>
                                  将基于当前库里已识别出的排班频道执行采集，当前可用频道数：{collectChannelOptions.length}
                                </Paragraph>
                              ) : null}
                              {collectMode === "selected" ? (
                                <Form.Item label="选择已有排班频道" name="selectedLinks" rules={[{ required: true, message: "至少选择一个频道" }]}>
                                  <Select
                                    mode="multiple"
                                    allowClear
                                    showSearch
                                    optionFilterProp="label"
                                    placeholder="选择已有排班频道"
                                    options={collectChannelOptions.map((item) => ({
                                      value: item.normalizedUrl,
                                      label: `${item.displayLabel} | 排班 ${item.scheduleMessageHits} | 店员 ${item.scheduleStaffHits}`,
                                    }))}
                                  />
                                </Form.Item>
                              ) : null}
                              {collectMode === "manual" ? (
                                <Form.Item label="频道链接列表" name="linksText" rules={[{ required: true, message: "至少输入一个频道链接" }]}>
                                  <Input.TextArea rows={5} placeholder="每行一个链接" />
                                </Form.Item>
                              ) : null}
                              <Form.Item label="日期范围" name="range" rules={[{ required: true }]}>
                                <RangePicker style={{ width: "100%" }} />
                              </Form.Item>
                              <Space style={{ display: "flex", justifyContent: "space-between" }}>
                                <Form.Item label="从数据库原始消息重扫" name="fromDatabase" valuePropName="checked">
                                  <Switch />
                                </Form.Item>
                                <Form.Item label="必要时自动加入" name="join" valuePropName="checked">
                                  <Switch />
                                </Form.Item>
                              </Space>
                              <Button htmlType="submit" type="primary" block>启动采集</Button>
                            </Form>
                          </Card>
                        </div>

                        <Divider style={{ margin: "8px 0" }} />

                        <Card
                          className="table-card"
                          title="任务列表"
                          extra={
                            <Tag color={statusTag(liveConnectionState)}>
                              {liveConnectionState === "open" ? "任务实时更新中" : `任务流 ${liveConnectionState}`}
                            </Tag>
                          }
                        >
                          <Paragraph type="secondary" style={{ marginBottom: 12 }}>
                            这里只显示通过当前页面启动的任务，不包含服务器用 `docker compose` 直接启动的常驻 `watch` / `backfill` 容器。
                          </Paragraph>
                          <Table
                            rowKey="id"
                            loading={loading.tasks}
                            columns={taskColumns}
                            dataSource={tasks}
                            pagination={{ pageSize: 8 }}
                            scroll={{ x: 900 }}
                          />
                        </Card>
                      </Space>
                    ),
                  },
                  {
                    key: "channels",
                    label: "频道目录",
                    children: (
                      <Space direction="vertical" size={16} style={{ width: "100%" }}>
                        <Card className="table-card">
                          <Space wrap style={{ width: "100%", justifyContent: "space-between" }}>
                            <Input
                              value={channelFilter.keyword}
                              onChange={(event) => setChannelFilter((current) => ({ ...current, keyword: event.target.value }))}
                              placeholder="按链接/频道名/样本文本搜索"
                              style={{ width: 320 }}
                            />
                            <Space wrap>
                              <Select
                                value={channelFilter.category}
                                onChange={(value) => setChannelFilter((current) => ({ ...current, category: value }))}
                                style={{ width: 180 }}
                                options={[
                                  { value: "", label: "全部分类" },
                                  { value: "schedule", label: "schedule" },
                                  { value: "possible_schedule", label: "possible_schedule" },
                                  { value: "mixed_chat", label: "mixed_chat" },
                                  { value: "chat", label: "chat" },
                                  { value: "review", label: "review" },
                                  { value: "directory", label: "directory" },
                                  { value: "profile", label: "profile" },
                                  { value: "unresolved", label: "unresolved" },
                                  { value: "unknown", label: "unknown" },
                                ]}
                              />
                              <InputNumber
                                value={channelFilter.limit}
                                min={10}
                                max={500}
                                onChange={(value) => setChannelFilter((current) => ({ ...current, limit: value || 100 }))}
                              />
                              <Button type="primary" onClick={() => loadChannels()}>查询</Button>
                            </Space>
                          </Space>
                        </Card>
                        <Card className="table-card">
                          <Table
                            rowKey="normalizedUrl"
                            loading={loading.channels}
                            columns={channelColumns}
                            dataSource={channels}
                            pagination={{ pageSize: 10 }}
                            scroll={{ x: 1100 }}
                            locale={{ emptyText: <Empty description="暂无频道目录数据" /> }}
                          />
                        </Card>
                      </Space>
                    ),
                  },
                  {
                    key: "schedules",
                    label: "排班结果",
                    children: (
                      <Space direction="vertical" size={16} style={{ width: "100%" }}>
                        <Card className="table-card">
                          <Form
                            form={scheduleFilterForm}
                            layout="vertical"
                            onFinish={submitScheduleSearch}
                          >
                            <Row gutter={[16, 8]}>
                              <Col xs={24} md={12} xl={5}>
                                <Form.Item label="排班日期范围" name="dateRange">
                                  <RangePicker
                                    style={{ width: "100%" }}
                                    format="YYYY-MM-DD"
                                    locale={DATE_PICKER_ZH_CN}
                                    presets={DATE_RANGE_PRESETS}
                                  />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={4}>
                                <Form.Item label="店名/频道" name="storeKeyword">
                                  <Input allowClear placeholder="店名或频道名" />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={4}>
                                <Form.Item label="店员" name="staffKeyword">
                                  <Input allowClear placeholder="店员名" />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={4}>
                                <Form.Item label="地点" name="locationKeyword">
                                  <Input allowClear placeholder="地点/分店" />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={3}>
                                <Form.Item label="状态" name="isOnDuty">
                                  <Select
                                    options={[
                                      { value: "all", label: "全部状态" },
                                      { value: "true", label: "仅在店" },
                                      { value: "false", label: "仅休息" },
                                    ]}
                                  />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={4}>
                                <Form.Item label="说明" name="descriptionKeyword">
                                  <Input allowClear placeholder="优点、备注、体验说明" />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={4}>
                                <Form.Item label="在店时段" name="availableRange">
                                  <TimeRangePicker
                                    style={{ width: "100%" }}
                                    format="HH:mm"
                                    minuteStep={30}
                                    allowEmpty={[true, true]}
                                    placeholder={["几点之后在", "几点之前在"]}
                                  />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={4}>
                                <Form.Item label="操作">
                                  <Space wrap>
                                    <Button type="primary" htmlType="submit">查询</Button>
                                    <Button onClick={resetScheduleSearch}>重置</Button>
                                  </Space>
                                </Form.Item>
                              </Col>
                            </Row>
                          </Form>
                        </Card>
                        <Card className="table-card">
                          <Table
                            rowKey={(row) => `${row.chatId}-${row.scheduleDate}-${row.storeName}-${row.staffName}`}
                            loading={loading.schedules}
                            columns={scheduleColumns}
                            dataSource={scheduleResult.items}
                            pagination={{
                              current: scheduleResult.page,
                              pageSize: scheduleResult.pageSize,
                              total: scheduleResult.total,
                              showSizeChanger: true,
                              pageSizeOptions: [20, 50, 100, 200],
                              showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条 / 共 ${total} 条`,
                            }}
                            onChange={handleScheduleTableChange}
                            scroll={{ x: 1280 }}
                            locale={{ emptyText: <Empty description="暂无排班结果" /> }}
                          />
                        </Card>
                      </Space>
                    ),
                  },
                  {
                    key: "scheduleChannels",
                    label: "频道排班",
                    children: (
                      <Space direction="vertical" size={16} style={{ width: "100%" }}>
                        <Card className="table-card">
                          <Space wrap align="end" style={{ marginBottom: 16 }}>
                            <Space direction="vertical" size={4}>
                              <Text strong>当前位置</Text>
                              <Select
                                value={selectedMapLocationId}
                                onChange={setSelectedMapLocationId}
                                placeholder="选择已保存位置"
                                style={{ width: 360 }}
                                options={mapSavedLocations.map((item) => ({
                                  value: item.id,
                                  label: `${item.locationName}${item.amapLocation ? ` (${item.amapLocation})` : ""}`,
                                }))}
                              />
                            </Space>
                            <Button onClick={() => openMapLocationModal()}>新增位置</Button>
                            <Button
                              disabled={!selectedMapLocation}
                              onClick={() => selectedMapLocation && openMapLocationModal(selectedMapLocation)}
                            >
                              编辑位置
                            </Button>
                            <Button
                              type="primary"
                              loading={routeRefreshing}
                              disabled={!selectedMapLocationId}
                              onClick={refreshChannelRoutes}
                            >
                              刷新距离/时间
                            </Button>
                            <Text type="secondary">手动刷新当前页已绑定地址的频道，距离和驾车时间可单独排序。</Text>
                          </Space>
                          <Divider style={{ margin: "0 0 16px" }} />
                          <Form
                            form={scheduleChannelFilterForm}
                            layout="vertical"
                            onFinish={submitScheduleChannelSearch}
                          >
                            <Row gutter={[16, 8]}>
                              <Col xs={24} md={12} xl={5}>
                                <Form.Item label="排班日期范围" name="dateRange">
                                  <RangePicker
                                    style={{ width: "100%" }}
                                    format="YYYY-MM-DD"
                                    locale={DATE_PICKER_ZH_CN}
                                    presets={DATE_RANGE_PRESETS}
                                  />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={3}>
                                <Form.Item label="排班状态" name="schedulePresence">
                                  <Select
                                    options={[
                                      { value: "all", label: "全部频道" },
                                      { value: "with", label: "有排班" },
                                      { value: "without", label: "无排班" },
                                    ]}
                                  />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={4}>
                                <Form.Item label="频道/店名" name="storeKeyword">
                                  <Input allowClear placeholder="频道名或店名" />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={4}>
                                <Form.Item label="店员" name="staffKeyword">
                                  <Input allowClear placeholder="店员名" />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={4}>
                                <Form.Item label="地点" name="locationKeyword">
                                  <Input allowClear placeholder="地点/分店" />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={3}>
                                <Form.Item label="店员状态" name="isOnDuty">
                                  <Select
                                    options={[
                                      { value: "all", label: "全部状态" },
                                      { value: "true", label: "仅在店" },
                                      { value: "false", label: "仅休息" },
                                    ]}
                                  />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={4}>
                                <Form.Item label="说明" name="descriptionKeyword">
                                  <Input allowClear placeholder="优点、备注、体验说明" />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={4}>
                                <Form.Item label="在店时段" name="availableRange">
                                  <TimeRangePicker
                                    style={{ width: "100%" }}
                                    format="HH:mm"
                                    minuteStep={30}
                                    allowEmpty={[true, true]}
                                    placeholder={["几点之后在", "几点之前在"]}
                                  />
                                </Form.Item>
                              </Col>
                              <Col xs={24} md={12} xl={4}>
                                <Form.Item label="操作">
                                  <Space wrap>
                                    <Button type="primary" htmlType="submit">查询</Button>
                                    <Button onClick={resetScheduleChannelSearch}>重置</Button>
                                  </Space>
                                </Form.Item>
                              </Col>
                            </Row>
                          </Form>
                        </Card>
                        <Card className="table-card">
                          <Table
                            size="small"
                            rowKey="chatId"
                            loading={loading.scheduleChannels}
                            columns={scheduleChannelColumns}
                            dataSource={scheduleChannelResult.items}
                            pagination={{
                              current: scheduleChannelResult.page,
                              pageSize: scheduleChannelResult.pageSize,
                              total: scheduleChannelResult.total,
                              showSizeChanger: true,
                              pageSizeOptions: [20, 50, 100, 200],
                              showTotal: (total, range) => `第 ${range[0]}-${range[1]} 个频道 / 共 ${total} 个`,
                            }}
                            onChange={handleScheduleChannelTableChange}
                            expandable={{
                              onExpand: (expanded, row) => {
                                if (expanded && !channelStaffDetails[row.chatId]?.items?.length) {
                                  loadScheduleChannelStaff(row.chatId);
                                }
                              },
                              expandedRowRender: (row) => {
                                const detail = channelStaffDetails[row.chatId] || { loading: false, items: [] };
                                if (detail.error) {
                                  return <Text type="danger">{detail.error}</Text>;
                                }

                                return (
                                  <Table
                                    size="small"
                                    rowKey={(item) => `${item.chatId}-${item.scheduleDate}-${item.staffName}-${item.sourceSentAt}`}
                                    loading={detail.loading}
                                    columns={channelStaffColumns}
                                    dataSource={detail.items}
                                    pagination={false}
                                    scroll={{ x: 1100 }}
                                    locale={{ emptyText: <Empty description="暂无店员明细" /> }}
                                  />
                                );
                              },
                            }}
                            scroll={{ x: "max-content" }}
                            locale={{ emptyText: <Empty description="暂无频道排班数据" /> }}
                          />
                        </Card>
                      </Space>
                    ),
                  },
                  {
                    key: "messages",
                    label: "原始消息",
                    children: (
                      <Space direction="vertical" size={16} style={{ width: "100%" }}>
                        <Card className="table-card">
                          <Space wrap style={{ width: "100%", justifyContent: "space-between" }}>
                            <InputNumber
                              value={messageFilter.chatId}
                              onChange={(value) => setMessageFilter((current) => ({ ...current, chatId: value || "" }))}
                              style={{ width: 220 }}
                              placeholder="chat_id"
                            />
                            <Input
                              value={messageFilter.keyword}
                              onChange={(event) => setMessageFilter((current) => ({ ...current, keyword: event.target.value }))}
                              placeholder="按关键词搜索消息"
                              style={{ width: 280 }}
                            />
                            <InputNumber
                              value={messageFilter.limit}
                              min={20}
                              max={500}
                              onChange={(value) => setMessageFilter((current) => ({ ...current, limit: value || 100 }))}
                            />
                            <Button type="primary" onClick={() => loadMessages()}>查询</Button>
                          </Space>
                        </Card>
                        <Card className="table-card">
                          <Table
                            rowKey={(row) => `${row.chatId}-${row.messageId}`}
                            loading={loading.messages}
                            columns={messageColumns}
                            dataSource={messages}
                            pagination={{ pageSize: 10 }}
                            scroll={{ x: 1100 }}
                            locale={{ emptyText: <Empty description="暂无原始消息" /> }}
                          />
                        </Card>
                      </Space>
                    ),
                  },
                ]}
              />
            </div>
          </Content>

          <Modal
            title={`设置频道地址${channelAddressModal.row?.channelLabel ? `：${channelAddressModal.row.channelLabel}` : ""}`}
            open={channelAddressModal.open}
            onOk={saveChannelAddress}
            onCancel={closeChannelAddressModal}
            okText="保存"
            cancelText="取消"
            destroyOnHidden
          >
            <Space direction="vertical" size={12} style={{ width: "100%" }}>
              <Space direction="vertical" size={4} style={{ width: "100%" }}>
                <Text strong>地址名称</Text>
                <Input
                  value={channelAddressModal.addressName}
                  onChange={(event) => setChannelAddressModal((current) => ({ ...current, addressName: event.target.value }))}
                  placeholder="例如：杭州滨江店"
                />
              </Space>
              <Space direction="vertical" size={4} style={{ width: "100%" }}>
                <Text strong>坐标</Text>
                <Input
                  value={channelAddressModal.location}
                  onChange={(event) => setChannelAddressModal((current) => ({ ...current, location: event.target.value }))}
                  placeholder="经度,纬度，例如 120.197226,30.275227"
                />
              </Space>
              <Space direction="vertical" size={4} style={{ width: "100%" }}>
                <Text strong>地址文本</Text>
                <Input.TextArea
                  value={channelAddressModal.addressText}
                  onChange={(event) => setChannelAddressModal((current) => ({ ...current, addressText: event.target.value }))}
                  placeholder="没有坐标时可填地址，高德会尝试解析"
                  autoSize={{ minRows: 2, maxRows: 4 }}
                />
              </Space>
              <Space direction="vertical" size={4} style={{ width: "100%" }}>
                <Text strong>城市</Text>
                <Input
                  value={channelAddressModal.city}
                  onChange={(event) => setChannelAddressModal((current) => ({ ...current, city: event.target.value }))}
                  placeholder="用于地址解析，例如 杭州"
                />
              </Space>
            </Space>
          </Modal>

          <Modal
            title={mapLocationModal.id ? "编辑当前位置" : "新增当前位置"}
            open={mapLocationModal.open}
            onOk={saveMapLocation}
            onCancel={closeMapLocationModal}
            okText="保存"
            cancelText="取消"
            destroyOnHidden
          >
            <Space direction="vertical" size={12} style={{ width: "100%" }}>
              <Space direction="vertical" size={4} style={{ width: "100%" }}>
                <Text strong>位置名称</Text>
                <Input
                  value={mapLocationModal.locationName}
                  onChange={(event) => setMapLocationModal((current) => ({ ...current, locationName: event.target.value }))}
                  placeholder="例如：当前位置、公司、家"
                />
              </Space>
              <Space direction="vertical" size={4} style={{ width: "100%" }}>
                <Text strong>坐标</Text>
                <Input
                  value={mapLocationModal.location}
                  onChange={(event) => setMapLocationModal((current) => ({ ...current, location: event.target.value }))}
                  placeholder="经度,纬度，例如 120.215026,30.183776"
                />
              </Space>
              <Space direction="vertical" size={4} style={{ width: "100%" }}>
                <Text strong>地址文本</Text>
                <Input.TextArea
                  value={mapLocationModal.addressText}
                  onChange={(event) => setMapLocationModal((current) => ({ ...current, addressText: event.target.value }))}
                  placeholder="没有坐标时可填地址，高德会尝试解析"
                  autoSize={{ minRows: 2, maxRows: 4 }}
                />
              </Space>
              <Space direction="vertical" size={4} style={{ width: "100%" }}>
                <Text strong>城市</Text>
                <Input
                  value={mapLocationModal.city}
                  onChange={(event) => setMapLocationModal((current) => ({ ...current, city: event.target.value }))}
                  placeholder="用于地址解析，例如 杭州"
                />
              </Space>
            </Space>
          </Modal>

          <Drawer
            title={logState.title || "任务日志"}
            width={960}
            open={logState.open}
            onClose={closeLogDrawer}
            rootStyle={{
              top: 20,
              right: 20,
              bottom: 20,
              height: "calc(100vh - 40px)",
            }}
            styles={{
              content: {
                borderRadius: 24,
                overflow: "hidden",
              },
              body: {
                padding: 16,
                overflow: "hidden",
                display: "flex",
                flexDirection: "column",
              },
            }}
            extra={
              <Space>
                <Tag color={statusTag(logAutoScroll ? "open" : "closed")}>
                  {logAutoScroll ? "自动滚动开" : "自动滚动关"}
                </Tag>
                <Tag color={statusTag(logConnectionState)}>
                  {logConnectionState === "open" ? "日志实时推送中" : `日志流 ${logConnectionState}`}
                </Tag>
              </Space>
            }
          >
            {logState.loading ? (
              <Paragraph>正在加载日志...</Paragraph>
            ) : (
              <div
                ref={logContainerRef}
                className="task-log"
                onScroll={handleLogScroll}
                role="log"
                aria-live="off"
                aria-label="任务日志"
              >
                {logState.lines.join("\n") || "暂无日志"}
              </div>
            )}
          </Drawer>
        </Layout>
      </AntApp>
    </ConfigProvider>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
