import { buildDmsPrintBarcode } from './dmsPrintBarcode';
import type { DmsPrintAttempt, DmsPrintBatch as DmsAttemptBatch } from './dmsPrintAttempt';
import { attemptBatches, recoverDmsBatchReceipts, recordDmsBatchSubmitted, updateDmsPrintAttempt } from './dmsPrintAttempt';
import type { DmsPrintRenderItem } from '#/api/x/dms/print';
import type { DmsPageVariables, DmsPreviewPrintOptions, DmsPrintConfig, DmsPrintCopyBarcodes } from '#/components/GlobalOfficeDocumentModal/types';
import type { HiprintPrintConfig, HiprintPrintResult } from '#/utils/jnpf';

import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist';
import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url';
import { v4 as uuidv4 } from 'uuid';

import { createDmsFilePrintTicket, getDmsFilePageCount, renderDmsPrintPdf } from '#/api/x/dms/print';
import { watermarkParameters } from '#/api/x/dms/watermarkParameters';
import { onlineUtils } from '#/utils/jnpf';

GlobalWorkerOptions.workerSrc = pdfWorkerUrl;

const MAX_PAGE_VARIABLES = 1_000;
const MAX_SELECTED_PAGES = 1_000;
const MAX_VARIABLES_PER_PAGE = 50;
const MAX_VARIABLE_NAME_LENGTH = 100;
const MAX_STRING_LENGTH = 2_000;
const MAX_PRINT_OPTIONS_BYTES = 1024 * 1024;
const MAX_COPY_BARCODE_COPIES = 999;
const MAX_BATCH_COPIES = 20;
const MAX_BATCH_PAGES = 100;
const PRINT_CONFIRM_TIMEOUT_MILLIS = 60_000;

export class DmsPrintError extends Error {
  constructor(
    public readonly code: 'INVALID_PAGES' | 'INVALID_VARIABLES' | 'PRINT_FAILED',
    message: string,
  ) {
    super(message);
    this.name = 'DmsPrintError';
  }
}

export interface DmsPrintPageContext {
  canvas: HTMLCanvasElement;
  pageNo: number;
  variables: Readonly<Record<string, unknown>>;
}

export type DmsPrintPageVariableHandler = (context: DmsPrintPageContext) => Promise<void> | void;

const pageVariableHandlers = new Set<DmsPrintPageVariableHandler>();

/** 登记业务页面变量处理器；返回值用于在业务模块卸载时取消登记。 */
export function registerDmsPrintPageVariableHandler(handler: DmsPrintPageVariableHandler) {
  pageVariableHandlers.add(handler);
  return () => pageVariableHandlers.delete(handler);
}

function invalidPages(message = '打印页码无效') {
  return new DmsPrintError('INVALID_PAGES', message);
}

function invalidVariables(message = '页面打印参数无效') {
  return new DmsPrintError('INVALID_VARIABLES', message);
}

export function parseDmsPrintPages(value?: string, pageCount?: number): number[] | undefined {
  const text = value?.trim();
  if (!text) return undefined;

  const pages = new Set<number>();
  for (const part of text.split(',')) {
    const token = part.trim();
    if (!token) throw invalidPages();
    const match = /^(\d+)(?:\s*-\s*(\d+))?$/.exec(token);
    if (!match) throw invalidPages();
    const start = Number(match[1]);
    const end = Number(match[2] ?? match[1]);
    if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < start) throw invalidPages();
    const actualEnd = pageCount === undefined ? end : Math.min(end, pageCount);
    for (let pageNo = start; pageNo <= actualEnd; pageNo++) {
      if (pages.has(pageNo)) continue;
      if (pages.size >= MAX_SELECTED_PAGES) throw invalidPages('打印页数超过系统限制');
      pages.add(pageNo);
    }
  }
  return [...pages].sort((left, right) => left - right);
}

function validateVariableValue(value: unknown) {
  if (value === null || typeof value === 'boolean' || typeof value === 'number') return;
  if (typeof value === 'string') {
    if (value.length > MAX_STRING_LENGTH) throw invalidVariables('页面变量字符串超过长度限制');
    return;
  }
  if (Array.isArray(value)) {
    for (const item of value) {
      if (Array.isArray(item) || (typeof item === 'object' && item !== null) || item === undefined) throw invalidVariables();
      validateVariableValue(item);
    }
    return;
  }
  throw invalidVariables();
}

export function normalizeDmsPrintOptions(
  options?: DmsPreviewPrintOptions,
): Pick<DmsPreviewPrintOptions, 'copyBarcodes' | 'pages'> & Required<Pick<DmsPreviewPrintOptions, 'copies' | 'pageVariables'>> {
  const copyBarcodes = normalizeCopyBarcodes(options?.copyBarcodes);
  const copies = options?.copies ?? copyBarcodes?.length ?? 1;
  if (!Number.isInteger(copies) || copies < 1 || copies > 999) throw invalidVariables('打印份数必须为 1～999 的整数');
  if (copyBarcodes && copies !== copyBarcodes.length) throw invalidVariables('打印份数必须与逐份条码配置数量一致');

  const pages = options?.pages?.trim() || undefined;
  const selectedPages = parseDmsPrintPages(pages);
  const selectedPageSet = selectedPages ? new Set(selectedPages) : undefined;
  const pageVariables = options?.pageVariables ?? [];
  if (!Array.isArray(pageVariables) || pageVariables.length > MAX_PAGE_VARIABLES) throw invalidVariables();

  const pageNumbers = new Set<number>();
  for (const item of pageVariables) {
    if (!item || !Number.isInteger(item.pageNo) || item.pageNo < 1 || pageNumbers.has(item.pageNo)) throw invalidVariables();
    if (selectedPageSet && !selectedPageSet.has(item.pageNo)) throw invalidVariables('页面变量引用了未选择的打印页');
    if (!item.variables || Array.isArray(item.variables) || typeof item.variables !== 'object') throw invalidVariables();
    const entries = Object.entries(item.variables);
    if (entries.length > MAX_VARIABLES_PER_PAGE) throw invalidVariables('单页变量数量超过系统限制');
    for (const [name, value] of entries) {
      if (!name || name.length > MAX_VARIABLE_NAME_LENGTH) throw invalidVariables('页面变量名称无效');
      validateVariableValue(value);
    }
    pageNumbers.add(item.pageNo);
  }

  if (new TextEncoder().encode(JSON.stringify({ copies, copyBarcodes, pages, pageVariables })).byteLength > MAX_PRINT_OPTIONS_BYTES) {
    throw invalidVariables('页面打印参数超过系统大小限制');
  }
  return { copies, copyBarcodes, pageVariables, pages };
}

function normalizeCopyBarcodes(value?: DmsPrintCopyBarcodes[]) {
  if (value === undefined) return undefined;
  if (!Array.isArray(value) || value.length < 1 || value.length > MAX_COPY_BARCODE_COPIES) {
    throw invalidVariables(`逐份条码配置数量必须为 1～${MAX_COPY_BARCODE_COPIES}`);
  }
  const normalized = value
    .map((item) => {
      if (!item || !Number.isInteger(item.copyNo) || item.copyNo < 1 || !Array.isArray(item.customBarcode) || item.customBarcode.length < 1) {
        throw invalidVariables('逐份条码配置无效');
      }
      return {
        copyNo: item.copyNo,
        customBarcode: item.customBarcode.map((barcode) => {
          if (
            !barcode ||
            (barcode.page !== undefined && barcode.page !== null && (!Number.isInteger(barcode.page) || barcode.page < 1)) ||
            typeof barcode.barValue !== 'string' ||
            !barcode.barValue.trim() ||
            typeof barcode.barText !== 'string' ||
            !barcode.barText.trim()
          ) {
            throw invalidVariables('逐份条码内容无效');
          }
          return { ...barcode };
        }),
      };
    })
    .sort((left, right) => left.copyNo - right.copyNo);
  normalized.forEach((item, index) => {
    if (item.copyNo !== index + 1) throw invalidVariables('打印份序必须从 1 开始连续且不可重复');
  });
  return normalized;
}

function pageVariablesMap(values: DmsPageVariables[]) {
  return new Map(values.map((item) => [item.pageNo, Object.freeze({ ...item.variables })]));
}

function printableHtml(pages: Array<{ dataUrl: string; height: number; width: number }>) {
  const body = pages
    .map((page) => `<section class="jnpf-dms-print-page" style="width:${page.width}pt;height:${page.height}pt"><img alt="" src="${page.dataUrl}" /></section>`)
    .join('');
  return `<style>
    .jnpf-dms-print-pages { margin: 0; padding: 0; }
    .jnpf-dms-print-page { box-sizing: border-box; margin: 0; overflow: hidden; break-after: page; page-break-after: always; }
    .jnpf-dms-print-page:last-child { break-after: auto; page-break-after: auto; }
    .jnpf-dms-print-page img { display: block; width: 100%; height: 100%; object-fit: fill; }
  </style><div class="jnpf-dms-print-pages">${body}</div>`;
}

export async function buildDmsPrintableHtml(pdf: ArrayBuffer | Blob, options: DmsPreviewPrintOptions, documentCopies = 1, sourcePageNumbers?: number[]) {
  const bytes = pdf instanceof Blob ? await pdf.arrayBuffer() : pdf;
  const task = getDocument({ data: new Uint8Array(bytes) });
  const pdfDocument = await task.promise;
  try {
    if (!Number.isInteger(documentCopies) || documentCopies < 1 || pdfDocument.numPages % documentCopies !== 0) {
      throw new DmsPrintError('PRINT_FAILED', '批量打印 PDF 页数与打印份数不匹配');
    }
    const sourcePageCount = pdfDocument.numPages / documentCopies;
    const serverFiltered = sourcePageNumbers !== undefined;
    const selectedPages =
      sourcePageNumbers ?? parseDmsPrintPages(options.pages, sourcePageCount) ?? Array.from({ length: sourcePageCount }, (_, index) => index + 1);
    if ((serverFiltered && selectedPages.length !== sourcePageCount) || selectedPages.some((pageNo) => !Number.isInteger(pageNo) || pageNo < 1)) {
      throw invalidPages('服务端打印页数与指定页码不一致');
    }
    const selectedPageSet = new Set(selectedPages);
    const variables = pageVariablesMap((options.pageVariables ?? []).filter((item) => selectedPageSet.has(item.pageNo)));

    const renderedPages: Array<{ dataUrl: string; height: number; width: number }> = [];
    for (let copyIndex = 0; copyIndex < documentCopies; copyIndex++) {
      for (const [pageIndex, selectedPage] of selectedPages.entries()) {
        const pageNo = selectedPage!;
        const page = await pdfDocument.getPage(copyIndex * sourcePageCount + (serverFiltered ? pageIndex + 1 : pageNo));
        const outputScale = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
        const viewport = page.getViewport({ scale: outputScale });
        const canvas = document.createElement('canvas');
        canvas.width = Math.ceil(viewport.width);
        canvas.height = Math.ceil(viewport.height);
        const context = canvas.getContext('2d', { alpha: false });
        if (!context) throw new DmsPrintError('PRINT_FAILED', '无法生成打印页面');
        await page.render({ canvasContext: context, viewport }).promise;
        const pageContext: DmsPrintPageContext = { canvas, pageNo, variables: variables.get(pageNo) ?? Object.freeze({}) };
        for (const handler of pageVariableHandlers) await handler(pageContext);
        renderedPages.push({
          dataUrl: canvas.toDataURL('image/png'),
          height: viewport.height / outputScale,
          width: viewport.width / outputScale,
        });
        page.cleanup();
      }
    }
    return printableHtml(renderedPages);
  } finally {
    await pdfDocument.destroy();
  }
}

interface DmsPrintBatch {
  items: DmsPrintRenderItem[];
  pageNumbers: number[];
}

function formatPageNumbers(pages: number[]) {
  const ranges: string[] = [];
  for (let index = 0; index < pages.length; index++) {
    const start = pages[index]!;
    let end = start;
    while (index + 1 < pages.length && pages[index + 1] === end + 1) end = pages[++index]!;
    ranges.push(start === end ? String(start) : `${start}-${end}`);
  }
  return ranges.join(',');
}

function withBatchPages(items: DmsPrintRenderItem[], pageNumbers: number[]) {
  const printPages = formatPageNumbers(pageNumbers);
  return items.map((item) => ({ ...item, printPages }));
}

function createPrintBatches(items: DmsPrintRenderItem[], pageNumbers: number[], copySpecific: boolean): DmsPrintBatch[] {
  const pageChunks: number[][] = [];
  for (let index = 0; index < pageNumbers.length; index += MAX_BATCH_PAGES) {
    pageChunks.push(pageNumbers.slice(index, index + MAX_BATCH_PAGES));
  }
  if (!copySpecific) {
    return pageChunks.map((pages) => ({ items: withBatchPages(items, pages), pageNumbers: pages }));
  }
  if (pageChunks.length > 1) {
    return items.flatMap((item) => pageChunks.map((pages) => ({ items: withBatchPages([item], pages), pageNumbers: pages })));
  }
  const pages = pageChunks[0]!;
  const batchCopies = Math.max(1, Math.min(MAX_BATCH_COPIES, Math.floor(MAX_BATCH_PAGES / pages.length)));
  const batches: DmsPrintBatch[] = [];
  for (let index = 0; index < items.length; index += batchCopies) {
    batches.push({ items: withBatchPages(items.slice(index, index + batchCopies), pages), pageNumbers: pages });
  }
  return batches;
}

function splitPrintBatch(batch: DmsPrintBatch): DmsPrintBatch[] | undefined {
  if (batch.items.length > 1) {
    const middle = Math.ceil(batch.items.length / 2);
    return [
      { items: batch.items.slice(0, middle), pageNumbers: batch.pageNumbers },
      { items: batch.items.slice(middle), pageNumbers: batch.pageNumbers },
    ];
  }
  if (batch.pageNumbers.length > 1) {
    const middle = Math.ceil(batch.pageNumbers.length / 2);
    const left = batch.pageNumbers.slice(0, middle);
    const right = batch.pageNumbers.slice(middle);
    return [
      { items: withBatchPages(batch.items, left), pageNumbers: left },
      { items: withBatchPages(batch.items, right), pageNumbers: right },
    ];
  }
}

async function isBatchPdfSizeError(error: any) {
  const responseData = error?.response?.data;
  let responseText = '';
  if (responseData instanceof Blob) responseText = await responseData.text();
  else if (typeof responseData === 'string') responseText = responseData;
  else if (responseData) responseText = JSON.stringify(responseData);
  return `${error?.message ?? ''} ${responseText}`.includes('打印 PDF 超过系统大小限制');
}

async function dispatchDmsPrintBatch(config: HiprintPrintConfig, onSubmitted?: () => Promise<unknown>) {
  let receiptSeen = false;
  let settled = false;
  let resolveConfirmation!: (result: HiprintPrintResult) => void;
  let rejectConfirmation!: (error: unknown) => void;
  const confirmation = new Promise<HiprintPrintResult>((resolve, reject) => {
    resolveConfirmation = resolve;
    rejectConfirmation = reject;
  });
  const timeoutId = window.setTimeout(() => {
    if (settled) return;
    settled = true;
    rejectConfirmation(new DmsPrintError('PRINT_FAILED', '打印客户端确认任务超时，发送结果未知，请核对客户端任务和实际输出，勿直接重复打印'));
  }, PRINT_CONFIRM_TIMEOUT_MILLIS);
  const resolveOnce = (result: HiprintPrintResult) => {
    if (receiptSeen) return;
    receiptSeen = true;
    window.clearTimeout(timeoutId);
    // 计时器只结束前端等待，不抛弃成功回执；持久化失败交由 outbox 恢复。
    void (async () => {
      try {
        await onSubmitted?.();
        if (!settled) {
          settled = true;
          resolveConfirmation(result);
        }
      } catch (error) {
        if (!settled) {
          settled = true;
          rejectConfirmation(error);
        }
      }
    })();
  };
  const rejectOnce = (error: unknown) => {
    if (settled) return;
    settled = true;
    window.clearTimeout(timeoutId);
    rejectConfirmation(error || new DmsPrintError('PRINT_FAILED', '打印客户端发送任务失败'));
  };

  try {
    const dispatched = await onlineUtils.hiprint({
      ...config,
      errorMessageMode: 'none',
      onError: rejectOnce,
      onPrint: resolveOnce,
    });
    if (!dispatched) rejectOnce(new DmsPrintError('PRINT_FAILED', '打印任务派发失败'));
  } catch (error) {
    rejectOnce(error);
  }
  return confirmation;
}

export async function printDmsDocument(fileName: string, printConfig: DmsPrintConfig, shouldContinue: () => boolean = () => true) {
  const normalized = normalizeDmsPrintOptions(printConfig);
  if (!shouldContinue()) return;
  const pageCountResponse = await getDmsFilePageCount(printConfig.fileVersionId);
  if (!shouldContinue()) return;
  const pageCount = pageCountResponse.data?.pageCount;
  if (!Number.isInteger(pageCount) || pageCount < 1) throw new DmsPrintError('PRINT_FAILED', '后端返回的文件页数无效');
  const selectedPages = parseDmsPrintPages(normalized.pages, pageCount) ?? Array.from({ length: pageCount }, (_, index) => index + 1);
  if (selectedPages.length === 0) return;
  const authorizedPages = formatPageNumbers(selectedPages);
  const copyParameters = normalized.copyBarcodes?.map((item) => ({
    ...watermarkParameters(printConfig),
    customBarcode: item.customBarcode,
  })) ?? [watermarkParameters(printConfig)];
  const renderItems: DmsPrintRenderItem[] = [];
  for (const parameters of copyParameters) {
    const ticketResponse = await createDmsFilePrintTicket({
      ...parameters,
      copies: normalized.copyBarcodes ? 1 : normalized.copies,
      operationId: uuidv4(),
      printPages: authorizedPages,
      resourceId: printConfig.fileVersionId,
      resourceType: 'FILE_VERSION',
    });
    if (!shouldContinue()) return;
    const ticket = ticketResponse.data;
    if (!ticket?.accessTicket || !ticket.fileVersionId || !ticket.fileName) throw new DmsPrintError('PRINT_FAILED', '后端返回的 DMS 打印票据不完整');
    if (ticket.fileVersionId !== printConfig.fileVersionId) throw new DmsPrintError('PRINT_FAILED', '打印文件版本与预览版本不一致');
    renderItems.push({ accessTicket: ticket.accessTicket, fileName: ticket.fileName, fileVersionId: ticket.fileVersionId });
  }

  const source = renderItems[0]!;
  const batches = createPrintBatches(renderItems, selectedPages, Boolean(normalized.copyBarcodes));
  let lastResult;
  for (let batchIndex = 0; batchIndex < batches.length; ) {
    const batch = batches[batchIndex]!;
    let pdf: Blob;
    try {
      pdf = await renderDmsPrintPdf({ items: batch.items, source });
    } catch (error) {
      const split = (await isBatchPdfSizeError(error)) && splitPrintBatch(batch);
      if (!split) throw error;
      batches.splice(batchIndex, 1, ...split);
      continue;
    }
    if (!shouldContinue()) return;
    const html = await buildDmsPrintableHtml(pdf, { ...normalized, pages: undefined }, normalized.copyBarcodes ? batch.items.length : 1, batch.pageNumbers);
    if (!shouldContinue()) return;
    lastResult = await dispatchDmsPrintBatch({
      clientOptions: {
        copies: normalized.copyBarcodes ? 1 : normalized.copies,
        title: batches.length > 1 ? `${fileName} (第 ${batchIndex + 1} 批)` : fileName,
      },
      html,
    });
    batchIndex++;
    // 已收到最后一批成功回执后必须完成业务确认，不能因预览关闭而丢弃记账。
    if (batchIndex < batches.length && !shouldContinue()) return;
  }
  await printConfig.onPrintSuccess?.();
  return lastResult;
}

export interface DmsManagedPrintTransport {
  recover: typeof recoverDmsBatchReceipts;
  update: typeof updateDmsPrintAttempt;
  submitted: typeof recordDmsBatchSubmitted;
  allowHistoricalSerial?: boolean;
}

/** 台账专用发送：所有恢复均读取服务端计划，SENDING 永不自动重发。 */
export async function printManagedDmsDocument(fileName: string, config: DmsPrintConfig, original: DmsPrintAttempt, transport: DmsManagedPrintTransport = {
  recover: recoverDmsBatchReceipts, update: updateDmsPrintAttempt, submitted: recordDmsBatchSubmitted,
}) {
  if (config.copyBarcodes || config.pages) throw new Error('台账打印必须发送完整文档，不能覆盖页段或逐份配置');
  const normalized = normalizeDmsPrintOptions({ ...config, copies: original.copies });
  let attempt = await transport.recover(original);
  if (attempt.state !== 'OPEN') return;
  const numbers: Array<{ serialNumber: string; countSerialNumber: string }> = JSON.parse(attempt.numbersJson || '[]');
  if (numbers.length !== attempt.copies || numbers.some((n) => !n.serialNumber || (!transport.allowHistoricalSerial && !/^\d{8}$/.test(n.countSerialNumber)))) {
    throw new Error('打印号码未完整冻结，请核对原发送任务，不能重新发送');
  }
  let batches = attemptBatches(attempt);
  if (batches.some((batch) => batch.state === 'SENDING')) throw new Error('打印发送结果未知，请核对原客户端任务并等待回传；系统已阻止重复发送');
  if (!batches.length) {
    const response = await getDmsFilePageCount(config.fileVersionId);
    const pageCount = response.data?.pageCount;
    if (!Number.isInteger(pageCount) || pageCount < 1 || pageCount > MAX_SELECTED_PAGES) throw new Error('后端返回的文件页数无效');
    const plan: DmsAttemptBatch[] = [];
    for (let copy = 0; copy < attempt.copies; copy++) {
      for (let page = 1; page <= pageCount; page += 100) {
        plan.push({ id: uuidv4(), pages: Array.from({ length: Math.min(100, pageCount - page + 1) }, (_, i) => page + i), items: [copy], state: 'READY' });
      }
    }
    attempt = await transport.update(attempt, 'fasong-jihua', { batches: plan });
    batches = attemptBatches(attempt);
  }
  // 所有待发送份的格式先校验；每份只读取一次，分页/拆批复用该结果。
  const barcodes = new Map<number, Awaited<ReturnType<typeof buildDmsPrintBarcode>>>();
  for (const batch of batches) {
    if (batch.state === 'SUBMITTED') continue;
    if (batch.state !== 'READY') throw new Error('打印发送结果未知，禁止重复发送');
    const copy = batch.items[0]!;
    const number = numbers[copy];
    if (!number || batch.items.length !== 1) throw new Error('批次与逐份号码不匹配');
    if (!barcodes.has(copy)) barcodes.set(copy, await buildDmsPrintBarcode(number, config.buildCustomBarcode));
  }
  for (let index = 0; index < batches.length; ) {
    const batch = batches[index]!;
    if (batch.state === 'SUBMITTED') {
      index++;
      continue;
    }
    if (batch.state !== 'READY') throw new Error('打印发送结果未知，禁止重复发送');
    const number = numbers[batch.items[0]!];
    if (!number || batch.items.length !== 1) throw new Error('批次与逐份号码不匹配');
    const ticketResponse = await createDmsFilePrintTicket({
      requireSerialWatermark: true,
      ...watermarkParameters(config),
      customBarcode: barcodes.get(batch.items[0]!)!,
      copies: 1,
      operationId: batch.id,
      printPages: formatPageNumbers(batch.pages),
      resourceId: config.fileVersionId,
      resourceType: 'FILE_VERSION',
    });
    const ticket = ticketResponse.data;
    if (!ticket?.accessTicket || ticket.fileVersionId !== attempt.fileVersionId || !ticket.fileName) throw new Error('打印票据版本或内容无效');
    const item = {
      accessTicket: ticket.accessTicket,
      fileName: ticket.fileName,
      fileVersionId: ticket.fileVersionId,
      printPages: formatPageNumbers(batch.pages),
    };
    let pdf: Blob;
    try {
      pdf = await renderDmsPrintPdf({ items: [item], source: item });
    } catch (error) {
      if (!(await isBatchPdfSizeError(error)) || batch.pages.length < 2) throw error;
      const middle = Math.ceil(batch.pages.length / 2);
      const parts = [batch.pages.slice(0, middle), batch.pages.slice(middle)].map((pages) => ({ id: uuidv4(), pages, items: [...batch.items], state: 'READY' }));
      attempt = await transport.update(attempt, 'chaifen-pici', { batchId: batch.id, batches: parts });
      batches = attemptBatches(attempt);
      continue;
    }
    const html = await buildDmsPrintableHtml(pdf, { ...normalized, pages: undefined }, 1, batch.pages);
    if (!(await onlineUtils.isHiprintClientReady())) throw new Error('打印客户端未连接，请启动客户端后恢复原任务');
    const token = uuidv4();
    // 只有成功取得一次派发许可才调用客户端；许可响应丢失按未知处理。
    await transport.update(attempt, 'kaishi-pici', { batchId: batch.id, token });
    await dispatchDmsPrintBatch(
      {
        html,
        clientOptions: { copies: 1, title: `${fileName} [${attempt.id}/${batch.id}]` },
      },
      async () => {
        attempt = await transport.submitted(attempt, batch, token);
      },
    );
    batch.state = 'SUBMITTED';
    index++;
  }
}
