"use client";

import {
  ChangeEvent,
  FormEvent,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import {
  formatShanghaiDate as formatDate,
  formatShanghaiToday,
} from "../lib/date-utils";
import type { AuthUser } from "../lib/user-auth";
import { parseResultScreenshotKeys } from "../lib/result-screenshots";
import { platformFromProfileUrl } from "../lib/resource-import";
import {
  sortWithNullsLast,
  type SortDirection,
} from "../lib/sort-utils";
import UsersPage from "./users-page";

type Partner = {
  id: string;
  name: string;
  wecom_name: string;
  owner: string;
  claimed_total: number;
  completed_total: number;
  wecom_external_user_id?: string | null;
};

type Task = {
  id: string;
  name: string;
  brand: string;
  quantity: number;
  claimed_quantity: number;
  due_at: string;
  status: string;
  task_type: "content_publish" | "screenshot_collect";
  platform: "小红书" | "抖音";
  content_format: "image_text" | "video";
  source_url: string;
  source_sheet_id: string;
  source_sheet_name: string;
  source_synced_at: string | null;
  share_token: string;
  collection_start_date: string | null;
  collection_days: string;
  collection_schedule_updated_at: string | null;
};

type Account = {
  id: string;
  platform: string;
  platform_uid: string;
  public_account_id: string;
  nickname: string;
  profile_url: string;
  latest_publish_url: string;
  ip_location: string;
  followers: number;
  gender: string;
  bio: string;
  tags: string;
  post_count: number;
  avg_views: number;
  cooperation_source: string;
  current_contact: string;
  first_seen_at: string;
  last_seen_at: string;
};

type ResourceImportPreview = {
  summary: { total: number; create: number; update: number; error: number };
  rows: Array<{
    rowNumber: number;
    platform: string;
    nickname: string;
    publicAccountId: string;
    ipLocation: string;
    followers: number;
    gender: string;
    bio: string;
    tags: string[];
    cooperationSource: string;
    action: "create" | "update" | "error";
    errors: string[];
  }>;
  truncated: boolean;
  deferredEnrichment?: number;
  maxRows?: number;
};

type Distribution = {
  id: string;
  task_id: string;
  content_id: string;
  partner_id: string;
  delegation_bundle_id: string | null;
  account_id: string | null;
  publish_url: string | null;
  publish_time: string | null;
  result_screenshot_key: string | null;
  result_submitted_at: string | null;
  status: string;
  claimed_at: string;
  screenshot_key: string | null;
  ocr_status: string;
  exposure: number | null;
  views: number | null;
  d2_likes: number | null;
  d2_comments: number | null;
  d2_collects: number | null;
  d5_likes: number | null;
  d5_comments: number | null;
  d5_collects: number | null;
  d7_likes: number | null;
  d7_comments: number | null;
  d7_collects: number | null;
  latest_likes: number | null;
  latest_comments: number | null;
  latest_collects: number | null;
  latest_shares: number | null;
  collection_status: string;
  collection_status_description: string | null;
  collection_updated_at: string | null;
  last_collection_day: number | null;
  updated_at: string;
  content_title: string;
  partner_name: string;
  claimant_name: string | null;
  account_nickname: string | null;
  account_platform: string | null;
  task_name: string;
  task_brand: string;
  task_type: "content_publish" | "screenshot_collect";
  task_platform: "小红书" | "抖音";
  content_format: "image_text" | "video";
  due_at: string;
};

type RecoverySortKey =
  | "publish_time"
  | "likes"
  | "collects"
  | "shares"
  | "comments"
  | "total"
  | "collection_updated_at";

type DashboardData = {
  partners: Partner[];
  tasks: Task[];
  accounts: Account[];
  distributions: Distribution[];
  portal_url: string;
};

type FeishuPreview = {
  sheetId: string;
  sheetName: string;
  syncedAt: string;
  rowCount: number;
  imageCount: number;
  videoCount: number;
  columns: string[];
  preview: Array<{
    sourceRow: number;
    title: string;
    body: string;
  }>;
};

type NavKey =
  | "overview"
  | "tasks"
  | "distributions"
  | "resources"
  | "partners"
  | "recovery"
  | "users";

const NAV_ITEMS: Array<{ key: NavKey; label: string; mark: string }> = [
  { key: "overview", label: "工作台", mark: "⌂" },
  { key: "tasks", label: "任务中心", mark: "▤" },
  { key: "distributions", label: "任务分发", mark: "↗" },
  { key: "resources", label: "KOC资源", mark: "◎" },
  { key: "partners", label: "合作方", mark: "Ⓒ" },
  { key: "recovery", label: "数据回收", mark: "◫" },
  { key: "users", label: "用户管理", mark: "♙" },
];

const EMPTY_DATA: DashboardData = {
  partners: [],
  tasks: [],
  accounts: [],
  distributions: [],
  portal_url: "",
};

async function readApiResponse<T>(response: Response, fallback: string) {
  const text = await response.text();
  const contentType = response.headers.get("content-type") ?? "";
  if (!contentType.includes("json")) {
    if (response.status === 403) {
      throw new Error("请求被安全网关拦截，请刷新页面后重试");
    }
    throw new Error(`${fallback}（服务返回异常，HTTP ${response.status}）`);
  }
  try {
    return JSON.parse(text) as T & { error?: string };
  } catch {
    throw new Error(`${fallback}（服务返回的数据无法解析）`);
  }
}

function formatNumber(value: number | null | undefined) {
  if (value === null || value === undefined) return "—";
  if (value >= 10000) return `${(value / 10000).toFixed(value >= 100000 ? 0 : 1)}万`;
  return new Intl.NumberFormat("zh-CN").format(value);
}

function resourceProfileLink(account: Account) {
  const storedProfileUrl = account.profile_url?.trim();
  if (account.platform !== "抖音") {
    return storedProfileUrl
      ? { href: storedProfileUrl, label: "查看主页 ↗" }
      : null;
  }

  const publicAccountId = account.public_account_id?.trim();
  const hasResolvedPublicId = Boolean(
    publicAccountId && publicAccountId !== "待识别",
  );
  if (storedProfileUrl && hasResolvedPublicId) {
    return { href: storedProfileUrl, label: "查看主页 ↗" };
  }

  const latestPublishUrl = account.latest_publish_url?.trim();
  if (latestPublishUrl) {
    return {
      href: latestPublishUrl,
      label: "通过作品查看主页 ↗",
    };
  }

  const keyword = hasResolvedPublicId ? publicAccountId : account.nickname.trim();
  return keyword
    ? {
        href: `https://www.douyin.com/search/${encodeURIComponent(keyword)}?type=user`,
        label: "搜索主页 ↗",
      }
    : null;
}

function PlatformBadge({
  platform,
  compact = false,
}: {
  platform: string | null | undefined;
  compact?: boolean;
}) {
  const normalized = platform === "抖音" ? "抖音" : platform === "小红书" ? "小红书" : "其他";
  return (
    <span className={`platform-badge ${compact ? "compact" : ""}`}>
      <span className={`platform-logo ${normalized === "抖音" ? "douyin" : normalized === "小红书" ? "xiaohongshu" : "other"}`} aria-hidden="true">
        {normalized === "抖音" ? (
          <svg viewBox="0 0 24 24" focusable="false">
            <path className="douyin-cyan" d="M14.2 3.2v9.5a4.7 4.7 0 1 1-3.4-4.5v2.7a2.1 2.1 0 1 0 .8 1.7V3.2h2.6Zm0 0c.4 2.4 2 3.8 4.3 4.1V10c-1.7-.1-3.2-.7-4.3-1.7V3.2Z" />
            <path className="douyin-red" d="M15.2 2.5V12a4.7 4.7 0 1 1-3.4-4.5v2.7a2.1 2.1 0 1 0 .8 1.7V2.5h2.6Zm0 0c.4 2.4 2 3.8 4.3 4.1v2.7c-1.7-.1-3.2-.7-4.3-1.7V2.5Z" />
            <path className="douyin-white" d="M14.7 2.9v9.5a4.7 4.7 0 1 1-3.4-4.5v2.7a2.1 2.1 0 1 0 .8 1.7V2.9h2.6Zm0 0c.4 2.4 2 3.8 4.3 4.1v2.7c-1.7-.1-3.2-.7-4.3-1.7V2.9Z" />
          </svg>
        ) : normalized === "小红书" ? (
          <b>小红书</b>
        ) : (
          <b>媒</b>
        )}
      </span>
      <span>{platform || "未知平台"}</span>
    </span>
  );
}

function statusLabel(status: string) {
  const labels: Record<string, string> = {
    claimed: "待发布",
    published: "已发布",
    collecting: "采集中",
    complete: "已完成",
    active: "分发中",
    closed: "已结束",
  };
  return labels[status] || status;
}

function latestPublicMetrics(distribution: Distribution) {
  const legacyDay =
    distribution.d7_likes !== null
      ? 7
      : distribution.d5_likes !== null
        ? 5
        : distribution.d2_likes !== null
          ? 2
          : null;
  const likes =
    distribution.latest_likes ??
    (legacyDay ? distribution[`d${legacyDay}_likes`] : null);
  const comments =
    distribution.latest_comments ??
    (legacyDay ? distribution[`d${legacyDay}_comments`] : null);
  const collects =
    distribution.latest_collects ??
    (legacyDay ? distribution[`d${legacyDay}_collects`] : null);
  const shares = distribution.latest_shares ?? 0;
  return {
    likes,
    comments,
    collects,
    shares,
    total:
      likes === null
        ? null
        : likes + (comments ?? 0) + (collects ?? 0) + shares,
  };
}

function recoverySortValue(
  distribution: Distribution,
  key: RecoverySortKey,
): number | null {
  const metrics = latestPublicMetrics(distribution);
  if (key === "likes") return metrics.likes;
  if (key === "collects") return metrics.collects;
  if (key === "shares") return metrics.shares;
  if (key === "comments") return metrics.comments;
  if (key === "total") return metrics.total;

  const value =
    key === "publish_time"
      ? distribution.publish_time
      : distribution.collection_updated_at ||
        (metrics.likes !== null ? distribution.updated_at : null);
  if (!value) return null;
  const timestamp = Date.parse(value);
  return Number.isNaN(timestamp) ? null : timestamp;
}

function sortRecoveryDistributions(
  distributions: Distribution[],
  key: RecoverySortKey,
  direction: SortDirection,
) {
  return sortWithNullsLast(
    distributions,
    (distribution) => recoverySortValue(distribution, key),
    direction,
  );
}

function SortableRecoveryHeader({
  label,
  column,
  activeColumn,
  direction,
  onSort,
}: {
  label: string;
  column: RecoverySortKey;
  activeColumn: RecoverySortKey | null;
  direction: SortDirection;
  onSort: (column: RecoverySortKey) => void;
}) {
  const active = activeColumn === column;
  const arrow = active ? (direction === "asc" ? "↑" : "↓") : "↕";
  return (
    <th aria-sort={active ? (direction === "asc" ? "ascending" : "descending") : "none"}>
      <button
        type="button"
        className={`recovery-sort-button${active ? " active" : ""}`}
        onClick={() => onSort(column)}
        title={`${label}按${active && direction === "desc" ? "正序" : "倒序"}排列`}
      >
        <span>{label}</span>
        <b aria-hidden="true">{arrow}</b>
      </button>
    </th>
  );
}

function hasCreatorMetrics(distribution: Distribution) {
  return distribution.exposure !== null && distribution.views !== null;
}

function parseCollectionDays(value: string | null | undefined) {
  try {
    const parsed = JSON.parse(value || "[]") as unknown;
    if (!Array.isArray(parsed)) return [];
    return [...new Set(parsed.map(Number))]
      .filter((day) => Number.isInteger(day) && day >= 1 && day <= 7)
      .sort((a, b) => a - b);
  } catch {
    return [];
  }
}

function todayInputValue() {
  const parts = new Intl.DateTimeFormat("zh-CN", {
    timeZone: "Asia/Shanghai",
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  }).formatToParts(new Date());
  const value = Object.fromEntries(
    parts.map((part) => [part.type, part.value]),
  );
  return `${value.year}-${value.month}-${value.day}`;
}

function addCalendarDays(value: string, days: number) {
  const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if (!match) return "";
  const date = new Date(
    Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]) + days),
  );
  return date.toISOString().slice(0, 10);
}

function publicPublishUrl(value: string | null) {
  if (!value) return null;
  try {
    const url = new URL(value);
    const hostname = url.hostname.toLowerCase();
    const isSupportedHost =
      hostname === "xiaohongshu.com" ||
      hostname.endsWith(".xiaohongshu.com") ||
      hostname === "xhslink.cn" ||
      hostname.endsWith(".xhslink.cn") ||
      hostname === "xhslink.com" ||
      hostname.endsWith(".xhslink.com") ||
      hostname === "douyin.com" ||
      hostname.endsWith(".douyin.com");
    return ["http:", "https:"].includes(url.protocol) && isSupportedHost
      ? url.toString()
      : null;
  } catch {
    return null;
  }
}

function collectionStatusLabel(status: string, hasMetrics: boolean) {
  if (hasMetrics && (!status || status === "pending")) return "采集成功";
  const labels: Record<string, string> = {
    pending: "待设置",
    scheduled: "已计划",
    collecting: "采集中",
    success: "采集成功",
    failed: "采集失败",
  };
  return labels[status] || status || "待设置";
}

function avatarColor(value: string) {
  const colors = ["coral", "cyan", "violet", "amber", "mint"];
  const index = [...value].reduce((sum, char) => sum + char.charCodeAt(0), 0);
  return colors[index % colors.length];
}

async function compressScreenshot(file: File) {
  if (file.size < 850_000 || !file.type.startsWith("image/")) return file;
  const bitmap = await createImageBitmap(file);
  const maxEdge = 1600;
  const scale = Math.min(1, maxEdge / Math.max(bitmap.width, bitmap.height));
  const canvas = document.createElement("canvas");
  canvas.width = Math.max(1, Math.round(bitmap.width * scale));
  canvas.height = Math.max(1, Math.round(bitmap.height * scale));
  const context = canvas.getContext("2d");
  if (!context) {
    bitmap.close();
    return file;
  }
  context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
  bitmap.close();
  const blob = await new Promise<Blob | null>((resolve) =>
    canvas.toBlob(resolve, "image/jpeg", 0.82),
  );
  if (!blob) return file;
  return new File([blob], `${file.name.replace(/\.[^.]+$/, "")}.jpg`, {
    type: "image/jpeg",
  });
}

export default function Home({ currentUser }: { currentUser: AuthUser }) {
  const [data, setData] = useState<DashboardData>(EMPTY_DATA);
  const [activeNav, setActiveNav] = useState<NavKey>("overview");
  const [loading, setLoading] = useState(true);
  const [working, setWorking] = useState(false);
  const [error, setError] = useState("");
  const [toast, setToast] = useState("");
  const [menuOpen, setMenuOpen] = useState(false);
  const [taskModalOpen, setTaskModalOpen] = useState(false);
  const [groupPushTask, setGroupPushTask] = useState<Task | null>(null);
  const [metricModal, setMetricModal] = useState<Distribution | null>(null);
  const [taskForm, setTaskForm] = useState({
    taskType: "content_publish" as "content_publish" | "screenshot_collect",
    platform: "小红书" as "小红书" | "抖音",
    contentFormat: "image_text" as "image_text" | "video",
    name: "",
    brand: "",
    dueAt: "2026-08-12",
    feishuUrl: "",
    keyword: "",
    instructions: "在小红书搜索指定关键词，打开 AI 总结并上传完整截图。",
    quantity: 1,
  });
  const [taskExampleImage, setTaskExampleImage] = useState<File | null>(null);
  const [sourcePreview, setSourcePreview] = useState<FeishuPreview | null>(null);
  const [sourceWorking, setSourceWorking] = useState(false);
  const [metricForm, setMetricForm] = useState({ exposure: "", views: "" });
  const [exportingTaskId, setExportingTaskId] = useState<string | null>(null);
  const [exportingResources, setExportingResources] = useState(false);
  const isManager = currentUser.role === "super_admin" || currentUser.role === "admin";
  const visibleNavItems = NAV_ITEMS.filter((item) => {
    if (item.key === "resources" && currentUser.role === "user") return false;
    if (item.key === "users" && !isManager) return false;
    return true;
  });

  const loadData = useCallback(async () => {
    try {
      setLoading(true);
      const response = await fetch("/api/bootstrap", { cache: "no-store" });
      const result = await readApiResponse<DashboardData>(response, "加载失败");
      if (!response.ok) throw new Error(result.error || "加载失败");
      setData(result);
    } catch (reason) {
      setError(reason instanceof Error ? reason.message : "加载失败");
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    const timer = window.setTimeout(() => {
      void loadData();
    }, 0);
    return () => window.clearTimeout(timer);
  }, [loadData]);

  useEffect(() => {
    if (!toast) return;
    const timer = window.setTimeout(() => setToast(""), 2600);
    return () => window.clearTimeout(timer);
  }, [toast]);

  const runAction = async (
    payload: Record<string, unknown>,
    successMessage: string,
  ) => {
    try {
      setWorking(true);
      const response = await fetch("/api/action", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });
      const result = await readApiResponse<DashboardData>(response, "操作失败");
      if (!response.ok) throw new Error(result.error || "操作失败");
      setData(result);
      setToast(successMessage);
      return true;
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "操作失败");
      return false;
    } finally {
      setWorking(false);
    }
  };

  const stats = useMemo(() => {
    const published = data.distributions.filter((item) =>
      item.task_type !== "screenshot_collect" &&
      ["published", "collecting", "complete"].includes(item.status),
    ).length;
    const completed = data.distributions.filter(
      (item) => item.status === "complete",
    ).length;
    const pendingRecovery = data.distributions.filter(
      (item) =>
        item.publish_url &&
        (latestPublicMetrics(item).likes === null ||
          !hasCreatorMetrics(item)),
    ).length;
    return {
      resources: data.accounts.length,
      activeTasks: data.tasks.filter((task) => task.status === "active").length,
      published,
      completed,
      pendingRecovery,
    };
  }, [data]);

  const recentDistributions = data.distributions
    .filter((item) => item.task_type !== "screenshot_collect")
    .slice(0, 6);

  const navigate = (key: NavKey) => {
    if (key === "resources" && currentUser.role === "user") return;
    if (key === "users" && !isManager) return;
    setActiveNav(key);
    setMenuOpen(false);
  };

  const submitTask = async (event: FormEvent) => {
    event.preventDefault();
    if (taskForm.taskType === "content_publish" && !sourcePreview) {
      setToast("请先读取飞书表格");
      return;
    }
    if (
      taskForm.taskType === "content_publish" &&
      taskForm.contentFormat === "video" &&
      (sourcePreview?.videoCount ?? 0) === 0
    ) {
      setToast("当前飞书表格没有识别到视频附件");
      return;
    }
    let exampleImageKey = "";
    if (taskForm.taskType === "screenshot_collect" && taskExampleImage) {
      try {
        setWorking(true);
        const form = new FormData();
        form.set("file", await compressScreenshot(taskExampleImage));
        const uploadResponse = await fetch("/api/task-example-upload", {
          method: "POST",
          body: form,
        });
        const uploadResult = await readApiResponse<{ key?: string }>(
          uploadResponse,
          "示例截图上传失败",
        );
        if (!uploadResponse.ok || !uploadResult.key) {
          throw new Error(uploadResult.error || "示例截图上传失败");
        }
        exampleImageKey = uploadResult.key;
      } catch (reason) {
        setToast(reason instanceof Error ? reason.message : "示例截图上传失败");
        setWorking(false);
        return;
      }
    }
    const success = await runAction(
      taskForm.taskType === "screenshot_collect"
        ? {
            action: "create_screenshot_task",
            name: taskForm.name,
            brand: taskForm.brand,
            dueAt: taskForm.dueAt,
            keyword: taskForm.keyword,
            instructions: taskForm.instructions,
            quantity: taskForm.quantity,
            exampleImageKey,
          }
        : { action: "create_task", ...taskForm },
      taskForm.taskType === "screenshot_collect"
        ? `截图回收任务已创建，${taskForm.quantity} 份任务进入可领取池`
        : `任务已创建，${sourcePreview?.rowCount ?? 0} 篇内容进入可领取池`,
    );
    if (success) {
      setTaskModalOpen(false);
      setSourcePreview(null);
      setTaskExampleImage(null);
      setTaskForm({
        taskType: "content_publish",
        platform: "小红书",
        contentFormat: "image_text",
        name: "",
        brand: "",
        dueAt: "2026-08-12",
        feishuUrl: "",
        keyword: "",
        instructions: "在小红书搜索指定关键词，打开 AI 总结并上传完整截图。",
        quantity: 1,
      });
    }
  };

  const inspectFeishu = async () => {
    if (!taskForm.feishuUrl.trim()) {
      setToast("请先粘贴飞书表格链接");
      return;
    }
    try {
      setSourceWorking(true);
      const response = await fetch("/api/action", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          action: "inspect_feishu",
          feishuUrl: taskForm.feishuUrl,
        }),
      });
      const result = await readApiResponse<FeishuPreview>(
        response,
        "读取飞书表格失败",
      );
      if (!response.ok) throw new Error(result.error || "读取飞书表格失败");
      setSourcePreview(result);
      setTaskForm((current) => ({
        ...current,
        name: current.name || result.sheetName.replace(/500篇$/, "分发任务"),
      }));
      setToast(
        `已读取 ${result.rowCount} 条有效内容，识别到 ${result.videoCount} 个视频`,
      );
    } catch (reason) {
      setSourcePreview(null);
      setToast(reason instanceof Error ? reason.message : "读取飞书表格失败");
    } finally {
      setSourceWorking(false);
    }
  };

  const copyTaskShareLink = async (task: Task) => {
    if (!data.portal_url || !task.share_token) {
      setToast("KOC外部站点尚未发布");
      return;
    }
    const url = `${data.portal_url.replace(/\/$/, "")}/?task=${task.share_token}`;
    try {
      await navigator.clipboard.writeText(url);
      setToast("KOC领取链接已复制");
    } catch {
      window.prompt("复制KOC领取链接", url);
    }
  };

  const exportScreenshotResults = async (task: Task) => {
    try {
      setExportingTaskId(task.id);
      const response = await fetch(
        `/api/screenshot-task-export?task=${encodeURIComponent(task.id)}`,
      );
      if (!response.ok) {
        const result = await readApiResponse<Record<string, never>>(
          response,
          "截图打包失败",
        );
        throw new Error(result.error || "截图打包失败");
      }
      const blob = await response.blob();
      const url = URL.createObjectURL(blob);
      const anchor = document.createElement("a");
      anchor.href = url;
      anchor.download = `${task.name}-截图回收.zip`;
      anchor.click();
      URL.revokeObjectURL(url);
      setToast("任务截图和回收清单已打包下载");
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "截图打包失败");
    } finally {
      setExportingTaskId(null);
    }
  };

  const collectMetrics = async (distribution: Distribution) => {
    await runAction(
      { action: "collect_now", distributionId: distribution.id },
      "公开数据已更新",
    );
  };

  const updateDistributionPublishUrl = async (
    distribution: Distribution,
  ) => {
    const publishInput = window.prompt(
      distribution.publish_url
        ? "更新小红书笔记链接（支持长链、短链或完整分享文案）"
        : "填写小红书笔记链接（支持长链、短链或完整分享文案）",
      distribution.publish_url || "",
    );
    if (publishInput === null) return;
    if (!publishInput.trim()) {
      setToast("请填写笔记链接");
      return;
    }
    await runAction(
      {
        action: "update_distribution_publish_url",
        distributionId: distribution.id,
        publishUrl: publishInput.trim(),
      },
      distribution.publish_url
        ? "笔记链接已更新，旧公开采集数据已清空"
        : "笔记链接已补充，可开始数据采集",
    );
  };

  const retryFailedMetrics = async (taskId: string, failedCount: number) => {
    if (failedCount === 0) {
      setToast("当前没有采集异常的笔记");
      return;
    }
    await runAction(
      { action: "retry_failed_collections", taskId },
      `已完成 ${failedCount} 篇异常笔记的一键补采`,
    );
  };

  const saveCollectionSchedule = async (
    taskId: string,
    startDate: string,
    days: number[],
  ) => {
    return runAction(
      {
        action: "save_collection_schedule",
        taskId,
        startDate,
        days,
      },
      `采集任务已创建，将在所选日期09:00自动执行`,
    );
  };

  const exportRecoveryData = async (task: Task) => {
    try {
      setExportingTaskId(task.id);
      const response = await fetch(
        `/api/recovery-export?task=${encodeURIComponent(task.id)}`,
        { cache: "no-store" },
      );
      if (!response.ok) {
        const result = await readApiResponse<{ error?: string }>(
          response,
          "导出失败",
        );
        throw new Error(result.error || "导出失败");
      }
      const blob = await response.blob();
      const objectUrl = URL.createObjectURL(blob);
      const anchor = document.createElement("a");
      anchor.href = objectUrl;
      anchor.download = `${task.name}-数据回收.xlsx`;
      document.body.appendChild(anchor);
      anchor.click();
      anchor.remove();
      window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
      setToast("Excel 已导出，图片和截图已嵌入表格");
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "导出失败");
    } finally {
      setExportingTaskId(null);
    }
  };

  const exportResources = async (accountIds: string[]) => {
    if (accountIds.length === 0) {
      setToast("当前筛选结果为空");
      return;
    }
    try {
      setExportingResources(true);
      const response = await fetch("/api/resources-export", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ accountIds }),
      });
      if (!response.ok) {
        const result = await readApiResponse<{ error?: string }>(
          response,
          "导出失败",
        );
        throw new Error(result.error || "导出失败");
      }
      const blob = await response.blob();
      const objectUrl = URL.createObjectURL(blob);
      const anchor = document.createElement("a");
      anchor.href = objectUrl;
      anchor.download = `KOC资源库-${todayInputValue().replaceAll("-", "")}.xlsx`;
      document.body.appendChild(anchor);
      anchor.click();
      anchor.remove();
      window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
      setToast(`已导出 ${accountIds.length} 个KOC账号`);
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "导出失败");
    } finally {
      setExportingResources(false);
    }
  };

  const completeResourceImport = async (message: string) => {
    await loadData();
    setToast(message);
  };

  const uploadScreenshot = async (
    distribution: Distribution,
    event: ChangeEvent<HTMLInputElement>,
  ) => {
    const file = event.target.files?.[0];
    if (!file) return;
    try {
      setWorking(true);
      const uploadFile = await compressScreenshot(file);
      const form = new FormData();
      form.append("distributionId", distribution.id);
      form.append("file", uploadFile);
      const response = await fetch("/api/upload", { method: "POST", body: form });
      const result = await readApiResponse<DashboardData>(response, "上传失败");
      if (!response.ok) throw new Error(result.error || "上传失败");
      setData(result);
      setToast("创作者中心截图已收回");
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "上传失败");
    } finally {
      setWorking(false);
      event.target.value = "";
    }
  };

  const submitManualMetrics = async (event: FormEvent) => {
    event.preventDefault();
    if (!metricModal) return;
    const success = await runAction(
      {
        action: "manual_metrics",
        distributionId: metricModal.id,
        exposure: metricForm.exposure,
        views: metricForm.views,
      },
      "创作者中心数据已确认",
    );
    if (success) {
      setMetricModal(null);
      setMetricForm({ exposure: "", views: "" });
    }
  };

  const openMetricFallback = (distribution: Distribution) => {
    setMetricModal(distribution);
    setMetricForm({
      exposure: distribution.exposure?.toString() || "",
      views: distribution.views?.toString() || "",
    });
  };

  const pageTitle: Record<NavKey, { title: string; subtitle: string }> = {
    overview: {
      title: "分发工作台",
      subtitle: "用最少动作，跑完内容领取、发布与数据回收。",
    },
    tasks: {
      title: "内容任务",
      subtitle: "从飞书同步内容，按数量开放给合作方领取。",
    },
    distributions: {
      title: "内容分发",
      subtitle: "先按任务查看进度，再进入单个任务处理每篇笔记。",
    },
    resources: {
      title: "KOC资源",
      subtitle: "只沉淀真实发布过的账号，不要求提前登记。",
    },
    partners: {
      title: "合作方",
      subtitle: "绑定企业微信外部联系人，临期催办自动推送到人。",
    },
    recovery: {
      title: "数据回收",
      subtitle: "按任务设置自动采集日，集中查看最新公开数据与创作者截图。",
    },
    users: {
      title: "用户管理",
      subtitle: "由后台统一创建账号和重置密码，不开放自助注册与个人改密。",
    },
  };

  const logout = async () => {
    await fetch("/api/auth/logout", { method: "POST" }).catch(() => null);
    window.location.assign("/login");
  };

  return (
    <div className="app-shell">
      <aside className={`sidebar ${menuOpen ? "open" : ""}`}>
        <div className="brand-lockup">
          <div className="brand-symbol"><span>K</span></div>
          <div>
            <strong>KOC LOOP</strong>
            <span>内容分发闭环</span>
          </div>
        </div>

        <nav className="main-nav" aria-label="主导航">
          <p className="nav-caption">运营中心</p>
          {visibleNavItems.map((item) => (
            <button
              key={item.key}
              className={activeNav === item.key ? "active" : ""}
              onClick={() => navigate(item.key)}
            >
              <span className="nav-mark">{item.mark}</span>
              {item.label}
              {item.key === "recovery" && stats.pendingRecovery > 0 && (
                <span className="nav-badge">{stats.pendingRecovery}</span>
              )}
            </button>
          ))}
        </nav>

        <div className="sidebar-summary">
          <div className="pulse-dot" />
          <div>
            <strong>闭环运行中</strong>
            <span>{stats.activeTasks} 个任务正在分发</span>
          </div>
        </div>
        <button className="sidebar-user" onClick={() => void logout()} title="退出登录">
          <div className="avatar mint">{currentUser.username.slice(0, 1).toUpperCase()}</div>
          <div>
            <strong>{currentUser.username}</strong>
            <span>{currentUser.role === "super_admin" ? "超级管理员" : currentUser.role === "admin" ? "管理员" : "普通用户"}</span>
          </div>
          <span className="chevron">退出</span>
        </button>
      </aside>

      {menuOpen && <button className="sidebar-scrim" onClick={() => setMenuOpen(false)} aria-label="关闭菜单" />}

      <main className="main-area">
        <header className="topbar">
          <button className="menu-button" onClick={() => setMenuOpen(true)} aria-label="打开菜单">
            ☰
          </button>
          <div className="breadcrumb">
            <span>KOC LOOP</span>
            <i>/</i>
            <strong>{pageTitle[activeNav].title}</strong>
          </div>
          <div className="top-actions">
            <span className="sync-state"><i />飞书内容表已接入</span>
            <button className="ghost-button" onClick={() => setActiveNav("tasks")}>
              获取KOC领取链接
            </button>
            <button className="primary-button" onClick={() => setTaskModalOpen(true)}>
              <span>＋</span> 新建任务
            </button>
          </div>
        </header>

        <section className="content-frame">
          <div className="page-heading">
            <div>
              <p className="eyebrow">MVP · 核心闭环</p>
              <h1>{pageTitle[activeNav].title}</h1>
              <p>{pageTitle[activeNav].subtitle}</p>
            </div>
            {activeNav === "overview" && (
              <div className="date-chip">
                <span>今天</span>
                <strong>{formatShanghaiToday()}</strong>
              </div>
            )}
          </div>

          {loading ? (
            <LoadingState />
          ) : error ? (
            <div className="empty-state">
              <div className="empty-icon">!</div>
              <h3>暂时无法打开工作台</h3>
              <p>{error}</p>
              <button className="primary-button" onClick={() => void loadData()}>
                重新加载
              </button>
            </div>
          ) : (
            <>
              {activeNav === "overview" && (
                <OverviewPage
                  stats={stats}
                  distributions={recentDistributions}
                  tasks={data.tasks}
                  onNavigate={navigate}
                  showResources={currentUser.role !== "user"}
                />
              )}
              {activeNav === "tasks" && (
                <TasksPage
                  tasks={data.tasks}
                  portalUrl={data.portal_url}
                  onCreate={() => setTaskModalOpen(true)}
                  onCopyShare={copyTaskShareLink}
                  onPushToGroups={(task) => setGroupPushTask(task)}
                />
              )}
              {activeNav === "distributions" && (
                <DistributionPage
                  tasks={data.tasks}
                  distributions={data.distributions}
                  canRelease={isManager}
                  working={working}
                  onRelease={async (distribution) =>
                    runAction(
                      {
                        action: "release_distribution",
                        distributionId: distribution.id,
                      },
                      `“${distribution.content_title}”已释放，可重新领取`,
                    )
                  }
                />
              )}
              {activeNav === "resources" && (
                <ResourcesPage
                  accounts={data.accounts}
                  distributions={data.distributions}
                  exporting={exportingResources}
                  onExport={exportResources}
                  onImported={completeResourceImport}
                />
              )}
              {activeNav === "partners" && isManager && (
                <PartnersPage
                  partners={data.partners}
                  working={working}
                  onRefresh={loadData}
                  setToast={setToast}
                />
              )}
              {activeNav === "users" && isManager && (
                <UsersPage currentUser={currentUser} />
              )}
              {activeNav === "recovery" && (
                <RecoveryPage
                  tasks={data.tasks}
                  distributions={data.distributions}
                  working={working}
                  canUpdatePublishUrl={isManager}
                  onCollect={collectMetrics}
                  onUpdatePublishUrl={updateDistributionPublishUrl}
                  onSaveSchedule={saveCollectionSchedule}
                  onUpload={uploadScreenshot}
                  onFallback={openMetricFallback}
                  onRetryFailed={retryFailedMetrics}
                  onExport={exportRecoveryData}
                  onExportScreenshots={exportScreenshotResults}
                  exportingTaskId={exportingTaskId}
                />
              )}
            </>
          )}
        </section>
      </main>

      {taskModalOpen && (
        <div className="modal-backdrop" role="presentation" onMouseDown={() => setTaskModalOpen(false)}>
          <form className="modal-card" onSubmit={submitTask} onMouseDown={(event) => event.stopPropagation()}>
            <div className="modal-heading">
              <div>
                <p className="eyebrow">任务类型明确，回收链路独立</p>
                <h2>新建任务</h2>
              </div>
              <button type="button" onClick={() => setTaskModalOpen(false)} aria-label="关闭">×</button>
            </div>
            <div className="task-type-picker" role="group" aria-label="选择任务类型">
              <button
                type="button"
                className={taskForm.taskType === "content_publish" ? "active" : ""}
                onClick={() => setTaskForm({ ...taskForm, taskType: "content_publish" })}
              >
                <b>内容发布</b>
                <span>从飞书导入笔记，回收链接与数据</span>
              </button>
              <button
                type="button"
                className={taskForm.taskType === "screenshot_collect" ? "active" : ""}
                onClick={() => {
                  setTaskForm({ ...taskForm, taskType: "screenshot_collect" });
                  setSourcePreview(null);
                }}
              >
                <b>截图回收</b>
                <span>下发搜索要求，只回收结果截图</span>
              </button>
            </div>
            {taskForm.taskType === "content_publish" ? (
              <>
                <div className="form-grid two">
                  <label>
                    <span className="platform-meta-line">
                      <span>发布平台</span>
                      <PlatformBadge platform={taskForm.platform} compact />
                    </span>
                    <select
                      value={taskForm.platform}
                      onChange={(event) =>
                        setTaskForm({
                          ...taskForm,
                          platform: event.target.value as "小红书" | "抖音",
                        })
                      }
                    >
                      <option value="小红书">小红书</option>
                      <option value="抖音">抖音</option>
                    </select>
                  </label>
                  <label>
                    <span>内容类型</span>
                    <select
                      value={taskForm.contentFormat}
                      onChange={(event) => {
                        setTaskForm({
                          ...taskForm,
                          contentFormat: event.target.value as "image_text" | "video",
                        });
                        setSourcePreview(null);
                      }}
                    >
                      <option value="image_text">图文</option>
                      <option value="video">视频</option>
                    </select>
                  </label>
                </div>
                <label>
                  <span>飞书内容表链接</span>
                  <div className="source-url-row">
                    <input
                      value={taskForm.feishuUrl}
                      onChange={(event) => {
                        setTaskForm({ ...taskForm, feishuUrl: event.target.value });
                        setSourcePreview(null);
                      }}
                      placeholder="粘贴飞书 Wiki / Sheets 链接"
                      required
                    />
                    <button
                      type="button"
                      className="ghost-button source-read-button"
                      disabled={sourceWorking || !taskForm.feishuUrl.trim()}
                      onClick={() => void inspectFeishu()}
                    >
                      {sourceWorking ? "读取中…" : "读取表格"}
                    </button>
                  </div>
                  <small>
                    {taskForm.contentFormat === "video"
                      ? "自动读取标题、正文和视频附件；空标题或无视频的行不会进入任务。"
                      : "自动读取标题、正文/笔记内容、标签和全部配图；空标题行不会进入任务。"}
                  </small>
                </label>
                {sourcePreview && (
                  <div className="source-preview">
                    <div className="source-preview-head">
                      <div>
                        <span className="source-check">✓</span>
                        <div>
                          <strong>{sourcePreview.sheetName}</strong>
                          <small>工作表 {sourcePreview.sheetId} · 已匹配 {sourcePreview.columns.length} 个字段</small>
                        </div>
                      </div>
                      <b>
                        {sourcePreview.rowCount} 条 · {taskForm.contentFormat === "video"
                          ? `${sourcePreview.videoCount} 个视频`
                          : `${sourcePreview.imageCount} 张图片`}
                      </b>
                    </div>
                    <div className="source-columns">
                      {sourcePreview.columns.map((column) => <span key={column}>{column}</span>)}
                    </div>
                    <div className="source-samples">
                      {sourcePreview.preview.map((row) => (
                        <div key={row.sourceRow}>
                          <span>{row.sourceRow}</span>
                          <strong>{row.title}</strong>
                        </div>
                      ))}
                    </div>
                  </div>
                )}
              </>
            ) : (
              <div className="screenshot-task-fields">
                <div className="form-grid two">
                  <label>
                    <span>小红书搜索关键词</span>
                    <input
                      value={taskForm.keyword}
                      onChange={(event) => setTaskForm({ ...taskForm, keyword: event.target.value })}
                      placeholder="例如：暑期亲子酒店"
                      required
                    />
                  </label>
                  <label>
                    <span>任务份数</span>
                    <input
                      type="number"
                      min="1"
                      max="500"
                      value={taskForm.quantity}
                      onChange={(event) => setTaskForm({ ...taskForm, quantity: Number(event.target.value) })}
                      required
                    />
                  </label>
                </div>
                <label>
                  <span>任务说明</span>
                  <textarea
                    value={taskForm.instructions}
                    onChange={(event) => setTaskForm({ ...taskForm, instructions: event.target.value })}
                    placeholder="说明搜索步骤、截图范围和提交要求"
                    rows={4}
                    required
                  />
                </label>
                <label>
                  <span>示例截图（可选）</span>
                  <div className={`task-example-picker ${taskExampleImage ? "has-file" : ""}`}>
                    <input
                      type="file"
                      accept="image/*"
                      onChange={(event) => setTaskExampleImage(event.target.files?.[0] ?? null)}
                    />
                    <strong>{taskExampleImage ? taskExampleImage.name : "点击上传示例截图"}</strong>
                    <small>帮助KOC确认截图范围，支持 JPG、PNG，最大8MB</small>
                  </div>
                </label>
              </div>
            )}
            <div className="form-grid two">
              <label>
                <span>任务名称</span>
                <input
                  value={taskForm.name}
                  onChange={(event) => setTaskForm({ ...taskForm, name: event.target.value })}
                  placeholder="例如：夏日轻盈计划"
                  required
                />
              </label>
              <label>
                <span>品牌/项目</span>
                <input
                  value={taskForm.brand}
                  onChange={(event) => setTaskForm({ ...taskForm, brand: event.target.value })}
                  placeholder="例如：青柠实验室"
                  required
                />
              </label>
            </div>
            <label>
              <span>{taskForm.taskType === "screenshot_collect" ? "提交截止日期" : "发布截止日期"}</span>
              <input
                type="date"
                value={taskForm.dueAt}
                onChange={(event) => setTaskForm({ ...taskForm, dueAt: event.target.value })}
                required
              />
            </label>
            <div className="modal-actions">
              <button type="button" className="ghost-button" onClick={() => setTaskModalOpen(false)}>取消</button>
              <button
                className="primary-button"
                disabled={
                  working ||
                  (taskForm.taskType === "content_publish" && !sourcePreview)
                }
              >
                {working ? "创建中…" : "创建并开放领取"}
              </button>
            </div>
          </form>
        </div>
      )}

      {metricModal && (
        <div className="modal-backdrop" role="presentation" onMouseDown={() => setMetricModal(null)}>
          <form className="modal-card compact" onSubmit={submitManualMetrics} onMouseDown={(event) => event.stopPropagation()}>
            <div className="modal-heading">
              <div>
                <p className="eyebrow">OCR异常兜底</p>
                <h2>确认创作者数据</h2>
              </div>
              <button type="button" onClick={() => setMetricModal(null)} aria-label="关闭">×</button>
            </div>
            <div className="context-strip">
              <strong>{metricModal.account_nickname || "待识别账号"}</strong>
              <span>{metricModal.content_title}</span>
            </div>
            <div className="form-grid two">
              <label>
                <span>曝光量</span>
                <input type="number" min="0" value={metricForm.exposure} onChange={(event) => setMetricForm({ ...metricForm, exposure: event.target.value })} required />
              </label>
              <label>
                <span>阅读量</span>
                <input type="number" min="0" value={metricForm.views} onChange={(event) => setMetricForm({ ...metricForm, views: event.target.value })} required />
              </label>
            </div>
            <div className="modal-actions">
              <button type="button" className="ghost-button" onClick={() => setMetricModal(null)}>取消</button>
              <button className="primary-button" disabled={working}>确认数据</button>
            </div>
          </form>
        </div>
      )}

      {groupPushTask && (
        <GroupPushModal
          task={groupPushTask}
          portalUrl={data.portal_url}
          onClose={() => setGroupPushTask(null)}
          setToast={setToast}
        />
      )}

      {toast && <div className="toast"><span>✓</span>{toast}</div>}
      {working && <div className="working-bar" />}
    </div>
  );
}

type WecomStatus = {
  robotConfigured: boolean;
  appConfigured: boolean;
  dueDays: number;
};

type WecomContact = {
  externalUserId: string;
  name: string;
  avatar: string;
  corpName: string;
  ownerUserId: string;
};

type WecomGroupChatRow = {
  chat_id: string;
  name: string;
  owner_user_id: string;
  member_count: number;
};

type WecomGroupPushResult = {
  sender: string;
  msgid: string;
  chatCount: number;
  failList: string[];
};

type WecomGroupPushRecord = {
  id: string;
  taskId: string;
  msgid: string;
  sender: string;
  chatIds: string[];
  failList: string[];
  createdAt: string;
};

function PartnersPage({
  partners,
  working,
  onRefresh,
  setToast,
}: {
  partners: Partner[];
  working: boolean;
  onRefresh: () => Promise<void>;
  setToast: (message: string) => void;
}) {
  const [status, setStatus] = useState<WecomStatus | null>(null);
  const [statusLoading, setStatusLoading] = useState(true);
  const [editingId, setEditingId] = useState<string | null>(null);
  const [draftId, setDraftId] = useState("");
  const [partnerWorkingId, setPartnerWorkingId] = useState<string | null>(null);
  const [pickerPartner, setPickerPartner] = useState<Partner | null>(null);
  const [pickerContacts, setPickerContacts] = useState<WecomContact[]>([]);
  const [pickerFetchedAt, setPickerFetchedAt] = useState(0);
  const [pickerLoading, setPickerLoading] = useState(false);
  const [pickerError, setPickerError] = useState("");
  const [pickerSearch, setPickerSearch] = useState("");
  const [pickerBindingId, setPickerBindingId] = useState<string | null>(null);

  const loadStatus = useCallback(async () => {
    let response: Response;
    try {
      response = await fetch("/api/action", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ action: "wecom_status" }),
      });
    } catch {
      setToast("读取企业微信配置失败");
      setStatusLoading(false);
      return;
    }
    const result = await readApiResponse<WecomStatus & { error?: string }>(
      response,
      "读取企业微信配置失败",
    );
    if (!response.ok) {
      setToast(result.error || "读取企业微信配置失败");
      setStatusLoading(false);
      return;
    }
    setStatus({
      robotConfigured: result.robotConfigured,
      appConfigured: result.appConfigured,
      dueDays: result.dueDays,
    });
    setStatusLoading(false);
  }, [setToast]);

  useEffect(() => {
    const timer = window.setTimeout(() => {
      void loadStatus();
    }, 0);
    return () => window.clearTimeout(timer);
  }, [loadStatus]);

  const refreshStatus = async () => {
    setStatusLoading(true);
    await loadStatus();
  };

  const startEdit = (partner: Partner) => {
    setEditingId(partner.id);
    setDraftId(partner.wecom_external_user_id ?? "");
  };

  const cancelEdit = () => {
    setEditingId(null);
    setDraftId("");
  };

  const saveEdit = async (partner: Partner) => {
    const value = draftId.trim();
    try {
      setPartnerWorkingId(partner.id);
      const response = await fetch("/api/action", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          action: "bind_wecom_external_id",
          partnerId: partner.id,
          wecomExternalUserId: value,
        }),
      });
      const result = await readApiResponse<{
        partnerId?: string;
        wecomExternalUserId?: string | null;
        error?: string;
      }>(response, "保存失败");
      if (!response.ok) throw new Error(result.error || "保存失败");
      setEditingId(null);
      setDraftId("");
      await onRefresh();
      setToast("企业微信外部联系人已保存");
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "保存失败");
    } finally {
      setPartnerWorkingId(null);
    }
  };

  const testRobot = async () => {
    try {
      setPartnerWorkingId("robot");
      const response = await fetch("/api/action", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ action: "send_test_wecom" }),
      });
      const result = await readApiResponse<{
        robot?: "ok" | "skipped";
        app?: "ok" | "skipped" | "failed";
        error?: string;
      }>(response, "测试失败");
      if (!response.ok) throw new Error(result.error || "测试失败");
      if (result.robot === "ok") setToast("群机器人测试消息已发送");
      else if (result.robot === "skipped") setToast("群机器人未配置，请先填写 WECOM_ROBOT_WEBHOOK");
      else setToast("群机器人测试失败");
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "测试失败");
    } finally {
      setPartnerWorkingId(null);
    }
  };

  const triggerDueSoon = async () => {
    try {
      setPartnerWorkingId("due-soon");
      const response = await fetch("/api/action", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ action: "trigger_wecom_due_soon" }),
      });
      const result = await readApiResponse<{
        dueSoonAttempted?: number;
        dueSoonSent?: number;
        dueSoonFailed?: number;
        dueSoonSkipped?: number;
        digestSent?: boolean;
        error?: string;
      }>(response, "触发催办失败");
      if (!response.ok) throw new Error(result.error || "触发催办失败");
      const sent = result.dueSoonSent ?? 0;
      const failed = result.dueSoonFailed ?? 0;
      const skipped = result.dueSoonSkipped ?? 0;
      const attempted = result.dueSoonAttempted ?? 0;
      const digest = result.digestSent ? "群汇总已推送" : "群汇总未推送（无内容或未配置 webhook）";
      setToast(
        `催办完成：尝试 ${attempted} 个合作方，应用消息已发 ${sent}、跳过 ${skipped}、失败 ${failed}；${digest}`,
      );
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "触发催办失败");
    } finally {
      setPartnerWorkingId(null);
    }
  };

  const testAppMessage = async (partner: Partner) => {
    if (!partner.wecom_external_user_id) {
      setToast("请先保存企业微信外部联系人 ID");
      return;
    }
    try {
      setPartnerWorkingId(partner.id);
      const response = await fetch("/api/action", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          action: "send_test_wecom",
          partnerId: partner.id,
        }),
      });
      const result = await readApiResponse<{
        robot?: "ok" | "skipped";
        app?: "ok" | "skipped" | "failed";
        error?: string;
      }>(response, "测试失败");
      if (!response.ok) throw new Error(result.error || "测试失败");
      if (result.app === "ok") setToast(`已向 ${partner.name} 发送测试消息`);
      else if (result.app === "skipped") setToast("应用消息未配置或未绑定外部联系人");
      else setToast(`应用消息发送失败：${partner.name} 的外部联系人 ID 无效`);
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "测试失败");
    } finally {
      setPartnerWorkingId(null);
    }
  };

  const loadContacts = useCallback(async () => {
    const cacheFresh = pickerFetchedAt > 0 && Date.now() - pickerFetchedAt < 5 * 60 * 1000;
    if (cacheFresh) return;
    setPickerLoading(true);
    setPickerError("");
    let response: Response;
    try {
      response = await fetch("/api/action", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ action: "wecom_list_external_contacts" }),
      });
    } catch {
      setPickerError("拉取企业微信外部联系人失败");
      setPickerLoading(false);
      return;
    }
    const result = await readApiResponse<{ contacts?: WecomContact[]; error?: string }>(
      response,
      "拉取企业微信外部联系人失败",
    );
    if (!response.ok) {
      setPickerError(result.error || "拉取企业微信外部联系人失败");
      setPickerLoading(false);
      return;
    }
    setPickerContacts(result.contacts ?? []);
    setPickerFetchedAt(Date.now());
    setPickerLoading(false);
  }, [pickerFetchedAt]);

  const openPicker = (partner: Partner) => {
    setPickerPartner(partner);
    setPickerSearch("");
    setPickerError("");
    void loadContacts();
  };

  const closePicker = () => {
    if (pickerBindingId) return;
    setPickerPartner(null);
    setPickerSearch("");
    setPickerError("");
  };

  const filteredContacts = pickerContacts.filter((contact) => {
    if (!pickerSearch.trim()) return true;
    const q = pickerSearch.trim().toLowerCase();
    return (
      contact.name.toLowerCase().includes(q) ||
      contact.corpName.toLowerCase().includes(q) ||
      contact.externalUserId.toLowerCase().includes(q) ||
      contact.ownerUserId.toLowerCase().includes(q)
    );
  });

  const pickContact = async (contact: WecomContact) => {
    if (!pickerPartner) return;
    try {
      setPickerBindingId(contact.externalUserId);
      const response = await fetch("/api/action", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          action: "bind_wecom_external_id",
          partnerId: pickerPartner.id,
          wecomExternalUserId: contact.externalUserId,
          wecomName: contact.name,
        }),
      });
      const result = await readApiResponse<{ error?: string }>(response, "绑定失败");
      if (!response.ok) throw new Error(result.error || "绑定失败");
      setToast(`已为 ${pickerPartner.name} 绑定 ${contact.name}`);
      setPickerPartner(null);
      setPickerSearch("");
      await onRefresh();
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "绑定失败");
    } finally {
      setPickerBindingId(null);
    }
  };

  return (
    <div className="stack">
      <section className="panel wecom-status-panel">
        <div className="panel-heading">
          <div>
            <h2>企业微信连通性</h2>
            <p>临期催办与应用消息依赖以下配置；不显示 Secret。</p>
          </div>
          <button
            type="button"
            className="ghost-button"
            disabled={statusLoading || working}
            onClick={() => void refreshStatus()}
          >
            {statusLoading ? "刷新中…" : "刷新状态"}
          </button>
        </div>
        <div className="wecom-status-grid">
          <div className={`wecom-status-card ${status?.robotConfigured ? "ok" : "off"}`}>
            <span>群机器人</span>
            <strong>{status ? (status.robotConfigured ? "已配置" : "未配置") : "加载中"}</strong>
            <small>用于当日催办汇总</small>
          </div>
          <div className={`wecom-status-card ${status?.appConfigured ? "ok" : "off"}`}>
            <span>自建应用消息</span>
            <strong>{status ? (status.appConfigured ? "已配置" : "未配置") : "加载中"}</strong>
            <small>需要 corpId + agentId + secret</small>
          </div>
          <div className="wecom-status-card">
            <span>临期阈值</span>
            <strong>{status ? `${status.dueDays} 天` : "—"}</strong>
            <small>WECOM_NOTIFY_DUE_DAYS</small>
          </div>
        </div>
        <div className="wecom-status-actions">
          <button
            type="button"
            className="primary-button soft"
            disabled={partnerWorkingId === "robot" || working || statusLoading}
            onClick={() => void testRobot()}
          >
            {partnerWorkingId === "robot" ? "发送中…" : "测试群机器人"}
          </button>
          <button
            type="button"
            className="ghost-button"
            disabled={partnerWorkingId === "due-soon" || working || statusLoading}
            onClick={() => void triggerDueSoon()}
          >
            {partnerWorkingId === "due-soon" ? "执行中…" : "立即跑一次催办"}
          </button>
          <p className="wecom-status-hint">
            立即跑一次 daily 09:00 的催办链路：按 partner 聚合发应用消息，再向群机器人发当日汇总。
          </p>
          {(!status?.robotConfigured || !status?.appConfigured) && (
            <p className="wecom-status-hint">
              未配置项可在 <code>.dev.vars</code> 或部署环境变量里补齐后重启服务。
            </p>
          )}
        </div>
      </section>
      <section className="panel table-panel">
        <div className="panel-heading">
          <div>
            <h2>合作方</h2>
            <p>每个合作方绑定一个企业微信外部联系人 ID，用于临期催办推送</p>
          </div>
        </div>
        <div className="partner-list">
          <div className="partner-row head">
            <span>合作方</span>
            <span>企业微信名</span>
            <span>负责人</span>
            <span>外部联系人 ID</span>
            <span>操作</span>
          </div>
          {partners.length === 0 && (
            <div className="partner-row empty">暂无合作方，先在飞书表格或领取流程中产生</div>
          )}
          {partners.map((partner) => {
            const editing = editingId === partner.id;
            const busy = partnerWorkingId === partner.id;
            return (
              <div className="partner-row" key={partner.id}>
                <span><strong>{partner.name}</strong></span>
                <span>{partner.wecom_name || "—"}</span>
                <span>{partner.owner || "—"}</span>
                <span className="partner-external-cell">
                  {editing ? (
                    <input
                      type="text"
                      value={draftId}
                      placeholder="企业微信外部联系人 external_userid"
                      onChange={(event) => setDraftId(event.target.value)}
                      disabled={busy}
                    />
                  ) : (
                    <code title={partner.wecom_external_user_id || "未绑定"}>
                      {partner.wecom_external_user_id || "未绑定"}
                    </code>
                  )}
                </span>
                <span className="partner-actions">
                  {editing ? (
                    <>
                      <button
                        type="button"
                        className="primary-button soft"
                        disabled={busy}
                        onClick={() => void saveEdit(partner)}
                      >
                        {busy ? "保存中…" : "保存"}
                      </button>
                      <button
                        type="button"
                        className="ghost-button"
                        disabled={busy}
                        onClick={cancelEdit}
                      >
                        取消
                      </button>
                    </>
                  ) : (
                    <>
                      <button
                        type="button"
                        className="ghost-button"
                        disabled={busy || working}
                        onClick={() => startEdit(partner)}
                      >
                        {partner.wecom_external_user_id ? "修改" : "绑定"}
                      </button>
                      <button
                        type="button"
                        className="ghost-button"
                        disabled={busy || working || pickerLoading}
                        onClick={() => openPicker(partner)}
                      >
                        从企业微信导入
                      </button>
                      <button
                        type="button"
                        className="ghost-button"
                        disabled={busy || working || !partner.wecom_external_user_id}
                        onClick={() => void testAppMessage(partner)}
                      >
                        {busy ? "测试中…" : "测试发送"}
                      </button>
                    </>
                  )}
                </span>
              </div>
            );
          })}
        </div>
      </section>
      {pickerPartner && (
        <div className="modal-backdrop" role="presentation" onMouseDown={closePicker}>
          <div className="modal-card wecom-picker-modal" onMouseDown={(event) => event.stopPropagation()}>
            <div className="modal-heading">
              <div>
                <h2>选择 {pickerPartner.name} 的企业微信外部联系人</h2>
                <p>从企业微信外部联系人列表中挑选一个，绑定后会自动写入 ID 与企业微信名。</p>
              </div>
              <button
                type="button"
                className="ghost-button"
                onClick={closePicker}
                disabled={!!pickerBindingId}
                aria-label="关闭"
              >
                ×
              </button>
            </div>
            <div className="import-guide-strip">
              <input
                type="search"
                className="wecom-picker-search"
                placeholder="按名称 / 企业 / ID / 负责人 搜索"
                value={pickerSearch}
                onChange={(event) => setPickerSearch(event.target.value)}
                disabled={pickerLoading || !!pickerBindingId}
              />
              <button
                type="button"
                className="ghost-button"
                onClick={() => void loadContacts()}
                disabled={pickerLoading || !!pickerBindingId}
              >
                {pickerLoading ? "拉取中…" : "刷新列表"}
              </button>
              {pickerFetchedAt > 0 && !pickerLoading && (
                <span className="wecom-picker-cache-hint">
                  缓存于 {new Date(pickerFetchedAt).toLocaleTimeString()}（5 分钟内复用）
                </span>
              )}
            </div>
            {pickerError && (
              <p className="form-error">{pickerError}</p>
            )}
            <div className="wecom-picker-list">
              {pickerLoading && pickerContacts.length === 0 ? (
                <div className="wecom-picker-empty">正在拉取外部联系人…</div>
              ) : pickerContacts.length === 0 ? (
                <div className="wecom-picker-empty">
                  {pickerError ? "拉取失败，请检查应用配置后重试" : "未拉取到任何外部联系人"}
                </div>
              ) : filteredContacts.length === 0 ? (
                <div className="wecom-picker-empty">没有匹配的联系人</div>
              ) : (
                filteredContacts.map((contact) => (
                  <button
                    key={contact.externalUserId}
                    type="button"
                    className="wecom-picker-row"
                    onClick={() => void pickContact(contact)}
                    disabled={!!pickerBindingId}
                  >
                    {contact.avatar ? (
                      <img src={contact.avatar} alt="" className="wecom-picker-avatar" />
                    ) : (
                      <span className="wecom-picker-avatar placeholder" aria-hidden="true">
                        {contact.name.slice(0, 1) || "?"}
                      </span>
                    )}
                    <span className="wecom-picker-meta">
                      <span className="wecom-picker-name">{contact.name || "（未命名）"}</span>
                      <span className="wecom-picker-sub">
                        {contact.corpName || "—"} · 负责人 {contact.ownerUserId || "—"}
                      </span>
                      <code className="wecom-picker-id">{contact.externalUserId}</code>
                    </span>
                    {pickerBindingId === contact.externalUserId ? (
                      <span className="wecom-picker-binding">绑定中…</span>
                    ) : pickerPartner.wecom_external_user_id === contact.externalUserId ? (
                      <span className="wecom-picker-current">当前</span>
                    ) : null}
                  </button>
                ))
              )}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

function buildGroupPushDraft(task: Task, portalUrl: string) {
  const portal = portalUrl.trim().replace(/\/$/, "");
  const claimUrl = portal && task.share_token ? `${portal}/?task=${task.share_token}` : "";
  const formatLabel =
    task.task_type === "screenshot_collect"
      ? "截图回收"
      : task.content_format === "video"
        ? "视频"
        : "图文";
  return [
    `【新任务】${task.name}`,
    `品牌：${task.brand}｜数量：${task.quantity} 份｜截止：${task.due_at}`,
    `平台：${task.platform}｜形式：${formatLabel}`,
    claimUrl ? `领取链接：${claimUrl}` : "领取链接生成失败，请联系管理员",
  ].join("\n");
}

function GroupPushModal({
  task,
  portalUrl,
  onClose,
  setToast,
}: {
  task: Task;
  portalUrl: string;
  onClose: () => void;
  setToast: (message: string) => void;
}) {
  const [groups, setGroups] = useState<WecomGroupChatRow[]>([]);
  const [groupsLoading, setGroupsLoading] = useState(true);
  const [syncing, setSyncing] = useState(false);
  const [selected, setSelected] = useState<string[]>([]);
  const [draft, setDraft] = useState(() => buildGroupPushDraft(task, portalUrl));
  const [sending, setSending] = useState(false);
  const [result, setResult] = useState<{ groupCount: number; failCount: number; results: WecomGroupPushResult[] } | null>(null);
  const [history, setHistory] = useState<WecomGroupPushRecord[]>([]);

  const postAction = useCallback(async <T,>(payload: Record<string, unknown>, fallback: string) => {
    const response = await fetch("/api/action", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
    const parsed = await readApiResponse<T & { error?: string }>(response, fallback);
    if (!response.ok) throw new Error(parsed.error || fallback);
    return parsed;
  }, []);

  const loadHistory = useCallback(async () => {
    try {
      const parsed = await postAction<{ pushes: WecomGroupPushRecord[] }>(
        { action: "wecom_task_group_pushes", taskId: task.id },
        "读取发送记录失败",
      );
      setHistory(parsed.pushes);
    } catch {
      // 历史加载失败不阻断弹窗
    }
  }, [postAction, task.id]);

  const refreshGroups = useCallback(async (mode: "auto" | "sync") => {
    if (mode === "auto") {
      try {
        const cached = await postAction<{ groups: WecomGroupChatRow[] }>(
          { action: "wecom_group_chats" },
          "读取客户群列表失败",
        );
        if (cached.groups.length > 0) {
          setGroups(cached.groups);
          return;
        }
      } catch {
        // 缓存读取失败时继续尝试同步
      }
    }
    setSyncing(true);
    try {
      const parsed = await postAction<{ groups: WecomGroupChatRow[]; syncedCount: number }>(
        { action: "wecom_sync_group_chats" },
        "同步客户群失败",
      );
      setGroups(parsed.groups);
      setSelected((previous) => previous.filter((id) => parsed.groups.some((group) => group.chat_id === id)));
      setToast(`已同步 ${parsed.syncedCount} 个客户群`);
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "同步客户群失败");
    } finally {
      setSyncing(false);
    }
  }, [postAction, setToast]);

  useEffect(() => {
    const timer = window.setTimeout(() => {
      void refreshGroups("auto").finally(() => setGroupsLoading(false));
      void loadHistory();
    }, 0);
    return () => window.clearTimeout(timer);
  }, [refreshGroups, loadHistory]);

  const toggleGroup = (chatId: string) => {
    setSelected((previous) =>
      previous.includes(chatId)
        ? previous.filter((id) => id !== chatId)
        : [...previous, chatId],
    );
  };

  const allSelected = groups.length > 0 && selected.length === groups.length;

  const submitPush = async () => {
    if (selected.length === 0) {
      setToast("请选择要发送的客户群");
      return;
    }
    setSending(true);
    try {
      const parsed = await postAction<{
        results: WecomGroupPushResult[];
        groupCount: number;
        failCount: number;
      }>(
        {
          action: "wecom_push_task_to_groups",
          taskId: task.id,
          chatIds: selected,
          text: draft,
        },
        "创建企业群发失败",
      );
      setResult({ results: parsed.results, groupCount: parsed.groupCount, failCount: parsed.failCount });
      setToast("群发任务已创建");
      await loadHistory();
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "创建企业群发失败");
    } finally {
      setSending(false);
    }
  };

  return (
    <div className="modal-backdrop" role="presentation" onMouseDown={onClose}>
      <div className="modal-card group-push-modal" onMouseDown={(event) => event.stopPropagation()}>
        <div className="modal-heading">
          <div>
            <p className="eyebrow">企业微信 · 企业群发</p>
            <h2>发布「{task.name}」到客户群</h2>
          </div>
          <button type="button" onClick={onClose} aria-label="关闭">×</button>
        </div>
        <div className="group-push-toolbar">
          <span className="group-push-count">
            {groupsLoading ? "加载群列表…" : `${groups.length} 个客户群 · 已选 ${selected.length} 个`}
          </span>
          <div className="group-push-toolbar-actions">
            <button type="button" className="ghost-button" onClick={() => setSelected(allSelected ? [] : groups.map((group) => group.chat_id))} disabled={groupsLoading || groups.length === 0}>
              {allSelected ? "清空选择" : "全选"}
            </button>
            <button type="button" className="ghost-button" onClick={() => void refreshGroups("sync")} disabled={syncing || groupsLoading}>
              {syncing ? "同步中…" : "同步群列表"}
            </button>
          </div>
        </div>
        <div className="wecom-picker-list group-push-list">
          {groupsLoading ? (
            <div className="wecom-picker-empty">正在读取客户群…</div>
          ) : groups.length === 0 ? (
            <div className="wecom-picker-empty">暂无客户群，请先配置企业微信应用并点击「同步群列表」</div>
          ) : (
            groups.map((group) => (
              <label key={group.chat_id} className="group-push-row">
                <input
                  type="checkbox"
                  checked={selected.includes(group.chat_id)}
                  onChange={() => toggleGroup(group.chat_id)}
                  disabled={sending}
                />
                <span className="wecom-picker-meta">
                  <span className="wecom-picker-name">{group.name || "（未命名群）"}</span>
                  <span className="wecom-picker-sub">群主 {group.owner_user_id || "—"} · {group.member_count} 人</span>
                </span>
                <code className="wecom-picker-id">{group.chat_id}</code>
              </label>
            ))
          )}
        </div>
        <label className="group-push-text">
          <span>群发文本（可编辑，发送前请确认）</span>
          <textarea rows={7} value={draft} onChange={(event) => setDraft(event.target.value)} disabled={sending} />
        </label>
        {result && (
          <div className="group-push-result">
            <p>
              已创建 {result.results.length} 个群发任务（覆盖 {result.groupCount} 个群）
              {result.failCount > 0 ? `，${result.failCount} 个群发送失败` : ""}。
              群主需在企微客户端「群发助手」点击发送后，消息才会送达客户群；每个群每月最多接收当月天数条群发。
            </p>
            <ul>
              {result.results.map((item) => (
                <li key={item.msgid || item.sender}>
                  群主 {item.sender} · {item.chatCount} 个群 · msgid {item.msgid || "—"}
                  {item.failList.length > 0 && (
                    <span className="group-push-fail">（失败群：{item.failList.join("、")}）</span>
                  )}
                </li>
              ))}
            </ul>
          </div>
        )}
        {history.length > 0 && (
          <div className="group-push-history">
            <p className="group-push-history-title">发送记录</p>
            <ul>
              {history.map((record) => (
                <li key={record.id}>
                  {formatDate(record.createdAt, true)} · 群主 {record.sender} · {record.chatIds.length} 个群
                  {record.failList.length > 0 ? ` · 失败 ${record.failList.length} 个` : ""}
                </li>
              ))}
            </ul>
          </div>
        )}
        <div className="modal-actions">
          <button type="button" className="ghost-button" onClick={onClose} disabled={sending}>关闭</button>
          <button type="button" className="primary-button" onClick={() => void submitPush()} disabled={sending || groupsLoading || selected.length === 0}>
            {sending ? "创建中…" : `创建群发（${selected.length} 个群）`}
          </button>
        </div>
      </div>
    </div>
  );
}

function LoadingState() {
  return (
    <div className="loading-layout" aria-label="正在加载">
      <div className="loading-card wide" />
      <div className="loading-card" />
      <div className="loading-card" />
      <div className="loading-table" />
    </div>
  );
}

function OverviewPage({
  stats,
  distributions,
  tasks,
  onNavigate,
  showResources,
}: {
  stats: {
    resources: number;
    activeTasks: number;
    published: number;
    completed: number;
    pendingRecovery: number;
  };
  distributions: Distribution[];
  tasks: Task[];
  onNavigate: (key: NavKey) => void;
  showResources: boolean;
}) {
  const cards = [
    {
      label: "已沉淀账号资源",
      value: stats.resources,
      hint: "均来自真实发布",
      tone: "ink",
    },
    {
      label: "正在分发任务",
      value: stats.activeTasks,
      hint: `${tasks.reduce((sum, task) => sum + task.claimed_quantity, 0)} 篇已被领取`,
      tone: "green",
    },
    {
      label: "已回填发布",
      value: stats.published,
      hint: "链接自动识别账号",
      tone: "blue",
    },
    {
      label: "待完成数据回收",
      value: stats.pendingRecovery,
      hint: stats.pendingRecovery > 0 ? "需要跟进" : "全部按时完成",
      tone: "orange",
    },
  ].filter((card) => showResources || card.label !== "已沉淀账号资源");
  return (
    <>
      <div className="stat-grid">
        {cards.map((card) => (
          <article className="stat-card" key={card.label}>
            <div className={`stat-icon ${card.tone}`}><span /></div>
            <p>{card.label}</p>
            <div className="stat-value">
              <strong>{card.value}</strong>
              <span>个</span>
            </div>
            <small>{card.hint}</small>
          </article>
        ))}
      </div>

      <div className="overview-grid">
        <section className="panel workflow-panel">
          <div className="panel-heading">
            <div>
              <h2>当前闭环</h2>
              <p>每篇内容只沿着一条记录向前走</p>
            </div>
            <button className="text-button" onClick={() => onNavigate("distributions")}>查看全部</button>
          </div>
          <div className="workflow-track">
            <div className="workflow-step done">
              <span>01</span>
              <div><strong>内容已同步</strong><small>{tasks.reduce((sum, task) => sum + task.quantity, 0)} 篇</small></div>
            </div>
            <i />
            <div className="workflow-step active">
              <span>02</span>
              <div><strong>KOC领取</strong><small>{tasks.reduce((sum, task) => sum + task.claimed_quantity, 0)} 篇</small></div>
            </div>
            <i />
            <div className="workflow-step">
              <span>03</span>
              <div><strong>发布回填</strong><small>{stats.published} 篇</small></div>
            </div>
            <i />
            <div className="workflow-step">
              <span>04</span>
              <div><strong>数据回收</strong><small>{stats.completed} 篇完成</small></div>
            </div>
          </div>
          <div className="focus-callout">
            <div className="callout-ring"><strong>{stats.pendingRecovery}</strong><span>待回收</span></div>
            <div>
              <p className="eyebrow">今天最需要推进</p>
              <h3>把已发布笔记的数据收回来</h3>
              <p>按任务选择采集日期，系统在当天09:00更新点赞、收藏、评论、转发和总互动。</p>
            </div>
            <button className="primary-button soft" onClick={() => onNavigate("recovery")}>进入数据回收</button>
          </div>
        </section>

        <section className="panel task-mini-panel">
          <div className="panel-heading">
            <div><h2>进行中的任务</h2><p>按截止时间排序</p></div>
          </div>
          <div className="task-mini-list">
            {tasks.map((task) => {
              const progress = Math.round((task.claimed_quantity / task.quantity) * 100);
              return (
                <article key={task.id}>
                  <div className="task-mini-top">
                    <div className={`avatar ${avatarColor(task.name)}`}>{task.brand.slice(0, 1)}</div>
                    <div>
                      <strong>{task.name}</strong>
                      <span className="platform-meta-line">
                        <span>{task.brand}</span>
                        <PlatformBadge platform={task.platform} compact />
                        <span>{task.content_format === "video" ? "视频" : "图文"}</span>
                      </span>
                    </div>
                    <b>{progress}%</b>
                  </div>
                  <div className="progress"><span style={{ width: `${progress}%` }} /></div>
                  <div className="task-mini-meta">
                    <span>{task.claimed_quantity}/{task.quantity} 篇已领取</span>
                    <span>{formatDate(task.due_at)}截止</span>
                  </div>
                </article>
              );
            })}
          </div>
          <button className="full-ghost-button" onClick={() => onNavigate("tasks")}>管理全部任务</button>
        </section>
      </div>

      <section className="panel table-panel">
        <div className="panel-heading">
          <div><h2>最近分发</h2><p>领取、发布账号与回收状态一眼看清</p></div>
          <button className="text-button" onClick={() => onNavigate("distributions")}>查看全部</button>
        </div>
        <DistributionTable distributions={distributions} compact />
      </section>
    </>
  );
}

function TasksPage({
  tasks,
  portalUrl,
  onCreate,
  onCopyShare,
  onPushToGroups,
}: {
  tasks: Task[];
  portalUrl: string;
  onCreate: () => void;
  onCopyShare: (task: Task) => void;
  onPushToGroups: (task: Task) => void;
}) {
  return (
    <div className="stack">
      <section className="task-hero">
        <div>
          <p className="eyebrow">两种固定任务类型</p>
          <h2>发布笔记，或回收任务截图</h2>
          <p>内容发布走飞书笔记链路；截图回收只下发关键词和要求，不进入发布数据采集。</p>
        </div>
        <button className="primary-button light" onClick={onCreate}>＋ 新建任务</button>
      </section>
      <section className="panel table-panel">
        <div className="panel-heading">
          <div><h2>任务列表</h2><p>{tasks.length} 个任务</p></div>
        </div>
        <div className="task-list">
          {tasks.map((task) => {
            const progress = Math.round((task.claimed_quantity / task.quantity) * 100);
            return (
              <article className="task-row" key={task.id}>
                <div className={`avatar large ${avatarColor(task.name)}`}>{task.brand.slice(0, 1)}</div>
                <div className="task-row-main">
                  <div><h3>{task.name}</h3><span>{task.brand}</span></div>
                  <small className="platform-meta-line">
                    {task.task_type === "screenshot_collect" ? (
                      <span>截图回收 · 关键词任务</span>
                    ) : (
                      <>
                        <PlatformBadge platform={task.platform} compact />
                        <span>{task.content_format === "video" ? "视频" : "图文"}</span>
                        <span>{task.source_sheet_name ? `飞书 ${task.source_sheet_name}` : "历史示例内容表"}</span>
                      </>
                    )}
                  </small>
                  <div className="progress"><span style={{ width: `${progress}%` }} /></div>
                </div>
                <div className="task-number"><strong>{task.claimed_quantity}</strong><span>/ {task.quantity} {task.task_type === "screenshot_collect" ? "份已领取" : "篇已领取"}</span></div>
                <div className="task-date"><span>截止日期</span><strong>{formatDate(task.due_at)}</strong></div>
                <span className={`status-pill ${task.status}`}>{statusLabel(task.status)}</span>
                <div className="task-share-actions">
                  <button
                    className="share-link-button"
                    onClick={() => onCopyShare(task)}
                    disabled={!portalUrl || !task.share_token}
                  >
                    复制领取链接
                  </button>
                  <button
                    className="share-link-button"
                    onClick={() => onPushToGroups(task)}
                  >
                    发布到企微群
                  </button>
                  {portalUrl && task.share_token && (
                    <a
                      className="round-arrow"
                      href={`${portalUrl.replace(/\/$/, "")}/?task=${task.share_token}`}
                      target="_blank"
                      rel="noreferrer"
                      aria-label="打开外部KOC领取页"
                    >
                      ↗
                    </a>
                  )}
                </div>
              </article>
            );
          })}
        </div>
      </section>
    </div>
  );
}

function TaskScopeList({
  tasks,
  distributions,
  mode,
  onOpen,
}: {
  tasks: Task[];
  distributions: Distribution[];
  mode: "distribution" | "recovery";
  onOpen: (taskId: string) => void;
}) {
  return (
    <section className="task-scope-grid">
      {tasks.map((task) => {
        const taskDistributions = distributions.filter(
          (item) => item.task_id === task.id,
        );
        const isScreenshotTask = task.task_type === "screenshot_collect";
        const published = taskDistributions.filter((item) => item.publish_url);
        const submitted = taskDistributions.filter(
          (item) => item.result_submitted_at,
        );
        const publicDataComplete = published.filter(
          (item) => latestPublicMetrics(item).likes !== null,
        ).length;
        const creatorComplete = published.filter(
          (item) => hasCreatorMetrics(item),
        ).length;
        const progress = task.quantity
          ? Math.round((task.claimed_quantity / task.quantity) * 100)
          : 0;
        return (
          <article className="task-scope-card" key={task.id}>
            <div className="task-scope-head">
              <div className={`avatar large ${avatarColor(task.name)}`}>
                {task.brand.slice(0, 1)}
              </div>
              <div>
                <h3>{task.name}</h3>
                <div className="task-scope-subline">
                  <span>{task.brand} · {formatDate(task.due_at)} 截止</span>
                  <PlatformBadge platform={task.platform} compact />
                  <span className={`content-format-badge ${isScreenshotTask ? "screenshot" : task.content_format === "video" ? "video" : "image-text"}`}>
                    {isScreenshotTask ? "截图回收" : task.content_format === "video" ? "视频" : "图文"}
                  </span>
                </div>
              </div>
              <span className={`status-pill ${task.status}`}>{statusLabel(task.status)}</span>
            </div>
            <div className="task-source-line">
              <span>{isScreenshotTask ? "任务类型" : "飞书来源"}</span>
              {isScreenshotTask ? (
                <strong>截图回收 · 只收结果截图</strong>
              ) : task.source_url ? (
                <a href={task.source_url} target="_blank" rel="noreferrer">
                  {task.source_sheet_name || `工作表 ${task.source_sheet_id}`} ↗
                </a>
              ) : (
                <strong>历史示例内容表</strong>
              )}
            </div>
            {mode === "distribution" ? (
              <>
                <div className="task-scope-metrics">
                  <div><span>{isScreenshotTask ? "任务总量" : "内容总量"}</span><strong>{task.quantity}</strong></div>
                  <div><span>已领取</span><strong>{task.claimed_quantity}</strong></div>
                  <div><span>{isScreenshotTask ? "已提交" : "已发布"}</span><strong>{isScreenshotTask ? submitted.length : published.length}</strong></div>
                </div>
                <div className="progress task-scope-progress">
                  <span style={{ width: `${progress}%` }} />
                </div>
              </>
            ) : (
              <div className="task-scope-metrics">
                {isScreenshotTask ? (
                  <>
                    <div><span>已领取</span><strong>{taskDistributions.length}</strong></div>
                    <div><span>已提交</span><strong>{submitted.length}</strong></div>
                    <div><span>待提交</span><strong>{taskDistributions.length - submitted.length}</strong></div>
                  </>
                ) : (
                  <>
                    <div><span>已发布</span><strong>{published.length}</strong></div>
                    <div><span>公开数据</span><strong>{publicDataComplete}</strong></div>
                    <div><span>创作者数据</span><strong>{creatorComplete}</strong></div>
                  </>
                )}
              </div>
            )}
            <button className="task-scope-open" onClick={() => onOpen(task.id)}>
              <span>{mode === "distribution" ? "查看分发明细" : "进入回收队列"}</span>
              <b>→</b>
            </button>
          </article>
        );
      })}
    </section>
  );
}

function TaskDetailHeader({
  task,
  label,
  onBack,
}: {
  task: Task;
  label: string;
  onBack: () => void;
}) {
  return (
    <section className="task-detail-header">
      <button className="detail-back" onClick={onBack}>← 返回任务列表</button>
      <div className={`avatar large ${avatarColor(task.name)}`}>{task.brand.slice(0, 1)}</div>
      <div>
        <p>{label}</p>
        <h2>{task.name}</h2>
        <span className="platform-meta-line">
          <span>{task.brand}</span>
          {task.task_type === "screenshot_collect" ? (
            <span>截图回收</span>
          ) : (
            <>
              <PlatformBadge platform={task.platform} compact />
              <span>{task.content_format === "video" ? "视频" : "图文"}</span>
            </>
          )}
          <span>{task.quantity} {task.task_type === "screenshot_collect" ? "份" : "篇"}</span>
          <span>{formatDate(task.due_at)} 截止</span>
        </span>
      </div>
      {task.task_type !== "screenshot_collect" && task.source_url && (
        <a href={task.source_url} target="_blank" rel="noreferrer">
          打开飞书原表 ↗
        </a>
      )}
    </section>
  );
}

function DistributionPage({
  tasks,
  distributions,
  canRelease,
  working,
  onRelease,
}: {
  tasks: Task[];
  distributions: Distribution[];
  canRelease: boolean;
  working: boolean;
  onRelease: (distribution: Distribution) => Promise<boolean>;
}) {
  const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
  const [taskQuery, setTaskQuery] = useState("");
  const [brandFilter, setBrandFilter] = useState("");
  const [contentTypeFilter, setContentTypeFilter] = useState("");
  const [platformFilter, setPlatformFilter] = useState("");
  const [openTaskFilter, setOpenTaskFilter] = useState<"brand" | "content" | "platform" | null>(null);
  const taskFiltersRef = useRef<HTMLDivElement | null>(null);
  const brandOptions = useMemo(
    () => Array.from(new Set(tasks.map((task) => task.brand).filter(Boolean))).sort((a, b) => a.localeCompare(b, "zh-CN")),
    [tasks],
  );
  const brandFilterOptions = useMemo(
    () => [{ value: "", label: "全部品牌/项目" }, ...brandOptions.map((brand) => ({ value: brand, label: brand }))],
    [brandOptions],
  );
  const contentTypeOptions = [
    { value: "", label: "全部内容类型" },
    { value: "图文", label: "图文" },
    { value: "视频", label: "视频" },
    { value: "截图回收", label: "截图回收" },
  ];
  const platformOptions = [
    { value: "", label: "全部平台" },
    { value: "小红书", label: "小红书" },
    { value: "抖音", label: "抖音" },
  ];
  const taskFilterControls = [
    { key: "brand" as const, label: "品牌/项目", value: brandFilter, options: brandFilterOptions },
    { key: "content" as const, label: "内容类型", value: contentTypeFilter, options: contentTypeOptions },
    { key: "platform" as const, label: "平台", value: platformFilter, options: platformOptions },
  ];
  useEffect(() => {
    const closeOnOutsideClick = (event: MouseEvent) => {
      if (!taskFiltersRef.current?.contains(event.target as Node)) {
        setOpenTaskFilter(null);
      }
    };
    document.addEventListener("mousedown", closeOnOutsideClick);
    return () => document.removeEventListener("mousedown", closeOnOutsideClick);
  }, []);
  const setTaskFilterValue = (key: "brand" | "content" | "platform", value: string) => {
    if (key === "brand") setBrandFilter(value);
    if (key === "content") setContentTypeFilter(value);
    if (key === "platform") setPlatformFilter(value);
  };
  const selectTaskFilter = (key: "brand" | "content" | "platform", value: string) => {
    setTaskFilterValue(key, value);
    setOpenTaskFilter(null);
  };
  const filteredTasks = useMemo(() => {
    const query = taskQuery.trim().toLocaleLowerCase("zh-CN");
    const brandQuery = brandFilter.trim().toLocaleLowerCase("zh-CN");
    const contentTypeQuery = contentTypeFilter.trim().toLocaleLowerCase("zh-CN");
    const platformQuery = platformFilter.trim().toLocaleLowerCase("zh-CN");
    return tasks.filter((task) => {
      const taskContentType = task.task_type === "screenshot_collect"
        ? "截图回收"
        : task.content_format === "video" ? "视频" : "图文";
      return (
        (!query || task.name.toLocaleLowerCase("zh-CN").includes(query)) &&
        (!brandQuery || task.brand.toLocaleLowerCase("zh-CN").includes(brandQuery)) &&
        (!contentTypeQuery || taskContentType.toLocaleLowerCase("zh-CN").includes(contentTypeQuery)) &&
        (!platformQuery || task.platform.toLocaleLowerCase("zh-CN").includes(platformQuery))
      );
    });
  }, [brandFilter, contentTypeFilter, platformFilter, taskQuery, tasks]);
  const hasTaskFilters = Boolean(
    taskQuery.trim() || brandFilter.trim() || contentTypeFilter.trim() || platformFilter.trim(),
  );
  const [releaseTarget, setReleaseTarget] = useState<Distribution | null>(null);
  const selectedTask = tasks.find((task) => task.id === selectedTaskId);
  if (!selectedTask) {
    return (
      <div className="stack">
        <div className="section-intro">
          <div><p className="eyebrow">按任务管理</p><h2>选择一个分发任务</h2></div>
          <p>每个任务独立查看领取、发布和账号识别进度，不混排不同项目的笔记。</p>
        </div>
        <section className="distribution-task-toolbar" aria-label="筛选分发任务">
          <label className="distribution-task-search">
            <span aria-hidden="true">⌕</span>
            <input
              type="search"
              value={taskQuery}
              onChange={(event) => setTaskQuery(event.target.value)}
              placeholder="搜索任务名称"
              aria-label="按任务名称模糊搜索"
            />
          </label>
          <div className="distribution-task-filter-group" ref={taskFiltersRef}>
            {taskFilterControls.map((filter) => {
              const normalizedFilterValue = filter.value.trim().toLocaleLowerCase("zh-CN");
              const visibleOptions = filter.options.filter((option) => (
                !normalizedFilterValue ||
                option.label.toLocaleLowerCase("zh-CN").includes(normalizedFilterValue)
              ));
              return (
                <div
                  className={`distribution-task-filter-combobox ${filter.key === "brand" ? "brand" : ""}`}
                  key={filter.key}
                >
                  <div className="distribution-task-filter-input">
                    <span aria-hidden="true">⌕</span>
                    <input
                      type="search"
                      role="combobox"
                      aria-autocomplete="list"
                      aria-expanded={openTaskFilter === filter.key}
                      aria-controls={`distribution-task-${filter.key}-menu`}
                      value={filter.value}
                      placeholder={`搜索${filter.label}`}
                      aria-label={`模糊搜索${filter.label}`}
                      onFocus={() => setOpenTaskFilter(filter.key)}
                      onChange={(event) => {
                        setTaskFilterValue(filter.key, event.target.value);
                        setOpenTaskFilter(filter.key);
                      }}
                      onKeyDown={(event) => {
                        if (event.key === "Escape") setOpenTaskFilter(null);
                      }}
                    />
                  </div>
                  {openTaskFilter === filter.key ? (
                  <div
                    id={`distribution-task-${filter.key}-menu`}
                    className="distribution-task-filter-menu"
                    role="listbox"
                    aria-label={`选择${filter.label}`}
                  >
                    <div className="distribution-task-filter-menu-options">
                      {visibleOptions.map((option) => (
                        <button
                          key={option.value}
                          type="button"
                          role="option"
                          aria-selected={filter.value === option.value}
                          className={filter.value === option.value ? "selected" : ""}
                          onClick={() => selectTaskFilter(filter.key, option.value)}
                        >
                          <span>{option.label}</span>
                          {filter.value === option.value ? <small>✓</small> : null}
                        </button>
                      ))}
                      {visibleOptions.length === 0 ? (
                        <span className="distribution-task-filter-empty">没有匹配项</span>
                      ) : null}
                    </div>
                  </div>
                ) : null}
                </div>
              );
            })}
          </div>
          <button
            type="button"
            className="distribution-task-clear"
            disabled={!hasTaskFilters}
            onClick={() => {
              setTaskQuery("");
              setBrandFilter("");
              setContentTypeFilter("");
              setPlatformFilter("");
              setOpenTaskFilter(null);
            }}
          >
            清空筛选
          </button>
          <span className="distribution-task-count">共 <b>{filteredTasks.length}</b> 个任务</span>
        </section>
        {filteredTasks.length > 0 ? (
          <TaskScopeList
            tasks={filteredTasks}
            distributions={distributions}
            mode="distribution"
            onOpen={setSelectedTaskId}
          />
        ) : (
          <section className="distribution-task-empty">
            <strong>没有符合条件的任务</strong>
            <span>可以调整任务名称或清空筛选条件</span>
          </section>
        )}
      </div>
    );
  }

  const taskDistributions = distributions.filter(
    (item) => item.task_id === selectedTask.id,
  );
  const isScreenshotTask = selectedTask.task_type === "screenshot_collect";
  const published = taskDistributions.filter((item) => item.publish_url).length;
  const submitted = taskDistributions.filter((item) => item.result_submitted_at).length;
  return (
    <div className="stack">
      <TaskDetailHeader
        task={selectedTask}
        label={isScreenshotTask ? "截图任务分发" : "内容分发"}
        onBack={() => setSelectedTaskId(null)}
      />
      <div className="inline-summary">
        <div><span>已领取</span><strong>{taskDistributions.length}</strong></div>
        <i />
        <div><span>{isScreenshotTask ? "已提交截图" : "已回填链接"}</span><strong>{isScreenshotTask ? submitted : published}</strong></div>
        <i />
        <div><span>{isScreenshotTask ? "待提交" : "待发布"}</span><strong>{taskDistributions.length - (isScreenshotTask ? submitted : published)}</strong></div>
        <div className="summary-note"><span className="pulse-dot" />当前仅展示“{selectedTask.name}”</div>
      </div>
      <section className="panel table-panel">
        <div className="panel-heading">
          <div><h2>{isScreenshotTask ? "截图任务分发表" : "内容分发表"}</h2><p>{isScreenshotTask ? "每份任务与结果截图一一对应" : "实际发布账号在链接回填后自动补齐"}</p></div>
        </div>
        {isScreenshotTask ? (
          <ScreenshotTaskTable distributions={taskDistributions} />
        ) : (
          <DistributionTable
            distributions={taskDistributions}
            onRelease={canRelease ? setReleaseTarget : undefined}
          />
        )}
      </section>
      {releaseTarget && (
        <div
          className="modal-backdrop"
          role="presentation"
          onMouseDown={() => { if (!working) setReleaseTarget(null); }}
        >
          <div
            className="modal-card compact release-distribution-modal"
            role="alertdialog"
            aria-modal="true"
            aria-labelledby="release-distribution-title"
            onMouseDown={(event) => event.stopPropagation()}
          >
            <div className="modal-heading">
              <div>
                <p className="eyebrow warning">释放领取</p>
                <h2 id="release-distribution-title">确认释放这篇笔记？</h2>
              </div>
              <button
                type="button"
                onClick={() => setReleaseTarget(null)}
                aria-label="关闭"
                disabled={working}
              >
                ×
              </button>
            </div>
            <div className="release-distribution-summary">
              <strong>{releaseTarget.content_title}</strong>
              <span>当前领取合作方：{releaseTarget.partner_name}</span>
            </div>
            <p className="release-distribution-warning">
              释放后，这篇笔记会从当前领取记录中移除并回到可领取池，其他 KOC 可以重新领取。此操作不能撤销。
            </p>
            <div className="modal-actions">
              <button
                type="button"
                className="ghost-button"
                onClick={() => setReleaseTarget(null)}
                disabled={working}
              >
                取消
              </button>
              <button
                type="button"
                className="release-confirm-button"
                onClick={async () => {
                  const released = await onRelease(releaseTarget);
                  if (released) setReleaseTarget(null);
                }}
                disabled={working}
              >
                {working ? "释放中…" : "确认释放"}
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

function DistributionTable({
  distributions,
  compact = false,
  onRelease,
}: {
  distributions: Distribution[];
  compact?: boolean;
  onRelease?: (distribution: Distribution) => void;
}) {
  return (
    <div className="table-scroll">
      <table className="data-table">
        <thead>
          <tr>
            <th>内容</th>
            <th>领取合作方</th>
            <th>实际发布账号</th>
            <th>发布时间</th>
            <th>数据状态</th>
            <th>状态</th>
            {onRelease && <th>操作</th>}
          </tr>
        </thead>
        <tbody>
          {distributions.map((item) => (
            <tr key={item.id}>
              <td>
                <div className="content-cell">
                  <span className="content-id">{item.content_id.split("-").at(-1)?.toUpperCase()}</span>
                  <div><strong>{item.content_title}</strong><span>{item.task_name} · {item.task_brand}</span></div>
                </div>
              </td>
              <td><strong className="plain-strong">{item.partner_name}</strong></td>
              <td>
                {item.account_nickname ? (
                  <div className="account-cell">
                    <div className={`avatar small ${avatarColor(item.account_nickname)}`}>{item.account_nickname.slice(0, 1)}</div>
                    <div><strong>{item.account_nickname}</strong><PlatformBadge platform={item.account_platform} compact /></div>
                  </div>
                ) : (
                  <span className="muted">回填链接后识别</span>
                )}
              </td>
              <td><span className="date-value">{formatDate(item.publish_time, true)}</span></td>
              <td>
                {item.d7_likes !== null ? (
                  <span className="metric-ready">D7 已回收</span>
                ) : item.d2_likes !== null ? (
                  <span className="metric-waiting">采集中</span>
                ) : (
                  <span className="muted">{item.publish_url ? "等待D2" : "尚未发布"}</span>
                )}
              </td>
              <td><span className={`status-pill ${item.status}`}>{statusLabel(item.status)}</span></td>
              {onRelease && (
                <td>
                  {!item.publish_url && !item.result_submitted_at ? (
                    <button
                      type="button"
                      className="release-distribution-button"
                      onClick={() => onRelease(item)}
                    >
                      释放
                    </button>
                  ) : (
                    <span className="muted">—</span>
                  )}
                </td>
              )}
            </tr>
          ))}
        </tbody>
      </table>
      {!compact && distributions.length === 0 && <div className="table-empty">暂无分发记录</div>}
    </div>
  );
}

type AdminPreviewImage = { src: string; title: string };

function AdminImageLightbox({
  images,
  activeIndex,
  onIndexChange,
  onClose,
}: {
  images: AdminPreviewImage[];
  activeIndex: number;
  onIndexChange?: (index: number) => void;
  onClose: () => void;
}) {
  const image = images[activeIndex] ?? null;
  const canNavigate = images.length > 1 && Boolean(onIndexChange);
  const showPrevious = () => {
    if (!canNavigate) return;
    onIndexChange?.((activeIndex - 1 + images.length) % images.length);
  };
  const showNext = () => {
    if (!canNavigate) return;
    onIndexChange?.((activeIndex + 1) % images.length);
  };
  useEffect(() => {
    if (!image) return;
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") onClose();
      if (event.key === "ArrowLeft" && canNavigate) {
        onIndexChange?.((activeIndex - 1 + images.length) % images.length);
      }
      if (event.key === "ArrowRight" && canNavigate) {
        onIndexChange?.((activeIndex + 1) % images.length);
      }
    };
    document.addEventListener("keydown", onKeyDown);
    const previousOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.removeEventListener("keydown", onKeyDown);
      document.body.style.overflow = previousOverflow;
    };
  }, [activeIndex, canNavigate, image, images.length, onClose, onIndexChange]);
  if (!image) return null;
  return (
    <div
      className="admin-image-lightbox"
      role="dialog"
      aria-modal="true"
      aria-label={image.title}
      onMouseDown={onClose}
    >
      <div
        className="admin-image-lightbox-card"
        onMouseDown={(event) => event.stopPropagation()}
      >
        <div className="admin-image-lightbox-heading">
          <div>
            <strong>{image.title}</strong>
            {canNavigate && (
              <span aria-live="polite">{activeIndex + 1} / {images.length}</span>
            )}
          </div>
          <button type="button" onClick={onClose}>关闭</button>
        </div>
        <div className="admin-image-lightbox-stage">
          {canNavigate && (
            <button type="button" className="previous" onClick={showPrevious} aria-label="上一张">
              上一张
            </button>
          )}
          {/* eslint-disable-next-line @next/next/no-img-element */}
          <img src={image.src} alt={image.title} />
          {canNavigate && (
            <button type="button" className="next" onClick={showNext} aria-label="下一张">
              下一张
            </button>
          )}
        </div>
      </div>
    </div>
  );
}

function ScreenshotTaskTable({ distributions }: { distributions: Distribution[] }) {
  const [previewGallery, setPreviewGallery] = useState<{
    images: AdminPreviewImage[];
    activeIndex: number;
  } | null>(null);
  return (
    <>
    <div className="table-scroll">
      <table className="data-table screenshot-task-table">
        <thead>
          <tr>
            <th>搜索关键词</th>
            <th>领取合作方</th>
            <th>领取时间</th>
            <th>提交时间</th>
            <th>结果截图</th>
            <th>状态</th>
          </tr>
        </thead>
        <tbody>
          {distributions.map((item) => (
            <tr key={item.id}>
              <td>
                <div className="content-cell">
                  <span className="content-id">搜</span>
                  <div><strong>{item.content_title}</strong><span>{item.task_name}</span></div>
                </div>
              </td>
              <td><strong className="plain-strong">{item.partner_name}</strong></td>
              <td><span className="date-value">{formatDate(item.claimed_at, true)}</span></td>
              <td><span className="date-value">{formatDate(item.result_submitted_at, true)}</span></td>
              <td>
                {parseResultScreenshotKeys(item.result_screenshot_key).length > 0 ? (
                  <div className="admin-result-gallery">
                    {parseResultScreenshotKeys(item.result_screenshot_key).map((_, index, screenshotKeys) => {
                      return (
                      <button
                        type="button"
                        onClick={() =>
                          setPreviewGallery({
                            images: screenshotKeys.map((__, imageIndex) => ({
                              src: `/api/task-result-image?distribution=${encodeURIComponent(item.id)}&index=${imageIndex + 1}`,
                              title: `${item.content_title} · 第${imageIndex + 1}张结果截图`,
                            })),
                            activeIndex: index,
                          })
                        }
                        key={`${item.id}-${index}`}
                        title={`查看第${index + 1}张截图`}
                      >
                        <img
                          src={`/api/task-result-image?distribution=${encodeURIComponent(item.id)}&index=${index + 1}`}
                          alt={`任务结果截图 ${index + 1}`}
                          loading="lazy"
                        />
                      </button>
                    )})}
                    <span>{parseResultScreenshotKeys(item.result_screenshot_key).length}张</span>
                  </div>
                ) : (
                  <span className="muted">待KOC上传</span>
                )}
              </td>
              <td>
                <span className={`status-pill ${item.result_submitted_at ? "complete" : "claimed"}`}>
                  {item.result_submitted_at ? "已提交" : "待提交"}
                </span>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      {distributions.length === 0 && <div className="table-empty">暂无领取记录</div>}
    </div>
    <AdminImageLightbox
      images={previewGallery?.images ?? []}
      activeIndex={previewGallery?.activeIndex ?? 0}
      onIndexChange={(activeIndex) =>
        setPreviewGallery((current) => current ? { ...current, activeIndex } : current)
      }
      onClose={() => setPreviewGallery(null)}
    />
    </>
  );
}

function ResourcesPage({
  accounts,
  distributions,
  exporting,
  onExport,
  onImported,
}: {
  accounts: Account[];
  distributions: Distribution[];
  exporting: boolean;
  onExport: (accountIds: string[]) => void;
  onImported: (message: string) => Promise<void>;
}) {
  const [query, setQuery] = useState("");
  const [platformFilter, setPlatformFilter] = useState("全部平台");
  const [ipFilter, setIpFilter] = useState("");
  const [sourceFilter, setSourceFilter] = useState("");
  const [importOpen, setImportOpen] = useState(false);
  const [importMode, setImportMode] = useState<"bulk" | "single">("bulk");
  const [importFile, setImportFile] = useState<File | null>(null);
  const [importPreview, setImportPreview] = useState<ResourceImportPreview | null>(null);
  const [importWorking, setImportWorking] = useState(false);
  const [importError, setImportError] = useState("");
  const [singleForm, setSingleForm] = useState({
    profileUrl: "",
    nickname: "",
    publicAccountId: "",
    ipLocation: "",
    followers: "",
    gender: "",
    bio: "",
    tags: "",
    cooperationSource: "",
  });
  const [singleWorking, setSingleWorking] = useState(false);
  const [singleError, setSingleError] = useState("");
  const resourceAccounts = useMemo(() => {
    const deliveriesByAccount = new Map<string, Distribution[]>();
    distributions.forEach((distribution) => {
      if (!distribution.account_id) return;
      const current = deliveriesByAccount.get(distribution.account_id) ?? [];
      current.push(distribution);
      deliveriesByAccount.set(distribution.account_id, current);
    });
    return accounts.map((account) => {
      const deliveries = deliveriesByAccount.get(account.id) ?? [];
      return {
        account,
        sources: [
          ...new Set(
            [
              ...deliveries
                .filter(
                  (item) =>
                    !item.claimant_name ||
                    item.partner_name !== item.claimant_name,
                )
                .map((item) => item.partner_name),
              ...(account.cooperation_source || "")
                .split(/[、,，;；|]/)
                .map((item) => item.trim()),
            ].filter(Boolean),
          ),
        ].sort((left, right) => left.localeCompare(right, "zh-CN")),
        partnerManagedOnly:
          deliveries.some((item) => item.delegation_bundle_id) &&
          deliveries.every((item) => item.delegation_bundle_id),
      };
    });
  }, [accounts, distributions]);
  const platformOptions = useMemo(
    () => [
      "小红书",
      "抖音",
      ...[...new Set(accounts.map((account) => account.platform).filter(Boolean))]
        .filter((platform) => platform !== "小红书" && platform !== "抖音")
        .sort((left, right) => left.localeCompare(right, "zh-CN")),
    ],
    [accounts],
  );
  const ipOptions = useMemo(
    () =>
      [
        ...new Set(
          accounts.map((account) => account.ip_location || "待识别"),
        ),
      ].sort((left, right) => {
        if (left === "待识别") return 1;
        if (right === "待识别") return -1;
        return left.localeCompare(right, "zh-CN");
      }),
    [accounts],
  );
  const sourceOptions = useMemo(
    () =>
      [...new Set(resourceAccounts.flatMap((item) => item.sources))].sort(
        (left, right) => left.localeCompare(right, "zh-CN"),
      ),
    [resourceAccounts],
  );
  const filteredAccounts = useMemo(() => {
    const keyword = query.trim().toLocaleLowerCase("zh-CN");
    const ipKeyword = ipFilter.trim().toLocaleLowerCase("zh-CN");
    const sourceKeyword = sourceFilter.trim().toLocaleLowerCase("zh-CN");
    return resourceAccounts.filter(({ account, sources }) => {
      const searchable = `${account.nickname} ${account.public_account_id} ${account.current_contact || ""} ${account.bio || ""} ${account.tags || ""}`.toLocaleLowerCase(
        "zh-CN",
      );
      const ipLocation = (account.ip_location || "待识别").toLocaleLowerCase(
        "zh-CN",
      );
      return (
        (!keyword || searchable.includes(keyword)) &&
        (platformFilter === "全部平台" ||
          account.platform === platformFilter) &&
        (!ipKeyword || ipLocation.includes(ipKeyword)) &&
        (!sourceKeyword ||
          sources.some((source) =>
            source.toLocaleLowerCase("zh-CN").includes(sourceKeyword),
          ))
      );
    });
  }, [ipFilter, platformFilter, query, resourceAccounts, sourceFilter]);
  const hasActiveFilters =
    Boolean(query.trim()) ||
    platformFilter !== "全部平台" ||
    Boolean(ipFilter.trim()) ||
    Boolean(sourceFilter.trim());
  const clearFilters = () => {
    setQuery("");
    setPlatformFilter("全部平台");
    setIpFilter("");
    setSourceFilter("");
  };
  const closeImport = () => {
    if (importWorking || singleWorking) return;
    setImportOpen(false);
    setImportFile(null);
    setImportPreview(null);
    setImportError("");
    setSingleForm({
      profileUrl: "",
      nickname: "",
      publicAccountId: "",
      ipLocation: "",
      followers: "",
      gender: "",
      bio: "",
      tags: "",
      cooperationSource: "",
    });
    setSingleError("");
  };
  const switchImportMode = (mode: "bulk" | "single") => {
    if (importWorking || singleWorking || mode === importMode) return;
    setImportMode(mode);
    setImportFile(null);
    setImportPreview(null);
    setImportError("");
    setSingleForm({
      profileUrl: "",
      nickname: "",
      publicAccountId: "",
      ipLocation: "",
      followers: "",
      gender: "",
      bio: "",
      tags: "",
      cooperationSource: "",
    });
    setSingleError("");
  };
  const submitSingle = async () => {
    if (!singleForm.profileUrl.trim()) {
      setSingleError("请填写账号主页链接");
      return;
    }
    try {
      setSingleWorking(true);
      setSingleError("");
      const response = await fetch("/api/resources-insert", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(singleForm),
      });
      const result = await readApiResponse<{ action: string; message?: string; error?: string }>(
        response,
        "保存失败",
      );
      if (!response.ok) throw new Error(result.error || "保存失败");
      setImportOpen(false);
      setImportMode("bulk");
      setSingleForm({
        profileUrl: "",
        nickname: "",
        publicAccountId: "",
        ipLocation: "",
        followers: "",
        gender: "",
        bio: "",
        tags: "",
        cooperationSource: "",
      });
      setSingleError("");
      await onImported(result.message || "已保存账号");
    } catch (reason) {
      setSingleError(reason instanceof Error ? reason.message : "保存失败");
    } finally {
      setSingleWorking(false);
    }
  };
  const submitImport = async (mode: "preview" | "commit") => {
    if (!importFile) {
      setImportError("请先选择需要导入的 Excel 文件");
      return;
    }
    try {
      setImportWorking(true);
      setImportError("");
      const form = new FormData();
      form.append("file", importFile);
      form.append("mode", mode);
      const response = await fetch("/api/resources-import", {
        method: "POST",
        body: form,
      });
      const result = await readApiResponse<
        ResourceImportPreview & { error?: string; message?: string }
      >(response, "导入失败");
      if (!response.ok) throw new Error(result.error || "导入失败");
      if (mode === "preview") setImportPreview(result);
      else {
        setImportOpen(false);
        setImportFile(null);
        setImportPreview(null);
        await onImported(result.message || "KOC资源已导入");
      }
    } catch (reason) {
      setImportError(reason instanceof Error ? reason.message : "导入失败");
    } finally {
      setImportWorking(false);
    }
  };
  return (
    <div className="stack">
      <section className="resource-intro">
        <div className="resource-figure">
          <strong>{accounts.length}</strong>
          <span>个真实发布账号</span>
        </div>
        <div>
          <h2>资源从合作与历史资产中持续沉淀</h2>
          <p>发布链接会自动建档，已有KOC资源也可以使用标准模板批量导入。</p>
        </div>
        <div className="resource-rule">
          <span>唯一去重规则</span>
          <strong>平台 + 账号主页</strong>
        </div>
      </section>
      <section className="panel table-panel">
        <div className="panel-heading">
          <div><h2>账号资源库</h2><p>统一管理合作沉淀与历史导入账号</p></div>
          <div className="resource-heading-actions">
            <a className="ghost-button" download href="/KOC资源导入模板.xlsx">
              下载导入模板
            </a>
            <button className="primary-button soft" onClick={() => setImportOpen(true)}>
              导入KOC资源
            </button>
            <div className="filter-chips">
              <button
                className={platformFilter === "全部平台" ? "active" : ""}
                onClick={() => setPlatformFilter("全部平台")}
              >
                全部平台
              </button>
              {platformOptions.map((platform) => (
                <button
                  className={platformFilter === platform ? "active" : ""}
                  key={platform}
                  onClick={() => setPlatformFilter(platform)}
                >
                  <PlatformBadge platform={platform} compact />
                </button>
              ))}
            </div>
          </div>
        </div>
        <div className="resource-toolbar">
          <div className="resource-search">
            <span aria-hidden="true">⌕</span>
            <input
              aria-label="搜索账号名称、账号ID或当前联系人"
              onChange={(event) => setQuery(event.target.value)}
              placeholder="搜索账号名称 / 账号ID / 当前联系人 / 标签"
              type="search"
              value={query}
            />
          </div>
          <div className="resource-filter-search">
            <span aria-hidden="true">⌕</span>
            <input
              aria-label="模糊搜索IP地区"
              list="resource-ip-options"
              onChange={(event) => setIpFilter(event.target.value)}
              placeholder="搜索IP地区"
              type="search"
              value={ipFilter}
            />
            <datalist id="resource-ip-options">
              {ipOptions.map((ip) => <option key={ip} value={ip} />)}
            </datalist>
          </div>
          <div className="resource-filter-search">
            <span aria-hidden="true">⌕</span>
            <input
              aria-label="模糊搜索合作来源"
              list="resource-source-options"
              onChange={(event) => setSourceFilter(event.target.value)}
              placeholder="搜索合作来源"
              type="search"
              value={sourceFilter}
            />
            <datalist id="resource-source-options">
              {sourceOptions.map((source) => (
                <option key={source} value={source} />
              ))}
            </datalist>
          </div>
          <button
            className="resource-clear-button"
            disabled={!hasActiveFilters}
            onClick={clearFilters}
          >
            清空筛选
          </button>
          <div className="resource-toolbar-summary">
            <span>共 <b>{filteredAccounts.length}</b> 个结果</span>
            <button
              className="export-data-button"
              disabled={exporting || filteredAccounts.length === 0}
              onClick={() =>
                onExport(filteredAccounts.map((item) => item.account.id))
              }
            >
              {exporting ? "正在导出…" : "导出筛选结果"}
            </button>
          </div>
        </div>
        <div className="resource-grid">
          {filteredAccounts.map(({ account, sources, partnerManagedOnly }) => {
            const profileLink = resourceProfileLink(account);
            const accountTags = (account.tags || "")
              .split(/[,，、;；|]/)
              .map((tag) => tag.trim())
              .filter(Boolean)
              .slice(0, 5);
            const rawBio = (account.bio || "").trim();
            const hasBio = Boolean(
              rawBio &&
                !/^(未填写简介|还没有简介|暂无简介|未填写|待识别)$/u.test(rawBio),
            );
            const accountIdLabel =
              account.platform === "小红书"
                ? "小红书号"
                : account.platform === "抖音"
                  ? "抖音号"
                  : "账号";
            return (
            <article
              className={`resource-card ${account.platform === "小红书" ? "platform-xhs" : account.platform === "抖音" ? "platform-douyin" : "platform-other"}`}
              key={account.id}
            >
              <div className="resource-card-head">
                <div className={`avatar xlarge resource-profile-avatar ${avatarColor(account.nickname)}`}>{account.nickname.slice(0, 1)}</div>
                <div className="resource-profile-main">
                  <div className="resource-name-line">
                    <h3>{account.nickname}</h3>
                    {account.gender === "男" && (
                      <span className="gender-icon male" aria-label="男性" title="男性">♂</span>
                    )}
                    {account.gender === "女" && (
                      <span className="gender-icon female" aria-label="女性" title="女性">♀</span>
                    )}
                  </div>
                  <div className="resource-account-number">
                    <span>{accountIdLabel}</span>
                    <strong>{account.public_account_id || "待识别"}</strong>
                  </div>
                  <div className="resource-platform-line">
                    <PlatformBadge platform={account.platform} compact />
                    <span className="resource-location">IP属地 · {account.ip_location || "待识别"}</span>
                  </div>
                  <div className="resource-current-contact">
                    <span>当前联系人</span>
                    <strong>{account.current_contact || "待补充"}</strong>
                  </div>
                </div>
              </div>
              <p className={`resource-bio ${hasBio ? "" : "empty"}`} title={hasBio ? rawBio : undefined}>
                {hasBio ? rawBio : "暂无简介"}
              </p>
              <div className={`resource-tags ${accountTags.length === 0 ? "empty" : ""}`}>
                <div>
                  {accountTags.length > 0
                    ? accountTags.map((tag) => <b key={tag}>{tag}</b>)
                    : <b>待打标</b>}
                </div>
              </div>
              <div className="resource-metrics" aria-label="账号数据">
                <div><span>粉丝</span><strong>{formatNumber(account.followers)}</strong></div>
                <div><span>合作发布</span><strong>{account.post_count}</strong></div>
                <div className="resource-latest"><span>最近合作</span><strong>{formatDate(account.last_seen_at)}</strong></div>
              </div>
              <div className="resource-card-foot">
                <div className="resource-source">
                  <span>合作来源</span>
                  <div>
                    {sources.map((source) => <b key={source}>{source}</b>)}
                    {partnerManagedOnly && (
                      <b className="partner-managed">合作社资源 · 不可直联</b>
                    )}
                    {sources.length === 0 && !partnerManagedOnly && <b className="empty">暂无</b>}
                  </div>
                </div>
                {profileLink && <a href={profileLink.href} target="_blank" rel="noreferrer">{profileLink.label}</a>}
              </div>
            </article>
            );
          })}
        </div>
        {filteredAccounts.length === 0 && (
          <div className="resource-empty">
            <strong>没有符合条件的KOC</strong>
            <span>可以调整搜索内容或清空筛选条件</span>
            <button className="ghost-button" onClick={clearFilters}>
              清空筛选
            </button>
          </div>
        )}
      </section>
      {importOpen && (
        <div className="modal-backdrop" role="presentation" onMouseDown={closeImport}>
          <div
            className="modal-card resource-import-modal"
            role="dialog"
            aria-modal="true"
            aria-labelledby="resource-import-title"
            onMouseDown={(event) => event.stopPropagation()}
          >
            <div className="modal-heading">
              <div>
                <p className="eyebrow">{importMode === "single" ? "单条新增" : "批量导入"}</p>
                <h2 id="resource-import-title">导入已有KOC资源</h2>
              </div>
              <button type="button" onClick={closeImport} aria-label="关闭">×</button>
            </div>
            <div className="import-mode-tabs" role="tablist">
              <button
                type="button"
                role="tab"
                aria-selected={importMode === "bulk"}
                className={importMode === "bulk" ? "active" : ""}
                onClick={() => switchImportMode("bulk")}
              >
                批量导入
              </button>
              <button
                type="button"
                role="tab"
                aria-selected={importMode === "single"}
                className={importMode === "single" ? "active" : ""}
                onClick={() => switchImportMode("single")}
              >
                单条新增
              </button>
            </div>
            {importMode === "bulk" && (
              <>
                <div className="import-guide-strip">
                  <span>1</span><p>账号链接必填，其余资料选填</p>
                  <i />
                  <span>2</span><p>系统只补全空缺的公开资料</p>
                  <i />
                  <span>3</span><p>确认后写入资源库</p>
                </div>
                <label className="resource-import-dropzone">
                  <input
                    accept=".xlsx,.csv"
                    type="file"
                    onChange={(event) => {
                      setImportFile(event.target.files?.[0] ?? null);
                      setImportPreview(null);
                      setImportError("");
                    }}
                  />
                  <strong>{importFile ? importFile.name : "选择 Excel / CSV 文件"}</strong>
                  <span>单次最多 10,000 个账号，文件不超过 20MB</span>
                </label>
                {!importPreview && (
                  <div className="import-template-note">
                    <div><strong>还没有模板？</strong><span>只有账号链接必填；性别、简介、标签等资料均可选填，多个标签请用逗号分隔且最多 5 个，系统只补全空缺字段。</span></div>
                    <a className="ghost-button" download href="/KOC资源导入模板.xlsx">下载模板</a>
                  </div>
                )}
                {importPreview && (
                  <div className="resource-import-preview">
                    <div className="import-summary-grid">
                      <div><span>数据总数</span><strong>{importPreview.summary.total}</strong></div>
                      <div className="create"><span>新增账号</span><strong>{importPreview.summary.create}</strong></div>
                      <div className="update"><span>更新账号</span><strong>{importPreview.summary.update}</strong></div>
                      <div className={importPreview.summary.error ? "error" : ""}><span>异常数据</span><strong>{importPreview.summary.error}</strong></div>
                    </div>
                    <div className="import-preview-table">
                      <div className="import-preview-row head">
                        <span>行号</span><span>账号</span><span>账号ID</span><span>IP / 来源</span><span>结果</span>
                      </div>
                      {importPreview.rows.slice(0, 8).map((row) => (
                        <div className="import-preview-row" key={row.rowNumber}>
                          <span>{row.rowNumber}</span>
                          <span><strong>{row.nickname || "—"}</strong><small><PlatformBadge platform={row.platform} compact /></small></span>
                          <span>{row.publicAccountId || "主页识别"}</span>
                          <span><strong>{row.ipLocation}</strong><small>{row.cooperationSource || "未填写来源"}</small></span>
                          <span className={`import-result ${row.action}`}>
                            {row.action === "create" ? "新增" : row.action === "update" ? "更新" : row.errors.join("；")}
                          </span>
                        </div>
                      ))}
                    </div>
                    {(importPreview.summary.total > 8 || importPreview.truncated) && (
                      <p className="import-preview-more">这里只预览 8 行，异常数据会优先展示；确认导入时会处理全部有效数据。</p>
                    )}
                    {importPreview.summary.error > 0 && (
                      <p className="import-preview-warning">
                        {importPreview.summary.error} 条异常数据将自动跳过，不会导入；其余 {importPreview.summary.create + importPreview.summary.update} 个有效账号可正常导入。
                      </p>
                    )}
                    {Boolean(importPreview.deferredEnrichment) && (
                      <p className="import-preview-more">
                        其中 {importPreview.deferredEnrichment} 个账号缺少公开资料；确认后会先导入资源库，再在后台逐步补全。
                      </p>
                    )}
                  </div>
                )}
                {importError && <div className="form-error">{importError}</div>}
                <div className="modal-actions">
                  <button type="button" className="ghost-button" onClick={closeImport}>取消</button>
                  {!importPreview ? (
                    <button
                      type="button"
                      className="primary-button"
                      disabled={!importFile || importWorking}
                      onClick={() => void submitImport("preview")}
                    >
                      {importWorking ? "校验中…" : "校验并预览"}
                    </button>
                  ) : (
                    <button
                      type="button"
                      className="primary-button"
                      disabled={importPreview.summary.create + importPreview.summary.update === 0 || importWorking}
                      onClick={() => void submitImport("commit")}
                    >
                      {importWorking ? "导入中…" : `确认导入 ${importPreview.summary.create + importPreview.summary.update} 个账号`}
                    </button>
                  )}
                </div>
              </>
            )}
            {importMode === "single" && (
              <>
                <div className="import-guide-strip">
                  <span>1</span><p>账号主页链接必填，平台自动识别</p>
                  <i />
                  <span>2</span><p>其余资料选填，空缺字段系统会补全</p>
                  <i />
                  <span>3</span><p>已存在的账号会更新资料</p>
                </div>
                <div className="form-grid two">
                  <label>
                    <span>账号主页链接 *</span>
                    <input
                      type="url"
                      value={singleForm.profileUrl}
                      onChange={(event) =>
                        setSingleForm((form) => ({ ...form, profileUrl: event.target.value }))
                      }
                      placeholder="https://www.xiaohongshu.com/user/profile/..."
                    />
                    <small>
                      {platformFromProfileUrl(singleForm.profileUrl)
                        ? `识别平台：${platformFromProfileUrl(singleForm.profileUrl)}`
                        : "支持小红书 / 抖音账号主页"}
                    </small>
                  </label>
                  <label>
                    <span>账号昵称</span>
                    <input
                      type="text"
                      value={singleForm.nickname}
                      onChange={(event) =>
                        setSingleForm((form) => ({ ...form, nickname: event.target.value }))
                      }
                      placeholder="选填"
                    />
                  </label>
                  <label>
                    <span>账号ID</span>
                    <input
                      type="text"
                      value={singleForm.publicAccountId}
                      onChange={(event) =>
                        setSingleForm((form) => ({ ...form, publicAccountId: event.target.value }))
                      }
                      placeholder="小红书号 / 抖音号"
                    />
                  </label>
                  <label>
                    <span>IP属地</span>
                    <input
                      type="text"
                      value={singleForm.ipLocation}
                      onChange={(event) =>
                        setSingleForm((form) => ({ ...form, ipLocation: event.target.value }))
                      }
                      placeholder="省份 / 地区 / 国家"
                    />
                  </label>
                  <label>
                    <span>粉丝数</span>
                    <input
                      type="text"
                      value={singleForm.followers}
                      onChange={(event) =>
                        setSingleForm((form) => ({ ...form, followers: event.target.value }))
                      }
                      placeholder="如 1.3万 / 8000"
                    />
                  </label>
                  <label>
                    <span>性别</span>
                    <select
                      value={singleForm.gender}
                      onChange={(event) =>
                        setSingleForm((form) => ({ ...form, gender: event.target.value }))
                      }
                    >
                      <option value="">未填写</option>
                      <option value="男">男</option>
                      <option value="女">女</option>
                    </select>
                  </label>
                  <label>
                    <span>合作来源</span>
                    <input
                      type="text"
                      value={singleForm.cooperationSource}
                      onChange={(event) =>
                        setSingleForm((form) => ({ ...form, cooperationSource: event.target.value }))
                      }
                      placeholder="历史合作来源 / 资源来源"
                    />
                  </label>
                  <label>
                    <span>标签</span>
                    <input
                      type="text"
                      value={singleForm.tags}
                      onChange={(event) =>
                        setSingleForm((form) => ({ ...form, tags: event.target.value }))
                      }
                      placeholder="逗号分隔，最多 5 个"
                    />
                  </label>
                </div>
                <label>
                  <span>简介</span>
                  <textarea
                    value={singleForm.bio}
                    onChange={(event) =>
                      setSingleForm((form) => ({ ...form, bio: event.target.value }))
                    }
                    placeholder="选填"
                  />
                </label>
                {singleError && <div className="form-error">{singleError}</div>}
                <div className="modal-actions">
                  <button type="button" className="ghost-button" onClick={closeImport}>取消</button>
                  <button
                    type="button"
                    className="primary-button"
                    disabled={!singleForm.profileUrl.trim() || singleWorking}
                    onClick={() => void submitSingle()}
                  >
                    {singleWorking ? "保存中…" : "保存账号"}
                  </button>
                </div>
              </>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

function CreatorScreenshotPreview({
  distributionId,
  title,
}: {
  distributionId: string;
  title: string;
}) {
  const [open, setOpen] = useState(false);
  const imageUrl = `/api/creator-screenshot?distribution=${encodeURIComponent(distributionId)}`;
  return (
    <>
      <button
        type="button"
        className="creator-screenshot-link"
        onClick={() => setOpen(true)}
      >
        {/* eslint-disable-next-line @next/next/no-img-element */}
        <img src={imageUrl} alt={`${title} 创作者中心截图`} loading="lazy" />
        <span>点击查看</span>
      </button>
      <AdminImageLightbox
        images={open ? [{ src: imageUrl, title: `${title} · 创作者中心截图` }] : []}
        activeIndex={0}
        onClose={() => setOpen(false)}
      />
    </>
  );
}

function RecoveryPage({
  tasks,
  distributions,
  working,
  canUpdatePublishUrl,
  onCollect,
  onUpdatePublishUrl,
  onSaveSchedule,
  onUpload,
  onFallback,
  onRetryFailed,
  onExport,
  onExportScreenshots,
  exportingTaskId,
}: {
  tasks: Task[];
  distributions: Distribution[];
  working: boolean;
  canUpdatePublishUrl: boolean;
  onCollect: (distribution: Distribution) => void;
  onUpdatePublishUrl: (distribution: Distribution) => void;
  onSaveSchedule: (
    taskId: string,
    startDate: string,
    days: number[],
  ) => Promise<boolean>;
  onUpload: (distribution: Distribution, event: ChangeEvent<HTMLInputElement>) => void;
  onFallback: (distribution: Distribution) => void;
  onRetryFailed: (taskId: string, failedCount: number) => void;
  onExport: (task: Task) => void;
  onExportScreenshots: (task: Task) => void;
  exportingTaskId: string | null;
}) {
  const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
  const [recoverySort, setRecoverySort] = useState<{
    column: RecoverySortKey | null;
    direction: SortDirection;
  }>({ column: null, direction: "desc" });
  const selectedTask = tasks.find((task) => task.id === selectedTaskId);
  if (!selectedTask) {
    return (
      <div className="stack">
        <div className="section-intro">
          <div><p className="eyebrow">按任务回收</p><h2>选择一个数据回收任务</h2></div>
          <p>进入任务后设置自动采集日期，统一查看每篇笔记的最新互动数据。</p>
        </div>
        <TaskScopeList
          tasks={tasks}
          distributions={distributions}
          mode="recovery"
          onOpen={setSelectedTaskId}
        />
      </div>
    );
  }

  const taskDistributions = distributions.filter(
    (item) => item.task_id === selectedTask.id,
  );
  if (selectedTask.task_type === "screenshot_collect") {
    const submitted = taskDistributions.filter(
      (item) => item.result_submitted_at,
    ).length;
    return (
      <div className="stack">
        <TaskDetailHeader
          task={selectedTask}
          label="截图回收"
          onBack={() => setSelectedTaskId(null)}
        />
        <div className="inline-summary">
          <div><span>已领取</span><strong>{taskDistributions.length}</strong></div>
          <i />
          <div><span>已提交截图</span><strong>{submitted}</strong></div>
          <i />
          <div><span>待提交</span><strong>{taskDistributions.length - submitted}</strong></div>
          <div className="summary-note"><span className="pulse-dot" />截图任务不进入自动采集</div>
        </div>
        <section className="panel table-panel recovery-panel screenshot-recovery-panel">
          <div className="panel-heading">
            <div>
              <h2>截图回收队列</h2>
              <p>直接查看每份任务的结果截图；不回收发布链接和互动数据。</p>
            </div>
            <button
              className="export-data-button"
              disabled={exportingTaskId === selectedTask.id || submitted === 0}
              onClick={() => onExportScreenshots(selectedTask)}
            >
              {exportingTaskId === selectedTask.id ? "正在打包…" : "批量下载截图"}
            </button>
          </div>
          <ScreenshotTaskTable distributions={taskDistributions} />
        </section>
      </div>
    );
  }
  const failedCollectionCount = taskDistributions.filter(
    (item) => item.collection_status === "failed",
  ).length;
  const sortedTaskDistributions = recoverySort.column
    ? sortRecoveryDistributions(
        taskDistributions,
        recoverySort.column,
        recoverySort.direction,
      )
    : taskDistributions;
  const changeRecoverySort = (column: RecoverySortKey) => {
    setRecoverySort((current) =>
      current.column === column
        ? {
            column,
            direction: current.direction === "desc" ? "asc" : "desc",
          }
        : { column, direction: "desc" },
    );
  };
  return (
    <div className="stack">
      <TaskDetailHeader
        task={selectedTask}
        label="数据回收"
        onBack={() => setSelectedTaskId(null)}
      />
      <CollectionScheduleCard
        key={`${selectedTask.id}:${selectedTask.collection_start_date}:${selectedTask.collection_days}`}
        task={selectedTask}
        working={working}
        onSave={onSaveSchedule}
      />
      <section className="panel table-panel recovery-panel">
        <div className="panel-heading">
          <div>
            <h2>数据回收队列</h2>
            <p>{taskDistributions.length} 篇已发布作品 · 展示最近一次采集结果</p>
          </div>
          <div className="recovery-panel-actions">
            <button
              className="export-data-button"
              disabled={working || exportingTaskId === selectedTask.id}
              onClick={() => onExport(selectedTask)}
            >
              {exportingTaskId === selectedTask.id
                ? "正在生成 Excel…"
                : "导出全部数据"}
            </button>
            <button
              className="retry-failed-button"
              disabled={working || failedCollectionCount === 0}
              onClick={() =>
                onRetryFailed(selectedTask.id, failedCollectionCount)
              }
            >
              一键补采异常数据
              {failedCollectionCount > 0 && <b>{failedCollectionCount}</b>}
            </button>
          </div>
        </div>
        <div className="table-scroll">
          <table className="data-table recovery-table">
            <thead>
              <tr>
                <th>内容 / 发布账号</th>
                <SortableRecoveryHeader
                  label="发布时间"
                  column="publish_time"
                  activeColumn={recoverySort.column}
                  direction={recoverySort.direction}
                  onSort={changeRecoverySort}
                />
                <SortableRecoveryHeader
                  label="点赞"
                  column="likes"
                  activeColumn={recoverySort.column}
                  direction={recoverySort.direction}
                  onSort={changeRecoverySort}
                />
                <SortableRecoveryHeader
                  label="收藏"
                  column="collects"
                  activeColumn={recoverySort.column}
                  direction={recoverySort.direction}
                  onSort={changeRecoverySort}
                />
                {selectedTask.platform === "抖音" && (
                  <SortableRecoveryHeader
                    label="转发"
                    column="shares"
                    activeColumn={recoverySort.column}
                    direction={recoverySort.direction}
                    onSort={changeRecoverySort}
                  />
                )}
                <SortableRecoveryHeader
                  label="评论"
                  column="comments"
                  activeColumn={recoverySort.column}
                  direction={recoverySort.direction}
                  onSort={changeRecoverySort}
                />
                <SortableRecoveryHeader
                  label="总互动"
                  column="total"
                  activeColumn={recoverySort.column}
                  direction={recoverySort.direction}
                  onSort={changeRecoverySort}
                />
                <SortableRecoveryHeader
                  label="数据更新时间"
                  column="collection_updated_at"
                  activeColumn={recoverySort.column}
                  direction={recoverySort.direction}
                  onSort={changeRecoverySort}
                />
                <th>采集状态</th>
                <th>创作者截图</th>
                <th>操作</th>
              </tr>
            </thead>
            <tbody>
              {sortedTaskDistributions.map((item) => {
                const metrics = latestPublicMetrics(item);
                const noteUrl = publicPublishUrl(item.publish_url);
                const hasMetrics = metrics.likes !== null;
                const collectionStatus =
                  item.collection_status ||
                  (hasMetrics ? "success" : "pending");
                return (
                  <tr key={item.id}>
                    <td>
                      <div className="account-cell recovery-content">
                        <div className={`avatar small ${avatarColor(item.content_title)}`}>{Array.from(item.content_title)[0] || "笔"}</div>
                        <div>
                          {noteUrl ? (
                            <a
                              className="recovery-title-link"
                              href={noteUrl}
                              target="_blank"
                              rel="noopener noreferrer"
                              aria-label={`打开${selectedTask.platform}作品：${item.content_title}`}
                              title={`打开${selectedTask.platform}作品`}
                            >
                              <strong>{item.content_title}</strong>
                            </a>
                          ) : (
                            <strong>{item.content_title}</strong>
                          )}
                          <span className="platform-meta-line">
                            <span>{item.account_nickname || "待识别账号"}</span>
                            {item.account_platform && <PlatformBadge platform={item.account_platform} compact />}
                          </span>
                        </div>
                      </div>
                    </td>
                    <td>
                      <div className="metric-cell publish-time">
                        <strong>{formatDate(item.publish_time, true)}</strong>
                      </div>
                    </td>
                    <td><div className="metric-cell"><strong>{formatNumber(metrics.likes)}</strong></div></td>
                    <td><div className="metric-cell"><strong>{formatNumber(metrics.collects)}</strong></div></td>
                    {selectedTask.platform === "抖音" && (
                      <td><div className="metric-cell"><strong>{formatNumber(metrics.shares)}</strong></div></td>
                    )}
                    <td><div className="metric-cell"><strong>{formatNumber(metrics.comments)}</strong></div></td>
                    <td>
                      <div className="metric-cell total">
                        <strong>{formatNumber(metrics.total)}</strong>
                        <span>{selectedTask.platform === "抖音" ? "赞 + 藏 + 转 + 评" : "赞 + 藏 + 评"}</span>
                      </div>
                    </td>
                    <td>
                      <div className="metric-cell update-time">
                        <strong>{formatDate(item.collection_updated_at || (hasMetrics ? item.updated_at : null), true)}</strong>
                        {item.last_collection_day && <span>第{item.last_collection_day}天采集</span>}
                      </div>
                    </td>
                    <td>
                      <div className="collection-status-cell">
                        <span className={`collection-status ${collectionStatus}`}>
                          {collectionStatusLabel(collectionStatus, hasMetrics)}
                        </span>
                        <small>
                          {item.collection_status_description ||
                            (hasMetrics ? "成功" : "等待采集计划")}
                        </small>
                      </div>
                    </td>
                    <td>
                      <div className="creator-screenshot-cell">
                        {item.screenshot_key ? (
                          <CreatorScreenshotPreview
                            distributionId={item.id}
                            title={item.content_title}
                          />
                        ) : (
                          <span className="creator-screenshot-pending">待KOC上传</span>
                        )}
                        {hasCreatorMetrics(item) ? (
                          <span className="creator-data-summary">
                            <b>曝光 {formatNumber(item.exposure)}</b>
                            <b>阅读 {formatNumber(item.views)}</b>
                          </span>
                        ) : item.screenshot_key ? (
                          <span className="creator-ocr-failed">
                            待KOC填写数据
                          </span>
                        ) : null}
                        {item.screenshot_key &&
                          !hasCreatorMetrics(item) && (
                            <label className="upload-button retry-upload">
                              <input
                                type="file"
                                accept="image/*"
                                onChange={(event) => onUpload(item, event)}
                              />
                              重新上传
                            </label>
                          )}
                      </div>
                    </td>
                    <td>
                      <div className="recovery-row-actions">
                        {item.screenshot_key && !hasCreatorMetrics(item) ? (
                          <button className="text-button" onClick={() => onFallback(item)}>手动填写</button>
                        ) : (
                          <button
                            className="collect-button"
                            disabled={working || !noteUrl}
                            onClick={() => onCollect(item)}
                          >
                            {noteUrl ? "立即采集" : "待填链接"}
                          </button>
                        )}
                        {canUpdatePublishUrl && (
                          <button
                            className="publish-url-edit-button"
                            disabled={working}
                            onClick={() => onUpdatePublishUrl(item)}
                          >
                            {noteUrl ? "更新链接" : "填写链接"}
                          </button>
                        )}
                      </div>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
          {taskDistributions.length === 0 && (
            <div className="table-empty compact">当前任务还没有已发布笔记</div>
          )}
        </div>
      </section>
    </div>
  );
}

function CollectionScheduleCard({
  task,
  working,
  onSave,
}: {
  task: Task;
  working: boolean;
  onSave: (
    taskId: string,
    startDate: string,
    days: number[],
  ) => Promise<boolean>;
}) {
  const [startDate, setStartDate] = useState(
    task.collection_start_date || todayInputValue(),
  );
  const [days, setDays] = useState<number[]>(
    parseCollectionDays(task.collection_days),
  );

  const toggleDay = (day: number) => {
    setDays((current) =>
      current.includes(day)
        ? current.filter((item) => item !== day)
        : [...current, day].sort((a, b) => a - b),
    );
  };

  return (
    <section className="panel collection-schedule-panel">
      <div className="collection-schedule-heading">
        <div>
          <p className="eyebrow">自动采集计划</p>
          <h2>选择开始日期与采集日</h2>
          <span>勾选第1天到第7天，所选日期均在北京时间09:00自动采集。</span>
        </div>
        <div className="schedule-save-area">
          <span>已选择 {days.length} 天</span>
          <button
            className="primary-button"
            disabled={working || !startDate || days.length === 0}
            onClick={() => void onSave(task.id, startDate, days)}
          >
            {working ? "保存中…" : "保存采集计划"}
          </button>
        </div>
      </div>
      <div className="schedule-grid">
        <label className="schedule-start">
          <span>开始采集日期</span>
          <input
            type="date"
            value={startDate}
            onChange={(event) => setStartDate(event.target.value)}
          />
        </label>
        <div className="schedule-days" role="group" aria-label="选择自动采集日">
          {Array.from({ length: 7 }, (_, index) => index + 1).map((day) => {
            const selected = days.includes(day);
            const date = addCalendarDays(startDate, day - 1);
            return (
              <button
                key={day}
                type="button"
                className={selected ? "selected" : ""}
                aria-pressed={selected}
                onClick={() => toggleDay(day)}
              >
                <span>第{day}天</span>
                <b aria-hidden="true">{selected ? "✓" : ""}</b>
                <small>{date ? date.slice(5).replace("-", "/") : "—"}</small>
              </button>
            );
          })}
        </div>
        <span className="schedule-mobile-hint">左右滑动查看第1—7天</span>
      </div>
      {task.collection_schedule_updated_at && (
        <p className="schedule-updated">
          当前计划更新于 {formatDate(task.collection_schedule_updated_at, true)}
        </p>
      )}
    </section>
  );
}
