const SHOPPING_STEPS = ['접수', '소스 분석', '대본', '음성·자막', '워터마크', '편집', '검수·업로드', '미리보기', 'SNS 발행'];
const ANALYSIS_STEPS = ['링크 해석', '제품 특징 식별', '쿠팡 후보 검색', '분석 완료'];

function classifyShoppingSource(value) {
  try {
    const url = new URL(value);
    if (url.protocol !== 'https:') return { valid: false, platform: '링크', kind: 'invalid', message: 'https 주소만 사용할 수 있습니다.' };
    const host = url.hostname.toLowerCase();
    const path = url.pathname.toLowerCase();
    const matches = (domain) => host === domain || host.endsWith(`.${domain}`);
    const term = url.searchParams.get('q') || url.searchParams.get('keyword') || url.searchParams.get('search_query') || '';
    const discovery = (platform) => ({ valid: true, platform, kind: 'discovery', message: term ? `검색 링크 · 자동 후보 탐색: ${term}` : '탐색 링크 · VPS가 영상 후보를 자동 선별합니다.' });
    if (matches('xiaohongshu.com')) return discovery('Xiaohongshu');
    if (matches('tiktok.com')) return host === 'vm.tiktok.com' || host === 'vt.tiktok.com' || path.includes('/video/') || path.startsWith('/t/') ? { valid: true, platform: 'TikTok', kind: 'video', message: '직접 영상 링크' } : discovery('TikTok');
    if (matches('douyin.com')) return host === 'v.douyin.com' || path.includes('/video/') ? { valid: true, platform: 'Douyin', kind: 'video', message: '직접 영상 링크' } : discovery('Douyin');
    if (matches('instagram.com')) return path.startsWith('/reel/') || path.startsWith('/reels/') || path.startsWith('/p/') ? { valid: true, platform: 'Instagram', kind: 'video', message: '직접 게시물 링크' } : discovery('Instagram');
    if (matches('youtube.com') || host === 'youtu.be') return host === 'youtu.be' || path.startsWith('/shorts/') || (path === '/watch' && url.searchParams.get('v')) ? { valid: true, platform: 'YouTube', kind: 'video', message: '직접 영상 링크' } : discovery('YouTube');
    return { valid: false, platform: '미지원', kind: 'invalid', message: '지원되는 SNS 주소가 아닙니다.' };
  } catch {
    return { valid: false, platform: '링크', kind: 'invalid', message: '올바른 URL 형식이 아닙니다.' };
  }
}

const SHOPPING_STAGE_INDEX = {
  downloading: 1,
  transcribing: 1,
  scripting: 2,
  voiceover: 3,
  watermarking: 4,
  rendering: 5,
  editing: 5,
  validating: 6,
  uploading: 6,
};

function shoppingProgress(job) {
  const stage = job.metadata?.progress_stage;
  if (job.status === 'published') return { index: 8, failed: false };
  if (job.status === 'publishing') return { index: 8, failed: false };
  if (['rendered', 'approved', 'scheduled'].includes(job.status)) return { index: 7, failed: false };
  if (job.status === 'rendering') {
    return { index: SHOPPING_STAGE_INDEX[stage] || 1, failed: false };
  }
  if (job.status === 'failed') {
    const index = SHOPPING_STAGE_INDEX[stage] || 7;
    return { index, failed: true };
  }
  return { index: 0, failed: false };
}

function ShoppingProgress({ job }) {
  const progress = shoppingProgress(job);
  return (
    <div className="shopping-progress">
      {SHOPPING_STEPS.map((step, index) => {
        const done = index < progress.index || job.status === 'published';
        const active = index === progress.index && !done;
        const className = progress.failed && active ? 'failed' : done ? 'done' : active ? 'active' : '';
        return (
          <div className={`shopping-step ${className}`} key={step}>
            <span className="shopping-step-dot" />
            <span>{step}</span>
          </div>
        );
      })}
    </div>
  );
}

function ShoppingPage() {
  const { useState: useS, useEffect: useE } = React;
  const [jobs, setJobs] = useS([]);
  const [loading, setLoading] = useS(false);
  const [submitting, setSubmitting] = useS(false);
  const [message, setMessage] = useS('');
  const [copiedCaption, setCopiedCaption] = useS('');
  const [analysis, setAnalysis] = useS(null);
  const [selectedProduct, setSelectedProduct] = useS(null);
  const [manualProductUrl, setManualProductUrl] = useS('');
  const [productApproved, setProductApproved] = useS(false);
  const [analyzing, setAnalyzing] = useS(false);
  const [approving, setApproving] = useS(false);
  const [analysisState, setAnalysisState] = useS('idle');
  const [analysisElapsed, setAnalysisElapsed] = useS(0);
  const [analysisError, setAnalysisError] = useS('');
  const [form, setForm] = useS({
    title: '',
    source_urls: '',
    video_url: '',
    affiliate_url: '',
    product_hint: '',
    platforms: ['youtube', 'instagram', 'tiktok'],
  });

  useE(() => {
    loadJobs();
    const timer = setInterval(loadJobs, 8000);
    return () => clearInterval(timer);
  }, []);

  useE(() => {
    if (analysisState !== 'running') return undefined;
    const timer = setInterval(() => setAnalysisElapsed((value) => value + 1), 1000);
    return () => clearInterval(timer);
  }, [analysisState]);

  async function loadJobs() {
    setLoading(true);
    try {
      const res = await fetch('/api/admin/social-videos?type=shopping&limit=30', { cache: 'no-store' });
      const data = await res.json().catch(() => null);
      if (!res.ok || !data?.ok) throw new Error(data?.error || '목록을 불러오지 못했습니다.');
      setJobs(data.jobs || []);
    } catch (error) {
      setMessage(error instanceof Error ? error.message : '목록을 불러오지 못했습니다.');
    } finally {
      setLoading(false);
    }
  }

  function togglePlatform(platform) {
    setForm((current) => ({
      ...current,
      platforms: current.platforms.includes(platform)
        ? current.platforms.filter((item) => item !== platform)
        : [...current.platforms, platform],
    }));
  }

  function sourceList() {
    return form.source_urls
      .split(/\n+/)
      .map((value) => value.trim())
      .filter((value, index, values) => value.startsWith('http') && values.indexOf(value) === index)
      .slice(0, 3);
  }

  function resetProductMatch() {
    setAnalysis(null);
    setSelectedProduct(null);
    setManualProductUrl('');
    setProductApproved(false);
    setAnalysisState('idle');
    setAnalysisElapsed(0);
    setAnalysisError('');
    setForm((current) => ({ ...current, affiliate_url: '', title: '', product_hint: '' }));
  }

  async function analyzeProduct() {
    const sources = sourceList();
    const invalid = sources.map(classifyShoppingSource).find((source) => !source.valid);
    setMessage('');
    if (!sources.length || invalid) {
      const error = invalid ? `${invalid.platform}: ${invalid.message}` : 'SNS 영상 또는 검색 링크를 입력해 주세요.';
      setAnalysisState('error');
      setAnalysisError(error);
      setMessage(error);
      return;
    }
    setAnalysisState('running');
    setAnalysisElapsed(0);
    setAnalysisError('');
    setAnalyzing(true);
    try {
      const res = await fetch('/api/admin/shopping-products', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'analyze', source_urls: sources }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok || !data?.ok || !data.analysis) throw new Error(data?.error || '제품을 분석하지 못했습니다.');
      const firstCandidate = data.analysis.candidates?.[0] || null;
      setAnalysis(data.analysis);
      setSelectedProduct(firstCandidate);
      setManualProductUrl(firstCandidate?.productUrl || '');
      setForm((current) => ({
        ...current,
        title: data.analysis.productName || '',
        product_hint: (data.analysis.features || []).join(', '),
        affiliate_url: '',
      }));
      setProductApproved(false);
      setAnalysisState('success');
      setMessage('AI 분석이 끝났습니다. 쿠팡 상품을 비교하고 승인해 주세요.');
    } catch (error) {
      const text = error instanceof Error ? error.message : '제품 분석 중 오류가 발생했습니다.';
      setAnalysisState('error');
      setAnalysisError(text);
      setMessage(text);
    } finally {
      setAnalyzing(false);
    }
  }

  async function approveProduct() {
    const productUrl = manualProductUrl.trim() || selectedProduct?.productUrl || '';
    if (!analysis || !productUrl) {
      setMessage('정확한 쿠팡 상품을 선택하거나 상품 URL을 입력해 주세요.');
      return;
    }
    setApproving(true);
    setMessage('');
    try {
      const res = await fetch('/api/admin/shopping-products', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'affiliate', product_url: productUrl }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok || !data?.ok) throw new Error(data?.error || '상품 승인에 실패했습니다.');
      setProductApproved(true);
      setForm((current) => ({ ...current, affiliate_url: data.affiliateUrl || '' }));
      setMessage(data.manualRequired ? data.message : '상품 승인과 파트너스 링크 생성이 완료되었습니다.');
    } catch (error) {
      setMessage(error instanceof Error ? error.message : '상품 승인 중 오류가 발생했습니다.');
    } finally {
      setApproving(false);
    }
  }

  async function handleSubmit(event) {
    event.preventDefault();
    setMessage('');
    const sources = sourceList();

    if (!sources.length && !form.video_url.trim()) {
      setMessage('소스 영상 링크 또는 완성 영상 URL을 입력해 주세요.');
      return;
    }
    if (sources.length && !productApproved) {
      setMessage('AI 분석 후 정확한 쿠팡 상품을 승인해 주세요.');
      return;
    }
    if (!form.affiliate_url.trim()) {
      setMessage('쿠팡 제휴 링크를 입력해 주세요.');
      return;
    }
    if (!form.platforms.length) {
      setMessage('발행할 플랫폼을 하나 이상 선택해 주세요.');
      return;
    }

    setSubmitting(true);
    try {
      const res = await fetch('/api/admin/social-videos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          action: 'create',
          type: 'shopping',
          title: form.title.trim() || null,
          source_url: sources[0] || null,
          source_urls: sources,
          video_url: form.video_url.trim() || null,
          affiliate_url: form.affiliate_url.trim(),
          product_url: manualProductUrl.trim() || selectedProduct?.productUrl || null,
          product_hint: form.product_hint.trim() || null,
          product_match_score: selectedProduct?.score || analysis?.confidence || null,
          product_match_evidence: selectedProduct?.evidence || analysis?.features || [],
          product_approved: sources.length ? productApproved : true,
          platforms: form.platforms,
          auto_publish: false,
        }),
      });
      setAnalysis(null);
      setSelectedProduct(null);
      setManualProductUrl('');
      setProductApproved(false);
      const data = await res.json().catch(() => null);
      if (!res.ok || !data?.ok) throw new Error(data?.error || '쇼핑쇼츠 등록에 실패했습니다.');

      setForm({
        title: '',
        source_urls: '',
        video_url: '',
        affiliate_url: '',
        product_hint: '',
        platforms: ['youtube', 'instagram', 'tiktok'],
      });
      setMessage('접수되었습니다. 영상과 플랫폼별 캡션을 확인한 뒤 직접 발행해 주세요.');
      await loadJobs();
    } catch (error) {
      setMessage(error instanceof Error ? error.message : '쇼핑쇼츠 등록에 실패했습니다.');
    } finally {
      setSubmitting(false);
    }
  }

  async function jobAction(jobId, action, scheduledAt) {
    try {
      const res = await fetch('/api/admin/social-videos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action, job_id: jobId, scheduled_at: scheduledAt }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok || !data?.ok) throw new Error(data?.error || '처리에 실패했습니다.');
      await loadJobs();
    } catch (error) {
      setMessage(error instanceof Error ? error.message : '처리에 실패했습니다.');
    }
  }

  async function copyCaption(platform, text) {
    await navigator.clipboard.writeText(text);
    setCopiedCaption(platform);
    setTimeout(() => setCopiedCaption((current) => current === platform ? '' : current), 1500);
  }

  const statusLabel = {
    queued: '접수',
    rendering: '편집 중',
    rendered: '발행 대기',
    approved: '승인',
    scheduled: '예약됨',
    publishing: '발행 중',
    published: '발행 완료',
    failed: '실패',
  };
  const statusStyle = {
    queued: { color: 'var(--fg-muted)', background: 'var(--bg-elevated)' },
    rendering: { color: '#60a5fa', background: 'rgba(59,130,246,0.16)' },
    rendered: { color: '#fbbf24', background: 'rgba(245,158,11,0.16)' },
    approved: { color: 'var(--status-approved-fg)', background: 'var(--status-approved-bg)' },
    scheduled: { color: 'var(--status-scheduled-fg)', background: 'var(--status-scheduled-bg)' },
    publishing: { color: '#fbbf24', background: 'rgba(245,158,11,0.16)' },
    published: { color: 'var(--status-published-fg)', background: 'var(--status-published-bg)' },
    failed: { color: 'var(--status-failed-fg)', background: 'var(--status-failed-bg)' },
  };

  return (
    <>
      <div className="page-header">
        <div>
          <h1 className="page-title">쇼핑쇼츠</h1>
          <p className="page-sub">자동 제작 후 미리보기와 캡션을 확인하고 발행</p>
        </div>
        <button className="btn outline" onClick={loadJobs} disabled={loading}>
          <Icon name="refreshCw" size={13} /> {loading ? '갱신 중' : '새로고침'}
        </button>
      </div>

      <div className="card" style={{ marginBottom: 20 }}>
        <div className="card-head">
          <span className="card-title"><Icon name="upload" size={14} /> 원클릭 제작</span>
          <span className="badge">2~3개 소스 권장</span>
        </div>
        <form onSubmit={handleSubmit} className="shopping-form">
          <div className="shopping-match-grid">
            <section className="shopping-match-panel">
              <header><b>1</b><span><strong>영상 링크 입력</strong><small>직접 영상과 검색 링크 모두 자동 처리</small></span></header>
              <label className="shopping-label">TikTok · Douyin · Xiaohongshu · Instagram · YouTube *</label>
              <textarea
                className="input"
                placeholder={'https://www.tiktok.com/search?q=...\nhttps://www.douyin.com/video/...'}
                value={form.source_urls}
                onChange={(event) => {
                  setForm((current) => ({ ...current, source_urls: event.target.value }));
                  resetProductMatch();
                }}
                rows={5}
                style={{ resize: 'vertical' }}
              />
              {sourceList().length ? <div className="shopping-source-checks">{sourceList().map((url) => { const source = classifyShoppingSource(url); return <div className={`shopping-source-check ${source.kind}`} key={url}><b>{source.valid ? source.kind === 'video' ? '영상' : '탐색' : '오류'}</b><span><strong>{source.platform}</strong><small>{source.message}</small></span></div>; })}</div> : null}
              <button className="btn outline shopping-match-button" type="button" onClick={analyzeProduct} disabled={analyzing || submitting}>{analyzing ? `분석 중 · ${analysisElapsed}초` : 'AI 제품 분석'}</button>
              {analysisState !== 'idle' ? <div className={`shopping-analysis-progress ${analysisState}`} role="status"><header><strong>{analysisState === 'running' ? '서버 분석 진행 중' : analysisState === 'success' ? '분석 완료' : '분석 실패'}</strong><span>{analysisState === 'running' ? `${analysisElapsed}초 경과` : ''}</span></header><div>{ANALYSIS_STEPS.map((step, index) => <span className={analysisState === 'success' ? 'done' : analysisState === 'error' ? index === 0 ? 'failed' : 'waiting' : index === 0 ? 'active' : 'waiting'} key={step}><i />{step}</span>)}</div>{analysisState === 'running' ? <small>서버가 링크 해석, AI 식별, 쿠팡 검색을 순서대로 처리합니다.</small> : null}{analysisError ? <small className="error">{analysisError}</small> : null}</div> : null}
            </section>

            <section className={`shopping-match-panel ${analysis ? 'ready' : 'disabled'}`}>
              <header><b>2</b><span><strong>쿠팡 후보 확인</strong><small>일치 근거 비교 후 선택</small></span></header>
              {analysis ? (
                <>
                  <div className="shopping-analysis"><strong>{analysis.productName}</strong><span>분석 신뢰도 {analysis.confidence}점</span><small>{analysis.features?.join(' · ') || analysis.searchKeyword}</small></div>
                  {analysis.candidates?.length ? (
                    <div className="shopping-candidates">
                      {analysis.candidates.map((candidate) => (
                        <label className={selectedProduct?.id === candidate.id ? 'selected' : ''} key={candidate.id}>
                          <input type="radio" name="shopping-candidate" checked={selectedProduct?.id === candidate.id} onChange={() => { setSelectedProduct(candidate); setManualProductUrl(candidate.productUrl); setProductApproved(false); setForm((current) => ({ ...current, affiliate_url: '' })); }} />
                          {candidate.imageUrl ? <img src={candidate.imageUrl} alt="" /> : null}
                          <span><strong>{candidate.name}</strong><small>{candidate.evidence?.join(' · ')}</small><b>{candidate.score}점 {candidate.price ? `· ${candidate.price.toLocaleString('ko-KR')}원` : ''}</b></span>
                        </label>
                      ))}
                    </div>
                  ) : null}
                  <div className="shopping-manual-match">
                    {!analysis.candidates?.length ? <a className="btn outline" href={analysis.coupangSearchUrl} target="_blank" rel="noreferrer">쿠팡에서 후보 찾기</a> : null}
                    <label className="shopping-label">쿠팡 상품 URL <b>수정 가능</b></label>
                    <input className="input" value={manualProductUrl} onChange={(event) => { const value = event.target.value; setManualProductUrl(value); setSelectedProduct(analysis.candidates?.find((candidate) => candidate.productUrl === value.trim()) || null); setProductApproved(false); setForm((current) => ({ ...current, affiliate_url: '' })); }} placeholder="https://www.coupang.com/vp/products/..." />
                    <small>{analysis.candidates?.length ? 'AI가 찾은 주소입니다. 다른 상품이면 정확한 쿠팡 주소로 바꿔 주세요.' : analysis.warning}</small>
                  </div>
                  <button className="btn primary shopping-match-button" type="button" onClick={approveProduct} disabled={approving || submitting}>{approving ? '링크 생성 중...' : '이 상품 승인'}</button>
                </>
              ) : <p className="shopping-match-placeholder">AI 분석 후 후보가 표시됩니다.</p>}
            </section>

            <section className={`shopping-match-panel ${productApproved ? 'ready' : 'disabled'}`}>
              <header><b>3</b><span><strong>파트너스 링크</strong><small>승인 상품에만 연결</small></span></header>
              {productApproved ? (
                <>
                  <label className="shopping-label">쿠팡 파트너스 링크 *</label>
                  <input className="input" placeholder="https://link.coupang.com/..." value={form.affiliate_url} onChange={(event) => setForm((current) => ({ ...current, affiliate_url: event.target.value }))} />
                  <small className="shopping-link-state">{form.affiliate_url ? '링크 확인 완료' : 'API 키 설정 전에는 생성한 제휴 링크를 입력하세요.'}</small>
                </>
              ) : <p className="shopping-match-placeholder">상품 승인 후 생성됩니다.</p>}
            </section>
          </div>

          <details className="shopping-details">
            <summary>직접 편집한 완성 영상 사용</summary>
            <div style={{ marginTop: 10 }}>
              <label className="shopping-label">완성 영상 URL</label>
              <input
                className="input"
                placeholder="https://.../video.mp4"
                value={form.video_url}
                onChange={(event) => setForm((current) => ({ ...current, video_url: event.target.value }))}
              />
            </div>
          </details>

          <div className="shopping-controls">
            <div className="shopping-platforms">
              {[
                { key: 'youtube', label: 'YouTube Shorts' },
                { key: 'instagram', label: 'Instagram Reels' },
                { key: 'tiktok', label: 'TikTok' },
              ].map((platform) => (
                <label key={platform.key} className="shopping-check">
                  <input
                    type="checkbox"
                    checked={form.platforms.includes(platform.key)}
                    onChange={() => togglePlatform(platform.key)}
                  />
                  <span>{platform.label}</span>
                </label>
              ))}
            </div>
            <div className="shopping-preview-note">
              <strong>미리보기 후 발행</strong>
              <span>완성 즉시 SNS에 올라가지 않습니다.</span>
            </div>
            <button type="submit" className="btn primary shopping-submit" disabled={submitting || (!form.video_url.trim() && !productApproved)}>
              <Icon name="upload" size={13} />
              {submitting ? '접수 중...' : '승인 상품으로 제작 시작'}
            </button>
          </div>
          {message ? <div className="shopping-message">{message}</div> : null}
        </form>
      </div>

      <div className="card">
        <div className="card-head">
          <span className="card-title">제작·발행 현황</span>
          <span style={{ fontSize: 11, color: 'var(--fg-muted)' }}>{jobs.length}건</span>
        </div>
        {loading && jobs.length === 0 ? (
          <div className="shopping-empty">불러오는 중...</div>
        ) : jobs.length === 0 ? (
          <div className="shopping-empty">등록된 쇼핑쇼츠가 없습니다.</div>
        ) : (
          <div className="shopping-job-list">
            {jobs.map((job) => {
              const metadata = job.metadata || {};
              const publishedUrls = metadata.published_urls && typeof metadata.published_urls === 'object'
                ? Object.entries(metadata.published_urls)
                : [];
              const captions = [
                ['youtube', 'YouTube Shorts', metadata.youtube_caption],
                ['instagram', 'Instagram Reels', metadata.instagram_caption],
                ['tiktok', 'TikTok', metadata.tiktok_caption],
              ].filter((caption) => typeof caption[2] === 'string' && caption[2].trim());
              return (
                <article className="shopping-job" key={job.id}>
                  <div className="shopping-job-head">
                    <div>
                      <strong>{job.title || '제품 정보 분석 중'}</strong>
                      <div className="shopping-job-meta">
                        {(job.platforms || []).join(' · ')} · {new Date(job.created_at).toLocaleString('ko-KR')}
                      </div>
                    </div>
                    <span className="badge" style={{ ...(statusStyle[job.status] || statusStyle.queued) }}>
                      <span className="dot" style={{ background: 'currentColor' }} />
                      {statusLabel[job.status] || job.status}
                    </span>
                  </div>
                  <ShoppingProgress job={job} />
                  {job.last_error ? <div className="shopping-error">{job.last_error}</div> : null}
                  <div className="shopping-job-actions">
                    {job.video_url ? <a className="btn outline" href={job.video_url} target="_blank" rel="noreferrer">완성 영상 미리보기</a> : null}
                    {metadata.affiliate_url ? <a className="btn outline" href={metadata.affiliate_url} target="_blank" rel="noreferrer">쿠팡 링크</a> : null}
                    {publishedUrls.map(([platform, url]) => (
                      <a className="btn outline" key={platform} href={url} target="_blank" rel="noreferrer">{platform} 게시물</a>
                    ))}
                    {['rendered', 'approved', 'scheduled'].includes(job.status) ? (
                      <button className="btn primary" onClick={() => jobAction(job.id, 'publish')}>즉시 발행</button>
                    ) : null}
                    {job.status === 'failed' ? (
                      <button className="btn primary" onClick={() => jobAction(job.id, 'retry')}>다시 시도</button>
                    ) : null}
                  </div>
                  {captions.length ? (
                    <details className="shopping-caption-review" open>
                      <summary>SNS별 발행 캡션 확인</summary>
                      <div className="shopping-caption-grid">
                        {captions.map(([platform, label, text]) => (
                          <section key={platform}>
                            <header>
                              <strong>{label}</strong>
                              <button type="button" onClick={() => copyCaption(platform, text)}>
                                {copiedCaption === platform ? '복사됨' : '복사'}
                              </button>
                            </header>
                            <p>{text}</p>
                          </section>
                        ))}
                      </div>
                    </details>
                  ) : null}
                </article>
              );
            })}
          </div>
        )}
      </div>

      <style>{`
        .shopping-form { padding: 18px 20px; display: flex; flex-direction: column; gap: 16px; }
        .shopping-match-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
        .shopping-match-panel { min-width: 0; border: 1px solid var(--border); padding: 13px; background: var(--bg-elevated); }
        .shopping-match-panel.disabled { opacity: 0.62; }
        .shopping-match-panel.ready { border-color: rgba(34,197,94,0.42); }
        .shopping-match-panel > header { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 11px; }
        .shopping-match-panel > header > b { display: grid; flex: 0 0 auto; place-items: center; width: 22px; height: 22px; border-radius: 50%; color: #bfdbfe; background: rgba(59,130,246,0.18); font-size: 10px; }
        .shopping-match-panel > header span { display: grid; gap: 2px; }
        .shopping-match-panel > header strong { font-size: 12px; }
        .shopping-match-panel > header small { color: var(--fg-muted); font-size: 9px; }
        .shopping-match-panel textarea { min-height: 112px; resize: vertical; }
        .shopping-match-button { width: 100%; justify-content: center; margin-top: 9px; }
        .shopping-source-checks { display: grid; gap: 5px; margin-top: 8px; }
        .shopping-source-check { display: grid; grid-template-columns: 34px minmax(0,1fr); gap: 7px; align-items: center; padding: 6px 7px; border: 1px solid var(--border); }
        .shopping-source-check > b { color: #86efac; font-size: 9px; }
        .shopping-source-check.discovery > b { color: #93c5fd; }
        .shopping-source-check.invalid > b { color: #fca5a5; }
        .shopping-source-check > span { display: grid; min-width: 0; gap: 1px; }
        .shopping-source-check strong { font-size: 9px; }
        .shopping-source-check small { overflow: hidden; color: var(--fg-muted); font-size: 8px; text-overflow: ellipsis; white-space: nowrap; }
        .shopping-analysis-progress { display: grid; gap: 7px; margin-top: 9px; padding: 9px; border: 1px solid var(--border); background: rgba(59,130,246,0.06); }
        .shopping-analysis-progress > header { display: flex; justify-content: space-between; gap: 7px; font-size: 10px; }
        .shopping-analysis-progress > header span { color: #93c5fd; }
        .shopping-analysis-progress > div { display: grid; gap: 4px; }
        .shopping-analysis-progress > div > span { display: flex; align-items: center; gap: 6px; color: var(--fg-muted); font-size: 9px; }
        .shopping-analysis-progress i { width: 7px; height: 7px; border: 1px solid #475569; border-radius: 50%; }
        .shopping-analysis-progress .active { color: #bfdbfe; }
        .shopping-analysis-progress .active i { border-color: #60a5fa; background: #60a5fa; box-shadow: 0 0 0 3px rgba(59,130,246,0.15); }
        .shopping-analysis-progress .done { color: #86efac; }
        .shopping-analysis-progress .done i { border-color: #22c55e; background: #22c55e; }
        .shopping-analysis-progress .failed, .shopping-analysis-progress small.error { color: #fca5a5; }
        .shopping-analysis-progress .failed i { border-color: #ef4444; background: #ef4444; }
        .shopping-analysis-progress > small { color: var(--fg-muted); font-size: 8px; line-height: 1.4; }
        .shopping-match-placeholder { min-height: 100px; display: grid; place-items: center; margin: 0; color: var(--fg-muted); text-align: center; font-size: 11px; }
        .shopping-analysis { display: grid; gap: 3px; padding: 9px; border-left: 3px solid #3b82f6; background: rgba(59,130,246,0.08); }
        .shopping-analysis strong { font-size: 11px; }
        .shopping-analysis span { color: #93c5fd; font-size: 10px; }
        .shopping-analysis small { color: var(--fg-muted); font-size: 9px; line-height: 1.45; }
        .shopping-candidates { display: grid; gap: 6px; max-height: 260px; margin-top: 8px; overflow: auto; }
        .shopping-candidates label { display: grid; grid-template-columns: auto 42px minmax(0,1fr); align-items: center; gap: 7px; border: 1px solid var(--border); padding: 7px; cursor: pointer; }
        .shopping-candidates label.selected { border-color: #3b82f6; background: rgba(59,130,246,0.08); }
        .shopping-candidates img { width: 42px; height: 42px; object-fit: cover; background: white; }
        .shopping-candidates label > span { display: grid; min-width: 0; gap: 2px; }
        .shopping-candidates strong { overflow: hidden; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
        .shopping-candidates small { color: var(--fg-muted); font-size: 8px; line-height: 1.35; }
        .shopping-candidates span > b { color: #86efac; font-size: 9px; }
        .shopping-manual-match { display: grid; gap: 7px; margin-top: 8px; }
        .shopping-manual-match .btn { justify-content: center; }
        .shopping-manual-match small, .shopping-link-state { display: block; margin-top: 7px; color: #fbbf24; font-size: 9px; line-height: 1.45; }
        .shopping-grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(280px, 0.8fr); gap: 16px; }
        .shopping-side-fields { display: flex; flex-direction: column; gap: 12px; }
        .shopping-label { display: block; margin-bottom: 5px; color: var(--fg-muted); font-size: 11px; }
        .shopping-details { border-top: 1px solid var(--border); padding-top: 12px; color: var(--fg-muted); font-size: 12px; }
        .shopping-details summary { cursor: pointer; }
        .shopping-controls { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; border-top: 1px solid var(--border); padding-top: 14px; }
        .shopping-platforms { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
        .shopping-check { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px; }
        .shopping-preview-note { margin-left: auto; display: grid; gap: 2px; color: #93c5fd; font-size: 11px; }
        .shopping-preview-note span { color: var(--fg-muted); font-size: 10px; }
        .shopping-submit { min-width: 154px; justify-content: center; }
        .shopping-message { padding: 10px 12px; border: 1px solid var(--border); background: var(--bg-elevated); font-size: 12px; }
        .shopping-empty { padding: 40px 20px; text-align: center; color: var(--fg-muted); font-size: 13px; }
        .shopping-job-list { display: flex; flex-direction: column; }
        .shopping-job { padding: 16px 20px; border-top: 1px solid var(--border); }
        .shopping-job:first-child { border-top: 0; }
        .shopping-job-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; }
        .shopping-job-head strong { display: block; font-size: 13px; }
        .shopping-job-meta { margin-top: 4px; color: var(--fg-muted); font-size: 10px; }
        .shopping-progress { display: grid; grid-template-columns: repeat(9, minmax(58px, 1fr)); margin-top: 16px; }
        .shopping-step { position: relative; display: flex; flex-direction: column; align-items: center; gap: 5px; color: var(--fg-muted); font-size: 10px; }
        .shopping-step::before { content: ''; position: absolute; top: 5px; right: 50%; width: 100%; height: 2px; background: var(--border); z-index: 0; }
        .shopping-step:first-child::before { display: none; }
        .shopping-step-dot { position: relative; z-index: 1; width: 12px; height: 12px; border-radius: 50%; background: var(--bg-elevated); border: 2px solid var(--border); }
        .shopping-step.done { color: var(--status-published-fg); }
        .shopping-step.done::before, .shopping-step.done .shopping-step-dot { background: var(--status-published-fg); border-color: var(--status-published-fg); }
        .shopping-step.active { color: #60a5fa; }
        .shopping-step.active .shopping-step-dot { background: #60a5fa; border-color: #60a5fa; box-shadow: 0 0 0 4px rgba(59,130,246,0.14); }
        .shopping-step.failed { color: var(--status-failed-fg); }
        .shopping-step.failed .shopping-step-dot { background: var(--status-failed-fg); border-color: var(--status-failed-fg); }
        .shopping-error { margin-top: 10px; color: var(--status-failed-fg); font-size: 11px; }
        .shopping-job-actions { display: flex; justify-content: flex-end; gap: 7px; flex-wrap: wrap; margin-top: 14px; }
        .shopping-caption-review { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 12px; }
        .shopping-caption-review summary { width: fit-content; color: var(--fg-muted); font-size: 11px; cursor: pointer; }
        .shopping-caption-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 10px; }
        .shopping-caption-grid section { min-width: 0; border: 1px solid var(--border); padding: 10px; background: var(--bg-elevated); }
        .shopping-caption-grid header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
        .shopping-caption-grid header strong { font-size: 11px; }
        .shopping-caption-grid button { border: 1px solid var(--border); padding: 3px 7px; color: #93c5fd; background: transparent; font-size: 10px; cursor: pointer; }
        .shopping-caption-grid p { max-height: 180px; margin: 9px 0 0; overflow: auto; color: var(--fg-subtle); font-size: 11px; line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere; }
        @media (max-width: 820px) {
          .shopping-match-grid { grid-template-columns: 1fr; }
          .shopping-grid { grid-template-columns: 1fr; }
          .shopping-preview-note { width: 100%; margin-left: 0; }
          .shopping-submit { width: 100%; }
          .shopping-progress { overflow-x: auto; grid-template-columns: repeat(9, 76px); padding-bottom: 6px; }
          .shopping-caption-grid { grid-template-columns: 1fr; }
        }
      `}</style>
    </>
  );
}

window.ShoppingPage = ShoppingPage;
