/**
 * 电商套图工作台 - Side Panel
 *
 * 布局：顶栏 + 目标平台
 *   ├─ 左 01 商品信息 ｜ 右 02 采集图片（两列等高，采集阶段）
 *   ├─ 03 出图方案（整行：方案列表 + AI 智能规划 + 风格 + 一键生成）
 *   └─ 04 生成结果（整行）
 *
 * 流程：采集页面 → 编辑/勾选 → AI 规划或用默认方案 → 一键生成 → 导出 ZIP
 */
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { App as AntApp, ConfigProvider, Popover, Progress, Select } from 'antd';
import { SettingOutlined, DownloadOutlined, ThunderboltOutlined, UploadOutlined, CloseOutlined } from '@ant-design/icons';
import type { ScanResult, ImageMaterial } from '../../src/collector/scan';
import { cleanFilename } from '../../src/collector/url';
import {
  buildGeneratePayload, downloadSuiteZip, uploadImage, exportImages,
  DEFAULT_PLAN, IMAGE_MODEL_OPTIONS, PLATFORM_OPTIONS, PLATFORM_SPECS, STYLE_SET_OPTIONS,
  type PlanItem, type SuiteInfo,
} from '../../src/api/client';
import { loadSettings, saveSettings, type BackendSettings } from '../../src/storage/settings';

/** 数据来源 → 展示含义 */
const SOURCE_LABELS: Record<string, string> = {
  state: '页面数据',
  ssr: '页面数据',
  jsonld: '结构化数据',
  api: '站内接口',
  dom: 'DOM解析',
  mixed: '混合来源',
};

/** 平台 → 展示名 */
const PLATFORM_LABELS: Record<string, string> = {
  ozon: 'Ozon',
  '1688': '1688',
  taobao: '淘宝/天猫',
};

/** 支持的站点（用于判断刷新是否有意义） */
const SUPPORTED_URL_RE = /^https?:\/\/([a-z0-9-]+\.ozon\.(ru|kz|by)|detail\.1688\.com|item\.taobao\.com|detail\.tmall\.com)\//i;

/** 页内悬浮面板模式（作为 iframe 嵌在商品页里运行）；独立 Side Panel 打开时为 false */
const IN_PAGE = window.self !== window.top;

/** 通知宿主页收起悬浮面板 */
function closePanel(): void {
  window.parent.postMessage({ type: 'sc-panel-close' }, '*');
}

/** 在指定 tab 执行采集，返回结果或 null（未就绪/不支持） */
async function tryScan(tabId: number): Promise<ScanResult | null> {
  const [res] = await chrome.scripting.executeScript({
    target: { tabId },
    func: () => (window as any).__SuiteCollector?.scan?.() ?? null,
  });
  return (res?.result ?? null) as ScanResult | null;
}

/** 等待 tab 加载完成（需在 reload 前注册监听），超时放行 */
function waitForTabComplete(tabId: number, timeoutMs = 30_000): Promise<void> {
  return new Promise((resolve) => {
    const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, timeoutMs);
    function listener(id: number, info: chrome.tabs.TabChangeInfo) {
      if (id === tabId && info.status === 'complete') {
        clearTimeout(timer);
        chrome.tabs.onUpdated.removeListener(listener);
        resolve();
      }
    }
    chrome.tabs.onUpdated.addListener(listener);
  });
}

function send<T>(action: string, payload: Record<string, unknown>): Promise<T> {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage({ action, ...payload }, (res: { ok: boolean; data?: T; error?: string }) => {
      if (chrome.runtime.lastError) return reject(new Error(chrome.runtime.lastError.message));
      if (!res?.ok) return reject(new Error(res?.error || '后台请求失败'));
      resolve(res.data as T);
    });
  });
}

// ── 小组件 ─────────────────────────────────────────────────────────────────

const Section: React.FC<{
  no: string; title: string; extra?: React.ReactNode; children: React.ReactNode; className?: string;
}> =
  ({ no, title, extra, children, className }) => (
    <div className={`section ${className ?? ''}`}>
      <div className="section-head">
        <span className="section-no">{no}</span>
        <span className="section-title">{title}</span>
        {extra && <span className="section-extra">{extra}</span>}
      </div>
      {children}
    </div>
  );

const Field: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => (
  <div className="field">
    <label>{label}</label>
    {children}
  </div>
);

/** 数量加减器 */
const Stepper: React.FC<{ value: number; onChange: (v: number) => void }> = ({ value, onChange }) => (
  <span className="stepper">
    <button className="step-btn" disabled={value <= 0} onClick={() => onChange(Math.max(0, value - 1))}>−</button>
    <span className="step-num">{value}</span>
    <button className="step-btn" disabled={value >= 5} onClick={() => onChange(Math.min(5, value + 1))}>＋</button>
  </span>
);

const App: React.FC = () => {
  const { modal } = AntApp.useApp();

  // 采集
  const [scanning, setScanning] = useState(false);
  const [retrying, setRetrying] = useState(false);
  const [result, setResult] = useState<ScanResult | null>(null);
  const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
  const [titleEdit, setTitleEdit] = useState('');
  const [priceEdit, setPriceEdit] = useState('');
  const [dimsEdit, setDimsEdit] = useState<{ l: string; w: string; h: string }>({ l: '', w: '', h: '' });
  const [weightEdit, setWeightEdit] = useState('');
  const [descEdit, setDescEdit] = useState('');
  const [paramsOpen, setParamsOpen] = useState(false);

  // 手动上传图片（补充参考图，独立「upload」分组）
  const [uploadedImages, setUploadedImages] = useState<ImageMaterial[]>([]);
  const [uploading, setUploading] = useState(false);
  const fileRef = useRef<HTMLInputElement | null>(null);
  const uploadSeqRef = useRef(0);

  // 服务端
  const [settings, setSettings] = useState<BackendSettings>({ baseUrl: 'http://127.0.0.1:3300', token: '', watermark: { enabled: false, type: 'image', text: 'xiongmaoyx', opacity: 30 } });

  // 出图方案
  const [platform, setPlatform] = useState<'ozon' | 'wb' | 'cn'>('ozon');
  const [styleSet, setStyleSet] = useState(1);
  /** 生图模型（默认 gpt-image-2-vip，走 RightAPI provider） */
  const [model, setModel] = useState<string>('gpt-image-2-vip');
  /** 用户改写的风格提示词（按风格 id 存，切风格不丢） */
  const [stylePrompts, setStylePrompts] = useState<Record<number, string>>({});
  /** 生图要求（最高优先级，强制约束，覆盖其他设定） */
  const [requirements, setRequirements] = useState('');
  const [plan, setPlan] = useState<PlanItem[]>(DEFAULT_PLAN.map(p => ({ ...p })));
  const [planSource, setPlanSource] = useState<'default' | 'ai'>('default');
  const [planSummary, setPlanSummary] = useState('');
  const [planning, setPlanning] = useState(false);
  const [planThenGenerate, setPlanThenGenerate] = useState(false);

  // 生成
  const [suite, setSuite] = useState<SuiteInfo | null>(null);
  const [generating, setGenerating] = useState(false);
  // 导出采集图片
  const [exporting, setExporting] = useState(false);
  // 导出生成结果 ZIP
  const [exportingZip, setExportingZip] = useState(false);
  const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
  /** 规划请求序号：重新采集时递增，用于丢弃重置后才返回的过期规划响应 */
  const planSeqRef = useRef(0);

  // 图片放大预览（画廊：可左右切换）
  const [preview, setPreview] = useState<{ list: string[]; index: number } | null>(null);

  const openPreview = (list: string[], index: number) => {
    if (list.length === 0) return;
    setPreview({ list, index: Math.max(0, Math.min(index, list.length - 1)) });
  };
  const previewPrev = () => setPreview(p => p && { ...p, index: (p.index - 1 + p.list.length) % p.list.length });
  const previewNext = () => setPreview(p => p && { ...p, index: (p.index + 1) % p.list.length });

  useEffect(() => {
    loadSettings().then(setSettings);
    return () => { if (pollRef.current) clearInterval(pollRef.current); };
  }, []);

  // 预览态的键盘导航：← → 切换，Esc 关闭
  useEffect(() => {
    if (!preview) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'ArrowLeft') previewPrev();
      else if (e.key === 'ArrowRight') previewNext();
      else if (e.key === 'Escape') setPreview(null);
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [preview]);

  // 页内面板模式：Esc 收起面板（预览打开时优先关预览，由上面的监听处理）
  useEffect(() => {
    if (!IN_PAGE) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape' && !preview) closePanel();
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [preview]);

  const brand = result?.texts.find(t => t.kind === 'brand')?.content ?? '';
  const sales = result?.texts.find(t => t.kind === 'sales')?.content ?? '';
  const shop = result?.texts.find(t => t.kind === 'shop')?.content ?? '';
  const paramPairs = result?.texts.find(t => t.kind === 'params')?.pairs ?? [];

  const handleScan = () => {
    // 生成中：二次确认（后台任务会继续完成，但本面板停止跟踪）
    if (generating) {
      modal.confirm({
        title: '有正在生成的任务',
        content: '重新采集将清空当前采集结果与出图方案，并停止在本面板跟踪正在生成的任务（已提交的后台生成会继续完成，但此处将看不到进度和导出入口）。确定要重新采集吗？',
        okText: '继续采集', cancelText: '取消',
        onOk: () => doScan(),
      });
      return;
    }
    doScan();
  };

  const doScan = async () => {
    // 规划中：直接重置规划并继续采集（递增序号，迟到的规划响应会被丢弃）
    if (planning) {
      planSeqRef.current++;
      setPlanning(false);
    }
    // 确认路径下停止对旧生成任务的本地跟踪（后台任务继续，不受影响）
    if (generating) {
      stopPolling();
      setGenerating(false);
    }
    setScanning(true);
    setRetrying(false);
    try {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
      if (!tab?.id) throw new Error('未找到活动标签页');

      // 首次尝试
      let r = await tryScan(tab.id);

      // 失败：支持站点上大概率是内容脚本未就绪 → 自动刷新重试一次（仅一次，防死循环）
      if (!r) {
        if (!SUPPORTED_URL_RE.test(tab.url ?? '')) {
          throw new Error('当前页面不是支持的商品详情页（支持 Ozon / 1688 / 淘宝 / 天猫）');
        }
        setRetrying(true);
        const waitLoaded = waitForTabComplete(tab.id);  // 先注册监听再刷新，避免错过事件
        await chrome.tabs.reload(tab.id);
        await waitLoaded;
        await new Promise(res => setTimeout(res, 1500));  // 给内容脚本注入留时间
        r = await tryScan(tab.id);
        if (!r) {
          throw new Error('采集失败：已自动刷新重试仍无法采集，请确认当前页面是商品详情页后手动重试');
        }
      }

      setResult(r);
      setSuite(null);
      setParamsOpen(false);
      // 默认全选主图 + SKU 图（SKU 图带规格名，AI 规划的 variant 绑定要用）
      const keys = new Set(r.images.filter(i => i.groupKey === 'main' || i.groupKey === 'sku').map(i => i.key));
      setSelectedKeys(keys);
      setTitleEdit(r.texts.find(t => t.kind === 'title')?.content ?? '');
      setDescEdit(r.texts.find(t => t.kind === 'desc')?.content ?? '');
      setPriceEdit(r.texts.find(t => t.kind === 'price')?.content ?? '');
      // 从参数表初始化尺寸/重量（编辑后随生成/规划请求覆盖回参数）
      const pairs = r.texts.find(t => t.kind === 'params')?.pairs ?? [];
      const dimPair = pairs.find(p => /尺寸|长宽高/i.test(p.key) && (String(p.value).match(/\d+(\.\d+)?/g) ?? []).length >= 3);
      const nums = dimPair ? (String(dimPair.value).match(/\d+(\.\d+)?/g) ?? []) : [];
      setDimsEdit(nums.length >= 3 ? { l: nums[0], w: nums[1], h: nums[2] } : { l: '', w: '', h: '' });
      const weightPair = pairs.find(p => /重量/i.test(p.key));
      setWeightEdit(weightPair ? String(weightPair.value) : '');
    } catch (e) {
      modal.error({ title: '采集失败', content: e instanceof Error ? e.message : String(e), okText: '知道了' });
    } finally {
      setScanning(false);
      setRetrying(false);
    }
  };

  const stopPolling = () => { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } };

  const pollSuite = useCallback((suiteId: string) => {
    stopPolling();
    const startedAt = Date.now();
    let failCount = 0;      // 连续轮询失败计数（容忍瞬时抖动，连续 3 次才判定后端不可达）
    let timeoutWarned = false;
    pollRef.current = setInterval(async () => {
      try {
        const s = await send<SuiteInfo>('getSuite', { baseUrl: settings.baseUrl, token: settings.token, suiteId });
        setSuite(s);
        failCount = 0;
        // 超时兜底：单张最长约 5 分钟，超预算仍 running 多半是任务已中断（如后端重启）
        const budgetMs = ((s.total ?? 1) * 5 + 10) * 60_000;
        if (!timeoutWarned && ['running', 'pending'].includes(s.status) && Date.now() - startedAt > budgetMs) {
          timeoutWarned = true;
          modal.warning({
            title: '任务耗时异常',
            content: '生成耗时远超预期，任务可能已中断。可稍等片刻观察，或刷新页面后重新生成。',
            okText: '知道了',
          });
        }
        if (['done', 'partial', 'failed'].includes(s.status)) {
          stopPolling();
          setGenerating(false);
          if (s.status === 'partial') modal.warning({ title: '部分生成失败', content: s.error || '可重试或更换风格重新生成', okText: '知道了' });
          if (s.status === 'failed') modal.error({ title: '生成失败', content: s.error || '未知错误', okText: '知道了' });
        }
      } catch (e) {
        failCount += 1;
        if (failCount >= 3) {
          stopPolling();
          setGenerating(false);
          modal.error({
            title: '无法连接后端',
            content: '连续查询生成进度失败，已停止跟踪。若刚重启过后端（./start.command），任务已被中断，请重新生成。',
            okText: '知道了',
          });
        }
      }
    }, 3000);
  }, [settings.baseUrl, settings.token, modal]);

  /** 当前编辑后的文本素材（规划与生成共用；尺寸/重量编辑值覆盖回参数表） */
  const editedTexts = () => {
    if (!result) return [];
    const orig = (kind: string) => result.texts.find(t => t.kind === kind);
    let pairs = [...(orig('params')?.pairs ?? [])];

    // 尺寸/重量覆盖：先移除旧的同义项，再写入编辑值（有值才写）
    const dropRe = [/尺寸|长宽高/i, /重量/i];
    pairs = pairs.filter(p => !dropRe.some(re => re.test(p.key)));
    const { l, w, h } = dimsEdit;
    if (l || w || h) pairs.push({ key: '产品尺寸', value: `${l || '?'}×${w || '?'}×${h || '?'}` });
    if (weightEdit) pairs.push({ key: '重量', value: weightEdit });

    const texts: Array<{ kind: string; content: string; pairs?: Array<{ key: string; value: string }> | null }> = [];
    const title = titleEdit || orig('title')?.content || '';
    const desc = descEdit || orig('desc')?.content || '';
    if (title) texts.push({ kind: 'title', content: title });
    if (priceEdit || orig('price')?.content) texts.push({ kind: 'price', content: priceEdit || orig('price')!.content });
    if (orig('brand')?.content) texts.push({ kind: 'brand', content: orig('brand')!.content });
    if (orig('sales')?.content) texts.push({ kind: 'sales', content: orig('sales')!.content });
    if (orig('shop')?.content) texts.push({ kind: 'shop', content: orig('shop')!.content });
    if (pairs.length) texts.push({ kind: 'params', content: '', pairs });
    if (orig('selling_point')?.content) texts.push({ kind: 'selling_point', content: orig('selling_point')!.content });
    if (desc) texts.push({ kind: 'desc', content: desc });
    return texts;
  };

  /** 提交生成任务（planOverride：刚规划出来的方案，避免闭包读到旧 state） */
  const startGenerate = async (planOverride?: PlanItem[]): Promise<void> => {
    if (!result) return;
    if (selectedKeys.size === 0) {
      modal.warning({ title: '请先在采集图片区勾选参考图' });
      return;
    }
    const activePlan = (planOverride ?? plan).filter(p => p.count > 0);
    if (activePlan.length === 0) {
      modal.warning({ title: '出图方案的张数都是 0' });
      return;
    }
    setGenerating(true);
    setSuite(null);
    try {
      const payload = buildGeneratePayload(
        allImages, selectedKeys, editedTexts(),
        { style_set: styleSet, style_prompt: currentStylePrompt, requirements: requirements.trim() || null, plan: activePlan, platform, model,
          // 水印开启才下发，关闭时不带（服务端按无水印处理）
          watermark: settings.watermark.enabled ? settings.watermark : undefined },
      );
      const { suite_id } = await send<{ suite_id: string }>('generateSuite', {
        baseUrl: settings.baseUrl, token: settings.token, payload,
      });
      pollSuite(suite_id);
    } catch (e) {
      setGenerating(false);
      modal.error({ title: '提交失败', content: e instanceof Error ? e.message : String(e), okText: '知道了' });
    }
  };

  /** AI 智能规划出图方案；勾选「规划并生成」时规划完成自动开始生成 */
  const handlePlan = async () => {
    if (!result) return modal.warning({ title: '请先采集商品页' });
    const seq = ++planSeqRef.current;
    setPlanning(true);
    try {
      const skuVariants = Array.from(new Set(
        result.images.filter(i => i.groupKey === 'sku' && i.variantName).map(i => i.variantName!)
      ));
      const data = await send<{ summary: string; items: PlanItem[] }>('planSuite', {
        baseUrl: settings.baseUrl, token: settings.token,
        payload: {
          texts: editedTexts(),
          sku_variants: skuVariants,
          image_stats: result.stats,
          platform,
          requirements: requirements.trim() || null,
        },
      });
      if (seq !== planSeqRef.current) return;  // 规划已被重新采集重置，丢弃过期响应
      setPlan(data.items);
      setPlanSource('ai');
      setPlanSummary(data.summary);
      if (planThenGenerate) {
        await startGenerate(data.items);  // 规划完成 → 直接生成（用刚返回的方案，不弹确认）
      } else {
        const total = data.items.reduce((s, i) => s + i.count, 0);
        modal.info({
          title: 'AI 方案已生成',
          content: `${data.summary}（共 ${total} 张）。可逐项调整数量，0 即不生成。`,
          okText: '好的',
        });
      }
    } catch (e) {
      if (seq !== planSeqRef.current) return;  // 已重置，过期错误不弹窗
      modal.error({ title: '规划失败', content: e instanceof Error ? e.message : String(e), okText: '知道了' });
    } finally {
      if (seq === planSeqRef.current) setPlanning(false);
    }
  };

  const totalPlanned = plan.reduce((s, i) => s + i.count, 0);
  const doneCount = suite?.images.filter(i => i.status === 'ok').length ?? 0;
  // 分母用后端返回的计划总张数：images 是逐张落库的，生成中 length 永远等于已完成数
  const suiteTotal = suite?.total ?? totalPlanned;
  /** 当前风格生效的提示词：用户改写值 > 该风格默认值 */
  const currentStylePrompt = stylePrompts[styleSet]
    ?? STYLE_SET_OPTIONS.find(s => s.value === styleSet)?.prompt ?? '';

  const handleGenerate = () => {
    if (!result) return;
    if (selectedKeys.size === 0) return modal.warning({ title: '请先在采集图片区勾选参考图' });
    if (totalPlanned === 0) return modal.warning({ title: '出图方案的张数都是 0' });
    const spec = PLATFORM_SPECS[platform];
    modal.confirm({
      title: '生成电商套图',
      content: (
        <div>
          <div>
            目标平台「{spec.label}」（{spec.lang}文案 · {spec.ratio}），模型「{model}」，风格「
            {STYLE_SET_OPTIONS.find(s => s.value === styleSet)?.label}」，共 {totalPlanned} 张、参考图{' '}
            {selectedKeys.size} 张。生成需要几分钟，可在下方查看进度。
          </div>
        </div>
      ),
      okText: '开始生成', cancelText: '取消',
      onOk: () => startGenerate(),
    });
  };

  const handleExport = async () => {
    if (!suite) return;
    setExportingZip(true);
    try {
      const blob = await downloadSuiteZip(settings.baseUrl, settings.token, suite.id);
      const objectUrl = URL.createObjectURL(blob);
      try {
        await chrome.downloads.download({
          url: objectUrl,
          filename: `${cleanFilename(productTitle) || '套图'}.zip`,
          saveAs: false,
        });
      } finally {
        // 延迟释放，给下载任务足够时间读取 blob
        setTimeout(() => URL.revokeObjectURL(objectUrl), 30_000);
      }
    } catch (e) {
      modal.error({ title: '导出失败', content: e instanceof Error ? e.message : String(e), okText: '知道了' });
    } finally {
      setExportingZip(false);
    }
  };

  const toggleKey = (key: string) => {
    const next = new Set(selectedKeys);
    if (next.has(key)) next.delete(key); else next.add(key);
    setSelectedKeys(next);
  };

  /** 全部可选图片：采集结果 + 手动上传（上传图单独一组「upload」） */
  const allImages: ImageMaterial[] = result ? [...result.images, ...uploadedImages] : uploadedImages;

  /** 导出文件名的商品标题（编辑态优先，回退采集标题） */
  const productTitle = titleEdit.trim()
    || result?.texts.find(t => t.kind === 'title')?.content?.trim()
    || '';

  const groupImages = (groupKey: string): ImageMaterial[] =>
    allImages.filter(i => i.groupKey === groupKey);

  /** 下载采集图片：只打包已勾选的图片（后端按分组名建文件夹），前端经 chrome.downloads 落盘 */
  const handleExportImages = async () => {
    const imgs = allImages.filter(i => i.type === 'img' && selectedKeys.has(i.key));
    if (imgs.length === 0) {
      modal.warning({ title: '请先勾选要下载的图片' });
      return;
    }
    setExporting(true);
    try {
      const blob = await exportImages(settings.baseUrl, settings.token, {
        title: productTitle,
        images: imgs.map(i => ({
          url: i.url,
          groupName: i.groupName,
          variantName: i.variantName ?? null,
          key: i.key,
        })),
      });
      const objectUrl = URL.createObjectURL(blob);
      try {
        await chrome.downloads.download({
          url: objectUrl,
          filename: `${cleanFilename(productTitle) || '采集图片'}.zip`,
          saveAs: false,
        });
      } finally {
        // 延迟释放，给下载任务足够时间读取 blob
        setTimeout(() => URL.revokeObjectURL(objectUrl), 30_000);
      }
    } catch (e) {
      modal.error({ title: '导出失败', content: e instanceof Error ? e.message : String(e), okText: '知道了' });
    } finally {
      setExporting(false);
    }
  };

  /** 预览用的全量图序列（主图→SKU→详情→上传，与展示顺序一致） */
  const collectedPreviewList: string[] = (['main', 'sku', 'detail', 'upload'] as const)
    .flatMap(g => groupImages(g).map(i => i.url));
  /** 生成结果的预览序列（仅成功的图） */
  const resultPreviewList: string[] = suite ? suite.images.filter(i => i.status === 'ok').map(i => i.url) : [];

  const toggleGroup = (groupKey: string, on: boolean) => {
    const next = new Set(selectedKeys);
    groupImages(groupKey).forEach(i => on ? next.add(i.key) : next.delete(i.key));
    setSelectedKeys(next);
  };

  const setPlanCount = (idx: number, count: number) => {
    setPlan(prev => prev.map((p, i) => i === idx ? { ...p, count } : p));
  };

  /** 全部方案是否都启用（任一为 0 即视为未全选，复选框随方案状态联动） */
  const planAllEnabled = plan.every(p => p.count >= 1);

  /** 批量开关：勾选 → 全部至少 1 张（用户已设 >1 的保留原数量）；取消勾选 → 全部 0，方便单独勾选一种方案 */
  const togglePlanAll = (on: boolean) => {
    setPlan(prev => prev.map(p => on ? { ...p, count: Math.max(p.count, 1) } : { ...p, count: 0 }));
  };

  /** 点击方案行：勾选（0 → 1）/ 取消勾选（>0 → 0），配合"全部方案"开关单独选一种 */
  const togglePlanRow = (idx: number) => {
    setPlan(prev => prev.map((p, i) => i === idx ? { ...p, count: p.count > 0 ? 0 : 1 } : p));
  };

  /** 手动上传：点击触发隐藏的 file input */
  const handleUpload = () => fileRef.current?.click();

  const onFilesChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = Array.from(e.target.files ?? []);
    e.target.value = '';  // 清空以便再次选择同名文件
    if (files.length === 0) return;
    setUploading(true);
    const added: ImageMaterial[] = [];
    for (const f of files) {
      try {
        const { url } = await uploadImage(settings.baseUrl, settings.token, f);
        added.push({
          key: `upload-${String(++uploadSeqRef.current).padStart(3, '0')}`,
          groupKey: 'upload',
          groupName: '手动上传',
          url,
          thumbUrl: url,
          index: added.length,
          type: 'img',
        });
      } catch (err) {
        modal.error({ title: '上传失败', content: `${f.name}：${err instanceof Error ? err.message : String(err)}`, okText: '知道了' });
      }
    }
    if (added.length > 0) {
      setUploadedImages(prev => [...prev, ...added]);
      setSelectedKeys(prev => {
        const next = new Set(prev);
        added.forEach(a => next.add(a.key));
        return next;
      });
      modal.success({ title: `已上传 ${added.length} 张图片` });
    }
    setUploading(false);
  };

  /** 源站图防盗链时的兜底：走服务端图片代理 */
  const proxied = (u: string) =>
    `${settings.baseUrl.replace(/\/$/, '')}/api/proxy-image?url=${encodeURIComponent(u)}`;

  /** 缩略图三级降级：thumbUrl → 原图 → 服务端代理 */
  const onThumbError = (e: React.SyntheticEvent<HTMLImageElement>, url: string) => {
    const el = e.currentTarget;
    if (el.dataset.step === '1') { el.dataset.step = '2'; el.src = url; }
    else if (el.dataset.step === '2') { el.dataset.step = '3'; el.src = proxied(url); }
  };

  /** 更新水印子设置并落盘（弹窗里控件共用） */
  const patchWatermark = (patch: Partial<BackendSettings['watermark']>) => {
    const next = { ...settings, watermark: { ...settings.watermark, ...patch } };
    setSettings(next);
    saveSettings(next);
  };

  const settingsPopup = (
    <div style={{ width: 300 }}>
      <Field label="后端地址">
        <input
          value={settings.baseUrl}
          onChange={(e) => setSettings({ ...settings, baseUrl: e.target.value })}
          onBlur={() => saveSettings(settings)}
        />
      </Field>
      <Field label="Token（可选）">
        <input
          value={settings.token}
          onChange={(e) => setSettings({ ...settings, token: e.target.value })}
          onBlur={() => saveSettings(settings)}
        />
      </Field>
      <div className="divider" style={{ margin: '10px 0' }} />
      <label className="auto-chk" title="开启后，生成的图在落盘前由服务端合成水印（导出即带水印）">
        <input
          type="checkbox"
          checked={settings.watermark.enabled}
          onChange={(e) => patchWatermark({ enabled: e.target.checked })}
        />
        生成图加水印
      </label>
      {settings.watermark.enabled && (
        <>
          <div className="field" style={{ marginTop: 8 }}>
            <label>水印类型</label>
            <div className="pills">
              <span className={`pill ${settings.watermark.type === 'image' ? 'on' : ''}`} onClick={() => patchWatermark({ type: 'image' })}>图片</span>
              <span className={`pill ${settings.watermark.type === 'text' ? 'on' : ''}`} onClick={() => patchWatermark({ type: 'text' })}>文字</span>
            </div>
          </div>
          {settings.watermark.type === 'text' && (
            <Field label="水印文字">
              <input
                value={settings.watermark.text}
                onChange={(e) => setSettings({ ...settings, watermark: { ...settings.watermark, text: e.target.value } })}
                onBlur={() => saveSettings(settings)}
              />
            </Field>
          )}
          <Field label="不透明度（%）">
            <input
              type="number" min={1} max={100} step={5}
              value={settings.watermark.opacity}
              onChange={(e) => setSettings({ ...settings, watermark: { ...settings.watermark, opacity: Number(e.target.value) || 1 } })}
              onBlur={() => saveSettings(settings)}
            />
          </Field>
          <div className="hint" style={{ marginTop: 4 }}>位置固定右下角；重新生成后生效，已生成的图不变</div>
        </>
      )}
    </div>
  );

  return (
    <div className="page">
      {/* ── 顶栏 ── */}
      <div className="topbar">
        {IN_PAGE && (
          <button className="icon-btn" title="收起面板（Esc）" onClick={closePanel}><CloseOutlined /></button>
        )}
        <div className="logo">套</div>
        <div>
          <h1>电商套图工作台</h1>
          <div className="sub">商品采集 · 套图生成 · 一键导出</div>
        </div>
        <div className="spacer" />
        <Popover content={settingsPopup} title="服务端设置" trigger="click" placement="bottomRight">
          <button className="icon-btn" title="服务端设置"><SettingOutlined /></button>
        </Popover>
        <button className="btn btn-primary btn-main" disabled={scanning} onClick={handleScan}>
          {retrying ? '刷新重试中…' : scanning ? '采集中…' : '快速采集'}
        </button>
      </div>

      {/* ── 目标平台切换（决定文案语言 + 图片比例）── */}
      <div className="platform-bar">
        <span className="platform-label">目标平台</span>
        <div className="seg-group">
          {PLATFORM_OPTIONS.map(p => (
            <button
              key={p.value}
              className={`seg-btn ${platform === p.value ? 'on' : ''}`}
              onClick={() => setPlatform(p.value)}
            >{p.label}</button>
          ))}
        </div>
        <span className="platform-spec">
          {PLATFORM_SPECS[platform].lang}文案 · {PLATFORM_SPECS[platform].ratio} 图片
        </span>
      </div>

      {/* ── 采集区：左信息 / 右图片，两列等高 ── */}
      <div className="two-col">
        <Section
          no="01"
          title="商品信息"
          extra={result
            ? `${PLATFORM_LABELS[result.platform] ?? result.platform} · ${SOURCE_LABELS[result.source] ?? result.source}`
            : undefined}
        >
          {!result ? (
            <div className="empty">点击右上角「快速采集」抓取当前商品页</div>
          ) : (
            <>
              <Field label="标题">
                <input value={titleEdit} onChange={(e) => setTitleEdit(e.target.value)} />
              </Field>
              <div style={{ display: 'flex', gap: 8 }}>
                <div style={{ flex: 1 }}>
                  <Field label="价格">
                    <input value={priceEdit} onChange={(e) => setPriceEdit(e.target.value)} />
                  </Field>
                </div>
                <div style={{ flex: 1 }}>
                  <Field label="品牌">
                    <input value={brand} readOnly style={{ color: 'var(--text-2)' }} />
                  </Field>
                </div>
              </div>
              <Field label="尺寸（长 × 宽 × 高）">
                <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                  <input placeholder="长" value={dimsEdit.l} onChange={(e) => setDimsEdit(d => ({ ...d, l: e.target.value }))} />
                  <span style={{ color: 'var(--text-2)' }}>×</span>
                  <input placeholder="宽" value={dimsEdit.w} onChange={(e) => setDimsEdit(d => ({ ...d, w: e.target.value }))} />
                  <span style={{ color: 'var(--text-2)' }}>×</span>
                  <input placeholder="高" value={dimsEdit.h} onChange={(e) => setDimsEdit(d => ({ ...d, h: e.target.value }))} />
                </div>
              </Field>
              <Field label="重量">
                <input placeholder="如 428g / 0.43kg" value={weightEdit} onChange={(e) => setWeightEdit(e.target.value)} />
              </Field>
              {(sales || shop) && (
                <div style={{ display: 'flex', gap: 8 }}>
                  <div style={{ flex: 1 }}>
                    <Field label="销量">
                      <input value={sales} readOnly style={{ color: 'var(--text-2)' }} />
                    </Field>
                  </div>
                  <div style={{ flex: 1 }}>
                    <Field label="店铺">
                      <input value={shop} readOnly style={{ color: 'var(--text-2)' }} />
                    </Field>
                  </div>
                </div>
              )}
              {paramPairs.length > 0 && (
                <div className="field">
                  <label style={{ cursor: 'pointer' }} onClick={() => setParamsOpen(v => !v)}>
                    规格 / 参数（{paramPairs.length} 项）{paramsOpen ? ' ▴' : ' ▾'}
                  </label>
                  {paramsOpen && (
                    <table className="kv-table">
                      <tbody>
                        {paramPairs.slice(0, 30).map((p, i) => (
                          <tr key={i}><td className="k">{p.key}</td><td>{p.value}</td></tr>
                        ))}
                      </tbody>
                    </table>
                  )}
                </div>
              )}
              <Field label="商品描述（用于生成图内文案）">
                <textarea rows={10} value={descEdit} onChange={(e) => setDescEdit(e.target.value)} />
              </Field>
              {result.warnings.length > 0 && (
                <div className="warn-box">{result.warnings.map((w, i) => <div key={i}>{w}</div>)}</div>
              )}
            </>
          )}
        </Section>

        <Section
          no="02"
          title="采集图片"
          className="section-images"
          extra={
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
              {(result || uploadedImages.length > 0) && (
                <span>已选 {selectedKeys.size} / {allImages.filter(i => i.type !== 'video').length}</span>
              )}
              <button className="btn btn-sm" onClick={handleUpload} disabled={uploading}>
                <UploadOutlined /> {uploading ? '上传中…' : '上传图片'}
              </button>
            </span>
          }
        >
          <input
            ref={fileRef}
            type="file"
            accept="image/*"
            multiple
            style={{ display: 'none' }}
            onChange={onFilesChange}
          />
          {allImages.length === 0 ? (
            <div className="empty">采集后在此勾选图片，或点右上角「上传图片」手动补充</div>
          ) : (
            <div className="img-groups">
              {(['main', 'sku', 'detail', 'upload'] as const).map(g => groupImages(g).length > 0 && (
                <div key={g} style={{ marginBottom: 10 }}>
                  <div className="group-head">
                    <span className="name">
                      {g === 'main' ? '主图' : g === 'sku' ? 'SKU图片' : g === 'detail' ? '详情图' : '手动上传'}
                    </span>
                    <span className="count">{groupImages(g).length}</span>
                    <span
                      className="mini-check"
                      onClick={() => {
                        const all = groupImages(g).every(i => selectedKeys.has(i.key));
                        toggleGroup(g, !all);
                      }}
                    >
                      {groupImages(g).every(i => selectedKeys.has(i.key)) ? '取消全选' : '全选'}
                    </span>
                  </div>
                  <div className="img-grid">
                    {groupImages(g).map(img => {
                      const on = selectedKeys.has(img.key);
                      return (
                        <div key={img.key} className={`img-cell ${on ? 'on' : ''}`}
                          title="点击放大预览，勾选圆点选择图片"
                          onClick={() => openPreview(collectedPreviewList, collectedPreviewList.indexOf(img.url))}>
                          <img
                            src={img.thumbUrl || img.url}
                            referrerPolicy="no-referrer"
                            data-step="1"
                            onError={(e) => onThumbError(e, img.url)}
                          />
                          <span
                            className="tick"
                            onClick={(e) => { e.stopPropagation(); toggleKey(img.key); }}
                          >✓</span>
                          {img.variantName && <span className="variant">{img.variantName}</span>}
                        </div>
                      );
                    })}
                  </div>
                </div>
              ))}
            </div>
          )}
          {allImages.some(i => i.type === 'img') && (
            <div className="img-export-bar">
              <button className="btn btn-sm" onClick={handleExportImages} disabled={exporting}>
                <DownloadOutlined /> {exporting ? '下载中…' : '下载采集图片'}
              </button>
              <span className="hint">下载已勾选图片（{allImages.filter(i => i.type === 'img' && selectedKeys.has(i.key)).length} 张）</span>
            </div>
          )}
        </Section>
      </div>

      {/* ── 出图方案（整行）── */}
      <Section
        no="03"
        title={`出图方案（共 ${totalPlanned} 张）`}
        extra={
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
            {planSource === 'ai' && <span className="ai-tag">AI 方案</span>}
            <label className="auto-chk" title="勾选后，AI 规划完成将自动开始生成">
              <input
                type="checkbox"
                checked={planThenGenerate}
                onChange={(e) => setPlanThenGenerate(e.target.checked)}
              />
              规划并生成
            </label>
            <button className="btn btn-ai btn-main" disabled={!result || planning} onClick={handlePlan}>
              <ThunderboltOutlined /> {planning ? '规划中…' : 'AI 智能规划'}
            </button>
          </span>
        }
      >
        <div className="plan-list">
          {plan.map((p, idx) => (
            <div
              key={idx}
              className={`plan-row ${p.count === 0 ? 'off' : ''}`}
              title="点击勾选/取消该方案（数量用右侧 ± 调整）"
              onClick={() => togglePlanRow(idx)}
            >
              <div className="plan-main">
                <span className="plan-title">{p.title}</span>
                {p.variant_name && <span className="variant-chip">{p.variant_name}</span>}
                {p.detail && <span className="plan-detail" title={p.detail}>{p.detail}</span>}
                {p.prompt_hint && <span className="plan-detail plan-hint" title={p.prompt_hint}>🎯 {p.prompt_hint}</span>}
              </div>
              <span onClick={(e) => e.stopPropagation()}>
                <Stepper value={p.count} onChange={(v) => setPlanCount(idx, v)} />
              </span>
            </div>
          ))}
        </div>
        <div className="plan-all-toggle">
          <label className="auto-chk" title="勾选：全部方案至少 1 张（>1 张保留原数量）；取消勾选：全部改为 0，方便单独勾选一种方案">
            <input
              type="checkbox"
              checked={planAllEnabled}
              onChange={(e) => togglePlanAll(e.target.checked)}
            />
            全部方案
          </label>
          <span className="hint">取消勾选后全部为 0，点选需要的方案行即可单独出图</span>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, margin: '10px 0 4px' }}>
          {planSource === 'ai' && (
            <button
              className="btn btn-sm"
              onClick={() => { setPlan(DEFAULT_PLAN.map(p => ({ ...p }))); setPlanSource('default'); setPlanSummary(''); }}
            >恢复默认方案</button>
          )}
          <span className="hint" style={{ flex: 1 }}>
            {planSummary || '方案与张数由规划器根据商品信息自动决定，可手动微调，0 即不生成'}
          </span>
        </div>

        <div className="divider" />
        <div className="field">
          <label>视觉风格</label>
          <div className="pills">
            {STYLE_SET_OPTIONS.map(s => (
              <span
                key={s.value}
                className={`pill ${styleSet === s.value ? 'on' : ''}`}
                onClick={() => setStyleSet(s.value)}
              >{s.label}</span>
            ))}
          </div>
        </div>
        <div className="field">
          <label>
            风格提示词（可编辑，直接决定生成画面的风格）
            {stylePrompts[styleSet] !== undefined && (
              <span
                className="mini-check"
                style={{ marginLeft: 8, fontWeight: 400 }}
                onClick={() => setStylePrompts(p => {
                  const n = { ...p };
                  delete n[styleSet];
                  return n;
                })}
              >恢复默认</span>
            )}
          </label>
          <textarea
            rows={4}
            value={currentStylePrompt}
            onChange={(e) => setStylePrompts(p => ({ ...p, [styleSet]: e.target.value }))}
          />
        </div>
        <div className="field">
          <label>生图要求（优先级最高，强制要求，会覆盖其他设定）</label>
          <textarea
            rows={3}
            value={requirements}
            onChange={(e) => setRequirements(e.target.value)}
            placeholder="选填，例如：必须保留商品正面品牌标识；背景必须为纯黑色；不得添加任何文字水印"
          />
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 4 }}>
          {generating && (
            <div style={{ flex: 6 }}>
              {/* 进度条与右侧弹性占位 6:1，长度 = 剩余空间的 6/7（两次加长后的累积比例） */}
              <Progress
                percent={suiteTotal ? Math.round(doneCount / suiteTotal * 100) : 0}
                size={['100%', 10]} status="active" format={() => `${doneCount}/${suiteTotal}`}
              />
            </div>
          )}
          <span style={{ flex: 1 }} />
          <Select
            value={model}
            onChange={setModel}
            style={{ width: 260 }}
            popupMatchSelectWidth={false}
            disabled={generating}
          >
            {IMAGE_MODEL_OPTIONS.map(m => (
              <Select.Option key={m.value} value={m.value} label={m.label}>
                <div className="model-opt">
                  <div className="model-opt-name">{m.label}</div>
                  <div className="model-opt-desc">{m.desc}</div>
                </div>
              </Select.Option>
            ))}
          </Select>
          <button
            className="btn btn-primary btn-main"
            style={{ alignSelf: 'stretch' }}
            disabled={!result || generating || selectedKeys.size === 0 || totalPlanned === 0}
            onClick={handleGenerate}
          >
            {generating ? '生成中…' : `一键生图（${totalPlanned} 张）`}
          </button>
        </div>
      </Section>

      {/* ── 生成结果（整行）── */}
      <Section
        no="04"
        title="生成结果"
        extra={
          suite && ['done', 'partial'].includes(suite.status) && (
            <button className="btn btn-sm" onClick={handleExport} disabled={exportingZip}>
              <DownloadOutlined /> {exportingZip ? '导出中…' : '导出 ZIP'}
            </button>
          )
        }
      >
        {!suite ? (
          <div className="empty">生成后在此查看与导出</div>
        ) : (
          <>
            <div style={{ marginBottom: 8 }} className="hint">
              状态：<b style={{ color: suite.status === 'done' ? 'var(--green)' : suite.status === 'failed' ? 'var(--red)' : 'var(--primary)' }}>
                {suite.status === 'running' ? '生成中' : suite.status === 'done' ? '完成' : suite.status === 'partial' ? '部分失败' : suite.status === 'pending' ? '排队中' : '失败'}
              </b>
              {' '}· {PLATFORM_SPECS[suite.platform]?.label ?? suite.platform}
              {' '}· {PLATFORM_SPECS[suite.platform]?.lang ?? suite.lang}文案 · {suite.ratio}
              {' '}· 风格「{STYLE_SET_OPTIONS.find(s => s.value === suite.style_set)?.label ?? '自定义'}」
            </div>
            <div className="result-grid">
              {/* 只渲染终态（ok/failed）格子：生成中的 pending 占位不出现，避免"…/✗"占位格被误点 */}
              {suite.images.filter(img => img.status === 'ok' || img.status === 'failed').map(img => (
                <div
                  key={img.type_id + img.name}
                  className={`result-cell ${img.status !== 'ok' ? 'fail' : ''}`}
                  title={img.error || img.name}
                  style={{ cursor: img.status === 'ok' ? 'zoom-in' : 'default' }}
                  onClick={() => img.status === 'ok' && openPreview(resultPreviewList, resultPreviewList.indexOf(img.url))}
                >
                  {img.status === 'ok' ? (
                    <img src={img.url} referrerPolicy="no-referrer" />
                  ) : (
                    <div style={{ aspectRatio: suite.ratio === '3:4' ? '3/4' : '1', background: 'var(--card-soft)' }} />
                  )}
                  {img.status !== 'ok' && <span className="fail-tag">✗</span>}
                  <div className="cap" title={img.error || img.name}>
                    {img.status === 'failed' && img.error ? img.error : img.name}
                  </div>
                </div>
              ))}
            </div>
            {suite.error && <div className="warn-box">{suite.error}</div>}
          </>
        )}
      </Section>

      {/* ── 图片放大预览（画廊：← → 切换）── */}
      {preview && (
        <div className="lightbox" onClick={() => setPreview(null)}>
          <img
            src={preview.list[preview.index]}
            referrerPolicy="no-referrer"
            onClick={(e) => e.stopPropagation()}
          />
          {preview.list.length > 1 && (
            <>
              <button
                className="lightbox-nav prev"
                onClick={(e) => { e.stopPropagation(); previewPrev(); }}
                title="上一张（←）"
              >‹</button>
              <button
                className="lightbox-nav next"
                onClick={(e) => { e.stopPropagation(); previewNext(); }}
                title="下一张（→）"
              >›</button>
              <div className="lightbox-counter" onClick={(e) => e.stopPropagation()}>
                {preview.index + 1} / {preview.list.length}
              </div>
            </>
          )}
          <span className="lightbox-tip">点击任意处关闭 · ← → 切换</span>
        </div>
      )}
    </div>
  );
};

const Root: React.FC = () => (
  <ConfigProvider
    theme={{
      token: {
        colorPrimary: '#8b5cf6',
        colorLink: '#8b5cf6',
        borderRadius: 8,
        fontFamily: "Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif",
      },
    }}
  >
    <AntApp>
      <App />
    </AntApp>
  </ConfigProvider>
);

createRoot(document.getElementById('root')!).render(<Root />);
