'use strict';

/**
 * 快速核价 · v1.2 · Floating Panel
 * 独立悬浮面板：右上角浮窗，可拖拽 / 最小化 / 关闭，关闭后变小球可恢复
 */

(function () {
  if (!window.location.pathname.includes('/product/')) return;
  if (window.__JZC_OZON_PROFIT_LITE_LOADED__) return;
  window.__JZC_OZON_PROFIT_LITE_LOADED__ = true;

  const P = 'jzc';
  const LS_STATE = 'jz_calc_state_v3';
  const LS_SKU_SPECS = 'jz_calc_sku_specs_v1'; // { [sku]: { price, cost, weight, dimL, dimW, dimH, ts } }
  const SKU_SPECS_MAX = 60;                    // LRU 上限
  const LABEL_FEE = 2.0;
  const PLATFORM_FEE_RATE = 0.04;
  const DPI_FACTOR = 12;
  const SAFETY_RATE = 0.15;

  // ── Freight tables ──────────────────────────────
  // base 单位：¥/包；rates 单位：¥/g
  // 货值边界（卢布）来自义乌仓 122 资费表：xs/budget ≤ 1500₽，small/big 1501-7000₽，premium 7001+
  const VALUE_LO = 1500;
  const VALUE_HI = 7000;
  const W_XS = 500;
  const W_SMALL = 2000;
  const W_PSMALL = 5000;
  
  // 资费来源：义乌仓 122 资费表（2026-04-15）
  // 关键事实：
  //   · CEL 的 Big / Premium Big 没有 Express（只有 Standard + Economy）
  //   · GUOO 的 Budget / Big / Premium Big 只有 Economy
  //   · GUOO 的 Premium Small 只有 Express + Economy（无 Standard）
  //   · GUOO 的 Extra Small Economy 基础票费是 ¥3.00 而非 ¥3.12
  //   · 兴远 XY 没有 Express；pBig 的 std/economy base 不同（64 vs 64.48）
  const CEL_FREIGHT = {
    xs:     { base: 3.12,  rates: { economy: 0.026,   std: 0.0364,  fast: 0.0468  } },
    budget: { base: 23.92, rates: { economy: 0.01768, std: 0.026,   fast: 0.03432 } },
    small:  { base: 16.64, rates: { economy: 0.026,   std: 0.0364,  fast: 0.0468  } },
    big:    { base: 37.44, rates: { economy: 0.01768, std: 0.026                  } },
    pSmall: { base: 22.88, rates: { economy: 0.026,   std: 0.0364,  fast: 0.0468  } },
    pBig:   { base: 64.48, rates: { economy: 0.02392, std: 0.02912                } },
  };
  const GUOO_FREIGHT = {
    xs:     { base: 3.12,  baseEconomy: 3.00,
              rates: { economy: 0.026,   std: 0.0364,  fast: 0.0468  } },
    budget: { base: 23.92, rates: { economy: 0.01768                              } },
    small:  { base: 16.64, rates: { economy: 0.026,   std: 0.0364,  fast: 0.0468  } },
    big:    { base: 37.44, rates: { economy: 0.01768                              } },
    pSmall: { base: 22.88, rates: { economy: 0.026,                  fast: 0.0468 } },
    pBig:   { base: 64.48, rates: { economy: 0.02392                              } },
  };
  const XY_FREIGHT = {
    xs:     { base: 3.12,  rates: { economy: 0.026,   std: 0.0364   } },
    budget: { base: 23.92, rates: { economy: 0.01768, std: 0.026    } },
    small:  { base: 16.64, rates: { economy: 0.026,   std: 0.0364   } },
    big:    { base: 37.44, rates: { economy: 0.01768, std: 0.026    } },
    pSmall: { base: 22.88, rates: { economy: 0.026,   std: 0.0364   } },
    pBig:   { base: 64,    baseEconomy: 64.48,
             rates: { economy: 0.02392, std: 0.02912 } },
  };
  // 中通 ZTO rFBS（v6.3）：与 CEL 大体相同，除 Big 仅有 Standard
  const ZTO_FREIGHT = {
    xs:     { base: 3.12,  rates: { economy: 0.026,   std: 0.0364,  fast: 0.0468  } },
    budget: { base: 23.92, rates: { economy: 0.01768, std: 0.026,   fast: 0.03432 } },
    small:  { base: 16.64, rates: { economy: 0.026,   std: 0.0364,  fast: 0.0468  } },
    big:    { base: 37.44, rates: {                   std: 0.026                  } },
    pSmall: { base: 22.88, rates: { economy: 0.026,   std: 0.0364,  fast: 0.0468  } },
    pBig:   { base: 64.48, rates: { economy: 0.02392, std: 0.02912                } },
  };
  // China Post to OZON：用户提供的 Ozon 服务表（PUDO）。
  // 页面只给出「起价」，未给出 500g 内的逐克/续重资费；核价中明确标为起价，不能当成完整账单费率。
  const CHINA_POST_SERVICES = {
    economy: {
      label: 'China Post to OZON Economy', short: 'Economy PUDO', delivery: '20–25 天',
      startPrice: 1.93, priceMin: 0.01, priceMax: 135, weightMin: 1, weightMax: 500,
    },
    std: {
      label: 'China Post to OZON Standard', short: 'Standard PUDO', delivery: '10–15 天',
      startPrice: 1.97, priceMin: 0.01, priceMax: 135, weightMin: 1, weightMax: 500,
    },
  };

  const TIERED_TABLES = { CEL: CEL_FREIGHT, GUOO: GUOO_FREIGHT, XY: XY_FREIGHT, ZTO: ZTO_FREIGHT };
  const FLAT_TABLES   = { POST: CHINA_POST_SERVICES };
  const TIERED_CHANNELS = Object.keys(TIERED_TABLES);
  const FLAT_CHANNELS   = Object.keys(FLAT_TABLES);
  const ALL_CHANNELS    = [...TIERED_CHANNELS, ...FLAT_CHANNELS];

  const CHANNEL_LABELS = {
    POST: { short: 'China Post', full: 'China Post to OZON PUDO' },
    CEL:  { short: 'CEL',   full: 'CEL 物流' },
    GUOO: { short: 'GUOO',  full: 'GUOO 物流' },
    XY:   { short: '兴远',  full: '兴远 XY 物流' },
    ZTO:  { short: 'ZTO',   full: '中通 ZTO rFBS' },
  };

  // 表内费率版本只用于透明展示；并非 Ozon 实时接口，运营更新资费后需同步维护。
  const RATE_TABLE_META = {
    POST: { source: 'Ozon China Post 服务表', version: '截图核验' },
    CEL:  { source: '义乌仓 122 资费表', version: '2026-04-15' },
    GUOO: { source: '义乌仓 122 资费表', version: '2026-04-15' },
    XY:   { source: '义乌仓 122 资费表', version: '2026-04-15' },
    ZTO:  { source: '义乌仓 122 资费表', version: '2026-04-15' },
  };

  // 各分档渠道的计抛规则：哪些档位计抛 + 体积重除数
  // 默认 6000；ZTO rFBS 沿用 6000，且 pSmall · Economy 用 24000
  const VOLUMETRIC_RULES = {
    CEL:  { divisor: 6000, tiers: new Set(['big', 'pBig']) },
    GUOO: { divisor: 6000, tiers: new Set(['big', 'pBig']) },
    XY:   { divisor: 6000, tiers: new Set(['big', 'pBig']) },
    ZTO:  { divisor: 6000, tiers: new Set(['budget', 'small', 'big', 'pSmall', 'pBig']) },
  };
  function getVolDivisor(channel, tierKey, transport) {
    if (channel === 'ZTO' && tierKey === 'pSmall' && transport === 'economy') return 24000;
    return VOLUMETRIC_RULES[channel]?.divisor || 6000;
  }

  function isFlatChannel(channelName) { return channelName in FLAT_TABLES; }
  function freightTableFor(channelName) {
    return TIERED_TABLES[channelName] || null;
  }
  function channelCfgRef(channelName) {
    if (channelName === 'CEL')  return celConfig;
    if (channelName === 'GUOO') return guooConfig;
    if (channelName === 'XY')   return xyConfig;
    if (channelName === 'ZTO')  return ztoConfig;
    return null;
  }
  function setChannelCfg(channelName, next) {
    if (channelName === 'CEL')  celConfig = next;
    if (channelName === 'GUOO') guooConfig = next;
    if (channelName === 'XY')   xyConfig = next;
    if (channelName === 'ZTO')  ztoConfig = next;
  }

  // 由 recalcAll 传入未折算的 dim_LWH(cm³)，calcFreight 内部按渠道+档位+运输方式选择正确除数
  function calcFreight(channelName, actualW, dimVolume, priceCNY) {
    if (isFlatChannel(channelName)) {
      return calcFlatFreight(channelName, actualW, priceCNY);
    }
    const cfg = channelCfgRef(channelName);
    const table = freightTableFor(channelName);
    // 用实重决定档位，避免因体积重把轻小件错判到 Big
    const tierKey = matchTierKey(actualW, priceCNY);
    const tier = table[tierKey];
    let transport = (cfg && cfg[tierKey]) || 'economy';
    if (tier.rates[transport] == null) {
      // 找一个可用 transport 兜底（economy 优先，再 std，再 fast）
      transport = ['economy', 'std', 'fast'].find((k) => tier.rates[k] != null) || 'economy';
    }
    const ratePerGram = tier.rates[transport];
    const base = (tier.baseEconomy != null && transport === 'economy')
      ? tier.baseEconomy
      : tier.base;
    const usesVol = VOLUMETRIC_RULES[channelName]?.tiers.has(tierKey);
    const divisor = getVolDivisor(channelName, tierKey, transport);
    const volW = (usesVol && dimVolume > 0) ? (dimVolume * 1000 / divisor) : 0;
    const billed = Math.max(actualW || 0, volW);
    const fee = base + billed * ratePerGram;
    return { fee, base, ratePerGram, tierKey, transport, billed, divisor, type: 'tiered' };
  }

  function calcFlatFreight(channelName, actualW, priceCNY) {
    if (channelName === 'POST') {
      const service = CHINA_POST_SERVICES[postService] || CHINA_POST_SERVICES.economy;
      const w = actualW || 0;
      const price = priceCNY || 0;
      return {
        fee: service.startPrice,
        base: service.startPrice,
        ratePerGram: null,
        billed: w,
        type: 'flat',
        isStartingPrice: true,
        service: postService,
        serviceLabel: service.short,
        delivery: service.delivery,
        overWeight: w < service.weightMin || w > service.weightMax,
        overValue: price < service.priceMin || price > service.priceMax,
        note: `${service.short} · ${service.delivery} · 按实重`,
      };
    }
    const t = FLAT_TABLES[channelName];
    const w = actualW || 0;
    const fee = Math.max(t.minFee || 0, t.base + w * t.perGram);
    const overWeight = t.weightCap != null && w > t.weightCap;
    const overValue  = t.valueCapCNY != null && (priceCNY || 0) > t.valueCapCNY;
    return {
      fee, base: t.base, ratePerGram: t.perGram, billed: w,
      type: 'flat', overWeight, overValue, note: t.note,
    };
  }

  // 把保存的渠道配置中已不存在的运输方式归回到该档位实际可用的（优先 economy）
  function normalizeChannelCfg(channelName, cfg) {
    const table = freightTableFor(channelName);
    const out = {};
    TIER_KEYS.forEach((k) => {
      const wanted = cfg?.[k] || 'economy';
      if (table[k].rates[wanted] != null) {
        out[k] = wanted;
      } else {
        out[k] = ['economy', 'std', 'fast'].find((t) => table[k].rates[t] != null) || 'economy';
      }
    });
    return out;
  }

  function tierFromPrice(p) {
    if (!p || p <= 0) return null;
    if (p <= 135) return 1;
    if (p <= 437) return 2;
    return 3;
  }
  // 佣金只接受当前商品从 Ozon Seller Portal 读取到的真实值；不再使用默认档位或手动输入。
  let productCommission = { rate: null, source: '正在读取 Ozon 商品佣金…' };

  const fmt = (n, d = 2) =>
    n == null || isNaN(n) ? '—'
      : Number(n).toLocaleString('zh-CN', {
          minimumFractionDigits: d,
          maximumFractionDigits: d,
        });

  // ── Storage helpers ─────────────────────────────
  function storageGet(keys) {
    return new Promise((resolve) => {
      try { chrome.storage.local.get(keys, (v) => resolve(v || {})); }
      catch { resolve({}); }
    });
  }
  function storageSet(obj) {
    return new Promise((resolve) => {
      try { chrome.storage.local.set(obj, () => resolve()); }
      catch { resolve(); }
    });
  }

  // ── Extract product data from page ──────────────
  // Ozon 价格语义：
  //   黑标 (price)     = "без Ozon Банком" 普通价（不带 Ozon Bank）
  //   绿标 (cardPrice) = "с Ozon Банком" Ozon Bank 优惠价（UI 绿色背景）
  //   originalPrice    = 历史划线价，与黑/绿标无关
  function parsePriceNum(v) {
    if (v == null) return NaN;
    const t = typeof v === 'string' ? v : v.price ?? v.value ?? '';
    const n = parseFloat(String(t).replace(/[^\d.,]/g, '').replace(',', '.'));
    return n;
  }
  function extractProductData() {
    const data = {};
    try {
      const els = document.querySelectorAll('[data-state]');
      for (const el of els) {
        const raw = el.getAttribute('data-state');
        if (!raw || raw.length < 10) continue;
        try {
          const p = JSON.parse(raw);
          if (!p || typeof p !== 'object') continue;
          if (p.price && data.blackPrice == null) {
            const n = parsePriceNum(p.price);
            if (!isNaN(n) && n > 0) data.blackPrice = n;
          }
          if (p.cardPrice && data.greenPrice == null) {
            const n = parsePriceNum(p.cardPrice);
            if (!isNaN(n) && n > 0) data.greenPrice = n;
          }
        } catch (_) {}
      }
    } catch (_) {}
    if (!data.blackPrice) {
      const el = document.querySelector('[data-widget="webPrice"] span');
      if (el) {
        const n = parseFloat(el.textContent.replace(/[^\d.,]/g, '').replace(',', '.'));
        if (!isNaN(n) && n > 0) data.blackPrice = n;
      }
    }
    // 没参与 Ozon Bank 折扣时只有 cardPrice 没有 price：cardPrice 即黑标
    if (data.blackPrice == null && data.greenPrice != null) {
      data.blackPrice = data.greenPrice;
    }
    // 公式要求 黑 ≥ 绿；若反了或缺失，绿标视作等于黑标（无折扣）
    if (data.blackPrice != null && (data.greenPrice == null || data.greenPrice > data.blackPrice)) {
      data.greenPrice = data.blackPrice;
    }
    const m = location.pathname.match(/\/product\/[^/]*-(\d+)/);
    if (m) data.sku = m[1];
    Object.assign(data, extractWeightAndDims());
    return data;
  }

  // ── 抓商品页特征：重量(g) + 长宽高(cm) ──────────────
  // Ozon 商品页常见 4 种结构：
  //   1) [data-widget="webShortCharacteristics"] data-state.characteristics: [{title|key, shortValue|values|value}]
  //   2) [data-widget="webCharacteristics"]      data-state.characteristics: 同上，列表更全
  //   3) DOM dl/dt/dd 或 table tr/td 兜底
  //   4) "Размеры, мм" 形如 "1500 x 2000 x 300" 一次性给出长宽高
  function extractWeightAndDims() {
    const out = {};
    const reW   = /вес|масса|брутто|нетто|weight|net\s*weight|gross|重量|净重|毛重|质量|重\s*量/i;
    const reLen = /длина|length|长[度]?(?!.*?宽)|глубин|глубина|depth|深/i;
    const reWid = /ширина|width|宽[度]?/i;
    const reHei = /высота|height|высоту|толщин|高[度]?|厚[度]?/i;
    const reDim = /размер[ыа]?|габарит|dimension|пакет|упаков|объ[её]мн|尺寸|外形|包装|长.*宽.*高/i;

    // 单位转换：返回克 (g)
    const toGrams = (numStr, unit) => {
      const n = parseFloat(String(numStr).replace(',', '.'));
      if (!isFinite(n) || n <= 0) return null;
      const u = String(unit || '').toLowerCase();
      if (/кг|kg|公斤|千克/.test(u)) return Math.round(n * 1000);
      if (/мг|mg|毫克/.test(u))      return Math.round(n / 1000);
      return Math.round(n); // г / g / 克 / 缺省
    };
    // 单位转换：返回厘米 (cm)
    const toCm = (numStr, unit) => {
      const n = parseFloat(String(numStr).replace(',', '.'));
      if (!isFinite(n) || n <= 0) return null;
      const u = String(unit || '').toLowerCase();
      if (/мм|mm|毫米/.test(u)) return +(n / 10).toFixed(1);
      if (/^м$|^m$|метр|米/.test(u)) return +(n * 100).toFixed(1);
      return +n.toFixed(1); // см / cm / 缺省按 cm
    };

    // 三维尺寸正则：捕获 "1500 x 2000 x 300 мм" / "150×200×30" / "96 х 96х 110 мм"
    // 注意：Ozon 用 西里尔字母 х (U+0445) 作分隔符，必须显式包含；空格也允许缺省
    const SEP = '[\\s]*[\\u0445\\u0425×x*✕✖][\\s]*';
    const triRe = new RegExp(
      '(\\d+(?:[.,]\\d+)?)' + SEP +
      '(\\d+(?:[.,]\\d+)?)' + SEP +
      '(\\d+(?:[.,]\\d+)?)\\s*(мм|mm|см|cm|метр|m|米|毫米)?',
      'i'
    );
    const numUnit = /(\d+(?:[.,]\d+)?)\s*(мм|mm|см|cm|кг|kg|г|g|公斤|千克|克|毫米|米|m)?/i;

    const scan = (kRaw, vRaw) => {
      if (!kRaw) return;
      const k = String(kRaw);
      const v = vRaw == null ? '' : (Array.isArray(vRaw)
        ? vRaw.map((x) => (x && (x.text || x.value || x.title)) || x).join(' ')
        : (typeof vRaw === 'object' ? (vRaw.text || vRaw.value || vRaw.title || JSON.stringify(vRaw)) : String(vRaw)));
      if (!v) return;

      // 一次性三维尺寸（优先级最高，因为信息最完整）
      if (reDim.test(k) || triRe.test(v)) {
        const m = v.match(triRe);
        if (m) {
          const unit = m[4] || (/мм|mm/i.test(k) ? 'mm' : (/см|cm/i.test(k) ? 'cm' : 'mm'));
          const a = toCm(m[1], unit), b = toCm(m[2], unit), c = toCm(m[3], unit);
          if (a && b && c) {
            // Ozon 习惯：长 × 宽 × 高
            if (out.dimL == null) out.dimL = a;
            if (out.dimW == null) out.dimW = b;
            if (out.dimH == null) out.dimH = c;
            return;
          }
        }
      }

      // 重量
      if (out.weight == null && reW.test(k)) {
        const m = v.match(numUnit);
        if (m) {
          const unitHint = m[2] || (/кг|kg/i.test(k) ? 'kg' : (/г\b|,?\s*г$|,\s*g/i.test(k) ? 'g' : ''));
          const g = toGrams(m[1], unitHint);
          if (g) out.weight = g;
        }
        return;
      }

      // 单维度
      const single = v.match(numUnit);
      if (!single) return;
      const unitHint = single[2] || (/мм|mm/i.test(k) ? 'mm' : (/см|cm/i.test(k) ? 'cm' : ''));
      const cm = toCm(single[1], unitHint);
      if (!cm) return;
      if (out.dimL == null && reLen.test(k)) out.dimL = cm;
      else if (out.dimW == null && reWid.test(k)) out.dimW = cm;
      else if (out.dimH == null && reHei.test(k)) out.dimH = cm;
    };

    // 解析复合字段：可能是 string、{textRs:[{content}]}、[{text|content|value}]、{text|content|value}
    const flatten = (x) => {
      if (x == null) return '';
      if (typeof x === 'string') return x;
      if (Array.isArray(x)) return x.map(flatten).filter(Boolean).join(' ');
      if (typeof x !== 'object') return String(x);
      if (x.textRs) return flatten(x.textRs);
      if (x.content != null) return flatten(x.content);
      if (x.text != null)    return flatten(x.text);
      if (x.value != null)   return flatten(x.value);
      if (x.title != null)   return flatten(x.title);
      return '';
    };

    // 递归遍历 JSON：把 key/value 都用 flatten 统一抽出文本
    const seen = new WeakSet();
    const walk = (node) => {
      if (!node || typeof node !== 'object' || seen.has(node)) return;
      seen.add(node);
      if (Array.isArray(node)) { node.forEach(walk); return; }
      const keyField = node.key ?? node.name ?? node.title;
      const valField = node.shortValue ?? node.fullValue ?? node.value ?? node.values ?? node.text;
      if (keyField != null && valField != null) {
        const k = flatten(keyField);
        const v = flatten(valField);
        if (k && v) scan(k, v);
      }
      for (const k of Object.keys(node)) {
        const v = node[k];
        if (v && typeof v === 'object') walk(v);
      }
    };

    // 1) 主路径：参考极掌主插件，定位含 characteristics 数组的 data-state 节点
    //    Ozon 特征结构：{ title: { textRs: [{content}] }, values: [{text}] }
    let charsBlock = null;
    document.querySelectorAll('[data-state]').forEach((el) => {
      if (charsBlock) return;
      const raw = el.getAttribute('data-state');
      if (!raw || raw.length < 10) return;
      try {
        const data = JSON.parse(raw);
        if (data && typeof data === 'object' && !Array.isArray(data)) {
          // characteristics 可能嵌套在子层，先查顶层；找不到再 walk 全树后回填
          if (Array.isArray(data.characteristics)) charsBlock = data;
          // 同时执行通用 walk
          walk(data);
        }
      } catch (_) {}
    });
    if (charsBlock?.characteristics) {
      charsBlock.characteristics.forEach((c) => {
        if (!c || typeof c !== 'object') return;
        const title = flatten(c.title) || flatten(c.name) || flatten(c.key);
        const value = flatten(c.values) || flatten(c.shortValue) || flatten(c.fullValue) || flatten(c.value);
        if (title && value) scan(title, value);
      });
    }

    // 2) DOM 兜底：dl/dt/dd 与 table tr
    const allFilled = () =>
      out.weight != null && out.dimL != null && out.dimW != null && out.dimH != null;
    if (!allFilled()) {
      document.querySelectorAll('dl').forEach((dl) => {
        const dts = dl.querySelectorAll('dt');
        const dds = dl.querySelectorAll('dd');
        const n = Math.min(dts.length, dds.length);
        for (let i = 0; i < n; i++) scan(dts[i].textContent, dds[i].textContent);
      });
      document.querySelectorAll('tr').forEach((tr) => {
        const tds = tr.querySelectorAll('td,th');
        if (tds.length >= 2) scan(tds[0].textContent, tds[1].textContent);
      });
    }

    // 3) DOM 同级两子元素（Ozon 新布局：div > 两个 span/div 文本对）
    if (!allFilled()) {
      const widgets = document.querySelectorAll(
        '[data-widget*="haracteristic"], [data-widget="webShortCharacteristic"]'
      );
      const roots = widgets.length ? widgets : [document.body];
      roots.forEach((root) => {
        root.querySelectorAll('div, li').forEach((row) => {
          const kids = Array.from(row.children).filter(
            (c) => (c.textContent || '').trim().length
          );
          if (kids.length !== 2) return;
          const k = (kids[0].textContent || '').trim();
          const v = (kids[1].textContent || '').trim();
          if (!k || !v || k.length > 80 || v.length > 80) return;
          scan(k, v);
        });
      });
    }

    // 4) 文本兜底：拼接特征区可见文本，整段正则匹配
    if (!allFilled()) {
      const widgets = document.querySelectorAll(
        '[data-widget*="haracteristic"], [data-widget*="haracteristics"]'
      );
      let text = '';
      widgets.forEach((w) => { text += '\n' + (w.textContent || ''); });
      if (!text.trim()) text = document.body?.textContent || '';
      // 重量：键词 + 数字 + 单位
      if (out.weight == null) {
        const m = text.match(
          /(?:Вес|вес|Масса|масса|Weight|weight|重量|净重|毛重)[^\d]{0,40}(\d+(?:[.,]\d+)?)\s*(г|кг|g|kg|克|千克|公斤)\b/i
        );
        if (m) {
          const g = toGrams(m[1], m[2]);
          if (g) out.weight = g;
        }
      }
      // 三维尺寸：键词 + 三个数字
      if (out.dimL == null || out.dimW == null || out.dimH == null) {
        const m = text.match(
          /(?:Габариты|габариты|Размер[ыа]?|размер[ыа]?|Dimensions?|尺寸|外形|包装)[^\d]{0,60}(\d+(?:[.,]\d+)?)\s*[×x*✕✖]\s*(\d+(?:[.,]\d+)?)\s*[×x*✕✖]\s*(\d+(?:[.,]\d+)?)\s*(мм|mm|см|cm)?/i
        );
        if (m) {
          const unit = m[4] || (m[0] && /мм|mm/i.test(m[0]) ? 'mm' : (m[0] && /см|cm/i.test(m[0]) ? 'cm' : 'mm'));
          const a = toCm(m[1], unit), b = toCm(m[2], unit), c = toCm(m[3], unit);
          if (a && b && c) {
            if (out.dimL == null) out.dimL = a;
            if (out.dimW == null) out.dimW = b;
            if (out.dimH == null) out.dimH = c;
          }
        }
      }
    }

    return out;
  }

  // ── 远程兜底：Ozon composer-api ───────────────────
  // 当商品页 DOM 没渲染特征 widget（常见于低价商品/简版页面）时，
  // 走 ozon.ru 内部 JSON layout 接口拿完整 characteristics（同源无需登录）。
  let _composerFetched = false;
  async function fetchCharsFromComposer() {
    if (_composerFetched) return null;
    _composerFetched = true;
    const basePath = location.pathname.replace(/\/+$/, '');
    const featPath = basePath + '/features/' + location.search;
    const charsPath = basePath + '/characteristics/' + location.search;
    const mainPath = basePath + '/' + location.search;
    const descPath = basePath + '/description/' + location.search;
    const endpoints = [
      `https://www.ozon.ru/api/entrypoint-api.bx/page/json/v2?url=${encodeURIComponent(featPath)}`,
      `https://www.ozon.ru/api/entrypoint-api.bx/page/json/v2?url=${encodeURIComponent(charsPath)}`,
      `https://www.ozon.ru/api/entrypoint-api.bx/page/json/v2?url=${encodeURIComponent(descPath)}`,
      `https://www.ozon.ru/api/entrypoint-api.bx/page/json/v2?url=${encodeURIComponent(mainPath)}`,
      `https://www.ozon.ru/api/composer-api.bx/page/json/v2?url=${encodeURIComponent(featPath)}`,
      `https://www.ozon.ru/api/composer-api.bx/page/json/v2?url=${encodeURIComponent(charsPath)}`,
    ];
    for (const url of endpoints) {
      try {
        const resp = await fetch(url, {
          credentials: 'include',
          headers: { 'x-o3-app-name': 'dweb_client', 'accept': 'application/json' },
        });
        if (!resp.ok) {
          console.warn('[快速核价] composer fetch HTTP', resp.status, url);
          continue;
        }
        const data = await resp.json();
        const states = data?.widgetStates || {};
        // 收集所有 short/long 嵌套的 spec 条目，扁平化成 [{name, value}]
        const flat = [];
        const seen = new WeakSet();
        const find = (node, depth = 0) => {
          if (!node || typeof node !== 'object' || depth > 8 || seen.has(node)) return;
          seen.add(node);
          // /features/ 结构：characteristics[*].short[*] / characteristics[*].long[*]
          if (Array.isArray(node.short)) {
            node.short.forEach((c) => {
              const name = c?.name || c?.title || c?.key;
              const val = c?.values?.[0]?.text || c?.value;
              if (name && val != null) flat.push({ name: String(name), value: String(val) });
            });
          }
          if (Array.isArray(node.long)) {
            node.long.forEach((c) => {
              const name = c?.name || c?.title || c?.key;
              const val = c?.values?.[0]?.text || c?.value;
              if (name && val != null) flat.push({ name: String(name), value: String(val) });
            });
          }
          // /characteristics/ 结构：characteristics[*].{title,values}
          if (Array.isArray(node.characteristics)) {
            node.characteristics.forEach((c) => {
              if (c && (c.short || c.long)) return; // 已被上面分支处理
              const tt = c?.title;
              const name =
                typeof tt === 'string' ? tt : tt?.textRs?.[0]?.content || c?.name || c?.key;
              const val = c?.values?.[0]?.text || c?.shortValue || c?.value;
              if (name && val != null) flat.push({ name: String(name), value: String(val) });
            });
          }
          if (Array.isArray(node)) node.forEach((x) => find(x, depth + 1));
          else for (const k of Object.keys(node)) find(node[k], depth + 1);
        };
        for (const k of Object.keys(states)) {
          let v = states[k];
          if (typeof v === 'string') { try { v = JSON.parse(v); } catch (_) { continue; } }
          find(v);
        }
        if (flat.length) {
          console.log('[快速核价] composer 抓到', flat.length, '条特征', url);
          return flat;
        }
        console.warn('[快速核价] composer 无特征条目', Object.keys(states).length, 'states', url);
      } catch (e) {
        console.warn('[快速核价] composer fetch failed', url, e);
      }
    }
    return null;
  }

  async function autoFillFromComposer() {
    const out = {};
    const flat = await fetchCharsFromComposer();
    if (!Array.isArray(flat) || !flat.length) return out;
    const reW   = /вес|масса|брутто|нетто|weight|net\s*weight|gross|重量|净重|毛重|质量|重\s*量/i;
    const reLen = /длина|length|长[度]?(?!.*?宽)|глубин|глубина|depth|深/i;
    const reWid = /ширина|width|宽[度]?/i;
    const reHei = /высота|height|высоту|толщин|高[度]?|厚[度]?/i;
    const reDim = /размер[ыа]?|габарит|dimension|пакет|упаков|объ[её]мн|尺寸|外形|包装/i;
    // 同一份 SEP，与 extractWeightAndDims 保持一致（含西里尔 х/Х）
    const SEP = '[\\s]*[\\u0445\\u0425×x*✕✖][\\s]*';
    const triRe = new RegExp(
      '(\\d+(?:[.,]\\d+)?)' + SEP +
      '(\\d+(?:[.,]\\d+)?)' + SEP +
      '(\\d+(?:[.,]\\d+)?)\\s*(мм|mm|см|cm)?',
      'i'
    );
    const numUnit = /(\d+(?:[.,]\d+)?)\s*(мм|mm|см|cm|кг|kg|г|g)?/i;
    const toGrams = (s, u) => {
      const n = parseFloat(String(s).replace(',', '.'));
      if (!isFinite(n) || n <= 0) return null;
      u = String(u || '').toLowerCase();
      if (/кг|kg/.test(u)) return Math.round(n * 1000);
      return Math.round(n);
    };
    const toCm = (s, u) => {
      const n = parseFloat(String(s).replace(',', '.'));
      if (!isFinite(n) || n <= 0) return null;
      u = String(u || '').toLowerCase();
      if (/мм|mm/.test(u)) return +(n / 10).toFixed(1);
      return +n.toFixed(1);
    };

    for (const { name: k, value: v } of flat) {
      if (!k || !v) continue;
      // 三维（优先级最高）
      if (reDim.test(k) || triRe.test(v)) {
        const m = v.match(triRe);
        if (m) {
          const u = m[4] || (/мм|mm/i.test(k) ? 'mm' : 'cm');
          const a = toCm(m[1], u), b = toCm(m[2], u), cc = toCm(m[3], u);
          if (a && b && cc) {
            if (out.dimL == null) out.dimL = a;
            if (out.dimW == null) out.dimW = b;
            if (out.dimH == null) out.dimH = cc;
            continue;
          }
        }
      }
      // 重量
      if (out.weight == null && reW.test(k)) {
        const m = v.match(numUnit);
        if (m) {
          const u = m[2] || (/кг|kg/i.test(k) ? 'kg' : 'g');
          const g = toGrams(m[1], u);
          if (g) out.weight = g;
        }
        continue;
      }
      // 单维
      const m = v.match(numUnit);
      if (!m) continue;
      const u = m[2] || (/мм|mm/i.test(k) ? 'mm' : 'cm');
      const cm = toCm(m[1], u);
      if (!cm) continue;
      if (out.dimL == null && reLen.test(k)) out.dimL = cm;
      else if (out.dimW == null && reWid.test(k)) out.dimW = cm;
      else if (out.dimH == null && reHei.test(k)) out.dimH = cm;
    }
    return out;
  }

  // ── State ───────────────────────────────────────
  let panelEl = null;
  let triggerEl = null;
  let channel = 'CEL';
  let useOldFormula = false; // 平台涨价定价：勾选后黑标 ≤80 用旧公式

  // 6 档 × 3 运输方式（默认全部 economy 陆运经济）
  const TIER_KEYS = ['xs', 'budget', 'small', 'big', 'pSmall', 'pBig'];
  const TIER_META = {
    xs:     { label: 'Extra Small',   range: '0-500g · 货值≤1500₽' },
    budget: { label: 'Budget',        range: '>500g · 货值≤1500₽' },
    small:  { label: 'Small',         range: '0-2kg · 1501-7000₽ · 不计抛' },
    big:    { label: 'Big',           range: '>2kg · 1501-7000₽ · 计抛' },
    pSmall: { label: 'Premium Small', range: '0-5kg · 货值>7000₽' },
    pBig:   { label: 'Premium Big',   range: '>5kg · 货值>7000₽ · 计抛' },
  };
  const TRANSPORT_TYPES = [
    { key: 'fast',    label: '陆空特快',  icon: '✈️' },
    { key: 'std',     label: '陆空标准',  icon: '🛫' },
    { key: 'economy', label: '陆运经济',  icon: '🚛' },
  ];
  const defaultChannelCfg = () => Object.fromEntries(TIER_KEYS.map((k) => [k, 'economy']));
  let postService = 'economy';
  let celConfig  = normalizeChannelCfg('CEL',  defaultChannelCfg());
  let guooConfig = normalizeChannelCfg('GUOO', defaultChannelCfg());
  let xyConfig   = normalizeChannelCfg('XY',   defaultChannelCfg());
  let ztoConfig  = normalizeChannelCfg('ZTO',  defaultChannelCfg());

  // 根据计费重 + 货值，匹配档位 key
  // 用「实重 + 货值」决定档位（货值边界 1500/7000 卢布与 Excel 一致）
  function matchTierKey(actualWeightG, priceCNY) {
    const w = actualWeightG || 0;
    const p = priceCNY || 0;
    const v = p * DPI_FACTOR;
    if (v <= VALUE_LO) return w <= W_XS ? 'xs' : 'budget';
    if (v <= VALUE_HI) return w <= W_SMALL ? 'small' : 'big';
    return w <= W_PSMALL ? 'pSmall' : 'pBig';
  }

  function setProductCommission(rate, source) {
    const parsed = Number(rate);
    productCommission = {
      rate: Number.isFinite(parsed) && parsed >= 0 && parsed <= 100 ? parsed : null,
      source: source || (parsed == null ? '未从 Ozon Seller Portal 读取到真实佣金' : 'Ozon Seller Portal'),
    };
    if (!panelEl) return;
    const rateEl = q('[data-f="commissionRate"]');
    const sourceEl = q('[data-f="commissionSource"]');
    const statusEl = q('[data-f="commissionStatus"]');
    const cardEl = q('[data-f="commissionCard"]');
    if (rateEl) rateEl.textContent = productCommission.rate == null ? '—' : fmt(productCommission.rate, 2) + '%';
    if (sourceEl) sourceEl.textContent = productCommission.source;
    if (statusEl) statusEl.textContent = productCommission.rate == null ? '未获取' : '已同步';
    if (cardEl) cardEl.classList.toggle('unavailable', productCommission.rate == null);
  }

  function findRealCommissionRate(payload) {
    const candidates = [];
    const seen = new WeakSet();
    const add = (key, value) => {
      const name = String(key || '').toLowerCase();
      if (!/(commission|комисс|sales?_?percent|sale_percent|fbs.*percent|percent.*fbs)/i.test(name)) return;
      if (/(amount|sum|fee|cost|руб|price)/i.test(name)) return;
      let rate = Number(String(value).replace(',', '.'));
      if (!Number.isFinite(rate)) return;
      if (rate > 100 && rate <= 10000) rate /= 100;
      if (rate < 0 || rate > 100) return;
      let score = 10;
      if (/rfbs|real.?fbs|fbs/.test(name)) score = 100;
      else if (/sales?_?percent|sale_percent/.test(name)) score = 80;
      else if (/commission|комисс/.test(name)) score = 60;
      candidates.push({ rate, score, name });
    };
    const walk = (node, key = '') => {
      if (node == null) return;
      if (typeof node === 'number' || typeof node === 'string') { add(key, node); return; }
      if (typeof node !== 'object' || seen.has(node)) return;
      seen.add(node);
      if (Array.isArray(node)) { node.forEach((v) => walk(v, key)); return; }
      Object.entries(node).forEach(([k, v]) => walk(v, k));
    };
    walk(payload);
    candidates.sort((a, b) => b.score - a.score);
    return candidates[0] || null;
  }

  function buildChannelConfigModal(channelName, title, subtitle) {
    return `
      <div class="${P}-modal" data-modal="cfg-${channelName}" role="dialog" aria-modal="true" aria-label="${title}">
        <div class="${P}-modal-card ${P}-transport-modal">
          <div class="${P}-modal-head">
            <div>
              <span class="${P}-modal-title">${title}</span>
              <span class="${P}-modal-autosave">自动保存</span>
            </div>
            <button class="${P}-modal-close" data-action="close-modal" aria-label="关闭">×</button>
          </div>
          <div class="${P}-modal-sub">${subtitle}</div>
          <div class="${P}-modal-body" data-cfg-body="${channelName}"></div>
          <div class="${P}-modal-foot">
            ${channelName === 'POST'
              ? `<span class="${P}-modal-foot-note">请选择实际使用的 PUDO 服务</span>`
              : `<button class="${P}-modal-link" data-action="reset-channel-cfg" data-channel="${channelName}">恢复推荐方案</button>`}
            <button class="${P}-modal-btn" data-action="close-modal">完成</button>
          </div>
        </div>
      </div>
    `;
  }

  function buildPanel() {
    const panel = document.createElement('div');
    panel.className = `${P}-panel`;
    panel.setAttribute('lang', 'zh-Hans');
    panel.innerHTML = `
      <div class="${P}-header" data-drag>
        <div class="${P}-brand-mark">快</div>
        <div class="${P}-brand-text">
          <span class="${P}-brand-name">快速核价</span>
          <span class="${P}-brand-sub">Ozon 跨境利润计算</span>
        </div>
        <div class="${P}-spacer"></div>
        <div class="${P}-sku" data-f="sku">—</div>
        <div class="${P}-actions">
          <button data-action="min" title="最小化">−</button>
          <button data-action="close" title="关闭">×</button>
        </div>
      </div>

      <div class="${P}-body">
        <!-- Hero -->
        <div class="${P}-hero empty" data-f="hero">
          <div class="${P}-hero-top">
            <div class="${P}-chip" data-f="statusChip">待输入</div>
          </div>
          <div class="${P}-profit-row">
            <div class="${P}-profit-main">
              <div class="${P}-profit-label">预估单件净利润</div>
              <div class="${P}-profit-num">
                <span class="sym">¥</span>
                <span class="big" data-f="profit">0.00</span>
              </div>
            </div>
            <div class="${P}-rate-badge">
              <div class="l">利润率</div>
              <div class="v" data-f="rate">0%</div>
            </div>
          </div>
          <div class="${P}-ruler">
            <div class="${P}-ruler-scale">
              <span>0%</span><span class="mid">安全线 15%</span><span>40%+</span>
            </div>
            <div class="${P}-ruler-track">
              <div class="${P}-ruler-fill" data-f="rulerFill" style="width:0%"></div>
              <div class="${P}-ruler-tick"></div>
            </div>
          </div>
        </div>

        <div class="${P}-banner" data-f="banner">
          <div class="${P}-banner-ic">!</div>
          <div data-f="bannerText"></div>
        </div>

        <!-- Price conversion -->
        <div class="${P}-section">
          <div class="${P}-section-head">
            <span class="${P}-section-label">Ozon 价格换算</span>
            <span class="${P}-section-hint" data-f="formula">—</span>
          </div>
          <div class="${P}-pricetags">
            <label class="${P}-pricetag">
              <span class="${P}-pricetag-badge" style="background:#0a0c14">黑标</span>
              <input class="num" type="number" step="0.01" data-f="blackPrice" placeholder="—"/>
            </label>
            <label class="${P}-pricetag">
              <span class="${P}-pricetag-badge" style="background:#00a046">绿标</span>
              <input class="num" type="number" step="0.01" data-f="greenPrice" placeholder="—"/>
            </label>
            <label class="${P}-pricetag strong">
              <span class="${P}-pricetag-badge" style="background:#005bff">实际售价</span>
              <input class="num" type="text" readonly data-f="actualPrice" placeholder="—"/>
            </label>
          </div>
        </div>

        <!-- Specs -->
        <div class="${P}-section">
          <div class="${P}-section-head">
            <span class="${P}-section-label">成本与规格</span>
          </div>
          <div class="${P}-specs">
            <label class="${P}-spec">
              <div class="${P}-spec-head"><span>定价</span><span>¥</span></div>
              <input type="number" step="0.01" data-f="price" placeholder="—"/>
            </label>
            <label class="${P}-spec">
              <div class="${P}-spec-head"><span>采购</span><span>¥</span></div>
              <input type="number" step="0.01" data-f="cost" placeholder="—"/>
            </label>
            <label class="${P}-spec">
              <div class="${P}-spec-head"><span>重量</span><span>g</span></div>
              <input type="number" step="1" data-f="weight" placeholder="—"/>
            </label>
          </div>
        </div>

        <!-- Commission -->
        <div class="${P}-section">
          <div class="${P}-section-head">
            <span class="${P}-section-label">Ozon 真实佣金</span>
            <span class="${P}-section-hint primary" data-f="commissionStatus">正在读取</span>
          </div>
          <div class="${P}-commission-card" data-f="commissionCard">
            <div>
              <div class="${P}-commission-title">当前商品佣金率</div>
              <div class="${P}-commission-source" data-f="commissionSource">需登录 Ozon Seller Portal 自动读取</div>
            </div>
            <div class="${P}-commission-rate" data-f="commissionRate">—</div>
          </div>
        </div>

        <!-- Channel -->
        <div class="${P}-section">
          <div class="${P}-channels">
            <span class="${P}-channels-label">国际物流</span>
            <div class="${P}-channels-group">
              <div class="${P}-ch-cell" data-ch-cell="POST">
                <button class="${P}-ch" data-ch="POST">
                  <div class="${P}-ch-name">China Post</div>
                  <div class="${P}-ch-sub" data-f="chSub_POST">Economy PUDO</div>
                </button>
                <button class="${P}-ch-gear" data-action="open-cfg" data-channel="POST" title="China Post 运输方式配置">⚙</button>
              </div>
              <div class="${P}-ch-cell active" data-ch-cell="CEL">
                <button class="${P}-ch active" data-ch="CEL">
                  <div class="${P}-ch-name">CEL</div>
                  <div class="${P}-ch-sub" data-f="chSub_CEL">陆运经济</div>
                </button>
                <button class="${P}-ch-gear" data-action="open-cfg" data-channel="CEL" title="CEL 渠道运输方式配置">⚙</button>
              </div>
              <div class="${P}-ch-cell" data-ch-cell="GUOO">
                <button class="${P}-ch" data-ch="GUOO">
                  <div class="${P}-ch-name">GUOO</div>
                  <div class="${P}-ch-sub" data-f="chSub_GUOO">陆运经济</div>
                </button>
                <button class="${P}-ch-gear" data-action="open-cfg" data-channel="GUOO" title="GUOO 渠道运输方式配置">⚙</button>
              </div>
              <div class="${P}-ch-cell" data-ch-cell="XY">
                <button class="${P}-ch" data-ch="XY">
                  <div class="${P}-ch-name">兴远</div>
                  <div class="${P}-ch-sub" data-f="chSub_XY">陆运经济</div>
                </button>
                <button class="${P}-ch-gear" data-action="open-cfg" data-channel="XY" title="兴远 XY 渠道运输方式配置">⚙</button>
              </div>
              <div class="${P}-ch-cell" data-ch-cell="ZTO">
                <button class="${P}-ch" data-ch="ZTO">
                  <div class="${P}-ch-name">ZTO</div>
                  <div class="${P}-ch-sub" data-f="chSub_ZTO">陆运经济</div>
                </button>
                <button class="${P}-ch-gear" data-action="open-cfg" data-channel="ZTO" title="中通 ZTO rFBS 渠道运输方式配置">⚙</button>
              </div>
            </div>
          </div>
        </div>

        <!-- Breakdown · 视觉层级强化：总计置顶 + 列表降噪 -->
        <div class="${P}-section ${P}-breakdown-section">
          <div class="${P}-break-hero">
            <div class="${P}-break-hero-top">
              <span class="${P}-break-hero-kicker">≈ 每件运营总投入</span>
              <span class="${P}-break-hero-unit">单件 · 人民币</span>
            </div>
            <div class="${P}-break-hero-num" data-f="bkTotal">¥—</div>
          </div>
          <div class="${P}-break-list-label">分项</div>
          <div class="${P}-breakdown">
            <div class="${P}-break-row" data-cat="cost">
              <span class="${P}-break-ic"></span>
              <span class="k">商品采购</span>
              <span class="v" data-f="bkCost">—</span>
            </div>
            <div class="${P}-break-row" data-cat="freight">
              <span class="${P}-break-ic"></span>
              <div class="${P}-break-row-label">
                <span class="k">国际运费</span>
                <span class="${P}-break-row-sub" data-f="bkFreightSub">CEL · Extra Small · 陆运经济</span>
              </div>
              <span class="v" data-f="bkFreight">—</span>
            </div>
            <div class="${P}-break-row" data-cat="comm">
              <span class="${P}-break-ic"></span>
              <span class="k" data-f="bkCommLabel">Ozon 佣金 · T1 · 13%</span>
              <span class="v" data-f="bkComm">—</span>
            </div>
            <div class="${P}-break-row" data-cat="plat">
              <span class="${P}-break-ic"></span>
              <span class="k">平台处理费 · 4%</span>
              <span class="v" data-f="bkPlat">—</span>
            </div>
            <div class="${P}-break-row" data-cat="label">
              <span class="${P}-break-ic"></span>
              <span class="k">贴单服务</span>
              <span class="v">−¥5.00</span>
            </div>
          </div>
        </div>

        <!-- Volumetric -->
        <div class="${P}-section">
          <div class="${P}-section-head">
            <span class="${P}-section-label">计抛检查 · L×W×H / 6000</span>
            <span class="${P}-vol-tag" data-f="volTag" style="display:none"></span>
          </div>
          <div class="${P}-specs">
            <label class="${P}-spec">
              <div class="${P}-spec-head"><span>长</span><span>cm</span></div>
              <input type="number" step="0.1" data-f="dimL" placeholder="—"/>
            </label>
            <label class="${P}-spec">
              <div class="${P}-spec-head"><span>宽</span><span>cm</span></div>
              <input type="number" step="0.1" data-f="dimW" placeholder="—"/>
            </label>
            <label class="${P}-spec">
              <div class="${P}-spec-head"><span>高</span><span>cm</span></div>
              <input type="number" step="0.1" data-f="dimH" placeholder="—"/>
            </label>
          </div>
        </div>
      </div>

      <div class="${P}-quick-actions">
        <button class="${P}-quick-btn warn" data-action="open-prohibited">🚫 禁运清单</button>
      </div>

      <div class="${P}-section ${P}-old-formula-row">
        <label class="${P}-checkbox-label">
          <input type="checkbox" data-f="useOldFormula"/>
          <span>平台涨价定价</span>
          <span class="${P}-checkbox-hint">勾选后，黑标 ≤80 使用旧公式计算</span>
        </label>
      </div>

      <div class="${P}-footer">
        <button class="${P}-cta" data-action="export">📋 复制结果</button>
      </div>

      ${buildChannelConfigModal('POST', 'China Post 运输方式配置', '选择 Economy PUDO 或 Standard PUDO；两者均按实重，重量 1–500g。')}
      ${buildChannelConfigModal('CEL', 'CEL 运输方式配置', '按重量/货值档位选择方式；点击后立即保存并用于核价。')}
      ${buildChannelConfigModal('GUOO', 'GUOO 运输方式配置', '按重量/货值档位选择方式；灰色项表示该档不支持。')}
      ${buildChannelConfigModal('XY', '兴远 运输方式配置', '兴远不提供陆空特快；其余配置按档位自动保存。')}
      ${buildChannelConfigModal('ZTO', 'ZTO 运输方式配置', 'Big 仅陆空标准；Premium Small 的陆运经济按 24000 计抛。')}

      <div class="${P}-modal" data-modal="prohibited">
        <div class="${P}-modal-card">
          <div class="${P}-modal-head">
            <span class="${P}-modal-title">🚫 禁运清单</span>
            <button class="${P}-modal-close" data-action="close-modal">×</button>
          </div>
          <div class="${P}-modal-body">
            <p style="margin:0 0 10px;font-size:12px;color:#3a3e47;line-height:1.7;">
              <b>以下品类禁止运输到 Ozon 仓：</b>
            </p>
            <ul class="${P}-prohibited-list">
              <li>🔋 纯电产品（移动电源、电池等）</li>
              <li>🧪 白色粉末、化学制剂</li>
              <li>💧 液体、膏体（化妆品、洗护用品等）</li>
              <li>⚠️ 危险品、易燃易爆品</li>
              <li>💊 处方药、保健品</li>
              <li>🔫 仿真枪械、刀具</li>
              <li>🚬 烟酒</li>
              <li>📡 涉政、宗教、色情敏感物品</li>
            </ul>
            <p style="margin:10px 0 0;font-size:11px;color:#9aa0a8;line-height:1.5;">
              ⓘ 如不确定您的商品是否禁运，建议联系物流商确认，避免清关被扣。
            </p>
          </div>
          <div class="${P}-modal-foot">
            <button class="${P}-modal-btn" data-action="close-modal">我知道了</button>
          </div>
        </div>
      </div>
    `;
    return panel;
  }

  function formatFreightRule(tier, transport) {
    const base = tier.baseEconomy != null && transport === 'economy' ? tier.baseEconomy : tier.base;
    return `首票 ¥${fmt(base)} + ¥${fmt(tier.rates[transport], 4)}/g`;
  }

  // 渲染单个渠道配置 modal。China Post 提供两种 PUDO 服务，其余四个渠道分 6 档配置。
  function renderChannelCfgBody(channelName) {
    const body = panelEl.querySelector(`[data-cfg-body="${channelName}"]`);
    if (!body) return;
    const rateMeta = RATE_TABLE_META[channelName] || {};
    if (channelName === 'POST') {
      const serviceCards = Object.entries(CHINA_POST_SERVICES).map(([key, service]) => {
        const active = key === postService;
        return `
          <button class="${P}-post-service${active ? ' active' : ''}" data-post-service="${key}" aria-pressed="${active}">
            <span class="${P}-post-service-check">${active ? '✓' : ''}</span>
            <span class="${P}-post-service-main">
              <span class="${P}-post-service-name">${service.label}</span>
              <span class="${P}-post-service-sub">${service.short} · ${service.delivery} · 按物理重量</span>
            </span>
            <span class="${P}-post-service-rate">¥${fmt(service.startPrice)}起</span>
          </button>
        `;
      }).join('');
      body.innerHTML = `
        <div class="${P}-cfg-summary">
          <span class="${P}-cfg-summary-ok">已选择 1/2 服务</span>
          <span>${rateMeta.source || '内置资费表'} · ${rateMeta.version || '待核验'}</span>
        </div>
        <div class="${P}-post-service-list">${serviceCards}</div>
        <div class="${P}-flat-rule-note">限制：商品价 0.01–135 元，物理重量 1–500g，不计长宽高。页面仅提供“起价”，核价结果会标记为起价，完整续重费率需以 Ozon 后台账单为准。</div>
      `;
      return;
    }
    const cfg = channelCfgRef(channelName);
    const table = freightTableFor(channelName);
    if (!cfg || !table) return;
    body.innerHTML = `
      <div class="${P}-cfg-summary">
        <span class="${P}-cfg-summary-ok">已配置 ${TIER_KEYS.length}/${TIER_KEYS.length} 档</span>
        <span>${rateMeta.source || '内置资费表'} · ${rateMeta.version || '待核验'}</span>
      </div>
    ` + TIER_KEYS.map((k) => {
      const meta = TIER_META[k];
      const tier = table[k];
      const selected = cfg[k];
      const selectedTransport = TRANSPORT_TYPES.find((t) => t.key === selected);
      const buttons = TRANSPORT_TYPES.map((t) => {
        const available = tier.rates[t.key] != null;
        const active = available && cfg[k] === t.key;
        const cls = `${P}-tt-btn${active ? ' active' : ''}${available ? '' : ' disabled'}`;
        const attrs = available
          ? `data-cfg-set="${channelName}" data-cfg-tier="${k}" data-cfg-type="${t.key}" aria-pressed="${active}"`
          : `disabled aria-disabled="true" title="${channelName} · ${meta.label} 不支持${t.label}"`;
        return `<button class="${cls}" ${attrs}>
          <span class="${P}-tt-ic">${active ? '✓' : t.icon}</span>${t.label}
        </button>`;
      }).join('');
      return `
        <div class="${P}-tt-tier">
          <div class="${P}-tt-tier-head">
            <span class="${P}-tt-tier-name">${meta.label}</span>
            <span class="${P}-tt-tier-range">${meta.range}</span>
          </div>
          <div class="${P}-tt-tier-rule">当前：${selectedTransport?.label || '—'} · ${formatFreightRule(tier, selected)}</div>
          <div class="${P}-tt-tier-btns">${buttons}</div>
        </div>
      `;
    }).join('');
  }

  function showTrigger() {
    if (triggerEl) return;
    const btn = document.createElement('button');
    btn.className = `${P}-trigger`;
    btn.title = '快速核价 · 点击展开';
    btn.textContent = '快';
    btn.addEventListener('click', () => {
      btn.remove();
      triggerEl = null;
      mountPanel();
    });
    const wrapper = document.querySelector('.qe-float-wrapper');
    if (wrapper) {
      wrapper.appendChild(btn);
    } else {
      document.body.appendChild(btn);
    }
    triggerEl = btn;
  }

  function mountPanel() {
    if (panelEl && document.contains(panelEl)) return;
    if (!document.body) {
      setTimeout(mountPanel, 200);
      return;
    }
    document.querySelectorAll(`.${P}-panel`).forEach((n) => n.remove());

    const panel = buildPanel();
    document.body.appendChild(panel);
    panelEl = panel;
    setProductCommission(productCommission.rate, productCommission.source);

    makeDraggable(panel);
    bindEvents();
    loadSettings();          // 内部依次：恢复偏好 → 恢复 SKU specs → autoFillFromPage(true) → recalcAll
    ALL_CHANNELS.forEach(renderChannelCfgBody);
  }

  // ── Drag ────────────────────────────────────────
  function makeDraggable(panel) {
    const header = panel.querySelector(`.${P}-header`);
    let sx, sy, ox, oy;
    header.addEventListener('mousedown', (e) => {
      if (e.target.closest('button')) return;
      e.preventDefault();
      sx = e.clientX; sy = e.clientY;
      const r = panel.getBoundingClientRect();
      ox = r.left; oy = r.top;
      const mv = (ev) => {
        panel.style.left = (ox + ev.clientX - sx) + 'px';
        panel.style.top  = (oy + ev.clientY - sy) + 'px';
        panel.style.right = 'auto';
      };
      const up = () => {
        document.removeEventListener('mousemove', mv);
        document.removeEventListener('mouseup', up);
        savePosition(panel);
      };
      document.addEventListener('mousemove', mv);
      document.addEventListener('mouseup', up);
    });
  }

  function savePosition(panel) {
    const r = panel.getBoundingClientRect();
    storageSet({
      [`${LS_STATE}_pos`]: { left: r.left, top: r.top },
    });
  }

  // ── Events ──────────────────────────────────────
  const q = (sel) => panelEl && panelEl.querySelector(sel);
  const qa = (sel) => (panelEl ? panelEl.querySelectorAll(sel) : []);
  const val = (sel) => parseFloat(q(sel)?.value) || 0;

  function bindEvents() {
    qa('input[data-f]').forEach((el) => {
      if (el.readOnly) return;
      el.addEventListener('input', () => {
        // 用户手输 → 标记，自动抓取（DOM/composer/seller portal）永不覆盖
        el.dataset.jzcUserEdited = '1';
        recalcAll();
        saveState();
      });
    });

    qa(`.${P}-ch`).forEach((btn) => {
      btn.addEventListener('click', () => {
        qa(`.${P}-ch`).forEach((b) => b.classList.remove('active'));
        btn.classList.add('active');
        channel = btn.dataset.ch;
        saveState();
        recalcAll();
      });
    });

    panelEl.addEventListener('click', (e) => {
      const postBtn = e.target.closest('[data-post-service]');
      if (postBtn) {
        postService = postBtn.dataset.postService in CHINA_POST_SERVICES ? postBtn.dataset.postService : 'economy';
        renderChannelCfgBody('POST');
        saveState();
        recalcAll();
        return;
      }
      const tBtn = e.target.closest('[data-cfg-set]');
      if (tBtn) {
        const ch = tBtn.dataset.cfgSet;
        const tier = tBtn.dataset.cfgTier;
        const type = tBtn.dataset.cfgType;
        const cfg = channelCfgRef(ch);
        if (cfg) cfg[tier] = type;
        renderChannelCfgBody(ch);
        saveState();
        recalcAll();
        return;
      }
      const t = e.target.closest('[data-action]');
      if (!t) return;
      const a = t.dataset.action;
      if (a === 'close') {
        panelEl.remove();
        panelEl = null;
        showTrigger();
      } else if (a === 'min') {
        const minimized = panelEl.classList.toggle('minimized');
        t.textContent = minimized ? '+' : '−';
        saveState();
      } else if (a === 'export') {
        copyBreakdown(t);
      } else if (a === 'open-cfg') {
        const ch = t.dataset.channel;
        const m = panelEl.querySelector(`[data-modal="cfg-${ch}"]`);
        if (m) {
          renderChannelCfgBody(ch);
          m.classList.add('show');
        }
      } else if (a === 'open-prohibited') {
        const m = panelEl.querySelector('[data-modal="prohibited"]');
        if (m) m.classList.add('show');
      } else if (a === 'close-modal') {
        const m = t.closest(`.${P}-modal`);
        if (m) m.classList.remove('show');
      } else if (a === 'reset-channel-cfg') {
        const ch = t.dataset.channel;
        if (freightTableFor(ch)) {
          setChannelCfg(ch, normalizeChannelCfg(ch, defaultChannelCfg()));
          renderChannelCfgBody(ch);
          saveState();
          recalcAll();
        }
      }
    });

    // Checkbox: 平台涨价定价
    const oldFormulaCb = q('[data-f="useOldFormula"]');
    if (oldFormulaCb) {
      oldFormulaCb.addEventListener('change', (e) => {
        useOldFormula = !!e.target.checked;
        saveState();
        recalcAll();
      });
    }

  }

  function flashBtn(btn, msg) {
    const orig = btn.textContent;
    btn.textContent = msg;
    setTimeout(() => { btn.textContent = orig; }, 1200);
  }

  function copyBreakdown(btn) {
    const v = (sel) => q(sel)?.textContent || '—';
    const text = [
      `快速核价 · ${v(`[data-f="sku"]`)}`,
      `利润: ${v(`[data-f="profit"]`)}  利润率: ${v(`[data-f="rate"]`)}`,
      `---`,
      `黑标: ${q(`[data-f="blackPrice"]`)?.value || '—'}₽  绿标: ${q(`[data-f="greenPrice"]`)?.value || '—'}₽  实际售价: ${q(`[data-f="actualPrice"]`)?.value || '—'}₽`,
      `定价: ${q(`[data-f="price"]`)?.value || '—'}¥  采购: ${q(`[data-f="cost"]`)?.value || '—'}¥  重量: ${q(`[data-f="weight"]`)?.value || '—'}g`,
      `---`,
      `运费: ${v(`[data-f="bkFreight"]`)}  佣金: ${v(`[data-f="bkComm"]`)}  平台费: ${v(`[data-f="bkPlat"]`)}  贴单: −¥5.00`,
      `总成本: ${v(`[data-f="bkTotal"]`)}`,
    ].join('\n');
    navigator.clipboard.writeText(text).then(() => flashBtn(btn, '✓ 已复制')).catch(() => flashBtn(btn, '✗ 失败'));
  }

  // ── Recalc ──────────────────────────────────────
  function recalcAll() {
    if (!panelEl) return;
    calcActual();
    const price    = val(`[data-f="price"]`);
    const cost     = val(`[data-f="cost"]`);
    const weight   = val(`[data-f="weight"]`);
    const commRate = productCommission.rate;

    const dimL = val(`[data-f="dimL"]`), dimW = val(`[data-f="dimW"]`), dimH = val(`[data-f="dimH"]`);
    const dimVolume = (dimL > 0 && dimW > 0 && dimH > 0) ? dimL * dimW * dimH : 0;  // cm³
    const volTag = q(`[data-f="volTag"]`);
    volTag.style.display = 'none';

    const hero = q(`[data-f="hero"]`);
    const chip = q(`[data-f="statusChip"]`);
    const profitEl = q(`[data-f="profit"]`);
    const rateEl = q(`[data-f="rate"]`);
    const fill = q(`[data-f="rulerFill"]`);
    const banner = q(`[data-f="banner"]`);

    hero.classList.remove('ok', 'warn', 'danger', 'empty');

    if (price <= 0) {
      hero.classList.add('empty');
      chip.textContent = '待输入';
      profitEl.textContent = '0.00';
      rateEl.textContent = '0%';
      fill.style.width = '0%';
      banner.classList.remove('show', 'warn', 'danger');
      q(`[data-f="bkCost"]`).textContent = '—';
      q(`[data-f="bkFreight"]`).textContent = '—';
      q(`[data-f="bkComm"]`).textContent = '—';
      q(`[data-f="bkPlat"]`).textContent = '—';
      q(`[data-f="bkTotal"]`).textContent = '¥—';
      return;
    }

    const freightInfo = calcFreight(channel, weight, dimVolume, price);
    const freight = freightInfo.fee;
    if (freightInfo.billed > weight && weight > 0) {
      volTag.style.display = '';
      volTag.className = `${P}-vol-tag warn`;
      volTag.textContent = `⚠ 按体积重 ${Math.round(freightInfo.billed)}g`;
    } else if (weight > 0) {
      volTag.style.display = '';
      volTag.className = `${P}-vol-tag ok`;
      volTag.textContent = '✓ 按实重';
    }
    if (commRate == null) {
      hero.classList.add('warn');
      chip.textContent = '佣金待读取';
      profitEl.textContent = '—';
      rateEl.textContent = '—';
      fill.style.width = '0%';
      banner.classList.add('show', 'warn');
      banner.classList.remove('danger');
      q(`[data-f="bannerText"]`).innerHTML = '<b>未读取到当前商品的真实 Ozon 佣金</b>，利润暂不计算。请确认 seller.ozon.ru 已登录后刷新商品页。';
      q(`[data-f="bkCost"]`).textContent = '−¥' + fmt(cost);
      q(`[data-f="bkFreight"]`).textContent = '−¥' + fmt(freight) + (freightInfo.isStartingPrice ? ' 起' : '');
      q(`[data-f="bkCommLabel"]`).textContent = 'Ozon 真实佣金 · 未读取';
      q(`[data-f="bkComm"]`).textContent = '—';
      q(`[data-f="bkPlat"]`).textContent = '−¥' + fmt(price * PLATFORM_FEE_RATE);
      q(`[data-f="bkTotal"]`).textContent = '¥—';
      return;
    }
    const comm = price * (commRate / 100);
    const platFee = price * PLATFORM_FEE_RATE;
    const totalCost = cost + freight + comm + platFee + LABEL_FEE;
    const profit = price - totalCost;
    const rate = totalCost > 0 ? (profit / totalCost) * 100 : 0;

    const danger = profit < 0;
    const warn = !danger && rate < SAFETY_RATE * 100;
    const state = danger ? 'danger' : warn ? 'warn' : 'ok';
    hero.classList.add(state);
    chip.textContent = danger ? '亏损' : warn ? '利润偏薄' : '利润健康';

    profitEl.textContent = (profit > 0 ? '+' : '') + fmt(profit);
    rateEl.textContent = fmt(rate, 1) + '%';
    fill.style.width = Math.max(0, Math.min(100, rate * 2)) + '%';

    if (danger) {
      banner.classList.add('show', 'danger');
      banner.classList.remove('warn');
      q(`[data-f="bannerText"]`).innerHTML =
        `<b>正在亏损</b>，建议调价至 <b>¥${fmt(price + Math.abs(profit) + SAFETY_RATE * price, 0)}</b>（达 15% 安全线）`;
    } else if (warn) {
      banner.classList.add('show', 'warn');
      banner.classList.remove('danger');
      q(`[data-f="bannerText"]`).innerHTML =
        `距 <b>15% 安全线</b>还差 <b>¥${fmt(SAFETY_RATE * price - profit)}</b>/单，可略微提价或换 ${channel === 'CEL' ? 'GUOO' : 'CEL'}`;
    } else {
      banner.classList.remove('show', 'warn', 'danger');
    }

    q(`[data-f="bkCost"]`).textContent = '−¥' + fmt(cost);
    const chLbl = CHANNEL_LABELS[channel]?.short || channel;
    let freightSub;
    if (freightInfo.type === 'flat') {
      const flags = [];
      if (freightInfo.overWeight) flags.push('⚠超重');
      if (freightInfo.overValue)  flags.push('⚠超额');
      if (freightInfo.isStartingPrice) flags.unshift(`¥${fmt(freightInfo.fee)}起`);
      freightSub = [chLbl, freightInfo.note, ...flags].filter(Boolean).join(' · ');
    } else {
      const tierLbl  = TIER_META[freightInfo.tierKey]?.label || '';
      const transLbl = TRANSPORT_TYPES.find((t) => t.key === freightInfo.transport)?.label || '';
      freightSub = `${chLbl} · ${tierLbl} · ${transLbl}`;
    }
    q(`[data-f="bkFreightSub"]`).textContent = freightSub;
    q(`[data-f="bkFreight"]`).textContent = '−¥' + fmt(freight) + (freightInfo.isStartingPrice ? ' 起' : '');
    q(`[data-f="bkCommLabel"]`).textContent = `Ozon 真实佣金 · ${fmt(commRate, 2)}%`;
    q(`[data-f="bkComm"]`).textContent = '−¥' + fmt(comm);
    q(`[data-f="bkPlat"]`).textContent = '−¥' + fmt(platFee);
    q(`[data-f="bkTotal"]`).textContent = '¥' + fmt(totalCost);

    // 更新国际物流按钮副标题：tiered 渠道按当前档位实际生效的运输方式显示
    // 若 freight 计算用的是当前 channel，tierKey 已知；否则按实重重新匹配档位
    const matchedTier = isFlatChannel(channel)
      ? matchTierKey(weight, price)
      : freightInfo.tierKey;
    TIERED_CHANNELS.forEach((ch) => {
      const el = q(`[data-f="chSub_${ch}"]`);
      if (!el) return;
      const cfg = channelCfgRef(ch);
      const tier = freightTableFor(ch)[matchedTier];
      let t = (cfg && cfg[matchedTier]) || 'economy';
      if (!tier?.rates?.[t]) {
        t = ['economy', 'std', 'fast'].find((k) => tier?.rates?.[k] != null) || 'economy';
      }
      el.textContent = TRANSPORT_TYPES.find((x) => x.key === t)?.label || '陆运经济';
    });
    const postSub = q('[data-f="chSub_POST"]');
    if (postSub) postSub.textContent = (CHINA_POST_SERVICES[postService] || CHINA_POST_SERVICES.economy).short;
  }

  function calcActual() {
    const black = val(`[data-f="blackPrice"]`);
    const green = val(`[data-f="greenPrice"]`);
    const apEl = q(`[data-f="actualPrice"]`);
    const fmEl = q(`[data-f="formula"]`);
    if (black <= 0) { apEl.value = ''; fmEl.textContent = '—'; return; }
    // 默认始终使用新公式 (黑−绿)×2.25 + 黑
    // 勾选「平台涨价定价」时，黑标 ≤80 切到旧公式 黑 ÷ 1.0715
    let actual, formula;
    if (useOldFormula && black <= 80) {
      actual = black / 1.0715;
      formula = '黑 ÷ 1.0715 (≤80 旧公式)';
    } else {
      actual = (black - green) * 2.25 + black;
      formula = '(黑−绿)×2.25 + 黑';
    }
    apEl.value = fmt(actual, 0);
    fmEl.textContent = formula;
  }

  function autoFillFromPage(onlyEmpty = false) {
    const d = extractProductData();
    // setIf 守则：
    //   ① 用户手输过的字段（jzcUserEdited=1）永不覆盖
    //   ② force=true 时绕过 onlyEmpty 限制（仅 seller portal 走这条）
    //   ③ 否则按 onlyEmpty 模式处理
    const setIf = (key, val, force = false) => {
      if (val == null) return false;
      const el = q(`[data-f="${key}"]`);
      if (!el) return false;
      if (el.dataset.jzcUserEdited === '1') return false;
      const next = String(val);
      if (el.value === next) return false;
      if (!force && onlyEmpty && el.value) return false;
      el.value = next;
      el.classList.add(`${P}-autofilled`);
      setTimeout(() => el.classList.remove(`${P}-autofilled`), 1600);
      return true;
    };

    // ── 价格段：DOM 抓的（同步）
    setIf('blackPrice', d.blackPrice);
    setIf('greenPrice', d.greenPrice);
    if (d.sku) q(`[data-f="sku"]`).textContent = 'SKU ' + d.sku;
    // 定价默认 = 绿标价（页面取到的已是人民币，不做汇率换算）
    if (d.greenPrice != null) {
      setIf('price', d.greenPrice);
    } else if (d.blackPrice != null) {
      setIf('price', d.blackPrice);
    }

    // ── 重量/尺寸：DOM 先填占位（最低优先级），seller portal 回来会强制覆盖
    setIf('weight', d.weight);
    setIf('dimL',   d.dimL);
    setIf('dimW',   d.dimW);
    setIf('dimH',   d.dimH);
    recalcAll();
    saveState();

    // ── 异步抓取（优先级：seller portal > composer-api > DOM 占位）
    const sku = d.sku || currentSku();
    const stillMissing = () =>
      ['weight', 'dimL', 'dimW', 'dimH'].some((f) => !q(`[data-f="${f}"]`)?.value);
    console.log('[快速核价] DOM 取数:', { weight: d.weight, dimL: d.dimL, dimW: d.dimW, dimH: d.dimH });

    const applyExtra = (extra, force = false) => {
      let t = false;
      ['weight', 'dimL', 'dimW', 'dimH'].forEach((f) => {
        if (extra[f] != null) t = setIf(f, extra[f], force) || t;
      });
      if (t) { recalcAll(); saveState(); }
      return t;
    };

    const composerFallback = () => {
      if (!stillMissing()) return;
      autoFillFromComposer().then((extra) => {
        console.log('[快速核价] composer 取数:', extra);
        applyExtra(extra);
      });
    };

    if (sku) {
      // 最高优先级：seller portal。结果回来后强制覆盖 DOM 占位
      autoFillFromSellerPortal(sku).then((sp) => {
        console.log('[快速核价] seller portal 取数:', sp);
        if (sp.commissionRate != null) {
          setProductCommission(sp.commissionRate, sp.commissionSource);
        } else {
          setProductCommission(null, '未从 Ozon Seller Portal 返回当前商品佣金');
        }
        recalcAll();
        const got = ['weight', 'dimL', 'dimW', 'dimH'].some((f) => sp[f] != null);
        if (got) applyExtra(sp, /* force */ true);
        else composerFallback();
      });
    } else {
      composerFallback();
    }
    return d;
  }

  // Seller Portal: 通过 background 调 /api/v1/search-variant-model
  // 返回值 attributes 数组，约定 key:
  //   4383 = 重量(g)，回退 4497 = 含包装重量(g)
  //   9454 = depth(mm) → length；9455 = width(mm)；9456 = height(mm)
  // 需用户已登录 seller.ozon.ru；未登录时 background 抛错，本函数返回空对象
  // 同 sku 多次调用复用缓存（autoFillFromPage 在 retry 节奏里会被多次触发）
  const _sellerPortalCache = new Map();
  async function autoFillFromSellerPortal(sku) {
    if (!sku) return {};
    if (_sellerPortalCache.has(sku)) return _sellerPortalCache.get(sku);
    const promise = new Promise((resolve) => {
      let done = false;
      const finish = (val) => {
        if (done) return;
        done = true;
        resolve(val);
      };
      try {
        chrome.runtime.sendMessage({ type: 'jzc:searchVariants', sku }, (resp) => {
          if (chrome.runtime.lastError) {
            console.warn('[快速核价] seller portal: ' + chrome.runtime.lastError.message);
            finish({});
            return;
          }
          if (!resp?.ok) {
            console.warn('[快速核价] seller portal:', resp?.error || 'no response');
            finish({});
            return;
          }
          const items = resp.data?.items || [];
          const item =
            items.find((it) => String(it.variant_id) === String(sku)) ||
            items.find((it) => String(it.product_id) === String(sku)) ||
            items[0];
          if (!item) { finish({}); return; }
          const attr = new Map((item.attributes || []).map((a) => [String(a.key), a]));
          const num = (k) => Number(attr.get(k)?.value) || 0;
          const out = {};
          const w = num('4383') || num('4497');
          if (w > 0) out.weight = w;
          const depth = num('9454'), width = num('9455'), height = num('9456');
          if (depth > 0)  out.dimL = +(depth / 10).toFixed(1);
          if (width > 0)  out.dimW = +(width / 10).toFixed(1);
          if (height > 0) out.dimH = +(height / 10).toFixed(1);
          const commission = findRealCommissionRate(item);
          if (commission) {
            out.commissionRate = commission.rate;
            out.commissionSource = `Ozon Seller Portal · ${commission.name}`;
          }
          finish(out);
        });
      } catch (e) {
        console.warn('[快速核价] seller portal: ' + (e?.message || e));
        finish({});
      }
      // 安全超时（background 端 25s + executeScript 自身延时）
      setTimeout(() => finish({}), 30000);
    });
    _sellerPortalCache.set(sku, promise);
    return promise;
  }

  // Ozon 商品页的「特征」widget 经常懒加载，初次 mount 时取不到。
  // 双策略：
  //   A. 退避节奏定时探测（上限 12s，覆盖慢网络）
  //   B. MutationObserver 监听新增 [data-state]，命中即触发一次 autoFill
  let _autoFillObserver = null;
  function scheduleAutoFillRetries() {
    const allFilled = () =>
      ['weight', 'dimL', 'dimW', 'dimH'].every((f) => q(`[data-f="${f}"]`)?.value);

    [600, 1500, 3500, 7000, 12000].forEach((delay) => {
      setTimeout(() => {
        if (!allFilled()) autoFillFromPage(true);
      }, delay);
    });

    if (_autoFillObserver) _autoFillObserver.disconnect();
    let triggered = 0;
    _autoFillObserver = new MutationObserver((muts) => {
      if (allFilled() || triggered > 6) {
        _autoFillObserver?.disconnect();
        return;
      }
      const newStateNode = muts.some((m) =>
        Array.from(m.addedNodes).some(
          (n) =>
            n.nodeType === 1 &&
            (n.matches?.('[data-state]') || n.querySelector?.('[data-state]'))
        )
      );
      if (newStateNode) {
        triggered += 1;
        autoFillFromPage(true);
      }
    });
    _autoFillObserver.observe(document.body, { childList: true, subtree: true });
    setTimeout(() => _autoFillObserver?.disconnect(), 30000);
  }

  function currentSku() {
    return location.pathname.match(/\/product\/[^/]*-(\d+)/)?.[1] || null;
  }

  const SPEC_FIELDS = ['price', 'cost', 'weight', 'dimL', 'dimW', 'dimH'];

  function saveState() {
    const state = {
      channel,
      postService,
      useOldFormula,
      celConfig,
      guooConfig,
      xyConfig,
      ztoConfig,
      minimized: panelEl?.classList.contains('minimized'),
    };
    storageSet({ [LS_STATE]: state });
    saveSkuSpecs();
  }

  function saveSkuSpecs() {
    const sku = currentSku();
    if (!sku) return;
    const specs = { ts: Date.now() };
    SPEC_FIELDS.forEach((f) => {
      const el = q(`[data-f="${f}"]`);
      if (el && el.value) specs[f] = el.value;
    });
    storageGet([LS_SKU_SPECS]).then((r) => {
      const all = r[LS_SKU_SPECS] || {};
      all[sku] = specs;
      storageSet({ [LS_SKU_SPECS]: pruneSkuSpecs(all) });
    });
  }

  function pruneSkuSpecs(all) {
    const entries = Object.entries(all);
    if (entries.length <= SKU_SPECS_MAX) return all;
    entries.sort((a, b) => (b[1]?.ts || 0) - (a[1]?.ts || 0));
    return Object.fromEntries(entries.slice(0, SKU_SPECS_MAX));
  }

  function loadSettings() {
    storageGet([LS_STATE, `${LS_STATE}_pos`, LS_SKU_SPECS]).then((res) => {
      const state = res[LS_STATE];
      const pos = res[`${LS_STATE}_pos`];
      const allSpecs = res[LS_SKU_SPECS] || {};

      if (state) {
        if (state.channel) {
          // 5.2.54 起将旧的 E邮宝/E包裹入口收敛为 China Post，避免旧偏好造成空白核价。
          channel = ['EUB', 'EBP'].includes(state.channel) ? 'POST' : state.channel;
          if (!ALL_CHANNELS.includes(channel)) channel = 'CEL';
          qa(`.${P}-ch`).forEach((b) => b.classList.toggle('active', b.dataset.ch === channel));
        }
        if (state.postService in CHINA_POST_SERVICES) {
          postService = state.postService;
        }
        if (state.celConfig && typeof state.celConfig === 'object') {
          celConfig = normalizeChannelCfg('CEL', { ...defaultChannelCfg(), ...state.celConfig });
        }
        if (state.guooConfig && typeof state.guooConfig === 'object') {
          guooConfig = normalizeChannelCfg('GUOO', { ...defaultChannelCfg(), ...state.guooConfig });
        }
        if (state.xyConfig && typeof state.xyConfig === 'object') {
          xyConfig = normalizeChannelCfg('XY', { ...defaultChannelCfg(), ...state.xyConfig });
        }
        if (state.ztoConfig && typeof state.ztoConfig === 'object') {
          ztoConfig = normalizeChannelCfg('ZTO', { ...defaultChannelCfg(), ...state.ztoConfig });
        }
        if (state.useOldFormula) {
          useOldFormula = true;
          const cb = q('[data-f="useOldFormula"]');
          if (cb) cb.checked = true;
        }
        if (state.minimized) {
          panelEl.classList.add('minimized');
          const minBtn = panelEl.querySelector('[data-action="min"]');
          if (minBtn) minBtn.textContent = '+';
        }
        ALL_CHANNELS.forEach(renderChannelCfgBody);
      }

      // per-SKU spec 字段（先恢复用户编辑过的，再用页面数据补空）
      // 恢复的值视作"用户输入"，标记后自动抓取（DOM/composer/seller portal）不再覆盖
      const sku = currentSku();
      if (sku && allSpecs[sku]) {
        SPEC_FIELDS.forEach((f) => {
          if (allSpecs[sku][f] != null) {
            const el = q(`[data-f="${f}"]`);
            if (el) {
              el.value = allSpecs[sku][f];
              el.dataset.jzcUserEdited = '1';
            }
          }
        });
      }

      if (pos && typeof pos.left === 'number' && typeof pos.top === 'number') {
        const maxLeft = window.innerWidth - 60;
        const maxTop = window.innerHeight - 60;
        panelEl.style.left = Math.max(0, Math.min(maxLeft, pos.left)) + 'px';
        panelEl.style.top  = Math.max(0, Math.min(maxTop, pos.top)) + 'px';
        panelEl.style.right = 'auto';
      }

      autoFillFromPage(true);     // 仅填空字段（用户已存的不会被覆盖）
      scheduleAutoFillRetries();  // Ozon 特征 widget 常常懒加载，重试几次
      recalcAll();
    });
  }

  // ── Init ────────────────────────────────────────
  async function init() {
    showTrigger();
  }

  // 调试入口：在控制台执行 __jzc.debugWeight() 查看抓取过程
  // 用于针对性反馈（哪个 SKU 抓不到 → 把输出贴回来）
  window.__jzc = {
    debugWeight: async () => {
      console.group('[快速核价] debug');
      console.log('URL:', location.href);
      const dom = extractWeightAndDims();
      console.log('① DOM/data-state 抓取:', dom);
      _composerFetched = false;
      const composer = await autoFillFromComposer();
      console.log('② composer-api 抓取:', composer);
      const skuMatch = location.pathname.match(/\/product\/[^/]*-(\d+)/);
      const sku = skuMatch ? skuMatch[1] : '';
      const seller = sku ? await autoFillFromSellerPortal(sku) : {};
      console.log('③ seller portal 抓取:', seller, sku ? `(sku=${sku})` : '(no sku)');
      const stateKeys = [];
      document.querySelectorAll('[data-state]').forEach((el) => {
        try {
          const o = JSON.parse(el.getAttribute('data-state') || '{}');
          if (o && typeof o === 'object') stateKeys.push({
            widget: el.getAttribute('data-widget') || '',
            keys: Object.keys(o).slice(0, 12),
          });
        } catch (_) {}
      });
      console.log('④ 页面 data-state 概览:', stateKeys);
      console.groupEnd();
      return { dom, composer, seller, stateKeys };
    },
  };

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();
