"use client";

import { zipSync } from "fflate";
import { ChangeEvent, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  formatShanghaiDate as formatDate,
  parseStoredDate,
} from "./date-utils";
import {
  compactPartnerBatchWorkbookForUpload,
  PARTNER_BATCH_UPLOAD_MAX_BYTES,
} from "./batch-workbook-upload";

type Assignment = {
  id: string;
  status: string;
  publish_url: string | null;
  publish_time: string | null;
  publish_screenshot_key: string | null;
  creator_screenshot_key: string | null;
  result_screenshot_key: string | null;
  result_submitted_at: string | null;
  ocr_status: string;
  exposure: number | null;
  views: number | null;
  title: string;
  body: string;
  source_row: number | null;
  account_nickname: string | null;
  delegation_bundle_id?: string | null;
  delegation_label?: string | null;
  images: Array<{
    index: number;
    width: number | null;
    height: number | null;
  }>;
  videos: Array<{
    index: number;
    width: number | null;
    height: number | null;
  }>;
};

type DelegationSummary = {
  id: string;
  label: string;
  share_token: string;
  quantity: number;
  status: "active" | "revoked";
  created_at: string;
  revoked_at: string | null;
  completed_count: number;
  creator_completed_count: number;
  result_completed_count?: number;
};

type RecoveredClaim = {
  claimToken: string;
  quantity: number;
  completedCount: number;
  createdAt: string;
  firstTitle: string;
};

type TaskPayload = {
  task: {
    name: string;
    brand: string;
    quantity?: number;
    claimedQuantity?: number;
    availableQuantity?: number;
    dueAt: string;
    status: string;
    type: "content_publish" | "screenshot_collect";
    platform: "小红书" | "抖音";
    contentFormat: "image_text" | "video";
  };
  claim: null | {
    id: string;
    claimantName: string;
    quantity: number;
    createdAt: string;
    assignments: Assignment[];
    delegations: DelegationSummary[];
  };
  delegation: null | {
    id: string;
    label: string;
    quantity: number;
    createdAt: string;
    assignments: Assignment[];
  };
};

const CONFIGURED_ADMIN_ORIGIN =
  process.env.NEXT_PUBLIC_ADMIN_ORIGIN?.replace(/\/$/, "") ?? "";

function resolveAdminOrigin() {
  if (CONFIGURED_ADMIN_ORIGIN) return CONFIGURED_ADMIN_ORIGIN;
  if (window.location.hostname === "localhost" && window.location.port === "3001") {
    return "http://localhost:3000";
  }
  return window.location.origin;
}

function partnerApi(
  path:
    | "/api/partner"
    | "/api/partner-upload"
    | "/api/partner-batch-workbook",
) {
  return `${resolveAdminOrigin()}${path}`;
}

function isMobilePortalViewport() {
  return (
    typeof window !== "undefined" &&
    window.matchMedia("(max-width: 620px)").matches
  );
}

function statusLabel(item: Assignment, taskType = "content_publish") {
  if (taskType === "screenshot_collect") {
    return item.result_submitted_at ? "已提交" : "待提交";
  }
  return item.publish_url ? "已回填" : "待发布";
}

const MAX_TASK_RESULT_SCREENSHOTS = 9;
const PUBLISH_BACKFILL_SUCCESS = "这篇笔记的发布记录回填成功啦～";

function toastClassName(message: string) {
  return message === PUBLISH_BACKFILL_SUCCESS ? "toast success" : "toast";
}

function resultScreenshotKeys(value: string | null) {
  const text = String(value ?? "").trim();
  if (!text) return [];
  if (text.startsWith("task-results/")) return [text];
  try {
    const parsed = JSON.parse(text) as unknown;
    return Array.isArray(parsed)
      ? parsed.filter(
          (item): item is string =>
            typeof item === "string" && item.startsWith("task-results/"),
        )
      : [];
  } catch {
    return [];
  }
}

function creatorScreenshotDueAt(publishTime?: string | null) {
  if (!publishTime) return null;
  const date = parseStoredDate(publishTime);
  if (Number.isNaN(date.getTime())) return null;
  date.setDate(date.getDate() + 7);
  return date.toISOString();
}

function imageExtension(contentType: string) {
  const extensionByType: Record<string, string> = {
    "image/avif": "avif",
    "image/gif": "gif",
    "image/jpeg": "jpg",
    "image/png": "png",
    "image/webp": "webp",
  };
  return extensionByType[contentType] ?? "jpg";
}

function safeFileBase(item: Assignment) {
  return (
    item.title.replace(/[\\/:*?"<>|]/g, "").trim().slice(0, 30) ||
    `笔记-${item.source_row ?? "配图"}`
  );
}

function PlatformBadge({
  platform,
  compact = false,
  logoOnly = false,
}: {
  platform: "小红书" | "抖音";
  compact?: boolean;
  logoOnly?: boolean;
}) {
  return (
    <span
      className={`platform-badge ${compact ? "compact" : ""} ${logoOnly ? "logo-only" : ""}`}
      aria-label={logoOnly ? platform : undefined}
    >
      <span className={`platform-logo ${platform === "抖音" ? "douyin" : "xiaohongshu"}`} aria-hidden="true">
        {platform === "抖音" ? (
          <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>
        ) : (
          <b>小红书</b>
        )}
      </span>
      {!logoOnly && <span>{platform}</span>}
    </span>
  );
}

function exactArrayBuffer(bytes: Uint8Array) {
  const copy = new Uint8Array(bytes.byteLength);
  copy.set(bytes);
  return copy.buffer;
}

function downloadBlob(blob: Blob, fileName: string) {
  const objectUrl = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = objectUrl;
  anchor.download = fileName;
  document.body.appendChild(anchor);
  anchor.click();
  anchor.remove();
  window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
}

async function compressScreenshot(file: File) {
  if (file.size < 900_000 || !file.type.startsWith("image/")) return file;
  const bitmap = await createImageBitmap(file);
  const maxEdge = 1800;
  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.84),
  );
  if (!blob) return file;
  return new File([blob], `${file.name.replace(/\.[^.]+$/, "")}.jpg`, {
    type: "image/jpeg",
  });
}

function useFilePreview(file: File | null) {
  const previewUrl = useMemo(
    () => (file ? URL.createObjectURL(file) : ""),
    [file],
  );
  useEffect(
    () => () => {
      if (previewUrl) URL.revokeObjectURL(previewUrl);
    },
    [previewUrl],
  );
  return previewUrl;
}

function ImageLightbox({
  image,
  onClose,
}: {
  image: { src: string; alt: string } | null;
  onClose: () => void;
}) {
  useEffect(() => {
    if (!image) return;
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") onClose();
    };
    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;
    };
  }, [image, onClose]);
  if (!image) return null;
  return (
    <div
      className="image-lightbox"
      role="dialog"
      aria-modal="true"
      aria-label={image.alt}
      onMouseDown={onClose}
    >
      <div
        className="image-lightbox-card"
        onMouseDown={(event) => event.stopPropagation()}
      >
        <div className="image-lightbox-heading">
          <strong>{image.alt}</strong>
          <button type="button" onClick={onClose}>关闭</button>
        </div>
        {/* eslint-disable-next-line @next/next/no-img-element */}
        <img src={image.src} alt={image.alt} />
      </div>
    </div>
  );
}

export default function Home() {
  const [taskToken, setTaskToken] = useState("");
  const [claimToken, setClaimToken] = useState("");
  const [delegationToken, setDelegationToken] = useState("");
  const [selectedId, setSelectedId] = useState("");
  const [payload, setPayload] = useState<TaskPayload | null>(null);
  const [loading, setLoading] = useState(true);
  const [working, setWorking] = useState(false);
  const [error, setError] = useState("");
  const [toast, setToast] = useState("");
  const [claimantName, setClaimantName] = useState("");
  const [quantity, setQuantity] = useState(1);
  const [publishUrl, setPublishUrl] = useState("");
  const [screenshot, setScreenshot] = useState<File | null>(null);
  const [taskResultScreenshots, setTaskResultScreenshots] = useState<File[]>([]);
  const [creatorScreenshot, setCreatorScreenshot] = useState<File | null>(null);
  const [creatorExposure, setCreatorExposure] = useState("");
  const [creatorViews, setCreatorViews] = useState("");
  const [entryMode, setEntryMode] = useState<"claim" | "recover">("claim");
  const [recoveredClaims, setRecoveredClaims] = useState<RecoveredClaim[]>([]);
  const [adminOrigin, setAdminOrigin] = useState("");
  const [downloadingImage, setDownloadingImage] = useState<number | null>(null);
  const [batchDownloading, setBatchDownloading] = useState(false);
  const [batchWorkbookWorking, setBatchWorkbookWorking] = useState(false);
  const [creatorWorking, setCreatorWorking] = useState(false);
  const [creatorStage, setCreatorStage] = useState("");
  const [selectedForShare, setSelectedForShare] = useState<string[]>([]);
  const [shareLabel, setShareLabel] = useState("");
  const [shareComposerOpen, setShareComposerOpen] = useState(false);
  const [shareWorking, setShareWorking] = useState(false);
  const [previewImage, setPreviewImage] = useState<{
    src: string;
    alt: string;
  } | null>(null);
  const [noteContentCollapsed, setNoteContentCollapsed] = useState(false);
  const submitCardRef = useRef<HTMLFormElement | null>(null);
  const batchWorkbookInputRef = useRef<HTMLInputElement | null>(null);
  const publishScreenshotPreview = useFilePreview(screenshot);
  const creatorScreenshotPreview = useFilePreview(creatorScreenshot);
  const taskResultPreviews = useMemo(
    () =>
      taskResultScreenshots.map((file) => ({
        file,
        url: URL.createObjectURL(file),
      })),
    [taskResultScreenshots],
  );

  useEffect(
    () => () => {
      taskResultPreviews.forEach((preview) => URL.revokeObjectURL(preview.url));
    },
    [taskResultPreviews],
  );

  const updateUrl = useCallback(
    (nextClaim: string, nextNote = "", nextDelegation = "") => {
      const url = new URL(window.location.href);
      if (nextClaim) url.searchParams.set("claim", nextClaim);
      else url.searchParams.delete("claim");
      if (nextDelegation) url.searchParams.set("share", nextDelegation);
      else url.searchParams.delete("share");
      if (nextNote) url.searchParams.set("note", nextNote);
      else url.searchParams.delete("note");
      if (nextDelegation) url.searchParams.delete("task");
      window.history.replaceState({}, "", url);
    },
    [],
  );

  const loadTask = useCallback(async (
    task: string,
    claim: string,
    delegation = "",
  ) => {
    try {
      setLoading(true);
      setError("");
      const params = new URLSearchParams();
      if (task) params.set("task", task);
      if (claim) params.set("claim", claim);
      if (delegation) params.set("share", delegation);
      const response = await fetch(`${partnerApi("/api/partner")}?${params}`, {
        cache: "no-store",
      });
      const result = (await response.json()) as TaskPayload & { error?: string };
      if (!response.ok) throw new Error(result.error || "暂时无法打开任务");
      setPayload(result);
      if (result.claim) setClaimantName(result.claim.claimantName);
    } catch (reason) {
      setError(reason instanceof Error ? reason.message : "暂时无法打开任务");
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    const timer = window.setTimeout(() => {
      setAdminOrigin(resolveAdminOrigin());
      const params = new URLSearchParams(window.location.search);
      const task = params.get("task") ?? "";
      const claim = params.get("claim") ?? "";
      const delegation = params.get("share") ?? "";
      const note = params.get("note") ?? "";
      setTaskToken(task);
      setClaimToken(claim);
      setDelegationToken(delegation);
      setSelectedId(note);
      if (!task && !delegation) {
        setLoading(false);
        setError("任务链接不完整，请向运营重新获取");
        return;
      }
      void loadTask(task, claim, delegation);
    }, 0);
    return () => window.clearTimeout(timer);
  }, [loadTask]);

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

  const activeBatch = payload?.claim ?? payload?.delegation ?? null;
  const selected = useMemo(
    () => activeBatch?.assignments.find((item) => item.id === selectedId) ?? null,
    [activeBatch, selectedId],
  );

  useEffect(() => {
    if (!selected) return;
    const timer = window.setTimeout(() => {
      setPublishUrl(selected.publish_url ?? "");
      setScreenshot(null);
      setTaskResultScreenshots([]);
      setCreatorScreenshot(null);
      setCreatorExposure(
        selected.exposure === null ? "" : String(selected.exposure),
      );
      setCreatorViews(selected.views === null ? "" : String(selected.views));
      setNoteContentCollapsed(
        Boolean(selected.publish_url) && isMobilePortalViewport(),
      );
    }, 0);
    return () => window.clearTimeout(timer);
  }, [selected]);

  const claimNotes = async (event: FormEvent) => {
    event.preventDefault();
    try {
      setWorking(true);
      const response = await fetch(partnerApi("/api/partner"), {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          action: "claim",
          taskToken,
          claimantIdentifier: claimantName,
          quantity,
        }),
      });
      const result = (await response.json()) as {
        claimToken?: string;
        claimedCount?: number;
        error?: string;
      };
      if (!response.ok || !result.claimToken) {
        throw new Error(result.error || "领取失败");
      }
      setClaimToken(result.claimToken);
      setDelegationToken("");
      updateUrl(result.claimToken);
      await loadTask(taskToken, result.claimToken);
      setToast(
        payload?.task.type === "screenshot_collect"
          ? `已领取 ${result.claimedCount} 份，完成后逐份上传截图`
          : `已领取 ${result.claimedCount} 篇，逐篇发布即可`,
      );
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "领取失败");
    } finally {
      setWorking(false);
    }
  };

  const openRecoveredClaim = async (nextClaimToken: string) => {
    setClaimToken(nextClaimToken);
    setDelegationToken("");
    setSelectedId("");
    setShareComposerOpen(false);
    setSelectedForShare([]);
    updateUrl(nextClaimToken);
    await loadTask(taskToken, nextClaimToken);
  };

  const recoverClaims = async (event: FormEvent) => {
    event.preventDefault();
    try {
      setWorking(true);
      const response = await fetch(partnerApi("/api/partner"), {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          action: "recover",
          taskToken,
          claimantIdentifier: claimantName,
        }),
      });
      const result = (await response.json()) as {
        claims?: RecoveredClaim[];
        error?: string;
      };
      if (!response.ok || !result.claims?.length) {
        throw new Error(result.error || "没有找到领取记录");
      }
      setRecoveredClaims(result.claims);
      if (result.claims.length === 1) {
        await openRecoveredClaim(result.claims[0].claimToken);
      }
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "领取记录查找失败");
    } finally {
      setWorking(false);
    }
  };

  const showRecovery = async () => {
    setEntryMode("recover");
    setRecoveredClaims([]);
    setClaimToken("");
    setDelegationToken("");
    setSelectedId("");
    setShareComposerOpen(false);
    setSelectedForShare([]);
    updateUrl("");
    await loadTask(taskToken, "");
  };

  const noteImageUrl = (item: Assignment, imageIndex: number) => {
    const params = new URLSearchParams({
      distribution: item.id,
      index: String(imageIndex),
    });
    if (delegationToken) params.set("share", delegationToken);
    else {
      params.set("task", taskToken);
      params.set("claim", claimToken);
    }
    return `${adminOrigin}/api/partner-image?${params}`;
  };

  const noteVideoUrl = (
    item: Assignment,
    videoIndex: number,
    download = false,
  ) => {
    const params = new URLSearchParams({
      distribution: item.id,
      index: String(videoIndex),
      kind: "video",
    });
    if (download) params.set("download", "1");
    if (delegationToken) params.set("share", delegationToken);
    else {
      params.set("task", taskToken);
      params.set("claim", claimToken);
    }
    return `${adminOrigin}/api/partner-image?${params}`;
  };

  const taskResultImageUrl = (item: Assignment, imageIndex: number) => {
    const params = new URLSearchParams({
      distribution: item.id,
      index: String(imageIndex),
      kind: "result",
    });
    if (delegationToken) params.set("share", delegationToken);
    else {
      params.set("task", taskToken);
      params.set("claim", claimToken);
    }
    return `${adminOrigin}/api/partner-image?${params}`;
  };

  const evidenceImageUrl = (
    item: Assignment,
    kind: "publish" | "creator",
  ) => {
    const params = new URLSearchParams({
      distribution: item.id,
      index: "1",
      kind,
    });
    const evidenceKey =
      kind === "publish"
        ? item.publish_screenshot_key
        : item.creator_screenshot_key;
    if (evidenceKey) params.set("v", evidenceKey);
    if (delegationToken) params.set("share", delegationToken);
    else {
      params.set("task", taskToken);
      params.set("claim", claimToken);
    }
    return `${adminOrigin}/api/partner-image?${params}`;
  };

  const openNote = (item: Assignment) => {
    setSelectedId(item.id);
    updateUrl(claimToken, item.id, delegationToken);
  };

  const closeNote = () => {
    setSelectedId("");
    updateUrl(claimToken, "", delegationToken);
  };

  const copyText = async (value: string, successMessage: string) => {
    try {
      await navigator.clipboard.writeText(value);
      setToast(successMessage);
    } catch {
      setToast("复制失败，请长按对应文字复制");
    }
  };

  const delegationUrl = (shareToken: string) => {
    const url = new URL(window.location.href);
    url.search = "";
    url.hash = "";
    url.searchParams.set("share", shareToken);
    return url.toString();
  };

  const toggleShareSelection = (distributionId: string) => {
    setSelectedForShare((current) =>
      current.includes(distributionId)
        ? current.filter((id) => id !== distributionId)
        : [...current, distributionId],
    );
  };

  const createDelegation = async (event: FormEvent) => {
    event.preventDefault();
    if (!payload?.claim || selectedForShare.length === 0) return;
    try {
      setShareWorking(true);
      const response = await fetch(partnerApi("/api/partner"), {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          action: "create_delegation",
          taskToken,
          claimToken,
          delegationLabel: shareLabel,
          distributionIds: selectedForShare,
        }),
      });
      const result = (await response.json()) as {
        delegation?: {
          id: string;
          label: string;
          shareToken: string;
          quantity: number;
        };
        error?: string;
      };
      if (!response.ok || !result.delegation) {
        throw new Error(result.error || "分享链接生成失败");
      }
      await copyText(
        delegationUrl(result.delegation.shareToken),
        `已生成给 ${result.delegation.label} 的分享链接并复制`,
      );
      setSelectedForShare([]);
      setShareLabel("");
      setShareComposerOpen(false);
      await loadTask(taskToken, claimToken);
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "分享链接生成失败");
    } finally {
      setShareWorking(false);
    }
  };

  const revokeDelegation = async (bundle: DelegationSummary) => {
    if (!window.confirm(`确认撤销“${bundle.label}”的分享链接吗？`)) return;
    try {
      setShareWorking(true);
      const response = await fetch(partnerApi("/api/partner"), {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          action: "revoke_delegation",
          taskToken,
          claimToken,
          delegationBundleId: bundle.id,
        }),
      });
      const result = (await response.json()) as {
        revoked?: boolean;
        error?: string;
      };
      if (!response.ok || !result.revoked) {
        throw new Error(result.error || "撤销失败");
      }
      await loadTask(taskToken, claimToken);
      setSelectedForShare([]);
      setToast("分享链接已撤销，相关笔记可以重新转派");
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "撤销失败");
    } finally {
      setShareWorking(false);
    }
  };

  const batchWorkbookParams = () => {
    const params = new URLSearchParams();
    if (delegationToken) params.set("share", delegationToken);
    else {
      params.set("task", taskToken);
      params.set("claim", claimToken);
    }
    return params;
  };

  const exportBatchWorkbook = async () => {
    try {
      setBatchWorkbookWorking(true);
      const response = await fetch(
        `${partnerApi("/api/partner-batch-workbook")}?${batchWorkbookParams()}`,
        { cache: "no-store" },
      );
      if (!response.ok) {
        const result = (await response.json()) as { error?: string };
        throw new Error(result.error || "Excel导出失败");
      }
      const disposition = response.headers.get("Content-Disposition") || "";
      const encodedName = disposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1];
      const fileName = encodedName
        ? decodeURIComponent(encodedName)
        : `${payload?.task.name || "领取笔记"}-批量回填.xlsx`;
      downloadBlob(await response.blob(), fileName);
      setToast("Excel已导出，填写后从本页面上传即可批量回填");
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "Excel导出失败");
    } finally {
      setBatchWorkbookWorking(false);
    }
  };

  const importBatchWorkbook = async (event: ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    event.target.value = "";
    if (!file) return;
    try {
      setBatchWorkbookWorking(true);
      let uploadBody: Blob = file;
      let compacted = false;
      if (file.size > PARTNER_BATCH_UPLOAD_MAX_BYTES) {
        setToast("文件较大，正在保留回填截图并精简原图…");
        await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
        const compactedWorkbook = compactPartnerBatchWorkbookForUpload(
          new Uint8Array(await file.arrayBuffer()),
        );
        if (compactedWorkbook.bytes.byteLength > PARTNER_BATCH_UPLOAD_MAX_BYTES) {
          throw new Error("精简后的回填表仍超过80MB，请重新导出最新版回填表");
        }
        uploadBody = new Blob([exactArrayBuffer(compactedWorkbook.bytes)], {
          type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        });
        compacted = true;
      }
      const form = new FormData();
      form.set("file", uploadBody, file.name);
      form.set("taskToken", taskToken);
      form.set("claimToken", claimToken);
      form.set("delegationToken", delegationToken);
      const response = await fetch(partnerApi("/api/partner-batch-workbook"), {
        method: "POST",
        body: form,
      });
      const contentType = response.headers.get("Content-Type") || "";
      const result = (contentType.includes("application/json")
        ? await response.json()
        : {
            error:
              response.status === 413
                ? "回填表超过上传限制，请重新导出最新版回填表"
                : "批量回填服务暂时不可用，请稍后重试",
          }) as {
        error?: string;
        updatedRows?: number;
        publishedCount?: number;
        noteScreenshotCount?: number;
        analysisScreenshotCount?: number;
      };
      if (!response.ok) throw new Error(result.error || "批量回填失败");
      await loadTask(taskToken, claimToken, delegationToken);
      const details = [
        result.publishedCount
          ? `${result.publishedCount}篇发布信息`
          : "",
        result.noteScreenshotCount
          ? `${result.noteScreenshotCount}张笔记截图`
          : "",
        result.analysisScreenshotCount
          ? `${result.analysisScreenshotCount}张数据分析截图`
          : "",
      ].filter(Boolean);
      setToast(
        details.length > 0
          ? `${compacted ? "文件已自动精简，" : ""}已更新${details.join("、")}`
          : `${compacted ? "文件已自动精简，" : ""}表格已读取，没有需要更新的数据`,
      );
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "批量回填失败");
    } finally {
      setBatchWorkbookWorking(false);
    }
  };

  const prepareImage = async (item: Assignment, imageIndex: number) => {
    const response = await fetch(noteImageUrl(item, imageIndex));
    if (!response.ok) throw new Error("图片读取失败");
    const blob = await response.blob();
    const extension = imageExtension(blob.type);
    const fileName = `${safeFileBase(item)}-图片${imageIndex}.${extension}`;
    const buffer = await blob.arrayBuffer();
    return {
      bytes: new Uint8Array(buffer),
      file: new File([buffer], fileName, { type: blob.type || "image/jpeg" }),
    };
  };

  const shareImages = async (files: File[], title: string) => {
    if (
      typeof navigator.share !== "function" ||
      typeof navigator.canShare !== "function" ||
      !navigator.canShare({ files })
    ) {
      return false;
    }
    await navigator.share({ files, title });
    return true;
  };

  const downloadImage = async (item: Assignment, imageIndex: number) => {
    try {
      setDownloadingImage(imageIndex);
      const prepared = await prepareImage(item, imageIndex);
      try {
        if (await shareImages([prepared.file], item.title)) {
          setToast("请在系统菜单中选择“存储图像”");
          return;
        }
      } catch (reason) {
        if (reason instanceof DOMException && reason.name === "AbortError") return;
      }
      downloadBlob(
        new Blob([exactArrayBuffer(prepared.bytes)], {
          type: prepared.file.type,
        }),
        prepared.file.name,
      );
      setToast(`图片 ${imageIndex} 已下载到文件`);
    } catch {
      setToast("图片下载失败，请点击图片后保存");
    } finally {
      setDownloadingImage(null);
    }
  };

  const downloadAllImages = async (item: Assignment) => {
    try {
      setBatchDownloading(true);
      const prepared = await Promise.all(
        item.images.map((image) => prepareImage(item, image.index)),
      );
      const files = prepared.map((image) => image.file);
      try {
        if (await shareImages(files, `${item.title}｜全部配图`)) {
          setToast("请在系统菜单中选择“存储图像”批量保存");
          return;
        }
      } catch (reason) {
        if (reason instanceof DOMException && reason.name === "AbortError") return;
      }
      const archive = zipSync(
        Object.fromEntries(
          prepared.map((image) => [image.file.name, image.bytes]),
        ),
        { level: 0 },
      );
      downloadBlob(
        new Blob([exactArrayBuffer(archive)], { type: "application/zip" }),
        `${safeFileBase(item)}-全部配图.zip`,
      );
      setToast("当前浏览器不支持直接存相册，已下载全部配图 ZIP");
    } catch {
      setToast("批量保存失败，请尝试用系统浏览器打开");
    } finally {
      setBatchDownloading(false);
    }
  };

  const chooseScreenshot = (event: ChangeEvent<HTMLInputElement>) => {
    setScreenshot(event.target.files?.[0] ?? null);
  };

  const chooseTaskResultScreenshots = (event: ChangeEvent<HTMLInputElement>) => {
    const incoming = Array.from(event.target.files ?? []);
    if (incoming.length === 0) return;
    const existingCount = selected
      ? resultScreenshotKeys(selected.result_screenshot_key).length
      : 0;
    setTaskResultScreenshots((current) => {
      const merged = [...current, ...incoming].filter(
        (file, index, files) =>
          files.findIndex(
            (candidate) =>
              candidate.name === file.name &&
              candidate.size === file.size &&
              candidate.lastModified === file.lastModified,
          ) === index,
      );
      const allowed = Math.max(0, MAX_TASK_RESULT_SCREENSHOTS - existingCount);
      if (merged.length > allowed) {
        setToast(`每份任务最多上传${MAX_TASK_RESULT_SCREENSHOTS}张截图`);
      }
      return merged.slice(0, allowed);
    });
    event.target.value = "";
  };

  const removeTaskResultScreenshot = (target: File) => {
    setTaskResultScreenshots((current) =>
      current.filter((file) => file !== target),
    );
  };

  const chooseCreatorScreenshot = (event: ChangeEvent<HTMLInputElement>) => {
    setCreatorScreenshot(event.target.files?.[0] ?? null);
  };

  const uploadEvidence = async (
    selectedItem: Assignment,
    file: File,
    kind: "publish" | "creator-center" | "task-result",
  ): Promise<{ exposure?: number | null; views?: number | null; ocrStatus?: string }> => {
    const compressed = await compressScreenshot(file);
    const headers: Record<string, string> = {
      "Content-Type": compressed.type || "image/jpeg",
      "X-KOC-Distribution": selectedItem.id,
      "X-KOC-File-Name": encodeURIComponent(compressed.name),
      "X-KOC-Upload-Kind": kind,
    };
    if (delegationToken) {
      headers["X-KOC-Delegation"] = delegationToken;
    } else {
      headers["X-KOC-Task"] = taskToken;
      headers["X-KOC-Claim"] = claimToken;
    }
    const uploadResponse = await fetch(partnerApi("/api/partner-upload"), {
      method: "POST",
      headers,
      body: compressed,
    });
    const uploadResult = (await uploadResponse.json()) as {
      error?: string;
      exposure?: number | null;
      views?: number | null;
      ocrStatus?: string;
    };
    if (!uploadResponse.ok) {
      throw new Error(
        uploadResult.error ||
          (kind === "creator-center"
            ? "创作者中心截图上传失败"
            : kind === "task-result"
              ? "任务截图上传失败"
            : "发布截图上传失败"),
      );
    }
    return uploadResult;
  };

  const submitNote = async (event: FormEvent) => {
    event.preventDefault();
    if (!selected) return;
    const isFirstBackfill = !selected.publish_url;
    try {
      setWorking(true);
      if (screenshot) {
        await uploadEvidence(selected, screenshot, "publish");
      } else if (!selected.publish_screenshot_key) {
        throw new Error("请上传这篇笔记的发布截图");
      }

      const response = await fetch(partnerApi("/api/partner"), {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          action: "submit",
          taskToken,
          claimToken,
          delegationToken,
          distributionId: selected.id,
          publishUrl,
        }),
      });
      const result = (await response.json()) as { error?: string };
      if (!response.ok) throw new Error(result.error || "回填失败");
      await loadTask(taskToken, claimToken, delegationToken);
      setScreenshot(null);
      if (isFirstBackfill && isMobilePortalViewport()) {
        setNoteContentCollapsed(true);
        window.setTimeout(() => {
          submitCardRef.current?.scrollIntoView({
            behavior: "smooth",
            block: "start",
          });
        }, 120);
      }
      setToast(PUBLISH_BACKFILL_SUCCESS);
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "回填失败");
    } finally {
      setWorking(false);
    }
  };

  const submitScreenshotTask = async (event: FormEvent) => {
    event.preventDefault();
    if (!selected) return;
    try {
      setWorking(true);
      const existingScreenshots = resultScreenshotKeys(
        selected.result_screenshot_key,
      );
      if (taskResultScreenshots.length > 0) {
        for (const file of taskResultScreenshots) {
          await uploadEvidence(selected, file, "task-result");
          setTaskResultScreenshots((current) =>
            current.filter((candidate) => candidate !== file),
          );
        }
      } else if (existingScreenshots.length === 0) {
        throw new Error("请上传任务要求的截图");
      }
      const response = await fetch(partnerApi("/api/partner"), {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          action: "submit_screenshot_result",
          taskToken,
          claimToken,
          delegationToken,
          distributionId: selected.id,
        }),
      });
      const result = (await response.json()) as { error?: string };
      if (!response.ok) throw new Error(result.error || "截图提交失败");
      await loadTask(taskToken, claimToken, delegationToken);
      setTaskResultScreenshots([]);
      setToast(
        taskResultScreenshots.length > 1
          ? `${taskResultScreenshots.length}张任务截图已提交`
          : "任务截图已提交",
      );
    } catch (reason) {
      setToast(reason instanceof Error ? reason.message : "截图提交失败");
    } finally {
      setWorking(false);
    }
  };

  const submitCreatorData = async () => {
    if (!selected) return;
    if (!creatorScreenshot && !selected.creator_screenshot_key) {
      setToast("请选择创作者中心截图");
      return;
    }
    try {
      setCreatorWorking(true);
      let exposureValue = creatorExposure;
      let viewsValue = creatorViews;
      if (creatorScreenshot) {
        setCreatorStage("正在上传截图…");
        const ocrResult = await uploadEvidence(
          selected,
          creatorScreenshot,
          "creator-center",
        );
        if (typeof ocrResult.exposure === "number") {
          exposureValue = String(ocrResult.exposure);
          setCreatorExposure(exposureValue);
        }
        if (typeof ocrResult.views === "number") {
          viewsValue = String(ocrResult.views);
          setCreatorViews(viewsValue);
        }
        if (ocrResult.ocrStatus === "success") {
          setCreatorStage("已识别截图数据，正在保存…");
        }
      }
      if (!/^\d{1,12}$/.test(exposureValue) || !/^\d{1,12}$/.test(viewsValue)) {
        throw new Error("未识别到完整数据，请填写曝光量和阅读量");
      }
      setCreatorStage("正在保存数据…");
      const response = await fetch(partnerApi("/api/partner"), {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          action: "submit_creator_metrics",
          taskToken,
          claimToken,
          delegationToken,
          distributionId: selected.id,
          exposure: exposureValue,
          views: viewsValue,
        }),
      });
      const result = (await response.json()) as { error?: string };
      if (!response.ok) {
        throw new Error(result.error || "创作者数据提交失败");
      }
      await loadTask(taskToken, claimToken, delegationToken);
      setCreatorScreenshot(null);
      setToast("创作者截图、曝光量和阅读量已提交");
    } catch (reason) {
      setToast(
        reason instanceof Error ? reason.message : "创作者数据提交失败",
      );
    } finally {
      setCreatorWorking(false);
      setCreatorStage("");
    }
  };

  if (loading) {
    return (
      <main className="portal-shell loading-shell">
        <div className="loading-mark">K</div>
        <p>正在打开任务…</p>
      </main>
    );
  }

  if (error || !payload) {
    return (
      <main className="portal-shell">
        <section className="message-card">
          <div className="message-icon">!</div>
          <p className="micro">KOC LOOP</p>
          <h1>暂时无法打开任务</h1>
          <p>{error || "任务不存在"}</p>
          <button onClick={() => window.location.reload()}>重新加载</button>
        </section>
      </main>
    );
  }

  if (selected && activeBatch) {
    if (payload.task.type === "screenshot_collect") {
      return (
        <main className="portal-shell detail-shell">
          <header className="portal-topbar">
            <div className="portal-brand"><span>K</span><strong>KOC LOOP</strong></div>
            <div className="claimant-chip">
              {payload.claim ? payload.claim.claimantName : "合作社转派 · 无需登录"}
            </div>
          </header>
          <button className="back-link" onClick={closeNote}>← 返回我的任务</button>
          <div className="detail-grid screenshot-task-detail">
            <article className="note-document">
              <div className="note-document-meta">
                <span>截图任务 {activeBatch.assignments.findIndex((item) => item.id === selected.id) + 1}</span>
                <span>只需上传结果截图</span>
              </div>
              <div className="document-label-row">
                <p className="document-label">小红书搜索关键词</p>
                <PlatformBadge platform={payload.task.platform} compact />
              </div>
              <div className="note-title-row">
                <h1>{selected.title}</h1>
                <button
                  type="button"
                  className="inline-copy"
                  onClick={() => void copyText(selected.title, "搜索关键词已复制")}
                >
                  复制关键词
                </button>
              </div>
              <div className="document-rule" />
              <p className="document-label">任务说明</p>
              <div className="note-body">{selected.body}</div>
              {selected.images.length > 0 && (
                <section className="note-images">
                  <div className="note-images-heading">
                    <div>
                      <p className="document-label">示例截图</p>
                      <span>请参考示例，确保小红书 AI 总结完整可见</span>
                    </div>
                  </div>
                  <div className="note-image-grid">
                    {selected.images.map((image, index) => (
                      <div className="note-image-card" key={image.index}>
                        <a className="note-image-preview" href={noteImageUrl(selected, image.index)} target="_blank" rel="noreferrer">
                          {/* eslint-disable-next-line @next/next/no-img-element */}
                          <img src={noteImageUrl(selected, image.index)} alt={`示例截图 ${index + 1}`} />
                        </a>
                      </div>
                    ))}
                  </div>
                </section>
              )}
            </article>
            <form className="submit-card" onSubmit={submitScreenshotTask}>
              <div className="submit-heading">
                <div>
                  <p className="micro">截图回收</p>
                  <h2>上传小红书 AI 总结截图</h2>
                </div>
                <span className={selected.result_submitted_at ? "done" : "pending"}>
                  {statusLabel(selected, payload.task.type)}
                </span>
              </div>
              <div className="mapping-note">
                <span>1 : N</span>
                <p>这些截图只会绑定到当前任务，不需要填写发布链接或其他数据。</p>
              </div>
              <label>
                <span className="task-result-label-row">
                  <span>任务结果截图</span>
                  <small>
                    已选择 {resultScreenshotKeys(selected.result_screenshot_key).length + taskResultScreenshots.length}/{MAX_TASK_RESULT_SCREENSHOTS} 张
                  </small>
                </span>
                <div className="task-result-gallery">
                  {resultScreenshotKeys(selected.result_screenshot_key).map((_, index) => {
                    const imageUrl = taskResultImageUrl(selected, index + 1);
                    return (
                    <button
                      type="button"
                      className="task-result-thumb uploaded"
                      onClick={() =>
                        setPreviewImage({
                          src: imageUrl,
                          alt: `已上传任务截图 ${index + 1}`,
                        })
                      }
                      key={`uploaded-${index}`}
                    >
                      <img src={imageUrl} alt={`已上传任务截图 ${index + 1}`} />
                      <span>已上传</span>
                    </button>
                  )})}
                  {taskResultPreviews.map(({ file, url }, index) => (
                    <div className="task-result-thumb pending-upload" key={`${file.name}-${file.lastModified}`}>
                      <img src={url} alt={`待上传任务截图 ${index + 1}`} />
                      <button
                        type="button"
                        aria-label={`移除 ${file.name}`}
                        onClick={() => removeTaskResultScreenshot(file)}
                      >
                        ×
                      </button>
                      <span>待提交</span>
                    </div>
                  ))}
                  {resultScreenshotKeys(selected.result_screenshot_key).length + taskResultScreenshots.length < MAX_TASK_RESULT_SCREENSHOTS && (
                    <div className="task-result-add">
                      <input
                        type="file"
                        accept="image/*"
                        multiple
                        onChange={chooseTaskResultScreenshots}
                      />
                      <b>＋</b>
                      <strong>
                        {taskResultScreenshots.length || selected.result_screenshot_key
                          ? "继续添加"
                          : "选择截图"}
                      </strong>
                    </div>
                  )}
                </div>
                <small className="task-result-hint">支持一次选择或继续追加，最多9张，每张不超过8MB；点击图片可在当前页面查看大图。</small>
              </label>
              <button className="submit-primary" disabled={working}>
                {working
                  ? "正在提交…"
                  : selected.result_submitted_at
                    ? "更新任务截图"
                    : "确认提交任务截图"}
              </button>
              {selected.result_submitted_at && (
                <p className="submitted-at">上次提交：{formatDate(selected.result_submitted_at, true)}</p>
              )}
            </form>
          </div>
          <ImageLightbox image={previewImage} onClose={() => setPreviewImage(null)} />
          {toast && <div className={toastClassName(toast)}>{toast}</div>}
        </main>
      );
    }
    const creatorDueAt = creatorScreenshotDueAt(selected.publish_time);
    const creatorDataReady =
      Boolean(selected.creator_screenshot_key) &&
      selected.exposure !== null &&
      selected.views !== null;
    return (
      <main className="portal-shell detail-shell">
        <header className="portal-topbar">
          <div className="portal-brand"><span>K</span><strong>KOC LOOP</strong></div>
          <div className="claimant-chip">
            {payload.claim ? payload.claim.claimantName : "合作社转派 · 无需登录"}
          </div>
        </header>
        <button className="back-link" onClick={closeNote}>← 返回我的笔记</button>
        <div className="detail-grid">
          <article
            className={`note-document ${
              noteContentCollapsed ? "mobile-collapsed" : ""
            }`}
          >
            <div className="mobile-note-summary">
              <div>
                <span>笔记内容已收起</span>
                <strong>{selected.title}</strong>
                <small>
                  {selected.videos.length > 0
                    ? `${selected.videos.length} 个视频`
                    : `${selected.images.length} 张配图`} · 已完成首次回填
                </small>
              </div>
              <button
                type="button"
                aria-expanded={!noteContentCollapsed}
                onClick={() => setNoteContentCollapsed(false)}
              >
                展开笔记内容
              </button>
            </div>
            <div className="note-document-content">
            <div className="note-document-meta">
              <span>笔记 {activeBatch.assignments.findIndex((item) => item.id === selected.id) + 1}</span>
              <PlatformBadge platform={payload.task.platform} compact />
              <span>飞书源行 {selected.source_row ?? "—"}</span>
            </div>
            {selected.publish_url && (
              <button
                type="button"
                className="mobile-note-collapse-trigger"
                onClick={() => setNoteContentCollapsed(true)}
              >
                收起笔记内容
              </button>
            )}
            <div className="note-title-row">
              <h1>{selected.title}</h1>
              <button
                type="button"
                className="inline-copy"
                onClick={() => void copyText(selected.title, "标题已复制")}
              >
                复制标题
              </button>
            </div>
            <div className="document-rule" />
            <div className="document-label-row">
              <p className="document-label">正文与话题</p>
              <button
                type="button"
                className="inline-copy"
                onClick={() => void copyText(selected.body, "文案已复制")}
              >
                复制文案
              </button>
            </div>
            <div className="note-body">{selected.body || "暂无正文"}</div>
            {selected.images.length > 0 && (
              <section className="note-images">
                <div className="note-images-heading">
                  <div>
                    <p className="document-label">发布配图</p>
                    <span>手机优先打开系统保存菜单，其他浏览器会下载文件</span>
                  </div>
                  <div className="note-images-tools">
                    <b>{selected.images.length} 张</b>
                    {selected.images.length > 1 && (
                      <button
                        type="button"
                        disabled={batchDownloading}
                        onClick={() => void downloadAllImages(selected)}
                      >
                        {batchDownloading ? "正在准备…" : "批量保存图片"}
                      </button>
                    )}
                  </div>
                </div>
                <div className="note-image-grid">
                  {selected.images.map((image, index) => (
                    <div className="note-image-card" key={image.index}>
                      <a
                        className="note-image-preview"
                        href={noteImageUrl(selected, image.index)}
                        target="_blank"
                        rel="noreferrer"
                      >
                        {/* Private claim-bound images intentionally bypass the public image optimizer. */}
                        {/* eslint-disable-next-line @next/next/no-img-element */}
                        <img
                          src={noteImageUrl(selected, image.index)}
                          alt={`${selected.title} 配图 ${index + 1}`}
                          width={image.width ?? undefined}
                          height={image.height ?? undefined}
                          loading={index === 0 ? "eager" : "lazy"}
                        />
                      </a>
                      <div className="note-image-actions">
                        <span>图片 {index + 1}</span>
                        <button
                          type="button"
                          disabled={downloadingImage === image.index}
                          onClick={() => void downloadImage(selected, image.index)}
                        >
                          {downloadingImage === image.index ? "下载中…" : "下载原图"}
                        </button>
                      </div>
                    </div>
                  ))}
                </div>
              </section>
            )}
            {selected.videos.length > 0 && (
              <section className="note-videos">
                <div className="note-images-heading">
                  <div>
                    <p className="document-label">发布视频</p>
                    <span>点击播放，可使用播放器菜单保存原视频</span>
                  </div>
                  <b>{selected.videos.length} 个</b>
                </div>
                <div className="note-video-grid">
                  {selected.videos.map((video, index) => (
                    <div className="note-video-card" key={video.index}>
                      <video
                        controls
                        preload="metadata"
                        playsInline
                        src={noteVideoUrl(selected, video.index)}
                      />
                      <div className="note-image-actions">
                        <span>视频 {index + 1}</span>
                        <a
                          href={noteVideoUrl(selected, video.index, true)}
                          download={`${safeFileBase(selected)}-视频-${index + 1}.mp4`}
                        >
                          下载视频
                        </a>
                      </div>
                    </div>
                  ))}
                </div>
              </section>
            )}
            </div>
          </article>

          <form ref={submitCardRef} className="submit-card" onSubmit={submitNote}>
            <div className="submit-heading">
              <div>
                <p className="micro">逐篇回填</p>
                <h2>填写这篇笔记的发布信息</h2>
              </div>
              <span className={selected.publish_url ? "done" : "pending"}>
                {statusLabel(selected)}
              </span>
            </div>
            <div className="mapping-note">
              <span>1 : 1</span>
              <p>以下信息只会绑定到当前笔记，不按顺序批量匹配。</p>
            </div>
            <label>
              <span>发布链接</span>
              <input
                type="text"
                inputMode="url"
                value={publishUrl}
                onChange={(event) => setPublishUrl(event.target.value)}
                placeholder={`可粘贴${payload.task.platform}作品链接或整段分享文案`}
                required
              />
              <small className="field-hint">
                系统会自动提取链接，并识别发布账号
              </small>
            </label>
            <div className="evidence-field">
              <span>发布截图</span>
              <div
                className={`screenshot-picker ${
                  screenshot || selected.publish_screenshot_key
                    ? "has-file preview-mode"
                    : ""
                }`}
              >
                {screenshot || selected.publish_screenshot_key ? (
                  <button
                    type="button"
                    className="evidence-preview-button"
                    onClick={() =>
                      setPreviewImage({
                        src:
                          publishScreenshotPreview ||
                          evidenceImageUrl(selected, "publish"),
                        alt: "发布截图",
                      })
                    }
                  >
                    {/* eslint-disable-next-line @next/next/no-img-element */}
                    <img
                      src={
                        publishScreenshotPreview ||
                        evidenceImageUrl(selected, "publish")
                      }
                      alt="发布截图"
                    />
                    <span>点击查看大图</span>
                  </button>
                ) : (
                  <div className="evidence-empty-state">
                    <b>＋</b>
                    <strong>点击上传这篇笔记的发布截图</strong>
                    <small>支持 JPG、PNG，最大8MB</small>
                  </div>
                )}
                <label className="evidence-upload-action">
                  <input type="file" accept="image/*" onChange={chooseScreenshot} />
                  {screenshot || selected.publish_screenshot_key
                    ? "重新选择"
                    : "选择图片"}
                </label>
              </div>
            </div>
            <button className="submit-primary" disabled={working}>
              {working ? "正在提交…" : selected.publish_url ? "更新这篇笔记" : "确认回填这篇笔记"}
            </button>
            {selected.publish_time && (
              <p className="submitted-at">上次回填：{formatDate(selected.publish_time, true)}</p>
            )}
            {selected.publish_url && (
              <section className="creator-recovery">
                <div className="creator-recovery-heading">
                  <div>
                    <p className="micro">第7天数据回收</p>
                    <h3>上传截图并填写数据</h3>
                  </div>
                  <span className={creatorDataReady ? "done" : ""}>
                    {creatorDataReady ? "已提交" : "待提交"}
                  </span>
                </div>
                <p className="creator-recovery-tip">
                  请在发布满7天后，上传创作者中心数据页完整截图，并填写页面显示的数据。
                  {creatorDueAt && ` 建议上传日期：${formatDate(creatorDueAt)}。`}
                </p>
                <div className="evidence-field compact-field">
                  <span>创作者中心截图</span>
                  <div
                    className={`screenshot-picker compact ${
                      creatorScreenshot || selected.creator_screenshot_key
                        ? "has-file preview-mode"
                        : ""
                    }`}
                  >
                    {creatorScreenshot || selected.creator_screenshot_key ? (
                      <button
                        type="button"
                        className="evidence-preview-button compact-preview"
                        onClick={() =>
                          setPreviewImage({
                            src:
                              creatorScreenshotPreview ||
                              evidenceImageUrl(selected, "creator"),
                            alt: "创作者中心截图",
                          })
                        }
                      >
                        {/* eslint-disable-next-line @next/next/no-img-element */}
                        <img
                          src={
                            creatorScreenshotPreview ||
                            evidenceImageUrl(selected, "creator")
                          }
                          alt="创作者中心截图"
                        />
                        <span>点击查看大图</span>
                      </button>
                    ) : (
                      <div className="evidence-empty-state">
                        <b>＋</b>
                        <strong>点击选择第7天创作者中心截图</strong>
                        <small>上传后自动识别曝光量和阅读量，识别失败可手动填写</small>
                      </div>
                    )}
                    <label className="evidence-upload-action">
                      <input
                        type="file"
                        accept="image/*"
                        onChange={chooseCreatorScreenshot}
                      />
                      {creatorScreenshot || selected.creator_screenshot_key
                        ? "重新选择"
                        : "选择图片"}
                    </label>
                  </div>
                </div>
                <div className="creator-metric-grid">
                  <label>
                    <span>曝光量</span>
                    <input
                      type="number"
                      inputMode="numeric"
                      min="0"
                      step="1"
                      max="999999999999"
                      value={creatorExposure}
                      onChange={(event) => setCreatorExposure(event.target.value)}
                      placeholder="填写截图中的曝光量"
                    />
                  </label>
                  <label>
                    <span>阅读量</span>
                    <input
                      type="number"
                      inputMode="numeric"
                      min="0"
                      step="1"
                      max="999999999999"
                      value={creatorViews}
                      onChange={(event) => setCreatorViews(event.target.value)}
                      placeholder="填写截图中的阅读量"
                    />
                  </label>
                </div>
                <button
                  type="button"
                  className="creator-submit"
                  disabled={
                    creatorWorking ||
                    (!creatorScreenshot && !selected.creator_screenshot_key) ||
                    !creatorExposure ||
                    !creatorViews
                  }
                  onClick={() => void submitCreatorData()}
                >
                  {creatorWorking
                    ? creatorStage || "正在处理…"
                    : creatorDataReady
                      ? "更新创作者数据"
                      : "提交创作者数据"}
                </button>
              </section>
            )}
          </form>
        </div>
        <ImageLightbox image={previewImage} onClose={() => setPreviewImage(null)} />
        {toast && <div className={toastClassName(toast)}>{toast}</div>}
      </main>
    );
  }

  if (activeBatch) {
    const isScreenshotTask = payload.task.type === "screenshot_collect";
    const completed = activeBatch.assignments.filter((item) =>
      isScreenshotTask ? item.result_submitted_at : item.publish_url,
    ).length;
    const isClaimOwner = Boolean(payload.claim);
    const shareableCount = payload.claim?.assignments.filter(
      (item) =>
        !(isScreenshotTask ? item.result_submitted_at : item.publish_url) &&
        !item.delegation_bundle_id,
    ).length ?? 0;
    return (
      <main className="portal-shell">
        <header className="portal-topbar">
          <div className="portal-brand"><span>K</span><strong>KOC LOOP</strong></div>
          {payload.claim ? (
            <div className="claim-tools">
              <button onClick={() => void showRecovery()}>找回其他批次</button>
              <div className="claimant-chip">{payload.claim.claimantName}</div>
            </div>
          ) : (
            <div className="claimant-chip">合作社转派 · 无需登录</div>
          )}
        </header>
        <section className="claimed-hero">
          <div>
            <p className="micro">
              {isClaimOwner
                ? "我的领取任务"
                : isScreenshotTask
                  ? "合作社转派截图任务包"
                  : "合作社转派发布包"}
            </p>
            <h1>{payload.task.name}</h1>
            <p className="platform-meta-line">
              <span>{payload.task.brand}</span>
              <PlatformBadge platform={payload.task.platform} compact />
              <span>{formatDate(payload.task.dueAt)}前{isScreenshotTask ? "提交" : "发布"}</span>
              {payload.delegation ? ` · ${payload.delegation.label}` : ""}
            </p>
          </div>
          <div className="completion-ring">
            <strong>{completed}/{activeBatch.assignments.length}</strong>
            <span>已回填</span>
          </div>
        </section>
        <section className="notes-section">
          <div className="section-heading">
            <div>
              <h2>
                {isClaimOwner
                  ? isScreenshotTask ? "我领取的截图任务" : "我领取的笔记"
                  : isScreenshotTask ? "需要完成的截图任务" : "需要发布的笔记"}
              </h2>
              <p>
                {isClaimOwner
                  ? isScreenshotTask
                    ? "打开一份查看关键词和要求，完成后单独上传截图；也可以转派给底层KOC"
                    : "可逐篇回填，也可导出Excel填写后批量上传；还可以选择笔记转派给底层KOC"
                  : isScreenshotTask
                    ? "打开任务查看搜索关键词和要求，完成后逐份上传截图"
                    : "打开一篇查看完整内容，发布后逐篇回填"}
              </p>
            </div>
            <div className="section-actions">
              <span>{activeBatch.assignments.length} {isScreenshotTask ? "份" : "篇"}</span>
              {!isScreenshotTask && (
                <>
                  <button
                    type="button"
                    disabled={batchWorkbookWorking}
                    onClick={() => void exportBatchWorkbook()}
                  >
                    {batchWorkbookWorking ? "处理中…" : "导出Excel"}
                  </button>
                  <button
                    type="button"
                    disabled={batchWorkbookWorking}
                    onClick={() => batchWorkbookInputRef.current?.click()}
                  >
                    上传回填表
                  </button>
                  <input
                    ref={batchWorkbookInputRef}
                    className="batch-workbook-input"
                    type="file"
                    accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
                    onChange={(event) => void importBatchWorkbook(event)}
                  />
                </>
              )}
              {isClaimOwner && (
                <button
                  type="button"
                  className={shareComposerOpen ? "active" : ""}
                  disabled={shareableCount === 0 && !shareComposerOpen}
                  onClick={() => {
                    setShareComposerOpen((current) => !current);
                    setSelectedForShare([]);
                    setShareLabel("");
                  }}
                >
                  {shareComposerOpen ? "取消选择" : isScreenshotTask ? "选择任务并分享" : "选择笔记并分享"}
                </button>
              )}
            </div>
          </div>
          {payload.claim && shareComposerOpen && (
            <form className="share-composer" onSubmit={createDelegation}>
              <div>
                <p className="micro">合作社转派</p>
                <h3>生成免登录分享链接</h3>
                <p>
                  {isScreenshotTask
                    ? "勾选待提交任务，底层KOC打开链接即可查看要求并上传截图。"
                    : "勾选待发布笔记，底层KOC打开链接即可查看内容并逐篇回填。"}
                </p>
              </div>
              <label>
                <span>接收人备注</span>
                <input
                  value={shareLabel}
                  maxLength={24}
                  onChange={(event) => setShareLabel(event.target.value)}
                  placeholder="例如：A、小王"
                  required
                />
              </label>
              <div className="share-composer-action">
                <strong>已选 {selectedForShare.length} {isScreenshotTask ? "份" : "篇"}</strong>
                <button
                  className="share-primary"
                  disabled={
                    shareWorking ||
                    selectedForShare.length === 0 ||
                    !shareLabel.trim()
                  }
                >
                  {shareWorking ? "生成中…" : "生成并复制分享链接"}
                </button>
              </div>
              <small>
                对方无需登录，也不会填写手机号、微信或企微；分享链接可用于后续第7天数据回收。
              </small>
            </form>
          )}
          <div className="note-list">
            {activeBatch.assignments.map((item, index) => {
              const shareDisabled = Boolean(
                (isScreenshotTask ? item.result_submitted_at : item.publish_url) ||
                  item.delegation_bundle_id,
              );
              return (
              <article
                key={item.id}
                className={`note-row ${shareComposerOpen && isClaimOwner ? "share-selecting" : ""}`}
              >
                {shareComposerOpen && isClaimOwner && (
                  <label
                    className={`share-checkbox ${shareDisabled ? "disabled" : ""}`}
                    title={
                      (isScreenshotTask ? item.result_submitted_at : item.publish_url)
                        ? isScreenshotTask ? "已提交任务不能转派" : "已发布笔记不能转派"
                        : item.delegation_bundle_id
                          ? `已转派给 ${item.delegation_label}`
                          : "选择这篇笔记"
                    }
                  >
                    <input
                      type="checkbox"
                      checked={selectedForShare.includes(item.id)}
                      disabled={shareDisabled}
                      onChange={() => toggleShareSelection(item.id)}
                      aria-label={`选择笔记：${item.title}`}
                    />
                    <span>✓</span>
                  </label>
                )}
                <span className={`note-index ${(isScreenshotTask ? item.result_submitted_at : item.publish_url) ? "done" : ""}`}>
                  {String(index + 1).padStart(2, "0")}
                </span>
                {item.images[0] ? (
                  <>
                    {/* Private claim-bound images intentionally bypass the public image optimizer. */}
                    {/* eslint-disable-next-line @next/next/no-img-element */}
                    <img
                      className="note-thumb"
                      src={noteImageUrl(item, item.images[0].index)}
                      alt=""
                      loading="lazy"
                    />
                  </>
                ) : item.videos.length > 0 ? (
                  <span className="note-thumb platform-video-thumb">
                    <PlatformBadge platform={payload.task.platform} logoOnly />
                  </span>
                ) : (
                  <span className="note-thumb empty">无图</span>
                )}
                <div>
                  <h3>{item.title}</h3>
                  <p>
                    {isScreenshotTask
                      ? `搜索关键词 · ${statusLabel(item, payload.task.type)}`
                      : <>
                          <span>{item.videos.length > 0 ? `${item.videos.length} 个视频` : `${item.images.length} 张配图`}</span>
                          <PlatformBadge platform={payload.task.platform} compact />
                          <span>飞书源行 {item.source_row ?? "—"} · {statusLabel(item, payload.task.type)}</span>
                        </>}
                    {!isScreenshotTask && item.creator_screenshot_key ? " · D7截图已交" : ""}
                    {item.delegation_label ? ` · 已转派给 ${item.delegation_label}` : ""}
                  </p>
                </div>
                <span
                  className={`note-status ${
                    (isScreenshotTask ? item.result_submitted_at : item.publish_url)
                      ? "done"
                      : item.delegation_bundle_id
                        ? "delegated"
                        : ""
                  }`}
                >
                  {(isScreenshotTask ? item.result_submitted_at : item.publish_url)
                    ? isScreenshotTask ? "已提交" : "已回填"
                    : item.delegation_bundle_id
                      ? "已转派"
                      : isScreenshotTask ? "待提交" : "待发布"}
                </span>
                <button onClick={() => openNote(item)}>
                  {(isScreenshotTask ? item.result_submitted_at : item.publish_url)
                    ? "查看 / 修改"
                    : isScreenshotTask ? "查看并上传" : "查看并回填"} →
                </button>
              </article>
              );
            })}
          </div>
          {payload.claim && payload.claim.delegations.length > 0 && (
            <section className="delegation-history">
              <div className="delegation-heading">
                <div>
                  <p className="micro">分享记录</p>
                  <h3>{isScreenshotTask ? "转派截图任务包" : "转派发布包"}</h3>
                </div>
                <span>{payload.claim.delegations.length} 个</span>
              </div>
              <div className="delegation-list">
                {payload.claim.delegations.map((bundle) => (
                  <article
                    key={bundle.id}
                    className={bundle.status === "revoked" ? "revoked" : ""}
                  >
                    <div>
                      <strong>{bundle.label}</strong>
                      <span>
                        {bundle.quantity} {isScreenshotTask ? "份" : "篇"} · {isScreenshotTask ? "已提交" : "已发布"}{" "}
                        {Number(isScreenshotTask ? bundle.result_completed_count ?? bundle.completed_count : bundle.completed_count ?? 0)}/{bundle.quantity}
                        {!isScreenshotTask && ` · D7完成 ${Number(bundle.creator_completed_count ?? 0)}`}
                      </span>
                    </div>
                    <small>{formatDate(bundle.created_at, true)}</small>
                    <span className={`delegation-status ${bundle.status}`}>
                      {bundle.status === "active" ? "分享中" : "已撤销"}
                    </span>
                    {bundle.status === "active" && (
                      <div className="delegation-actions">
                        <button
                          type="button"
                          onClick={() =>
                            void copyText(
                              delegationUrl(bundle.share_token),
                              `已复制给 ${bundle.label} 的分享链接`,
                            )
                          }
                        >
                          复制链接
                        </button>
                        {Number(isScreenshotTask ? bundle.result_completed_count ?? bundle.completed_count : bundle.completed_count ?? 0) === 0 && (
                          <button
                            type="button"
                            className="danger"
                            disabled={shareWorking}
                            onClick={() => void revokeDelegation(bundle)}
                          >
                            撤销
                          </button>
                        )}
                      </div>
                    )}
                  </article>
                ))}
              </div>
            </section>
          )}
        </section>
        <div className="safe-tip">
          {isClaimOwner
            ? "以后再次打开原任务链接，填写相同微信号或手机号即可找回全部领取批次和分享记录。"
            : isScreenshotTask
              ? "请保存当前分享链接；完成任务后通过此链接上传截图即可。"
              : "请保存当前分享链接；发布后仍需通过此链接回填第7天截图、曝光量和阅读量。"}
        </div>
        {toast && <div className={toastClassName(toast)}>{toast}</div>}
      </main>
    );
  }

  return (
    <main className="portal-shell">
      <header className="portal-topbar">
        <div className="portal-brand"><span>K</span><strong>KOC LOOP</strong></div>
        <span className="external-label">KOC领取页</span>
      </header>
      <section className="task-hero">
        <div className="hero-copy">
          <div className="hero-platform-line">
            <p className="micro">正在招募 · {payload.task.brand}</p>
            <PlatformBadge platform={payload.task.platform} />
          </div>
          <h1>{payload.task.name}</h1>
          <p>
            {payload.task.type === "screenshot_collect"
              ? "领取后查看搜索关键词和任务要求，完成后逐份上传截图。"
              : "领取后可查看完整笔记正文，每篇笔记独立回填发布信息。"}
          </p>
          <div className="hero-meta">
            <span>截止 {formatDate(payload.task.dueAt)}</span>
            <span>剩余 {payload.task.availableQuantity} {payload.task.type === "screenshot_collect" ? "份" : "篇"}</span>
          </div>
        </div>
        <aside className="claim-card">
          <div className="entry-switch" aria-label="领取入口">
            <button
              type="button"
              className={entryMode === "claim" ? "active" : ""}
              onClick={() => {
                setEntryMode("claim");
                setRecoveredClaims([]);
              }}
            >
              {payload.task.type === "screenshot_collect" ? "领取新任务" : "领取新笔记"}
            </button>
            <button
              type="button"
              className={entryMode === "recover" ? "active" : ""}
              onClick={() => {
                setEntryMode("recover");
                setRecoveredClaims([]);
              }}
            >
              找回领取记录
            </button>
          </div>
          {entryMode === "claim" ? (
            <form onSubmit={claimNotes}>
              <div>
                <p className="micro">01 · 确认领取</p>
                <h2>你要领取多少{payload.task.type === "screenshot_collect" ? "份" : "篇"}？</h2>
              </div>
              <label>
                <span>微信号 / 手机号</span>
                <input
                  value={claimantName}
                  onChange={(event) => setClaimantName(event.target.value)}
                  placeholder="请输入微信号或手机号"
                  autoCapitalize="none"
                  autoCorrect="off"
                  spellCheck={false}
                  maxLength={64}
                  required
                />
              </label>
              <div className="quantity-control">
                <button type="button" onClick={() => setQuantity(Math.max(1, quantity - 1))}>−</button>
                <div><strong>{quantity}</strong><span>{payload.task.type === "screenshot_collect" ? "份" : "篇"}</span></div>
                <button
                  type="button"
                  onClick={() =>
                    setQuantity(Math.min(payload.task.availableQuantity || 1, quantity + 1))
                  }
                >
                  ＋
                </button>
              </div>
              <button
                className="claim-primary"
                disabled={working || payload.task.availableQuantity === 0}
              >
                {payload.task.availableQuantity === 0
                  ? "当前任务已领完"
                  : working
                    ? "领取中…"
                    : `确认领取 ${quantity} ${payload.task.type === "screenshot_collect" ? "份" : "篇"}`}
              </button>
              <small>领取后系统会生成你的专属{payload.task.type === "screenshot_collect" ? "任务清单" : "笔记清单"}</small>
            </form>
          ) : (
            <form onSubmit={recoverClaims}>
              <div>
                <p className="micro">找回历史记录</p>
                <h2>继续之前领取的{payload.task.type === "screenshot_collect" ? "任务" : "笔记"}</h2>
                <p className="recover-intro">
                  输入领取时使用的微信号或手机号；同一任务多次领取会分批展示。
                </p>
              </div>
              <label>
                <span>领取时的微信号 / 手机号</span>
                <input
                  value={claimantName}
                  onChange={(event) => setClaimantName(event.target.value)}
                  placeholder="需与领取时完全一致"
                  autoCapitalize="none"
                  autoCorrect="off"
                  spellCheck={false}
                  maxLength={64}
                  required
                />
              </label>
              <button className="claim-primary" disabled={working}>
                {working ? "正在查找…" : "查找我的领取记录"}
              </button>
              {recoveredClaims.length > 0 && (
                <div className="recovery-list">
                  {recoveredClaims.map((claim, index) => (
                    <button
                      type="button"
                      key={claim.claimToken}
                      onClick={() => void openRecoveredClaim(claim.claimToken)}
                    >
                      <span>第 {recoveredClaims.length - index} 批 · {formatDate(claim.createdAt, true)}</span>
                      <strong>{claim.firstTitle}</strong>
                      <small>{claim.completedCount}/{claim.quantity} 已回填</small>
                      <i>继续填写 →</i>
                    </button>
                  ))}
                </div>
              )}
            </form>
          )}
        </aside>
      </section>
      <section className="steps">
        <div><span>1</span><strong>选择数量</strong><p>一次可领多篇</p></div>
        <i />
        <div><span>2</span><strong>{payload.task.type === "screenshot_collect" ? "逐份查看" : "逐篇查看"}</strong><p>{payload.task.type === "screenshot_collect" ? "关键词与要求完整展示" : "标题正文完整展示"}</p></div>
        <i />
        <div><span>3</span><strong>{payload.task.type === "screenshot_collect" ? "上传截图" : "单篇回填"}</strong><p>{payload.task.type === "screenshot_collect" ? "每份任务与截图一一对应" : "昵称、链接、截图一一对应"}</p></div>
      </section>
      <footer>由 KOC LOOP 提供任务与数据安全保障</footer>
      {toast && <div className={toastClassName(toast)}>{toast}</div>}
    </main>
  );
}
