/* ============================================================
   chat-panel.jsx — LLM 채팅 패널 (window.ChatPanel)
   전송은 기존 공용 래퍼 window.__bannerLLM(nukki-shared/llm.js) 를 그대로 재사용한다.
   래퍼가 off(=제공자 없음)거나 호출이 실패하면 그 사실을 대화에 그대로 표시한다(가짜 응답을 만들지 않는다).
   ============================================================ */
/* hooks provided globally in merged doc */

/* context — 현재 화면 맥락(예: 선택한 카테고리). 첫 안내 문구와 전송 프롬프트에 함께 실린다. */
/* 채팅에서 만들 수 있는 배너 유형 — 실제 진입은 guides 에서 같은 category 의 가이드를 찾아 onUseSample 로 넘긴다. */
const CHAT_BANNER_CATEGORIES = ['상품배너 550', '앱푸시', '상품배너 H1'];
// 유형 칩은 이 말풍선(asksType)이 대화의 마지막일 때만 띄운다 — 물어본 직후에만 필요하므로.
const ASK_BANNER_TYPE = { role: 'assistant', text: '이 상품으로 배너를 만들어 볼까요?', asksType: true };
const ASK_PICKED_TYPE = { role: 'assistant', asksType: true, pickAsk: true, text: '이 이미지로 어떤 배너를 만들까요?', wait: 1100 };
const NUKKI_DONE_NOTE = { role: 'assistant', text: '배경 제거가 완료되었습니다.', wait: 1300 };
const IMAGE_PICK_GUIDE = {
  role: 'assistant',
  pickGuide: true,
  text: '가운데에서 배너에 넣을 이미지를 골라 주세요.\n\n1. 상품 칩을 고릅니다.\n2. 이미지에 마우스를 올리고 화보 또는 누끼로 지정합니다.\n3. 아래 최종 선택에서 확인한 뒤 확정합니다.\n4. 확정하면 지정한 컷이 이 대화에 모입니다.'
};

function ChatPanel({ title = 'AI 어시스턴트', context, placeholder = '무엇이든 요청하세요', suggestions, selectedProduct = null, guides = [], onUseSample, onSearchMids, guideUrl = 'data/CJ_banner_LLM_data_lookup_canvas_guide.md' }) {
  const [turns, setTurns] = useState([]); // { role:'user'|'assistant'|'notice'|'product', text }
  const [draft, setDraft] = useState('');
  const [busy, setBusy] = useState(false);
  const [guideText, setGuideText] = useState('');
  const [pendingCategory, setPendingCategory] = useState(null);
  const [pickedImages, setPickedImages] = useState([]);
  const [pickerStep, setPickerStep] = useState('select'); // 'select' | 'nukki'(가운데 선택 대기)
  const [scriptQueue, setScriptQueue] = useState([]); // 아직 안 띄운 안내 말풍선 (한 개씩 순차 노출)
  const [cartGroups, setCartGroups] = useState([]);   // 가운데 담기와 동기화
  const [cartTotal, setCartTotal] = useState(0);
  const [cartItems, setCartItems] = useState([]);
  const [imagePicking, setImagePicking] = useState(false);
  const [picksConfirmed, setPicksConfirmed] = useState(false);
  const [nukkiPending, setNukkiPending] = useState(false);
  const [unavailableImageKeys, setUnavailableImageKeys] = useState(new Set());
  const listRef = useRef(null);
  const taRef = useRef(null);
  const shownProductRef = useRef(null);
  const cartCountRef = useRef(0);
  const picksSigRef = useRef('');
  const cutQueuedRef = useRef(new Set());
  const guideQueuedRef = useRef(false);
  const cutsLockRef = useRef(false); // 확정 컷 그룹은 선택 화면 동기화가 지우지 않는다
  const llm = window.__bannerLLM;
  const llmOn = !!(llm && typeof llm.available === 'function' && llm.available());
  const product = selectedProduct && selectedProduct.product;
  const productName = product && product.product_name;
  const productImages = (selectedProduct && selectedProduct.sourceImages) || [];
  const lastTurn = turns[turns.length - 1];
  const asksBannerType = !!(lastTurn && lastTurn.asksType); // 유형을 물어본 직후에만 칩 노출
  const bannerActions = CHAT_BANNER_CATEGORIES.map((category) => ({
    category,
    guide: (guides || []).find((g) => g.category === category)
  })).filter((entry) => entry.guide);

  // 상품을 새로 고르면 대화에 상품 카드와 요약 정보를 남긴다(같은 상품 반복 클릭은 무시).
  useEffect(() => {
    if (!product) { shownProductRef.current = null; return; }
    if (shownProductRef.current === product.mid) return;
    shownProductRef.current = product.mid;
    // 여러 상품 담기 중에는 채팅 상품 카드·안내를 반복하지 않는다(입력창 깜빡임 방지).
    if (cartCountRef.current > 0) return;
    setPendingCategory(null);
    setPickedImages([]);
    setUnavailableImageKeys(new Set());
    setPickerStep('select');
    setScriptQueue([]);
    window.dispatchEvent(new CustomEvent('bannerly-nukki-pick-abort'));
    const price = product.price ? `${Number(product.price).toLocaleString()}원` : '';
    const imageCount = (product.images || []).length;
    const info = [
      product.product_name ? `상품명: ${product.product_name}` : '',
      product.category && product.category.path ? `카테고리: ${product.category.path}` : '',
      product.brand ? `브랜드: ${product.brand}` : '',
      price ? `가격: ${price}` : '',
      product.review_count ? `리뷰: ${Number(product.review_count).toLocaleString()}개` : '',
      (product.benefits || []).filter(Boolean).length ? `혜택: ${product.benefits.filter(Boolean).join(', ')}` : '',
      imageCount ? `등록 이미지: ${imageCount}장` : ''
    ].filter(Boolean).join('\n');
    // 상품 카드만 먼저 띄우고, 안내는 대화하듯 한 줄씩 이어서 나온다.
    setTurns((prev) => [...prev,
      {
        role: 'product',
        text: product.product_name,
        image: product.primary_image && product.primary_image.url,
        images: productImages,
        brand: product.brand,
        price
      }]);
    setScriptQueue([
      ...(info ? [{ role: 'assistant', text: info }] : []),
      ASK_BANNER_TYPE]);
  }, [selectedProduct]);

  // 가운데 「담기」와 같은 담은 상품 목록을 받는다.
  useEffect(() => {
    const onCartUpdate = (event) => {
      const detail = event.detail || {};
      const groups = Array.isArray(detail.groups) ? detail.groups : [];
      cartCountRef.current = groups.length;
      setCartGroups(groups);
      setCartTotal(typeof detail.total === 'number' ? detail.total : 0);
      setCartItems(Array.isArray(detail.items) ? detail.items.map((item) => ({ ...item })) : []);
      if (!groups.length) setImagePicking(false);
    };
    window.addEventListener('bannerly-cart-update', onCartUpdate);
    return () => window.removeEventListener('bannerly-cart-update', onCartUpdate);
  }, []);

  useEffect(() => {
    const onOpen = () => {
      cutsLockRef.current = false;
      setImagePicking(true);
      setPicksConfirmed(false);
      setNukkiPending(false);
    };
    const onClose = () => setImagePicking(false);
    const onCancel = () => {
      setImagePicking(false);
      setPicksConfirmed(false);
      setNukkiPending(false);
      picksSigRef.current = '';
      cutQueuedRef.current = new Set();
      guideQueuedRef.current = false;
      cutsLockRef.current = false;
      setScriptQueue([]);
      setTurns((prev) => {
        const next = prev.filter((turn) => turn.role !== 'picks' && turn.role !== 'cutPicks' && turn.role !== 'cutPick' && !turn.pickGuide && !turn.pickAsk);
        const last = next[next.length - 1];
        if (last && last.pickCancel) return next;
        return [...next, {
          role: 'assistant',
          pickCancel: true,
          text: '이미지 선택을 취소했습니다. 담아 둔 상품을 모두 비웠습니다. 다시 담으면 선택할 수 있습니다.'
        }];
      });
    };
    const onConfirm = (event) => {
      const detail = event.detail || {};
      const items = (detail.items || []).map((item) => ({
        path: item.path,
        thumb: item.thumb || item.path,
        productName: item.productName || '상품',
        placeAs: item.placeAs
      }));
      const needsNukki = !!detail.needsNukki;
      cutsLockRef.current = true;
      setImagePicking(false);
      setNukkiPending(needsNukki);
      setPicksConfirmed(!needsNukki);
      const grouped = { role: 'cutPicks', items, wait: 1000 };
      setTurns((prev) => prev.filter((turn) => turn.role !== 'cutPick' && turn.role !== 'cutPicks' && !turn.pickAsk));
      setScriptQueue((queue) => [
        ...queue.filter((turn) => turn.role !== 'cutPick' && turn.role !== 'cutPicks' && !turn.pickAsk),
        grouped,
        needsNukki ?
          { role: 'assistant', text: '누끼로 지정한 이미지가 있어 배경 제거를 진행합니다.', wait: 1400 } :
          ASK_PICKED_TYPE
      ]);
    };
    window.addEventListener('bannerly-image-pick-open', onOpen);
    window.addEventListener('bannerly-image-pick-close', onClose);
    window.addEventListener('bannerly-image-pick-cancel', onCancel);
    window.addEventListener('bannerly-image-pick-confirm', onConfirm);
    const onWorkReset = () => {
      cutsLockRef.current = false;
      guideQueuedRef.current = false;
      picksSigRef.current = '';
      setPicksConfirmed(false);
      setNukkiPending(false);
      setImagePicking(false);
      setScriptQueue([]);
      setTurns((prev) => prev.filter((turn) =>
        turn.role !== 'picks' && turn.role !== 'cutPicks' && turn.role !== 'cutPick' && !turn.pickGuide && !turn.pickAsk));
    };
    window.addEventListener('bannerly-work-reset', onWorkReset);
    return () => {
      window.removeEventListener('bannerly-image-pick-open', onOpen);
      window.removeEventListener('bannerly-image-pick-close', onClose);
      window.removeEventListener('bannerly-image-pick-cancel', onCancel);
      window.removeEventListener('bannerly-image-pick-confirm', onConfirm);
      window.removeEventListener('bannerly-work-reset', onWorkReset);
    };
  }, []);

  // 선택 화면: 담은 상품만 대화에 남긴다. 지정 컷 썸네일은 가운데에서 확정한 뒤에만 붙인다.
  useEffect(() => {
    if (!imagePicking && picksSigRef.current === '') return;
    const sig = `${imagePicking ? '1' : '0'}|${cartGroups.map((g) => g.mid).join(',')}`;
    if (picksSigRef.current === sig) return;
    picksSigRef.current = sig;
    if (!cartGroups.length) {
      if (cutsLockRef.current) {
        picksSigRef.current = sig;
        return;
      }
      cutQueuedRef.current = new Set();
      guideQueuedRef.current = false;
      cutsLockRef.current = false;
      setTurns((prev) => prev.filter((turn) => turn.role !== 'picks' && turn.role !== 'cutPicks' && turn.role !== 'cutPick'));
      setScriptQueue((queue) => queue.filter((turn) => !turn.pickGuide && turn.role !== 'cutPick' && turn.role !== 'cutPicks'));
      picksSigRef.current = '';
      return;
    }
    if (!imagePicking || cutsLockRef.current) return;
    const picksTurn = {
      role: 'picks',
      groups: cartGroups.map((group) => ({ mid: group.mid, productName: group.productName, count: group.count }))
    };
    const outgoing = [];
    if (!guideQueuedRef.current) {
      guideQueuedRef.current = true;
      outgoing.push(IMAGE_PICK_GUIDE);
    }
    setTurns((prev) => {
      const without = prev.filter((row) => row.role !== 'picks');
      const guideIdx = without.findIndex((row) => row.pickGuide);
      return guideIdx >= 0 ?
        [...without.slice(0, guideIdx), picksTurn, ...without.slice(guideIdx)] :
        [...without, picksTurn];
    });
    if (outgoing.length) {
      setScriptQueue((queue) => queue.filter((turn) => turn.role !== 'cutPick').concat(outgoing));
    }
  }, [cartItems, cartGroups, imagePicking]);

  const removeFromCart = (mid) => {
    window.dispatchEvent(new CustomEvent('bannerly-cart-remove', { detail: { mid } }));
  };
  const removePickedChatImage = (path) => {
    window.dispatchEvent(new CustomEvent('bannerly-cart-remove', { detail: { path } }));
  };
  const clearCart = () => {
    window.dispatchEvent(new CustomEvent('bannerly-cart-clear'));
  };

  const scrollToImagePicker = () => {
    window.dispatchEvent(new CustomEvent('bannerly-image-pick-open'));
    setImagePicking(true);
  };

  // 대기 중인 안내·지정 컷을 하나씩 꺼내 띄운다. 어시스턴트 차례에만 입력 중 표시가 보인다.
  useEffect(() => {
    if (!scriptQueue.length) return;
    const next = scriptQueue[0];
    const wait = typeof next.wait === 'number' ? next.wait :
      (next.pickGuide || next.role === 'assistant' ? 1100 : 800);
    const timer = setTimeout(() => {
      setTurns((prev) => prev.some((turn) =>
        next.pickGuide ? turn.pickGuide :
        (next.role === 'cutPick' && turn.path === next.path)
      ) ? prev : [...prev, next]);
      setScriptQueue((queue) => queue.slice(1));
    }, wait);
    return () => clearTimeout(timer);
  }, [scriptQueue]);

  // 채팅은 유형을 먼저 고르므로 그 유형의 개수 규칙을 정확히 적용한다(그리드는 유형 미정이라 최대치를 쓴다).
  const cc = window.__commerceCategory;
  const pickLimit = (pendingCategory && cc && cc.limitFor) ?
    cc.limitFor(pendingCategory.category, product && product.category && product.category.large) : null;
  const maxPick = pickLimit ? pickLimit.total : Infinity;

  // 유형 칩은 사용자가 말한 것처럼 오른쪽에 남기고, 이미지 확인창은 대화 흐름 안(정보 말풍선 자리)에 띄운다.
  const startCategory = (entry) => {
    setPendingCategory(entry);
    setPickedImages([]);
    setPickerStep('select');
    setTurns((prev) => [...prev, { role: 'user', text: entry.category }]);
    setScriptQueue([
      { role: 'assistant', text: `${entry.category} 배너를 만들게요. 사용할 이미지를 선택해 주세요.` },
      { role: 'picker', category: entry.category }]);
  };

  // 이미 화보/누끼를 확정한 뒤 — 다시 고르지 않고 편집기로 넘긴다.
  const startConfirmedType = (entry) => {
    // 확정된 컷 목록을 이벤트에 명시 전달 — onStartBanner 가 confirmedWorkRef 에만 의존하는 취약점 보완.
    // cutPicks turn 이 있으면 거기서 items 를 읽고, 없으면 빈 배열(onStartBanner 폴백이 처리).
    const cutPicksTurn = turns.find((t) => t.role === 'cutPicks');
    const confirmedItems = (cutPicksTurn && Array.isArray(cutPicksTurn.items)) ? cutPicksTurn.items : [];
    setPicksConfirmed(false);
    setNukkiPending(false);
    setPendingCategory(null);
    setPickedImages([]);
    setPickerStep('select');
    setBusy(true);
    setTurns((prev) => [
      ...prev.map((turn) => turn.asksType ? { ...turn, asksType: false } : turn).filter((turn) => turn.role !== 'picker'),
      { role: 'user', text: entry.category }
    ]);
    const n = confirmedItems.length;
    setScriptQueue([
      { role: 'assistant', text: `${entry.category} 편집기로 이동합니다. ${n ? `선택한 이미지 ${n}장을 자동 배치합니다.` : '선택한 화보·누끼 이미지를 그대로 적용합니다.'}`, wait: 1200 }
    ]);
    window.setTimeout(() => {
      setBusy(false);
      window.dispatchEvent(new CustomEvent('bannerly-start-banner', {
        detail: { guide: entry.guide, category: entry.category, items: confirmedItems }
      }));
    }, 2200);
  };

  // 진행 중이던 이미지 확인창을 결과 문구로 바꿔 닫는다(대화 기록은 남긴다).
  const closePicker = (resolved) => {
    setTurns((prev) => prev.map((t) => t.role === 'picker' && !t.resolved ? { ...t, resolved } : t));
    setPendingCategory(null);
    setPickedImages([]);
    setPickerStep('select');
  };

  // 가운데 본문에서 배경 제거 선택이 끝나면 대화에 요약만 남긴다(편집기 인계는 목록이 담당).
  useEffect(() => {
    const onResult = (event) => {
      const detail = event.detail || {};
      if (detail.awaitType) {
        if (detail.status === 'done') {
          setNukkiPending(false);
          setPicksConfirmed(true);
          const items = (detail.items || []).map((item) => ({
            path: item.path,
            originalPath: item.originalPath || item.path,
            thumb: item.thumb || item.path,
            productName: item.productName || '상품',
            placeAs: item.placeAs,
            nukki: !!item.nukki
          }));
          // 확정 때 붙인 누끼 썸네일을 실제 배경 제거 결과로 교체한다(원본 썸네일 유지 금지).
          setTurns((prev) => prev.map((turn) => {
            if (turn.role !== 'cutPicks') return turn;
            return {
              ...turn,
              items: (turn.items || []).map((item) => {
                if (item.placeAs !== 'nukki' && !item.nukki) return item;
                const hit = items.find((row) =>
                  (row.originalPath && (row.originalPath === item.path || row.originalPath === item.originalPath)) ||
                  (row.path && row.path === item.path)
                );
                if (!hit || !hit.nukki) return item;
                return {
                  ...item,
                  path: hit.path,
                  originalPath: hit.originalPath || item.path,
                  thumb: hit.thumb || hit.path,
                  nukki: true,
                  placeAs: 'nukki'
                };
              })
            };
          }));
          setScriptQueue((queue) => [
            ...queue.filter((turn) => !turn.pickAsk && turn.role !== 'cutPicks'),
            NUKKI_DONE_NOTE,
            ASK_PICKED_TYPE
          ]);
        }
        if (detail.status === 'cancel') setNukkiPending(false);
        return;
      }
      if (pickerStep !== 'nukki') return;
      if (detail.status === 'done') {
        closePicker(detail.summary || '편집기를 엽니다.');
        return;
      }
      if (detail.status === 'cancel') setPickerStep('select');
    };
    window.addEventListener('bannerly-nukki-pick-result', onResult);
    return () => window.removeEventListener('bannerly-nukki-pick-result', onResult);
  }, [pickerStep]);

  // 채팅에서 고른 이미지를 가운데 배경 제거 선택 화면으로 넘긴다.
  const openCenterNukki = () => {
    if (!pendingCategory || !pickedImages.length) return;
    const images = pickedImages.map((index) => productImages[index]).filter(Boolean);
    if (!images.length) return;
    window.dispatchEvent(new CustomEvent('bannerly-nukki-pick', {
      detail: { guide: pendingCategory.guide, category: pendingCategory.category, images }
    }));
    setPickerStep('nukki');
  };

  const abortCenterNukki = () => {
    window.dispatchEvent(new CustomEvent('bannerly-nukki-pick-abort'));
    setPickerStep('select');
  };

  // 새 말풍선이 생기면 항상 마지막이 보이도록
  useEffect(() => {
    const el = listRef.current;
    if (el) el.scrollTop = el.scrollHeight;
  }, [turns, busy, scriptQueue]);

  useEffect(() => {
    let alive = true;
    fetch(guideUrl, { cache: 'no-store' })
      .then((response) => response.ok ? response.text() : '')
      .then((text) => { if (alive) setGuideText(text || ''); })
      .catch(() => { if (alive) setGuideText(''); });
    return () => { alive = false; };
  }, [guideUrl]);

  const autoGrow = () => {
    const el = taRef.current;
    if (!el) return;
    el.style.height = 'auto';
    el.style.height = Math.min(160, el.scrollHeight) + 'px';
  };

  // 입력이 MID(6자리 이상 숫자)로만 이뤄졌으면 LLM 대신 등록 이미지 DB 조회로 처리한다.
  // 숫자 외에 남는 글자가 있으면(문장이면) 평소대로 LLM 에 보낸다.
  const asMidQuery = (body) => {
    const mids = body.match(/\d{6,}/g);
    if (!mids || !onSearchMids) return null;
    const rest = body.replace(/\d{6,}/g, '').replace(/[\s,·/|+]+/g, '');
    return rest.length ? null : mids.filter((mid, i, all) => all.indexOf(mid) === i);
  };

  const runMidSearch = async (body, mids) => {
    setBusy(true);
    try {
      const result = await Promise.resolve(onSearchMids(mids));
      const found = result && Array.isArray(result.found) ? result.found : [];
      const missing = result && Array.isArray(result.missing) ? result.missing : mids;
      const ready = !!(result && result.ready);
      const lines = [];
      if (!ready) lines.push('등록 이미지 데이터베이스를 아직 불러오는 중입니다. 잠시 후 다시 시도해 주세요.');else
      {
        if (found.length) lines.push(`${found.length}건을 찾아 가운데 목록에 표시했습니다.`, ...found.map((entry) => `· ${entry.mid} — ${entry.name}`));
        if (missing.length) lines.push(`${found.length ? '\n' : ''}등록 이미지에 없는 MID ${missing.length}건입니다.`, ...missing.map((mid) => `· ${mid} — 검색되지 않음`));
      }
      setTurns((prev) => [...prev,
        { role: 'user', text: body },
        { role: found.length || !ready ? 'assistant' : 'notice', text: lines.join('\n') }]);
      setDraft('');
      if (taRef.current) taRef.current.style.height = 'auto';
    } catch (error) {
      setTurns((prev) => [...prev,
        { role: 'user', text: body },
        { role: 'notice', text: error && error.message ? error.message : 'MID 검색 중 오류가 발생했습니다.' }]);
    } finally {
      setBusy(false);
    }
  };

  const send = async (text) => {
    const body = (text === undefined ? draft : text).trim();
    if (!body || busy) return;
    const mids = asMidQuery(body);
    if (mids) { runMidSearch(body, mids); return; }
    // LLM 비활성 시 즉시 안내 — 가짜 로딩 없이 notice 표시
    if (!llmOn) {
      setTurns((prev) => [...prev, { role: 'user', text: body }, { role: 'notice', text: 'AI 채팅 기능이 현재 준비 중입니다. 유형 칩으로 배너를 시작하세요.' }]);
      setDraft(''); if (taRef.current) taRef.current.style.height = 'auto'; return;
    }
    const history = [...turns, { role: 'user', text: body }];
    setTurns(history);
    setDraft('');
    if (taRef.current) taRef.current.style.height = 'auto';
    setBusy(true);
    try {
      if (!llm) throw new Error('LLM 래퍼(window.__bannerLLM)가 로드되지 않았습니다');
      const productContext = selectedProduct ?
        `[선택 상품 태그]\n${selectedProduct.tag || productName || '상품'}\n\n` +
        `[선택 상품 표준 JSON]\n${JSON.stringify(selectedProduct.product, null, 2)}\n\n` +
        `[CJ 배너 LLM 데이터 조회·캔버스 호출 MD]\nsource: ${selectedProduct.sourceGuide || guideUrl}\n${guideText || '(MD 가이드 로딩 전입니다. MID를 canonical key로 쓰고, 상품 원본 데이터는 추측하지 마세요.)'}` :
        '';
      // 래퍼 인터페이스: complete({ messages }) → 텍스트(string) | null(off)
      const messages = [
        ...(context ? [{ role: 'user', content: [{ type: 'text', text: `[현재 화면 맥락] 선택한 카테고리: ${context}` }] }] : []),
        ...(productContext ? [{ role: 'user', content: [{ type: 'text', text: productContext }] }] : []),
        ...history.filter((t) => t.role === 'user' || t.role === 'assistant').
        map((t) => ({ role: t.role, content: [{ type: 'text', text: t.text }] }))];

      const reply = await llm.complete({ messages });
      setTurns((prev) => [...prev, reply ?
        { role: 'assistant', text: String(reply) } :
        { role: 'notice', text: 'LLM 제공자가 설정되지 않아 응답을 받을 수 없습니다. (nukki-shared/llm.js 의 PROVIDER/PROXY_URL 설정 필요)' }]);
    } catch (reason) {
      const msg = reason && reason.message ? String(reason.message) : '';
      const isServiceDown = /503|서비스가 준비|API 키/.test(msg);
      setTurns((prev) => [...prev, { role: 'notice', text: isServiceDown ? 'AI 채팅 기능이 현재 준비 중입니다. 유형 칩으로 배너를 시작하세요.' : 'AI 응답을 받지 못했습니다. 잠시 후 다시 시도해 주세요.' }]);
    } finally {
      setBusy(false);
    }
  };

  const onKeyDown = (e) => {
    if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {e.preventDefault();send();}
  };

  return (
    <section className="chatp">
      <div className="chatp-head">
        <span className="chatp-title">
          <S2Icon name="sparkles" size={15} /> {title}
        </span>
        {turns.length > 0 &&
        <button className="chatp-reset" onClick={() => { setTurns([]); setScriptQueue([]); }} disabled={busy}>새 대화</button>}
      </div>

      <div className="chatp-list" ref={listRef} aria-live="polite" aria-busy={busy}>
        {turns.length === 0 && !cartGroups.length ?
          <div className="chatp-intro">
          <div className="chatp-intro-mark"><S2Icon name="sparkles" size={20} /></div>
          <b>{productName ? `${productName} 배너를 어떻게 만들까요?` : context ? `${context} 배너를 어떻게 만들까요?` : '무엇을 만들까요?'}</b>
          <span>원하는 배너를 말로 설명하면 문구·구성을 함께 정리해 드려요.</span>
          {(suggestions || []).filter((s) => llmOn || !!asMidQuery(s)).length > 0 &&
          <div className="chatp-sugs">
            {suggestions.filter((s) => llmOn || !!asMidQuery(s)).map((s) =>
            <button key={s} className="chatp-sug" onClick={() => send(s)} disabled={busy}>{s}</button>)}
          </div>}
        </div> :

        turns.length === 0 && cartGroups.length > 0 ?
        <div className="chatp-intro chatp-intro-cart">
          <b>담은 상품 {cartGroups.length}개</b>
          <span>아래 「사용할 이미지 선택하기」를 누르면, 가운데에서 화보/누끼를 고를 수 있습니다.</span>
        </div> :

        turns.map((t, i) =>
        <div key={i} className={`chatp-turn ${t.role === 'product' || t.role === 'picks' || t.role === 'cutPicks' || t.role === 'cutPick' ? 'user' : t.role}`}>
          {t.role === 'notice' ?
          <span className="chatp-notice"><S2Icon name="bell" size={13} /> {t.text}</span> :
          t.role === 'product' ?
          <div className="chatp-product">
            {t.image && !unavailableImageKeys.has(t.image) &&
            <img src={t.image} alt={t.text} onError={() => setUnavailableImageKeys((prev) => new Set(prev).add(t.image))} />}
            <div className="chatp-product-body">
              <b>{t.text}</b>
              {t.brand && <span>{t.brand}{t.price ? ` · ${t.price}` : ''}</span>}
              {Array.isArray(t.images) && t.images.filter((image) => {
                const src = image.thumb || image.path || image.url || image.sourceUrl;
                return src && !unavailableImageKeys.has(src);
              }).length > 0 &&
              <div className="chatp-product-thumbs" aria-label="상품 연관 이미지">
                {t.images.filter((image) => {
                  const src = image.thumb || image.path || image.url || image.sourceUrl;
                  return src && !unavailableImageKeys.has(src);
                }).map((image, index) => {
                  const src = image.thumb || image.path || image.url || image.sourceUrl;
                  if (!src) return null;
                  return <span key={image.id || image.filename || `${src}-${index}`} className="chatp-product-thumb">
                    <img src={src} alt={`${t.text} ${index + 1}`} loading="lazy"
                      onError={() => setUnavailableImageKeys((prev) => new Set(prev).add(src))} />
                  </span>;
                })}
              </div>}
            </div>
          </div> :
          t.role === 'picks' ?
          <div className="chatp-picks">
            <span>담은 상품 {(t.groups || []).length}개</span>
            <ol className="chatp-picks-names">
              {(t.groups || []).map((group) =>
                <li key={group.mid}>{group.productName}</li>)}
            </ol>
          </div> :
          t.role === 'cutPick' ?
          <div className="chatp-cut-picks">
            <div className={`chatp-pick-thumb${t.placeAs === 'nukki' || t.nukki ? ' is-nukki' : ''}`}>
              <img src={t.thumb || t.path} alt={t.productName || ''} />
              <em>{t.placeAs === 'editorial' ? '화보' : '누끼'}</em>
              <button type="button" className="chatp-pick-remove" aria-label="빼기"
                onClick={() => removePickedChatImage(t.path)}>
                <S2Icon name="x" size={12} />
              </button>
            </div>
          </div> :
          t.role === 'cutPicks' ?
          <div className="chatp-cut-picks">
            {(t.items || []).map((item) =>
              <div key={item.originalPath || item.path} className={`chatp-pick-thumb${item.placeAs === 'nukki' || item.nukki ? ' is-nukki' : ''}`}>
                <img src={item.thumb || item.path} alt={item.productName || ''} />
                <em>{item.placeAs === 'editorial' ? '화보' : '누끼'}</em>
                <button type="button" className="chatp-pick-remove" aria-label="빼기"
                  onClick={() => removePickedChatImage(item.originalPath || item.path)}>
                  <S2Icon name="x" size={12} />
                </button>
              </div>)}
          </div> :
          t.role === 'picker' ?
          (t.resolved ?
          <div className="chatp-bubble">{t.resolved}</div> :
          pickerStep === 'select' ?
          <div className="chatp-picker">
            <div className="chatp-picker-head">
              사용할 이미지 선택 <span>({pickedImages.length}/{Math.min(productImages.length, maxPick)})</span>
            </div>
            <div className="chatp-picker-grid">
              {productImages.filter((image) => {
                const src = image.thumb || image.path || image.url || image.sourceUrl;
                return src && !unavailableImageKeys.has(src);
              }).map((image, index) => {
                const on = pickedImages.includes(index);
                const src = image.thumb || image.path || image.url || image.sourceUrl;
                return (
                  <button key={image.id ? image.id + image.filename : index} className={`chatp-thumb${on ? ' on' : ''}`} aria-pressed={on}
                    onClick={() => setPickedImages((prev) =>
                      prev.includes(index) ? prev.filter((i) => i !== index) :
                      prev.length >= maxPick ? prev : [...prev, index])}>
                    <img src={src} alt="" loading="lazy"
                      onError={() => setUnavailableImageKeys((prev) => new Set(prev).add(src))} />
                    {on && <span className="chatp-thumb-check"><S2Icon name="check" size={11} strokeWidth={3} /></span>}
                  </button>);
              })}
            </div>
            <button className="chatp-picker-go" disabled={!pickedImages.length}
              onClick={openCenterNukki}>
              사용할 이미지 선택
            </button>
            <button className="chatp-picker-cancel"
              onClick={() => { closePicker('이미지 선택을 취소했습니다.'); setScriptQueue([ASK_BANNER_TYPE]); }}>취소</button>
          </div> :
          // 2단계 — 가운데 본문에서 배경 제거 대상을 고르는 동안 채팅은 안내만 한다.
          <div className="chatp-picker">
            <div className="chatp-picker-head">배경 제거할 이미지 선택</div>
            <p className="chatp-picker-hint">가운데 화면에서 이미지를 크게 보고 골라 주세요.</p>
            <button className="chatp-picker-cancel" onClick={abortCenterNukki}>이미지 다시 고르기</button>
          </div>) :
          <div className="chatp-bubble">{t.text}</div>}
        </div>)}

        {(busy || (scriptQueue.length > 0 && (scriptQueue[0].role === 'assistant' || scriptQueue[0].pickGuide))) &&
        <div className="chatp-turn assistant" role="status" aria-label="응답 생성 중">
          <div className="chatp-bubble chatp-typing"><span /><span /><span /></div>
        </div>}

        {asksBannerType && !picksConfirmed && !nukkiPending && !cartGroups.length && bannerActions.length > 0 && !pendingCategory && !scriptQueue.length &&
        <div className="chatp-turn assistant">
          <div className="chatp-inline-types">
            <div className="chatp-actions">
              {bannerActions.map((entry) =>
                <button key={entry.category} className="chatp-action" onClick={() => startCategory(entry)}>
                  {entry.category}
                </button>)}
            </div>
          </div>
        </div>}
      </div>

      <div className="chatp-composer">
        {cartGroups.length > 0 && !pendingCategory && !imagePicking && !picksConfirmed && !nukkiPending &&
        <div className="chatp-cart" aria-label="담은 상품">
          <div className="chatp-cart-head">
            담은 상품 {cartGroups.length}개
            <button type="button" className="chatp-cart-clear" onClick={clearCart}>비우기</button>
          </div>
          <div className="chatp-cart-list">
            {cartGroups.map((group, index) =>
              <span key={group.mid} className="chatp-cart-item">
                <i>{index + 1}</i>
                <b title={group.productName}>{group.productName}</b>
                <em>{group.count}장</em>
                <button type="button" title="빼기" onClick={() => removeFromCart(group.mid)}>
                  <S2Icon name="x" size={12} />
                </button>
              </span>)}
          </div>

          <div className="chatp-cart-images">
            <button type="button" className="chatp-cart-go" onClick={scrollToImagePicker}>
              사용할 이미지 선택하기
            </button>
          </div>
        </div>}

        {picksConfirmed && asksBannerType && bannerActions.length > 0 && !pendingCategory && !scriptQueue.length &&
        <div className="chatp-type-chips" aria-label="배너 유형">
          {bannerActions.map((entry) =>
            <button key={entry.category} type="button" className="chatp-action" onClick={() => startConfirmedType(entry)}>
              {entry.category}
            </button>)}
        </div>}

        <textarea
          ref={taRef}
          className="chatp-input"
          rows={1}
          value={draft}
          placeholder={placeholder}
          onChange={(e) => {setDraft(e.target.value);autoGrow();}}
          onKeyDown={onKeyDown} />
        <div className="chatp-composer-foot">
          <button className="chatp-send" onClick={() => send()} disabled={busy || !draft.trim()} title="보내기 (Enter)">
            <S2Icon name={busy ? 'refresh' : 'arrowRight'} size={15} />
          </button>
        </div>
      </div>
    </section>);

}

window.ChatPanel = ChatPanel;
