'use strict';

(() => {
  const TEST_VERSION = 'ozon-upload-test-temu-auth-removed-20260720-62';
  const OFFICIAL_CURRENCIES = new Set(['RUB', 'BYN', 'KZT', 'EUR', 'USD', 'CNY']);
  const state = {
    initialized: false,
    accountId: '',
    templates: [],
    warehouses: [],
    warehouseAccountId: '',
    excelFile: null,
    assetFiles: [],
    rows: [],
    previewUrls: [],
    translating: false,
    uploading: false,
    batchId: '',
    taskId: ''
  };

  function text(value) {
    return String(value ?? '').trim();
  }

  function html(value) {
    return String(value ?? '')
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }

  function clearPreviewUrls() {
    state.previewUrls.forEach(url => {
      try { URL.revokeObjectURL(url); } catch (_) {}
    });
    state.previewUrls = [];
  }

  function createPreviewUrl(file) {
    if (!file || typeof URL?.createObjectURL !== 'function') return '';
    const url = URL.createObjectURL(file);
    state.previewUrls.push(url);
    return url;
  }

  function revokePreviewUrl(url = '') {
    if (!url) return;
    try { URL.revokeObjectURL(url); } catch (_) {}
    state.previewUrls = state.previewUrls.filter(item => item !== url);
  }

  function setSummary(message = '', tone = '') {
    const target = document.getElementById('ozonUploadTestSummary');
    if (!target) return;
    target.textContent = message;
    target.className = `ozon-upload-test-summary${tone ? ` is-${tone}` : ''}`;
  }

  function getSelectedAccountId() {
    return text(document.getElementById('ozonUploadTestAccountFilter')?.value || state.accountId || getDefaultOzonAccountId());
  }

  function getSelectedTemplateId() {
    return text(document.getElementById('ozonUploadTestTemplateSelect')?.value || '');
  }

  function getAccountCurrencyCode() {
    const account = getOzonAccountById(getSelectedAccountId()) || {};
    return text(account.currencyCode || account.currency_code || '').toUpperCase();
  }

  function getSelectedRowIndexes() {
    return state.rows.map((row, index) => row.selected ? index : -1).filter(index => index >= 0);
  }

  function renderSelectors() {
    const accounts = getAuthorizedOzonAccounts();
    const accountSelect = document.getElementById('ozonUploadTestAccountFilter');
    const templateSelect = document.getElementById('ozonUploadTestTemplateSelect');
    if (accountSelect) {
      const current = state.accountId || getDefaultOzonAccountId();
      accountSelect.innerHTML = accounts.map(account => `<option value="${html(account.id)}">${html(account.title || account.clientId || `店铺 ${account.id}`)}</option>`).join('') || '<option value="">暂无已授权店铺</option>';
      if (accounts.some(account => String(account.id) === String(current))) accountSelect.value = String(current);
      state.accountId = accountSelect.value || '';
    }
    if (templateSelect) {
      const current = templateSelect.value;
      templateSelect.innerHTML = '<option value="">请选择模板</option>' + state.templates.map(template => `<option value="${html(template.id)}">${html(template.templateName || template.sourceName || '未命名模板')} · Product ID ${html(template.sourceProductId || '')}</option>`).join('');
      if (state.templates.some(template => String(template.id) === String(current))) templateSelect.value = current;
    }
    renderTemplateList();
  }

  function renderTemplateList() {
    const target = document.getElementById('ozonUploadTestTemplateList');
    if (!target) return;
    if (!state.templates.length) {
      target.innerHTML = '<div class="ozon-empty">当前店铺没有可用模板，请先在正式“批量上架”TAB保存模板。</div>';
      return;
    }
    target.innerHTML = `<table class="ozon-table ozon-upload-test-template-table"><thead><tr><th>模板名称</th><th>来源商品</th><th>Product ID</th><th>类目 / 类型</th><th>属性</th><th>保存时间</th><th>操作</th></tr></thead><tbody>${state.templates.map(template => {
      const category = [template.categoryFullName, template.typeName].filter(Boolean).filter((value, index, values) => values.indexOf(value) === index).join(' / ') || '官方未返回类目名称';
      const savedAt = text(template.updatedAt || template.updated_at || template.createdAt || template.created_at || template.savedAt || template.saved_at);
      return `<tr><td><b>${html(template.templateName || '未命名模板')}</b></td><td>${html(template.sourceName || '--')}</td><td class="ozon-mono">${html(template.sourceProductId || '--')}</td><td>${html(category)}</td><td>${Number(template.attributeCount || 0)} 项</td><td>${html(savedAt ? formatOzonDateTime(savedAt) : '--')}</td><td><button class="ozon-upload-test-delete-template" type="button" data-upload-test-delete-template="${html(template.id)}" title="删除模板" aria-label="删除模板">×</button></td></tr>`;
    }).join('')}</tbody></table>`;
    target.querySelectorAll('[data-upload-test-delete-template]').forEach(button => {
      button.addEventListener('click', async () => {
        const templateId = text(button.dataset.uploadTestDeleteTemplate);
        const template = state.templates.find(item => String(item.id) === templateId);
        if (!templateId || !template || !window.confirm(`确定删除模板“${template.templateName || '未命名模板'}”吗？不会删除 OZON 已上架商品。`)) return;
        button.disabled = true;
        try {
          await ozonRequest(`/upload-templates/${encodeURIComponent(templateId)}`, { method: 'DELETE' });
          if (getSelectedTemplateId() === templateId) resetPrecheck('当前模板已删除，请重新选择模板。');
          await loadTemplates();
          setSummary('模板已删除。', 'success');
        } catch (error) {
          setSummary(`删除模板失败：${error?.message || String(error)}`, 'error');
          button.disabled = false;
        }
      });
    });
  }

  async function loadTemplates() {
    const accountId = getSelectedAccountId();
    state.accountId = accountId;
    if (!accountId) {
      state.templates = [];
      renderSelectors();
      return;
    }
    const result = await ozonRequest(`/upload-templates?ozonAccountId=${encodeURIComponent(accountId)}`, { cache: false, forceRefresh: true });
    state.templates = Array.isArray(result.templates) ? result.templates : [];
    renderSelectors();
  }

  async function loadWarehouses() {
    const accountId = getSelectedAccountId();
    if (!accountId) throw new Error('请先选择 OZON 店铺');
    if (state.warehouseAccountId === accountId && state.warehouses.length) return state.warehouses;
    const result = await ozonRequest(`/products/warehouses?ozonAccountId=${encodeURIComponent(accountId)}`, { cache: false, forceRefresh: true });
    const warehouses = (Array.isArray(result.warehouses) ? result.warehouses : []).filter(warehouse => {
      const warehouseId = Number(warehouse?.warehouseId);
      return Number.isInteger(warehouseId) && warehouseId > 0;
    });
    if (!warehouses.length) throw new Error('OZON 官方未返回当前店铺可用仓库');
    state.warehouses = warehouses;
    state.warehouseAccountId = accountId;
    return warehouses;
  }

  function fileItem(file, relativePath = '') {
    return { file, relativePath: String(relativePath || file?.webkitRelativePath || file?.name || '').replace(/\\/g, '/') };
  }

  function itemFile(item) {
    return item?.file && typeof item.file.arrayBuffer === 'function' ? item.file : item;
  }

  function itemPath(item) {
    const file = itemFile(item);
    return String(item?.relativePath || file?.webkitRelativePath || file?.name || '').replace(/\\/g, '/');
  }

  function findZipEnd(bytes) {
    const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
    for (let offset = bytes.length - 22, min = Math.max(0, bytes.length - 66000); offset >= min; offset -= 1) {
      if (view.getUint32(offset, true) === 0x06054b50) return offset;
    }
    return -1;
  }

  function readZipName(bytes, start, length, utf8) {
    const raw = bytes.slice(start, start + length);
    return new TextDecoder(utf8 ? 'utf-8' : undefined).decode(raw).replace(/\\/g, '/');
  }

  async function inflateZipEntry(data) {
    if (typeof DecompressionStream !== 'function') throw new Error('当前 Chrome 不支持解压 ZIP，请直接选择素材总文件夹');
    const stream = new Blob([data]).stream().pipeThrough(new DecompressionStream('deflate-raw'));
    return new Uint8Array(await new Response(stream).arrayBuffer());
  }

  function blobFile(bytes, name, type) {
    try {
      return new File([bytes], name, { type });
    } catch {
      const blob = new Blob([bytes], { type });
      Object.defineProperty(blob, 'name', { value: name });
      return blob;
    }
  }

  async function readAssetZip(zipFile) {
    const bytes = new Uint8Array(await zipFile.arrayBuffer());
    const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
    const end = findZipEnd(bytes);
    if (end < 0) throw new Error('素材 ZIP 结构不正确');
    const total = view.getUint16(end + 10, true);
    let offset = view.getUint32(end + 16, true);
    const files = [];
    for (let index = 0; index < total; index += 1) {
      if (view.getUint32(offset, true) !== 0x02014b50) throw new Error('素材 ZIP 中央目录不正确');
      const flags = view.getUint16(offset + 8, true);
      const method = view.getUint16(offset + 10, true);
      const compressedSize = view.getUint32(offset + 20, true);
      const nameLength = view.getUint16(offset + 28, true);
      const extraLength = view.getUint16(offset + 30, true);
      const commentLength = view.getUint16(offset + 32, true);
      const localOffset = view.getUint32(offset + 42, true);
      const relativePath = readZipName(bytes, offset + 46, nameLength, Boolean(flags & 0x0800));
      offset += 46 + nameLength + extraLength + commentLength;
      if (!relativePath || relativePath.endsWith('/') || relativePath.includes('__MACOSX/')) continue;
      if (!/(?:^|\/)(?:source\.json|\d+\.(?:jpe?g|png))$/i.test(relativePath)) continue;
      if (flags & 1) throw new Error(`素材 ZIP 条目已加密，无法读取：${relativePath}`);
      if (view.getUint32(localOffset, true) !== 0x04034b50) throw new Error(`素材 ZIP 条目损坏：${relativePath}`);
      const localNameLength = view.getUint16(localOffset + 26, true);
      const localExtraLength = view.getUint16(localOffset + 28, true);
      const dataStart = localOffset + 30 + localNameLength + localExtraLength;
      const compressed = bytes.slice(dataStart, dataStart + compressedSize);
      let content;
      if (method === 0) content = compressed;
      else if (method === 8) content = await inflateZipEntry(compressed);
      else throw new Error(`素材 ZIP 使用了不支持的压缩方式：${relativePath}`);
      const name = relativePath.split('/').pop() || 'file';
      const type = /source\.json$/i.test(name) ? 'application/json' : /\.png$/i.test(name) ? 'image/png' : 'image/jpeg';
      files.push(fileItem(blobFile(content, name, type), relativePath));
    }
    return files;
  }

  function getParentFolder(pathValue = '') {
    const parts = String(pathValue || '').split('/').filter(Boolean);
    return parts.length >= 2 ? text(parts[parts.length - 2]) : '';
  }

  async function parseSourcePackages(files = []) {
    const sources = new Map();
    const errors = new Map();
    for (const item of files) {
      const pathValue = itemPath(item);
      if (!/(?:^|\/)source\.json$/i.test(pathValue)) continue;
      const number = getParentFolder(pathValue);
      if (!number) continue;
      try {
        const file = itemFile(item);
        const parsed = JSON.parse(await file.text());
        const rowErrors = [];
        if (Number(parsed?.schemaVersion) !== 1) rowErrors.push('source.json schemaVersion 必须为 1');
        if (text(parsed?.sourcePlatform).toUpperCase() !== 'TEMU') rowErrors.push('source.json 来源平台不是 TEMU');
        if (!text(parsed?.sourceTitle)) rowErrors.push('source.json 缺少真实采集标题');
        if (!text(parsed?.sourceUrl)) rowErrors.push('source.json 缺少真实来源链接');
        else {
          try {
            const sourceUrl = new URL(parsed.sourceUrl);
            if (sourceUrl.protocol !== 'https:' || !(sourceUrl.hostname === 'temu.com' || sourceUrl.hostname.endsWith('.temu.com'))) rowErrors.push('source.json 来源链接不是 TEMU HTTPS 地址');
          } catch { rowErrors.push('source.json 来源链接格式错误'); }
        }
        if (text(parsed?.folderName) && text(parsed.folderName) !== number) rowErrors.push('source.json folderName 与文件夹名称不一致');
        if (!Array.isArray(parsed?.images) || !parsed.images.length) rowErrors.push('source.json 缺少图片清单');
        else if (parsed.images.some(image => !Number.isInteger(Number(image?.order)) || !text(image?.fileName) || !text(image?.sourceUrl))) rowErrors.push('source.json 图片清单字段不完整');
        if (sources.has(number)) rowErrors.push('同一编号存在多个 source.json');
        sources.set(number, parsed);
        if (rowErrors.length) errors.set(number, rowErrors);
      } catch (error) {
        errors.set(number, [`source.json 无法解析：${error?.message || String(error)}`]);
      }
    }
    return { sources, errors };
  }

  function groupImages(files = []) {
    const groups = new Map();
    files.forEach(item => {
      const file = itemFile(item);
      const pathValue = itemPath(item);
      const number = getParentFolder(pathValue);
      const name = pathValue.split('/').pop() || '';
      const match = name.match(/^(\d+)\.(jpg|jpeg|png)$/i);
      if (!number || !match) return;
      if (!groups.has(number)) groups.set(number, []);
      groups.get(number).push({ file, order: Number(match[1]), name });
    });
    groups.forEach(images => images.sort((a, b) => a.order - b.order || a.name.localeCompare(b.name)));
    return groups;
  }

  function rebuildRowImagePreviews(row) {
    if (!row) return;
    (Array.isArray(row.imagePreviews) ? row.imagePreviews : []).forEach(image => revokePreviewUrl(image.url));
    row.images = (Array.isArray(row.images) ? row.images : [])
      .filter(image => itemFile(image))
      .map((image, index) => ({
        ...image,
        file: itemFile(image),
        order: index + 1,
        name: text(image.name || itemFile(image)?.name || `${index + 1}.jpg`)
      }));
    row.imagePreviews = row.images.slice(0, 8).map(image => ({
      order: image.order,
      name: image.name,
      url: createPreviewUrl(image.file)
    }));
    row.uploadTotalUnits = row.images.length + 3;
  }

  function markRowImagesEdited(row) {
    if (!row) return;
    row.imagesEdited = true;
    row.imageUrls = [];
    if (row.uploadStatus && row.uploadStatus !== 'idle') {
      row.uploadStatus = 'idle';
      row.uploadCompletedUnits = 0;
      row.uploadMessage = '';
      row.uploadError = '';
      row.productId = '';
    }
  }

  function updateRowAfterImageChange(index, message = '') {
    const row = state.rows[index];
    if (!row) return;
    rebuildRowImagePreviews(row);
    revalidateBaseErrors();
    const element = document.querySelector(`[data-upload-test-row="${index}"]`);
    const imageCell = element?.querySelector('[data-upload-test-images-cell]');
    if (imageCell) imageCell.innerHTML = renderImages(row, index);
    updateRowCells(index);
    if (message) setSummary(message, liveRowErrors(row).length ? 'error' : 'success');
  }

  function removeRowImage(index, imageIndex) {
    if (state.uploading) return;
    const row = state.rows[index];
    if (!row || !Array.isArray(row.images)) return;
    const targetIndex = Number(imageIndex);
    if (!Number.isInteger(targetIndex) || targetIndex < 0 || targetIndex >= row.images.length) return;
    row.images.splice(targetIndex, 1);
    markRowImagesEdited(row);
    updateRowAfterImageChange(index, `已删除编号 ${row.number || index + 1} 的第 ${targetIndex + 1} 张图片。`);
  }

  function removeRow(index) {
    if (state.uploading) return;
    const rowIndex = Number(index);
    if (!Number.isInteger(rowIndex) || rowIndex < 0 || rowIndex >= state.rows.length) return;
    const [removed] = state.rows.splice(rowIndex, 1);
    (Array.isArray(removed?.imagePreviews) ? removed.imagePreviews : []).forEach(image => revokePreviewUrl(image.url));
    revalidateBaseErrors();
    renderRows();
    const label = removed?.number || removed?.offerId || rowIndex + 1;
    setSummary(`已从本次预检列表删除编号 ${label}，不会上传该行。`, 'success');
  }

  function addRowImages(index, files = []) {
    if (state.uploading) return;
    const row = state.rows[index];
    if (!row) return;
    const imageFiles = Array.from(files || []).filter(file => file && /^image\//i.test(file.type || ''));
    if (!imageFiles.length) {
      setSummary('请选择真实图片文件。', 'error');
      return;
    }
    row.images = Array.isArray(row.images) ? row.images : [];
    imageFiles.forEach(file => {
      row.images.push({ file, order: row.images.length + 1, name: file.name || `${row.images.length + 1}.jpg` });
    });
    markRowImagesEdited(row);
    updateRowAfterImageChange(index, `已为编号 ${row.number || index + 1} 添加 ${imageFiles.length} 张图片。`);
  }

  function staticRowErrors(row, seenNumbers, seenOffers) {
    const errors = [];
    if (!row.number) errors.push('缺少编号');
    seenNumbers.add(row.number);
    if (!row.source) errors.push('未找到对应 source.json');
    if (!row.sourceTitle) errors.push('缺少采集标题');
    if (!row.uploadTitle) errors.push('Excel 缺少上传标题');
    if (!/^\d+(?:\.\d{1,2})?$/.test(row.price) || Number(row.price) <= 0) errors.push('价格必须大于 0，最多两位小数');
    if (!row.currencyCode) errors.push('Excel 和店铺均未提供币种');
    else if (!OFFICIAL_CURRENCIES.has(row.currencyCode)) errors.push('币种只允许 RUB、BYN、KZT、EUR、USD、CNY');
    if (!row.offerId) errors.push('缺少货号');
    else if (row.offerId.length > 50) errors.push('货号超过 50 个字符');
    seenOffers.add(row.offerId);
    if (!row.images.length) errors.push('未找到对应图片');
    if (row.images.length > 15) errors.push('图片超过 OZON 官方上限 15 张');
    if (row.images.length && (row.images[0].order !== 1 || row.images.some((image, index) => image.order !== index + 1))) errors.push('图片编号必须从 1 连续递增');
    const sourceErrors = row.imagesEdited
      ? (row.sourceErrors || []).filter(error => !/图片清单|image/i.test(String(error || '')))
      : (row.sourceErrors || []);
    errors.push(...sourceErrors);
    return Array.from(new Set(errors));
  }

  function revalidateBaseErrors() {
    const seenNumbers = new Set();
    const seenOffers = new Set();
    const accountCurrency = getAccountCurrencyCode();
    state.rows.forEach(row => {
      row.baseErrors = staticRowErrors(row, seenNumbers, seenOffers);
      if (row.enteredCurrencyCode && accountCurrency && row.enteredCurrencyCode !== accountCurrency) {
        row.baseErrors.push(`币种必须与当前店铺 ${accountCurrency} 一致`);
      }
    });
  }

  function liveRowErrors(row) {
    const errors = [...row.baseErrors];
    if (row.selected && row.number && state.rows.filter(item => item.selected && item.number === row.number).length > 1) errors.push('编号重复');
    if (row.selected && row.offerId && state.rows.filter(item => item.selected && item.offerId === row.offerId).length > 1) errors.push('货号重复');
    if (row.translationStatus === 'loading') errors.push('正在翻译');
    else if (row.translationError) errors.push(`翻译失败：${row.translationError}`);
    else if (!row.translatedTitle) errors.push('缺少俄语翻译');
    else if (row.translationStale) errors.push('采集标题已修改，请重新翻译');
    if (!row.mergedTitle) errors.push('请生成合并标题');
    else if (row.mergedTitle.length > 500) errors.push('合并标题超过 500 个字符');
    else if (row.mergeStale) errors.push('标题内容已变化，请重新合并');
    const warehouseId = Number(row.warehouseId || 0);
    if (!Number.isInteger(warehouseId) || warehouseId <= 0 || !state.warehouses.some(warehouse => Number(warehouse.warehouseId) === warehouseId)) {
      errors.push('请选择当前店铺的 OZON 仓库');
    }
    if (!/^\d+$/.test(text(row.stock))) errors.push('库存必须是非负整数');
    return Array.from(new Set(errors));
  }

  function renderImages(row, rowIndex = -1) {
    const previews = Array.isArray(row.imagePreviews) ? row.imagePreviews : [];
    const images = Array.isArray(row.images) ? row.images : [];
    const more = Math.max(0, images.length - previews.length);
    const disabled = state.uploading ? ' disabled' : '';
    const previewHtml = previews.length
      ? previews.map((image, imageIndex) => `<div class="ozon-upload-test-image-item">
          <button class="ozon-upload-test-image-thumb" type="button" data-upload-test-preview-image="${html(image.url)}" data-upload-test-preview-title="编号 ${html(row.number || rowIndex + 1)} 第 ${html(image.order)} 张：${html(image.name)}" title="预览第 ${html(image.order)} 张图片"><img src="${html(image.url)}" alt="第 ${html(image.order)} 张商品图片"></button>
          <button class="ozon-upload-test-image-remove" type="button" data-upload-test-remove-image="${html(imageIndex)}"${disabled} title="删除第 ${html(image.order)} 张图片">×</button>
          <span>${html(image.order)}</span>
        </div>`).join('')
      : '<span class="ozon-upload-preview-empty">无图片</span>';
    return `<div class="ozon-upload-preview-cell">
      <div class="ozon-upload-preview-list">${previewHtml}${more ? `<span class="ozon-upload-preview-more">+${more}</span>` : ''}</div>
      <div class="ozon-upload-test-image-actions">
        <small>共 ${images.length} 张${row.imagesEdited ? ' · 已手动调整' : ''}</small>
        <button class="ozon-upload-test-image-add" type="button" data-upload-test-add-image${disabled}>添加图片</button>
      </div>
    </div>`;
  }

  function getRowPreviewItems(row, rowIndex = -1) {
    const previews = Array.isArray(row?.imagePreviews) ? row.imagePreviews : [];
    return previews.map((image, imageIndex) => ({
      url: image.url,
      title: `编号 ${row?.number || rowIndex + 1} 第 ${image.order} 张：${image.name}`,
      index: imageIndex
    }));
  }

  function openRowImagePreview(rowIndex, imageIndex = 0) {
    const row = state.rows[rowIndex];
    if (!row) return;
    const items = getRowPreviewItems(row, rowIndex);
    const item = items[imageIndex] || items[0];
    if (!item) return;
    if (typeof window.openOzonImagePreview === 'function') {
      window.openOzonImagePreview(item.url, item.title, {
        items,
        index: imageIndex,
        canDelete: !state.uploading,
        deleteEvent: 'ozon-upload-test-delete-preview-image',
        deleteDetail: { rowIndex }
      });
    } else {
      window.open(item.url, '_blank', 'noopener');
    }
  }

  function renderStatus(row) {
    const errors = liveRowErrors(row);
    if (!errors.length) return '<span class="ozon-status-pill">通过</span>';
    const waiting = errors.every(error => /翻译|合并/.test(error));
    return `<span class="ozon-status-pill ${waiting ? 'is-waiting' : 'is-error'}">${html(errors.join('；'))}</span>`;
  }

  function progressPercent(row) {
    const total = Math.max(1, row.uploadTotalUnits || row.images.length + 3);
    return Math.max(0, Math.min(100, Math.floor((Number(row.uploadCompletedUnits || 0) / total) * 100)));
  }

  function renderUploadResult(row) {
    const percent = progressPercent(row);
    if (row.uploadStatus === 'success') return `<div class="ozon-upload-test-upload is-success"><b>100% 上传成功</b><small>Product ID：${html(row.productId || '--')}<br>库存 ${html(row.stock)} · ${html(getWarehouseName(row.warehouseId))}</small></div>`;
    if (row.uploadStatus === 'stock_waiting') return `<div class="ozon-upload-test-upload"><b>商品已上传，库存待写入</b><small>${html(row.uploadError || 'OZON 商品状态尚未达到 price_sent，库存接口未提交')}</small></div>`;
    if (row.uploadStatus === 'stock_failed') return `<div class="ozon-upload-test-upload is-error"><b>商品已上传，库存失败</b><small>${html(row.uploadError || 'OZON 未确认库存更新')}</small></div>`;
    if (row.uploadStatus === 'failed') return `<div class="ozon-upload-test-upload is-error"><b>上传失败</b><small>${html(row.uploadError || '未知错误')}</small></div>`;
    if (row.uploadStatus === 'idle') return '<span class="ozon-muted">等待上传</span>';
    return `<div class="ozon-upload-test-upload"><div><b>${percent}%</b><span>${html(row.uploadMessage || '处理中')}</span></div><i><em style="width:${percent}%"></em></i></div>`;
  }

  function getWarehouseName(warehouseId) {
    const warehouse = state.warehouses.find(item => Number(item.warehouseId) === Number(warehouseId));
    return warehouse?.name || (warehouseId ? `仓库 ${warehouseId}` : '未选择仓库');
  }

  function renderWarehouseStockCell(row) {
    const options = state.warehouses.map(warehouse => {
      const warehouseId = String(warehouse.warehouseId);
      const selected = warehouseId === String(row.warehouseId || '') ? ' selected' : '';
      const label = `${warehouse.name || `仓库 ${warehouseId}`}${warehouse.status ? ` (${warehouse.status})` : ''}`;
      return `<option value="${html(warehouseId)}"${selected}>${html(label)}</option>`;
    }).join('');
    return `<div class="ozon-upload-test-stock-cell"><select data-upload-test-field="warehouseId"><option value="">请选择仓库</option>${options}</select><input type="number" inputmode="numeric" min="0" step="1" data-upload-test-field="stock" value="${html(row.stock)}" placeholder="库存"></div>`;
  }

  function setBatchStockModalVisible(visible) {
    const modal = document.getElementById('ozonUploadTestBatchStockModal');
    if (!modal) return;
    modal.classList.toggle('is-open', Boolean(visible));
    modal.setAttribute('aria-hidden', visible ? 'false' : 'true');
  }

  function openBatchStockModal() {
    const indexes = getSelectedRowIndexes();
    if (!indexes.length) {
      setSummary('请先勾选要批量设置仓库和库存的商品。', 'error');
      return;
    }
    const warehouseSelect = document.getElementById('ozonUploadTestBatchWarehouse');
    const stockInput = document.getElementById('ozonUploadTestBatchStock');
    const result = document.getElementById('ozonUploadTestBatchStockResult');
    if (warehouseSelect) {
      warehouseSelect.innerHTML = '<option value="">请选择仓库</option>' + state.warehouses.map(warehouse => `<option value="${html(warehouse.warehouseId)}">${html(warehouse.name || `仓库 ${warehouse.warehouseId}`)}</option>`).join('');
    }
    if (stockInput) stockInput.value = '';
    if (result) result.textContent = `将应用到 ${indexes.length} 个已勾选商品`;
    setBatchStockModalVisible(true);
  }

  function applyBatchStock() {
    const indexes = getSelectedRowIndexes();
    const warehouseId = text(document.getElementById('ozonUploadTestBatchWarehouse')?.value);
    const stock = text(document.getElementById('ozonUploadTestBatchStock')?.value);
    const warehouseValid = state.warehouses.some(warehouse => String(warehouse.warehouseId) === warehouseId);
    const result = document.getElementById('ozonUploadTestBatchStockResult');
    if (!indexes.length) {
      if (result) result.textContent = '请先勾选商品';
      return;
    }
    if (!warehouseValid) {
      if (result) result.textContent = '请选择当前店铺的 OZON 仓库';
      return;
    }
    if (!/^\d+$/.test(stock)) {
      if (result) result.textContent = '库存必须是非负整数';
      return;
    }
    indexes.forEach(index => {
      const row = state.rows[index];
      row.warehouseId = warehouseId;
      row.stock = stock;
      const rowElement = document.querySelector(`[data-upload-test-row="${index}"]`);
      const warehouseControl = rowElement?.querySelector('[data-upload-test-field="warehouseId"]');
      const stockControl = rowElement?.querySelector('[data-upload-test-field="stock"]');
      if (warehouseControl) warehouseControl.value = warehouseId;
      if (stockControl) stockControl.value = stock;
      updateRowCells(index);
    });
    setBatchStockModalVisible(false);
    setSummary(`已为 ${indexes.length} 个勾选商品设置 ${getWarehouseName(warehouseId)}，库存 ${stock}。`, 'success');
  }

  function renderRows() {
    const target = document.getElementById('ozonUploadTestResult');
    if (!target) return;
    const previousScrollTop = target.scrollTop;
    const previousScrollLeft = target.scrollLeft;
    if (!state.rows.length) {
      target.innerHTML = '<div class="ozon-empty">选择 Excel 和采集素材文件夹后点击“预检数据”。</div>';
      return;
    }
    target.innerHTML = `<table class="ozon-table ozon-upload-test-table"><thead><tr><th class="ozon-upload-test-check-column"><input id="ozonUploadTestSelectAll" type="checkbox" aria-label="全选商品"></th><th>数据序号</th><th>编号</th><th>图片预览</th><th>采集标题</th><th><div class="ozon-upload-test-th-action"><span>翻译结果</span><button id="ozonUploadTestTranslateSelectedBtn" class="ozon-upload-test-head-btn${state.translating ? ' is-loading' : ''}" type="button">${state.translating ? '正在翻译...' : '批量翻译'}</button></div></th><th>上传标题</th><th><div class="ozon-upload-test-th-action"><span>合并标题</span><button id="ozonUploadTestMergeSelectedBtn" class="ozon-upload-test-head-btn" type="button">批量合并</button></div></th><th>价格</th><th>币种</th><th>货号</th><th title="对应 OZON 后台：型号名称（针对合并为一张商品卡片）。填写相同值会请求合并到同一商品卡。">型号名称<br><small>合并卡片</small></th><th><div class="ozon-upload-test-th-action"><span>仓库 / 库存</span><button id="ozonUploadTestBatchStockBtn" class="ozon-upload-test-head-btn" type="button">批量添加</button></div></th><th>结果</th><th>上传结果</th></tr></thead><tbody>${state.rows.map((row, index) => `<tr data-upload-test-row="${index}"><td class="ozon-upload-test-check-column"><input type="checkbox" data-upload-test-select ${row.selected ? 'checked' : ''} ${state.uploading ? 'disabled' : ''} aria-label="选择第 ${index + 1} 条商品"></td><td><div class="ozon-upload-test-row-index"><span>${index + 1}</span><button class="ozon-upload-test-delete-row" type="button" data-upload-test-delete-row ${state.uploading ? 'disabled' : ''} title="从本次预检列表删除该链接">删除</button></div></td><td class="ozon-mono">${html(row.number)}</td><td data-upload-test-images-cell>${renderImages(row, index)}</td><td><textarea class="ozon-upload-test-title" data-upload-test-field="sourceTitle">${html(row.sourceTitle)}</textarea><a class="ozon-upload-test-source-link" href="${html(row.sourceUrl)}" target="_blank" rel="noopener">打开原链接</a></td><td><textarea class="ozon-upload-test-title" data-upload-test-field="translatedTitle" ${row.translationStatus === 'loading' ? 'disabled' : ''}>${html(row.translatedTitle)}</textarea><button class="btn btn-default btn-sm ozon-upload-test-inline-btn${row.translationStatus === 'loading' ? ' is-loading' : ''}" type="button" data-upload-test-translate ${state.translating || state.uploading ? 'disabled' : ''}>${row.translationStatus === 'loading' ? '正在翻译...' : '翻译'}</button></td><td><textarea class="ozon-upload-test-title" data-upload-test-field="uploadTitle">${html(row.uploadTitle)}</textarea></td><td><textarea class="ozon-upload-test-title" data-upload-test-field="mergedTitle">${html(row.mergedTitle)}</textarea><button class="btn btn-default btn-sm ozon-upload-test-inline-btn" type="button" data-upload-test-merge>合并</button></td><td><input class="ozon-upload-test-small-input" data-upload-test-field="price" value="${html(row.price)}"></td><td><input class="ozon-upload-test-small-input" data-upload-test-field="currencyCode" value="${html(row.currencyCode)}" maxlength="3"></td><td><input class="ozon-upload-test-medium-input" data-upload-test-field="offerId" value="${html(row.offerId)}" maxlength="50"></td><td><input class="ozon-upload-test-medium-input" data-upload-test-field="mergeModel" value="${html(row.mergeModel)}" title="相同型号名称会请求合并为一张 OZON 商品卡"></td><td>${renderWarehouseStockCell(row)}</td><td data-upload-test-status>${renderStatus(row)}</td><td data-upload-test-upload-result>${renderUploadResult(row)}</td></tr>`).join('')}</tbody></table>`;
    bindRowEvents(target);
    target.scrollTop = previousScrollTop;
    target.scrollLeft = previousScrollLeft;
    updateActionState();
  }

  function updateRowCells(index) {
    const row = state.rows[index];
    const element = document.querySelector(`[data-upload-test-row="${index}"]`);
    if (!row || !element) return;
    const status = element.querySelector('[data-upload-test-status]');
    const upload = element.querySelector('[data-upload-test-upload-result]');
    if (status) status.innerHTML = renderStatus(row);
    if (upload) upload.innerHTML = renderUploadResult(row);
    updateActionState();
  }

  function bindRowEvents(target) {
    const selectAll = target.querySelector('#ozonUploadTestSelectAll');
    const selectedCount = getSelectedRowIndexes().length;
    if (selectAll) {
      selectAll.checked = Boolean(state.rows.length) && selectedCount === state.rows.length;
      selectAll.indeterminate = selectedCount > 0 && selectedCount < state.rows.length;
      selectAll.disabled = state.uploading;
      selectAll.addEventListener('change', () => {
        if (state.uploading) return;
        state.rows.forEach(row => { row.selected = selectAll.checked; });
        target.querySelectorAll('[data-upload-test-select]').forEach(control => { control.checked = selectAll.checked; });
        selectAll.indeterminate = false;
        updateAllUploadCells();
      });
    }
    target.querySelectorAll('[data-upload-test-select]').forEach(control => {
      control.addEventListener('change', () => {
        const index = Number(control.closest('[data-upload-test-row]')?.dataset.uploadTestRow);
        if (state.uploading) {
          control.checked = Boolean(state.rows[index]?.selected);
          return;
        }
        if (state.rows[index]) state.rows[index].selected = control.checked;
        const count = getSelectedRowIndexes().length;
        if (selectAll) {
          selectAll.checked = Boolean(state.rows.length) && count === state.rows.length;
          selectAll.indeterminate = count > 0 && count < state.rows.length;
        }
        updateAllUploadCells();
      });
    });
    target.querySelectorAll('[data-upload-test-delete-row]').forEach(button => {
      button.addEventListener('click', () => {
        const index = Number(button.closest('[data-upload-test-row]')?.dataset.uploadTestRow);
        removeRow(index);
      });
    });
    target.querySelector('#ozonUploadTestTranslateSelectedBtn')?.addEventListener('click', () => {
      const indexes = getSelectedRowIndexes();
      if (!indexes.length) return setSummary('请先勾选要翻译的商品。', 'error');
      translateRows(indexes)
        .then(count => setSummary(`已完成 ${Number(count || 0)} 个勾选商品的俄语翻译，请检查翻译结果。`, 'success'))
        .catch(error => setSummary(`批量翻译失败：${error?.message || String(error)}`, 'error'));
    });
    target.querySelector('#ozonUploadTestMergeSelectedBtn')?.addEventListener('click', mergeSelectedRows);
    target.querySelector('#ozonUploadTestBatchStockBtn')?.addEventListener('click', openBatchStockModal);
    target.querySelectorAll('[data-upload-test-preview-image]').forEach(button => {
      button.addEventListener('click', () => {
        const rowIndex = Number(button.closest('[data-upload-test-row]')?.dataset.uploadTestRow);
        const imageIndex = Number(button.closest('.ozon-upload-test-image-item')?.querySelector('[data-upload-test-remove-image]')?.dataset.uploadTestRemoveImage || 0);
        openRowImagePreview(rowIndex, imageIndex);
      });
    });
    target.querySelectorAll('[data-upload-test-remove-image]').forEach(button => {
      button.addEventListener('click', () => {
        const index = Number(button.closest('[data-upload-test-row]')?.dataset.uploadTestRow);
        removeRowImage(index, Number(button.dataset.uploadTestRemoveImage));
      });
    });
    target.querySelectorAll('[data-upload-test-add-image]').forEach(button => {
      button.addEventListener('click', () => {
        if (state.uploading) return;
        const index = Number(button.closest('[data-upload-test-row]')?.dataset.uploadTestRow);
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = 'image/png,image/jpeg,image/webp';
        input.multiple = true;
        input.addEventListener('change', () => addRowImages(index, input.files));
        input.click();
      });
    });
    target.querySelectorAll('[data-upload-test-field]').forEach(control => {
      const updateField = () => {
        const rowElement = control.closest('[data-upload-test-row]');
        const index = Number(rowElement?.dataset.uploadTestRow);
        const row = state.rows[index];
        const field = control.dataset.uploadTestField;
        if (!row || !field || state.uploading) return;
        row[field] = field === 'currencyCode' ? control.value.trim().toUpperCase() : control.value.trim();
        if (field === 'currencyCode') row.enteredCurrencyCode = row.currencyCode;
        if (field === 'sourceTitle') {
          row.translationStale = true;
          row.mergeStale = true;
        } else if (field === 'translatedTitle' || field === 'uploadTitle') {
          if (field === 'translatedTitle') {
            row.translationError = '';
            row.translationStale = false;
          }
          row.mergeStale = true;
        } else if (field === 'mergedTitle') {
          row.mergeStale = false;
        }
        revalidateBaseErrors();
        state.rows.forEach((_, rowIndex) => updateRowCells(rowIndex));
      };
      control.addEventListener(control.tagName === 'SELECT' ? 'change' : 'input', updateField);
    });
    target.querySelectorAll('[data-upload-test-merge]').forEach(button => {
      button.addEventListener('click', () => {
        const index = Number(button.closest('[data-upload-test-row]')?.dataset.uploadTestRow);
        mergeRow(index);
      });
    });
    target.querySelectorAll('[data-upload-test-translate]').forEach(button => {
      button.addEventListener('click', () => {
        const index = Number(button.closest('[data-upload-test-row]')?.dataset.uploadTestRow);
        translateRows([index]).catch(error => {
          state.rows[index].translationError = error?.message || String(error);
          state.rows[index].translationStatus = 'error';
          updateRowCells(index);
        });
      });
    });
  }

  function mergeRow(index) {
    const row = state.rows[index];
    if (!row || state.uploading) return;
    if (!row.uploadTitle || !row.translatedTitle || row.translationStale) {
      row.mergeStale = true;
      updateRowCells(index);
      return;
    }
    row.mergedTitle = `${row.uploadTitle} ${row.translatedTitle}`.replace(/\s+/g, ' ').trim();
    row.mergeStale = false;
    const control = document.querySelector(`[data-upload-test-row="${index}"] [data-upload-test-field="mergedTitle"]`);
    if (control) control.value = row.mergedTitle;
    updateRowCells(index);
  }

  function mergeSelectedRows() {
    const indexes = getSelectedRowIndexes();
    if (!indexes.length) return setSummary('请先勾选要合并标题的商品。', 'error');
    let merged = 0;
    indexes.forEach(index => {
      mergeRow(index);
      if (state.rows[index]?.mergedTitle && state.rows[index]?.mergeStale === false) merged += 1;
    });
    setSummary(`已为 ${merged}/${indexes.length} 个勾选商品生成合并标题；未翻译的行不会合并。`, merged === indexes.length ? 'success' : 'error');
  }

  async function requestTranslations(items) {
    const token = await getServerAuthTokenForRead({ timeoutMs: 2200 });
    if (!token) throw new Error('请先登录账号');
    const response = await fetch(`${SERVER_URL}/ai/ozon/upload-test/translations`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ items })
    });
    const result = await response.json().catch(() => ({}));
    if (!response.ok || result.ok === false) throw new Error(result.error || result.message || `HTTP ${response.status}`);
    return result;
  }

  async function translateRows(indexes = []) {
    if (state.translating || state.uploading) return;
    const validIndexes = [...new Set(indexes)].filter(index => state.rows[index]?.sourceTitle);
    if (!validIndexes.length) return 0;
    state.translating = true;
    validIndexes.forEach(index => {
      const row = state.rows[index];
      row.translationStatus = 'loading';
      row.translationError = '';
      updateRowCells(index);
    });
    renderRows();
    try {
      for (let offset = 0; offset < validIndexes.length; offset += 20) {
        const chunk = validIndexes.slice(offset, offset + 20);
        const result = await requestTranslations(chunk.map(index => ({ key: String(index), sourceTitle: state.rows[index].sourceTitle })));
        const map = new Map((result.translations || []).map(item => [String(item.key), text(item.translation)]));
        chunk.forEach(index => {
          const row = state.rows[index];
          const translated = map.get(String(index));
          if (!translated) {
            row.translationStatus = 'error';
            row.translationError = '模型未返回该行翻译';
          } else {
            row.translatedTitle = translated;
            row.translationStatus = 'done';
            row.translationError = '';
            row.translationStale = false;
            row.mergeStale = true;
          }
        });
      }
    } catch (error) {
      validIndexes.forEach(index => {
        if (state.rows[index].translationStatus === 'loading') {
          state.rows[index].translationStatus = 'error';
          state.rows[index].translationError = error?.message || String(error);
        }
      });
      throw error;
    } finally {
      state.translating = false;
      renderRows();
    }
    return validIndexes.length;
  }

  async function precheck() {
    const templateId = getSelectedTemplateId();
    const excel = state.excelFile || document.getElementById('ozonUploadTestExcelInput')?.files?.[0];
    let files = state.assetFiles.length ? state.assetFiles : Array.from(document.getElementById('ozonUploadTestFolderInput')?.files || []).map(file => fileItem(file));
    if (!templateId) throw new Error('请选择一个已保存的 OZON 商品模板');
    if (!excel) throw new Error('请选择 Excel 文件');
    if (!files.length) throw new Error('请选择采集素材总文件夹');
    clearPreviewUrls();
    setSummary('正在读取当前店铺官方仓库、Excel、source.json 和图片...', '');
    await loadWarehouses();
    const XLSX = await ensureXlsxLoaded();
    const workbook = XLSX.read(await excel.arrayBuffer(), { type: 'array' });
    const sheet = workbook.Sheets[workbook.SheetNames[0]];
    if (!sheet) throw new Error('Excel 没有可读取的工作表');
    const data = XLSX.utils.sheet_to_json(sheet, { defval: '' });
    if (!data.length) throw new Error('Excel 没有数据行');
    const imageGroups = groupImages(files);
    const sourcePackages = await parseSourcePackages(files);
    const accountCurrency = getAccountCurrencyCode();
    state.rows = data.map((raw, index) => {
      const number = getOzonUploadCell(raw, ['编号', '链接序号', '序号', 'number', 'linkno']);
      const uploadTitle = getOzonUploadCell(raw, ['标题', '商品标题', '上传标题', 'title', 'name']);
      const price = getOzonUploadCell(raw, ['价格', '售价', 'price']);
      const enteredCurrency = getOzonUploadCell(raw, ['币种', 'currency', 'currencycode', 'currency_code']).toUpperCase();
      const currencyCode = enteredCurrency || accountCurrency;
      const offerId = getOzonUploadCell(raw, ['货号', 'offerid', 'offer_id']);
      const mergeModel = getOzonUploadCell(raw, ['型号名称（合并卡片）', '型号名称', '合并型号', '合并模型', 'mergemodel', 'merge_model', 'modelname', 'model_name']);
      const barcode = getOzonUploadCell(raw, ['条码', 'barcode']);
      const source = sourcePackages.sources.get(number) || null;
      const images = imageGroups.get(number) || [];
      const sourceErrors = [...(sourcePackages.errors.get(number) || [])];
      if (source && Array.isArray(source.images)) {
        const declaredNames = source.images.slice().sort((a, b) => Number(a.order || 0) - Number(b.order || 0)).map(image => text(image.fileName));
        const actualNames = images.map(image => image.name);
        if (declaredNames.length !== actualNames.length || declaredNames.some((name, imageIndex) => name !== actualNames[imageIndex])) {
          sourceErrors.push('source.json 图片清单与文件夹实际图片不一致');
        }
      }
      const row = {
        rowNumber: index + 2,
        number,
        source,
        sourceUrl: text(source?.sourceUrl),
        sourceTitle: text(source?.sourceTitle),
        translatedTitle: '',
        uploadTitle,
        mergedTitle: '',
        price,
        currencyCode,
        enteredCurrencyCode: enteredCurrency,
        offerId,
        mergeModel,
        barcode,
        warehouseId: '',
        stock: '',
        images,
        imagePreviews: images.slice(0, 8).map(image => ({ order: image.order, name: image.name, url: createPreviewUrl(image.file) })),
        imagesEdited: false,
        sourceErrors,
        baseErrors: [],
        translationStatus: 'pending',
        translationError: '',
        translationStale: false,
        mergeStale: true,
        selected: true,
        uploadStatus: 'idle',
        uploadCompletedUnits: 0,
        uploadTotalUnits: images.length + 3,
        uploadMessage: '',
        uploadError: '',
        productId: '',
        imageUrls: []
      };
      return row;
    });
    revalidateBaseErrors();
    renderRows();
    const structuralPassed = state.rows.filter(row => !row.baseErrors.length).length;
    setSummary(`预检完成：${structuralPassed}/${state.rows.length} 条结构通过。请勾选商品后使用表头“批量翻译”。`, structuralPassed === state.rows.length ? 'success' : 'error');
  }

  function updateActionState() {
    const precheckButton = document.getElementById('ozonUploadTestPrecheckBtn');
    const submitButton = document.getElementById('ozonUploadTestSubmitBtn');
    const selectedIndexes = getSelectedRowIndexes();
    const selectedPassed = selectedIndexes.length > 0 && selectedIndexes.every(index => !liveRowErrors(state.rows[index]).length);
    const translateButton = document.getElementById('ozonUploadTestTranslateSelectedBtn');
    const mergeButton = document.getElementById('ozonUploadTestMergeSelectedBtn');
    const batchStockButton = document.getElementById('ozonUploadTestBatchStockBtn');
    if (precheckButton) precheckButton.disabled = state.translating || state.uploading;
    if (translateButton) translateButton.disabled = !selectedIndexes.length || state.translating || state.uploading;
    if (mergeButton) mergeButton.disabled = !selectedIndexes.length || state.translating || state.uploading;
    if (batchStockButton) batchStockButton.disabled = !selectedIndexes.length || state.translating || state.uploading;
    if (submitButton) {
      submitButton.disabled = !selectedPassed || state.translating || state.uploading;
      submitButton.textContent = selectedIndexes.length ? `上传已勾选（${selectedIndexes.length}）` : '请勾选商品';
    }
  }

  function updateAllUploadCells() {
    state.rows.forEach((_, index) => updateRowCells(index));
  }

  async function pollBatch(batchId) {
    for (let attempt = 0; attempt < 40; attempt += 1) {
      const result = await ozonRequest(`/upload-test/batches/${encodeURIComponent(batchId)}/status`, { cache: false, forceRefresh: true });
      const byOffer = new Map((result.items || []).map(item => [text(item.offerId), item]));
      state.rows.forEach((row, index) => {
        const item = byOffer.get(row.offerId);
        if (!item) return;
        const status = text(item.status).toLowerCase();
        if (status === 'imported') {
          row.uploadStatus = 'stock_pending';
          row.uploadCompletedUnits = Math.max(0, row.uploadTotalUnits - 1);
          row.uploadMessage = '商品上传成功，等待写入库存';
          row.productId = text(item.productId);
          row.uploadError = '';
        } else if (status === 'failed') {
          row.uploadStatus = 'failed';
          row.uploadError = Array.isArray(item.errors) ? item.errors.map(error => error?.message || error?.code || JSON.stringify(error)).join('；') : 'OZON 返回失败';
        } else {
          row.uploadStatus = 'official';
          row.uploadMessage = 'OZON 官方处理中';
        }
        updateRowCells(index);
      });
      if (['imported', 'failed'].includes(text(result.status).toLowerCase())) return result;
      await new Promise(resolve => window.setTimeout(resolve, 3000));
    }
    throw new Error('OZON 官方任务仍在处理，请保留页面稍后继续查询');
  }

  async function submitBatchStocks(batchId) {
    state.rows.forEach((row, index) => {
      if (row.uploadStatus !== 'stock_pending') return;
      row.uploadMessage = `正在向 ${getWarehouseName(row.warehouseId)} 写入库存 ${row.stock}`;
      updateRowCells(index);
    });
    try {
      for (let attempt = 0; attempt < 100; attempt += 1) {
        const result = await ozonRequest(`/upload-test/batches/${encodeURIComponent(batchId)}/stocks`, {
          method: 'POST',
          body: JSON.stringify({})
        });
        const byOffer = new Map((result.items || []).map(item => [text(item.offerId), item]));
        state.rows.forEach((row, index) => {
          if (row.uploadStatus !== 'stock_pending') return;
          const item = byOffer.get(row.offerId);
          const stockStatus = text(item?.stockStatus).toLowerCase();
          if (stockStatus === 'success') {
            row.uploadStatus = 'success';
            row.uploadCompletedUnits = row.uploadTotalUnits;
            row.uploadMessage = '商品和库存均已成功';
            row.uploadError = '';
          } else if (stockStatus === 'failed' || stockStatus === 'skipped') {
            const errors = Array.isArray(item?.stockErrors) ? item.stockErrors : [];
            row.uploadStatus = 'stock_failed';
            row.uploadError = errors.map(error => error?.message || error?.code || JSON.stringify(error)).filter(Boolean).join('；') || (stockStatus === 'skipped' ? '商品上传失败，未写入库存' : 'OZON 未确认库存更新');
          } else {
            row.uploadMessage = '等待商品状态达到 price_sent 后写入库存';
          }
          updateRowCells(index);
        });
        if (Number(result.waiting || 0) === 0) return result;
        await new Promise(resolve => window.setTimeout(resolve, 3000));
      }
      const waitingError = new Error('OZON 商品状态在 5 分钟内未达到 price_sent，库存尚未提交；请稍后在商品列表同步后修改库存');
      waitingError.code = 'OZON_UPLOAD_TEST_STOCK_WAITING';
      throw waitingError;
    } catch (error) {
      state.rows.forEach((row, index) => {
        if (row.uploadStatus !== 'stock_pending') return;
        row.uploadStatus = error?.code === 'OZON_UPLOAD_TEST_STOCK_WAITING' ? 'stock_waiting' : 'stock_failed';
        row.uploadError = error?.message || String(error);
        updateRowCells(index);
      });
      throw error;
    }
  }

  async function submit() {
    const accountId = getSelectedAccountId();
    const templateId = getSelectedTemplateId();
    const selectedIndexes = getSelectedRowIndexes();
    if (!accountId || !templateId || !state.rows.length) throw new Error('请先完成预检并选择店铺和模板');
    if (!selectedIndexes.length) throw new Error('请先勾选要上传的商品');
    if (selectedIndexes.some(index => liveRowErrors(state.rows[index]).length)) throw new Error('已勾选商品中仍有未通过或未合并的行，不能上传');
    if (!window.confirm(`确定把 ${selectedIndexes.length} 条已勾选商品上传到 OZON，并按每行设置写入仓库库存吗？最终标题使用“合并标题”。`)) return;
    state.uploading = true;
    selectedIndexes.forEach(index => {
      const row = state.rows[index];
      row.uploadStatus = 'images';
      row.uploadCompletedUnits = 0;
      row.uploadTotalUnits = row.images.length + 3;
      row.uploadMessage = `准备上传 ${row.images.length} 张图片`;
      row.uploadError = '';
      row.imageUrls = [];
    });
    renderRows();
    setSummary('开始上传图片，表格会逐行显示真实任务进度。', '');
    try {
      for (const rowIndex of selectedIndexes) {
        const row = state.rows[rowIndex];
        for (let imageIndex = 0; imageIndex < row.images.length; imageIndex += 1) {
          row.uploadMessage = `上传图片 ${imageIndex + 1}/${row.images.length}`;
          updateRowCells(rowIndex);
          const dataUrl = await readOzonUploadFileAsDataUrl(row.images[imageIndex].file);
          const uploaded = await ozonRequest('/upload-images', {
            method: 'POST',
            body: JSON.stringify({ ozonAccountId: accountId, files: [{ dataUrl }] })
          });
          const url = text(uploaded.images?.[0]?.url);
          if (!url) throw new Error(`编号 ${row.number} 第 ${imageIndex + 1} 张图片未返回公开 URL`);
          row.imageUrls.push(url);
          row.uploadCompletedUnits += 1;
          updateRowCells(rowIndex);
        }
        row.uploadStatus = 'submitting';
        row.uploadMessage = '图片完成，等待提交任务';
        updateRowCells(rowIndex);
      }
      const payloadRows = selectedIndexes.map(index => state.rows[index]).map(row => ({
        rowNumber: row.rowNumber,
        number: row.number,
        sourceTitle: row.sourceTitle,
        translatedTitle: row.translatedTitle,
        uploadTitle: row.uploadTitle,
        mergedTitle: row.mergedTitle,
        price: row.price,
        currencyCode: row.currencyCode,
        offerId: row.offerId,
        mergeModel: row.mergeModel,
        barcode: row.barcode,
        warehouseId: Number(row.warehouseId),
        stock: Number(row.stock),
        imageUrls: row.imageUrls
      }));
      const result = await ozonRequest('/upload-test/batches/submit', {
        method: 'POST',
        body: JSON.stringify({ ozonAccountId: accountId, templateId, rows: payloadRows })
      });
      state.batchId = text(result.batchId);
      state.taskId = text(result.taskId);
      selectedIndexes.forEach(index => {
        const row = state.rows[index];
        row.uploadCompletedUnits += 1;
        row.uploadStatus = 'official';
        row.uploadMessage = `OZON 任务 ${state.taskId} 处理中`;
      });
      updateAllUploadCells();
      setSummary(`OZON 已接受测试上架任务 ${state.taskId}，正在读取官方逐条结果。`, 'success');
      const finalResult = await pollBatch(state.batchId);
      const imported = Number(finalResult.imported || 0);
      const importFailed = Number(finalResult.failed || 0);
      let stockResult = { succeeded: 0, failed: 0 };
      if (imported > 0) {
        setSummary(`商品导入完成：成功 ${imported}，失败 ${importFailed}；正在写入官方仓库库存。`, importFailed ? 'error' : 'success');
        stockResult = await submitBatchStocks(state.batchId);
      }
      const stockFailed = Number(stockResult.failed || 0);
      setSummary(`任务完成：商品上传成功 ${imported}，失败 ${importFailed}；库存成功 ${Number(stockResult.succeeded || 0)}，失败 ${stockFailed}。`, importFailed || stockFailed ? 'error' : 'success');
      await loadOzonProducts({ forceRefresh: true }).catch(() => {});
    } catch (error) {
      if (!state.batchId) {
        selectedIndexes.forEach(index => {
          const row = state.rows[index];
          if (row.uploadStatus === 'success') return;
          row.uploadStatus = 'failed';
          row.uploadError = `批次未提交：${error?.message || String(error)}`;
        });
      }
      updateAllUploadCells();
      setSummary(error?.message || String(error), 'error');
      throw error;
    } finally {
      state.uploading = false;
      renderRows();
    }
  }

  async function downloadTemplate() {
    try {
      const response = await fetch(`/ozon/ozon-upload-template.xlsx?v=${encodeURIComponent(TEST_VERSION)}`, { cache: 'no-store' });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const blob = await response.blob();
      if (!blob.size) throw new Error('模板文件为空');
      triggerBrowserDownload('OZON批量上架测试模板.xlsx', blob);
    } catch (error) {
      setSummary(`下载模板失败：${error?.message || String(error)}`, 'error');
    }
  }

  async function downloadCollectorExtension() {
    try {
      const response = await fetch(`/ozon/ozon-collector-extension.zip?v=${encodeURIComponent(TEST_VERSION)}`, { cache: 'no-store' });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const blob = await response.blob();
      if (!blob.size) throw new Error('插件安装包为空');
      triggerBrowserDownload('小青蛙OZON素材采集助手-0.1.3.zip', blob);
    } catch (error) {
      setSummary(`下载采集插件失败：${error?.message || String(error)}`, 'error');
    }
  }

  function resetPrecheck(message = '文件已变化，请重新预检。') {
    clearPreviewUrls();
    state.rows = [];
    state.batchId = '';
    state.taskId = '';
    renderRows();
    setSummary(message, '');
  }

  function bindFileInput(inputId, nameId, kind) {
    const input = document.getElementById(inputId);
    if (!input || input.dataset.uploadTestBound === '1') return;
    input.dataset.uploadTestBound = '1';
    input.addEventListener('change', () => {
      const files = Array.from(input.files || []);
      if (kind === 'excel') state.excelFile = files[0] || null;
      else state.assetFiles = files.map(file => fileItem(file));
      const label = document.getElementById(nameId);
      if (label) label.textContent = files.length ? (files.length === 1 ? files[0].name : `已选择 ${files.length} 个文件`) : '未选择';
      resetPrecheck(files.length ? '文件读取完成，请点击“预检数据”。' : '文件选择已清空。');
    });
  }

  function initialize() {
    if (state.initialized) return;
    state.initialized = true;
    bindFileInput('ozonUploadTestExcelInput', 'ozonUploadTestExcelName', 'excel');
    bindFileInput('ozonUploadTestFolderInput', 'ozonUploadTestFolderName', 'folder');
    document.getElementById('ozonUploadTestAccountFilter')?.addEventListener('change', () => {
      state.accountId = getSelectedAccountId();
      state.templates = [];
      state.warehouses = [];
      state.warehouseAccountId = '';
      resetPrecheck('店铺已变化，请选择该店铺模板并重新预检。');
      loadTemplates().catch(error => setSummary(error?.message || String(error), 'error'));
    });
    document.getElementById('ozonUploadTestTemplateSelect')?.addEventListener('change', () => resetPrecheck('模板已变化，请重新预检。'));
    document.getElementById('ozonUploadTestAddTemplateBtn')?.addEventListener('click', () => {
      const accountId = getSelectedAccountId();
      if (!accountId) {
        setSummary('请先选择 OZON 店铺', 'error');
        return;
      }
      if (typeof window.openOzonUploadTemplateModalForAccount !== 'function') {
        setSummary('模板弹窗尚未加载，请刷新页面后重试', 'error');
        return;
      }
      if (!window.openOzonUploadTemplateModalForAccount(accountId)) {
        setSummary('当前店铺无法打开模板弹窗，请刷新店铺授权后重试', 'error');
      }
    });
    document.getElementById('ozonUploadTestRefreshTemplatesBtn')?.addEventListener('click', () => loadTemplates().catch(error => setSummary(error?.message || String(error), 'error')));
    document.getElementById('ozonUploadTestDownloadTemplateBtn')?.addEventListener('click', downloadTemplate);
    document.getElementById('ozonUploadTestDownloadCollectorBtn')?.addEventListener('click', downloadCollectorExtension);
    document.getElementById('ozonUploadTestPrecheckBtn')?.addEventListener('click', () => precheck().catch(error => setSummary(error?.message || String(error), 'error')));
    document.getElementById('ozonUploadTestSubmitBtn')?.addEventListener('click', () => submit().catch(() => {}));
    document.getElementById('ozonUploadTestBatchStockCloseBtn')?.addEventListener('click', () => setBatchStockModalVisible(false));
    document.getElementById('ozonUploadTestBatchStockCancelBtn')?.addEventListener('click', () => setBatchStockModalVisible(false));
    document.getElementById('ozonUploadTestBatchStockApplyBtn')?.addEventListener('click', applyBatchStock);
    document.getElementById('ozonUploadTestBatchStockModal')?.addEventListener('click', event => {
      if (event.target?.id === 'ozonUploadTestBatchStockModal') setBatchStockModalVisible(false);
    });
    window.addEventListener('ozon:upload-templates-changed', event => {
      const changedAccountId = text(event?.detail?.accountId);
      if (changedAccountId && changedAccountId !== getSelectedAccountId()) return;
      loadTemplates().catch(error => setSummary(error?.message || String(error), 'error'));
    });
    window.addEventListener('ozon-upload-test-delete-preview-image', event => {
      const rowIndex = Number(event?.detail?.rowIndex);
      const imageIndex = Number(event?.detail?.imageIndex);
      removeRowImage(rowIndex, imageIndex);
    });
  }

  async function load() {
    initialize();
    state.accountId = state.accountId || getDefaultOzonAccountId();
    renderSelectors();
    await loadTemplates();
  }

  function render() {
    initialize();
    renderSelectors();
    renderRows();
  }

  window.loadOzonUploadTest = load;
  window.renderOzonUploadTest = render;
  window.ozonUploadTestState = state;
})();
