// ── Pipeline.jsx — n8n-style content automation pipeline ──────────────────

const STAGES = [
  { id:'write',     label:'AI 글 생성',     sub:'Claude가 스레드 본문 작성',      icon:'✍️', color:'#a78bfa' },
  { id:'posted',    label:'Threads 포스팅', sub:'실제 스레드에 업로드',            icon:'📱', color:'#38bdf8' },
  { id:'cards',     label:'카드 이미지',    sub:'슬라이드 이미지 자동 생성',      icon:'🖼️', color:'#34d399' },
  { id:'thumbnail', label:'썸네일 생성',    sub:'저장 배경 + 후킹 문구 합성', icon:'🎨', color:'#fb923c' },
  { id:'render',    label:'영상 렌더링',    sub:'VPS에서 영상 합성',              icon:'🎬', color:'#fbbf24' },
  { id:'publish',   label:'SNS 업로드',     sub:'YouTube · TikTok · Instagram',  icon:'🚀', color:'#f87171' },
];

const PLATFORM_ICONS = { youtube:'▶️', tiktok:'🎵', instagram:'📸' };
const VIDEO_ACTION_STORAGE_KEY = 'saju_pipeline_video_actions_v1';
const POST_ACTION_STORAGE_KEY = 'saju_pipeline_post_actions_v1';
const VIDEO_ACTION_MAX_AGE_MS = 10 * 60 * 1000;
const VIDEO_ACTION_LABELS = {
  approve: '승인 중...',
  delete: '삭제 중...',
  publish: '발행 중...',
  retry: '렌더링 재시작 중...',
  carousel: '캐러셀 발행 중...',
};

// ── 헬퍼 ──────────────────────────────────────────────────────────────────
function buildPipelineItems(posts, jobs, thumbCache, account) {
  const jobById = {};
  jobs.forEach(j => { jobById[j.id] = j; });
  const jobByPostId = {};
  jobs.forEach(j => { if (j.post_id && !jobByPostId[j.post_id]) jobByPostId[j.post_id] = j; });

  return posts.map(post => {
    const jobIds = Array.isArray(post.metadata?.auto_video_job_ids)
      ? post.metadata.auto_video_job_ids
      : post.metadata?.auto_video_job_id ? [post.metadata.auto_video_job_id] : [];
    const relatedJob = jobById[jobIds[0]] || jobByPostId[post.id] || null;

    // 썸네일: 방금 생성한 캐시 OR 포스트 메타 OR 잡 메타
    const thumbUrl =
      thumbCache[post.id] ||
      post.metadata?.auto_thumbnail_url ||
      relatedJob?.metadata?.thumbnail_image_url || null;

    const autoVideoStatus = post.metadata?.auto_video_status || null;
    const stage  = getStageIndex(post, relatedJob, thumbUrl, autoVideoStatus);
    const status = getItemStatus(post, relatedJob, stage, autoVideoStatus);
    return {
      id: post.id,
      title: post.title || post.content?.slice(0,60) || '(제목 없음)',
      content: post.content || '',
      createdAt: post.created_at,
      stage, status,
      job: relatedJob,
      thumbUrl,
      autoVideoStatus,
      autoVideoError: post.metadata?.auto_video_error || null,
      autoVideoRequestId: post.metadata?.auto_video_request_id || null,
      post,
      account,
    };
  });
}

function getStageIndex(post, job, thumbUrl, autoVideoStatus) {
  if (job?.status === 'published') return 5;
  if (job?.status === 'publishing') return 5;
  if (['approved','scheduled','rendered','rendering','queued','failed'].includes(job?.status)) return 4;
  if (autoVideoStatus === 'processing' || autoVideoStatus === 'failed') return 4;
  if (thumbUrl) return 3;                       // 썸네일 완료
  // ⚠️ 포스팅·카드 단계는 '실제 발행(external_post_id)' 후에만 인정.
  // 미발행 draft는 threads_series_parts가 있어도 stage 0 (AI 글 생성만 됨) → 발행된 글과 구분됨.
  if (post.external_post_id) {
    const hasParts = Array.isArray(post.metadata?.threads_series_parts) && post.metadata.threads_series_parts.length > 0;
    if (hasParts) return 2;                     // 발행됨 + 카드 준비됨
    return 1;                                   // 발행됨(Threads 포스팅 완료)
  }
  return 0;                                     // 미발행
}

function getItemStatus(post, job, stage, autoVideoStatus) {
  if (job?.status === 'failed')    return 'failed';
  if (job?.status === 'published') return 'done';
  if (['publishing', 'rendering', 'queued'].includes(job?.status)) return 'active';
  if (autoVideoStatus === 'failed') return 'failed';
  if (autoVideoStatus === 'processing') return 'active';
  if (stage >= 3) return 'done';
  // "카드만 자동" 이후: 발행됨 + 카드 준비됨(영상 잡 없음) → 카드 단계를 '현재 진행 단계'로 표시
  // → 발행 직후 다음 단계 진행 표시가 뜨고, 사용자가 영상/캐러셀을 진행하도록 안내.
  if (post.external_post_id && stage === 2) return 'active';
  return 'pending';
}

function stageStatusForItem(item, i) {
  if (i < item.stage) return 'done';
  if (i === item.stage) {
    if (item.status === 'failed') return 'failed';
    if (item.status === 'active') return 'active';
    return 'done';
  }
  return 'pending';
}

function fmtDate(v) {
  if (!v) return '';
  return new Date(v).toLocaleDateString('ko-KR', { month:'short', day:'numeric', hour:'2-digit', minute:'2-digit' });
}

// ══════════════════════════════════════════════════════════════
//  단계별 상세 패널
// ══════════════════════════════════════════════════════════════

// ── 공통: 본문 파트 파싱 유틸 ─────────────────────────────────────────────
function parsePostParts(item) {
  const metadataParts = item.post?.metadata?.threads_series_parts;
  const generatedParts = item.post?.metadata?.generated_thread?.posts;

  function clean(value) {
    const isTitleLine = (line) => {
      const normalized = line
        .normalize('NFD')
        .replace(/[\u0300-\u036f]/g, '')
        .trim()
        .toLowerCase();
      return /^#{1,6}\s+\S/.test(normalized) ||
        /^(?:title|t\S*tulo|titulo|titre|titel|translated title|context title|heading|headline|\uc81c\ubaa9|\u6807\u9898|\u6a19\u984c)\s*[:\uff1a]/.test(normalized);
    };
    return String(value || '')
      .replace(/\r\n/g, '\n')
      .replace(/^\s*===+\s*/g, '')
      .replace(/\s*===+\s*$/g, '')
      .split('\n')
      .filter(line => !isTitleLine(line))
      .join('\n')
      .replace(/^\s*\d+(?:\/\d+)?\.\s*/, '')
      .replace(/\.(?!\.\.)([ \t]+)(?=\S)/g, '.\n')
      .trim();
  }
  function splitLong(text, max = 500) {
    const c = clean(text);
    if (!c || c.length <= max) return c ? [c] : [];
    const sentences = c.match(/[^.!?]+[.!?]*/g) || [c];
    const res = []; let cur = '';
    for (const s of sentences) {
      const t = s.trim(); if (!t) continue;
      const n = cur ? cur + ' ' + t : t;
      if (n.length <= max) { cur = n; } else { if (cur) res.push(cur); cur = t; }
    }
    if (cur) res.push(cur);
    return res;
  }
  function pack(src, max = 500, firstMax = 300) {
    const res = [];
    src.forEach((part, index) => {
      const c = clean(part);
      if (!c) return;
      const limit = index === 0 ? firstMax : max;
      res.push(...splitLong(c, limit));
    });
    return res;
  }

  if (Array.isArray(metadataParts) && metadataParts.length > 0)
    return metadataParts.map(clean).filter(Boolean);
  if (Array.isArray(generatedParts) && generatedParts.length > 0)
    return generatedParts.map(clean).filter(Boolean);

  const raw = String(item.content || '').replace(/\r\n/g, '\n').trim();
  if (!raw) return [];
  const blocks = raw.split(/\n+(?=\d+(?:\/\d+)?\.\s)/);
  const src = blocks.length > 1 ? blocks.map(clean).filter(Boolean) : raw.split(/\n{2,}/).map(clean).filter(Boolean);
  return pack(src);
}

// ── 공통: 숫자 박스 파트 목록 ─────────────────────────────────────────────
function PartList({ parts, emptyText = '아직 생성된 본문이 없습니다.' }) {
  if (!parts.length) return <div className="sd-empty">{emptyText}</div>;
  return (
    <div className="sd-parts">
      {parts.map((p, i) => (
        <div key={i} className="sd-part">
          <div style={{display:'flex',flexDirection:'column',alignItems:'center',gap:4,flexShrink:0,minWidth:52}}>
            <span className="sd-part-num">{i+1}/{parts.length}</span>
            <span style={{fontSize:9,color:'var(--fg-subtle,#666)',textAlign:'center',lineHeight:1.2}}>Reply {i+1}</span>
          </div>
          <pre className="sd-text" style={{flex:1}}>{p}</pre>
        </div>
      ))}
    </div>
  );
}

// 0 — AI 글 생성
function StageDetail0({ item, onApprove, onPublish, publishingId }) {
  const contentParts = parsePostParts(item);
  const isPublishing = publishingId === item.post?.id;
  return (
    <div className="sd-wrap">
      <div className="sd-label">생성된 스레드 본문 ({contentParts.length}파트)</div>
      <PartList parts={contentParts} />
      <div className="sd-meta-row">
        <span>생성일: {fmtDate(item.createdAt)}</span>
        <span>상태: {item.post?.status || '-'}</span>
        {item.post?.metadata?.selected_topic?.category && (
          <span>카테고리: {item.post.metadata.selected_topic.category}</span>
        )}
      </div>
      {(item.post?.status === 'pending_approval' || item.post?.status === 'draft') && (
        <div className="sd-actions">
          <button className="btn sm primary" type="button" disabled={isPublishing} onClick={() => onApprove && onApprove(item.post.id)}>
            {isPublishing ? '⏳ 발행 중...' : '✅ 승인 & Threads 발행'}
          </button>
        </div>
      )}
      {item.post?.status === 'approved' && !item.post?.external_post_id && (
        <div className="sd-actions">
          <button className="btn sm primary" type="button" disabled={isPublishing} onClick={() => onPublish && onPublish(item.post.id)}>
            {isPublishing ? '⏳ 발행 중...' : '🚀 Threads 발행'}
          </button>
        </div>
      )}
    </div>
  );
}

// 1 — Threads 포스팅
function StageDetail1({ item, onPublish }) {
  const extId = item.post?.external_post_id;
  const url = extId ? `https://www.threads.net/@korea.saju.oracle/post/${extId}` : null;
  const parts = parsePostParts(item);
  return (
    <div className="sd-wrap">
      <div className="sd-label">Threads 발행 본문 미리보기 ({parts.length}파트)</div>
      <PartList parts={parts} emptyText="본문이 없습니다." />
      {extId ? (
        <div style={{display:'flex',alignItems:'center',gap:12,flexWrap:'wrap'}}>
          <div className="sd-kv"><span>포스트 ID</span><code style={{fontSize:11}}>{extId}</code></div>
          <a href={url} target="_blank" rel="noreferrer" className="sd-link">Threads에서 보기 ↗</a>
        </div>
      ) : (
        <div className="sd-empty" style={{marginTop:0}}>아직 Threads에 게시되지 않았습니다.</div>
      )}
      <div className="sd-meta-row"><span>발행일: {fmtDate(item.post?.published_at)}</span></div>
      {item.post?.id && (
        <div className="sd-actions">
          <button className="btn sm ghost" type="button" onClick={() => onPublish && onPublish(item.post.id)}>
            🔄 재발행
          </button>
        </div>
      )}
    </div>
  );
}

// 2 — 카드 이미지
function StageDetail2({ item, onCreateVideo, onCarousel }) {
  const [carouselLoading, setCarouselLoading] = React.useState(false);
  const [cacheKey,      setCacheKey]      = React.useState(() => Date.now());
  const [resetting,     setResetting]     = React.useState(false);
  const [resetMsg,      setResetMsg]      = React.useState('');
  const [videoLoading,  setVideoLoading]  = React.useState(false);
  const [videoMsg,      setVideoMsg]      = React.useState('');
  const [previewCard,   setPreviewCard]   = React.useState(null);

  const parts   = item.post?.metadata?.threads_series_parts;
  const count   = Array.isArray(parts) ? parts.length : 0;
  const hasPost = !!item.post?.external_post_id;

  async function handleResetParts() {
    if (!confirm('카드 내용을 현재 포스트 본문 기준으로 초기화합니다. 계속하시겠습니까?')) return;
    setResetting(true);
    setResetMsg('');
    try {
      const res  = await fetch('/api/admin/social-posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ postId: item.post.id, action: 'reset_series_parts' }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok || data?.error) throw new Error(data?.error || '초기화 실패');
      setResetMsg(`✅ ${data.parts?.length || 0}개 파트로 재생성됨 — 이제 영상 생성 시작 버튼을 누르세요`);
      setCacheKey(Date.now()); // 카드 이미지도 새로고침
    } catch (e) {
      setResetMsg('❌ ' + e.message);
    } finally {
      setResetting(false);
    }
  }

  return (
    <div className="sd-wrap">
      <div style={{display:'flex',alignItems:'center',gap:12}}>
        <div className="sd-label">카드 이미지 ({count}장)</div>
        {count > 0 && (
          <button
            className="btn sm ghost"
            type="button"
            style={{fontSize:11,padding:'2px 10px',marginBottom:2}}
            onClick={() => setCacheKey(Date.now())}
          >
            🔄 미리보기 새로고침
          </button>
        )}
      </div>
      {count > 0 ? (
        <div className="sd-card-grid">
          {parts.map((_,i) => (
            <div key={i} className="sd-card-wrap">
              <img
                src={`/api/threads/cards/${encodeURIComponent(item.id)}/${i}?t=${cacheKey}`}
                alt={`카드 ${i+1}`}
                className="sd-card-img"
                loading="lazy"
                onError={(e) => {
                  if (e.currentTarget.dataset.failed) return;
                  e.currentTarget.dataset.failed = '1';
                  e.currentTarget.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(
                    "<svg xmlns='http://www.w3.org/2000/svg' width='270' height='480'><rect width='100%' height='100%' fill='#1a1206'/><text x='50%' y='46%' fill='#f6b21a' font-size='16' text-anchor='middle'>카드 로드 실패</text><text x='50%' y='54%' fill='#c9a24b' font-size='12' text-anchor='middle'>이미지 라우트 오류(500)</text></svg>"
                  );
                }}
              />
              <button
                className="sd-card-preview-btn"
                type="button"
                onClick={() => setPreviewCard({
                  index: i,
                  src: `/api/threads/cards/${encodeURIComponent(item.id)}/${i}?t=${cacheKey}`,
                })}
              >
                크게 보기
              </button>
              <div className="sd-card-label">{i+1}/{count}</div>
            </div>
          ))}
        </div>
      ) : (
        <div className="sd-empty">카드 이미지가 아직 생성되지 않았습니다.</div>
      )}
      {resetMsg && (
        <div style={{fontSize:12,color:resetMsg.startsWith('✅')?'#34d399':'#f87171'}}>{resetMsg}</div>
      )}
      {item.post?.id && (
        <div className="sd-actions">
          <button
            className="btn sm ghost"
            type="button"
            disabled={resetting}
            onClick={handleResetParts}
          >
            {resetting ? '초기화 중...' : '♻️ 카드 내용 재생성'}
          </button>
          {hasPost && (
            <button
              className="btn sm primary"
              type="button"
              disabled={videoLoading}
              onClick={async (event) => {
                event.stopPropagation();
                if (!onCreateVideo) return;
                setVideoLoading(true);
                setVideoMsg(`⏳ 영상 생성 요청 전송 중... (${item.post.id.slice(0, 8)})`);
                try {
                  const result = await onCreateVideo(item.post.id);
                  if (result?.ok === false) throw new Error(result.error || result.reason || '영상 생성 요청 실패');
                  setVideoMsg('✅ 영상 생성 요청 완료! 렌더링 중...');
                } catch(e) {
                  setVideoMsg('❌ ' + (e.message || '요청 실패'));
                } finally {
                  setVideoLoading(false);
                }
              }}
            >
              {videoLoading
                ? <><span className="pr-dot-spin" style={{display:'inline-block',marginRight:6}} />요청 중...</>
                : '🎬 영상 생성 시작'}
            </button>
          )}
          {hasPost && onCarousel && (
            <button
              className="btn sm"
              type="button"
              disabled={carouselLoading || count < 2}
              title={count < 2 ? '카드가 2장 이상이어야 캐러셀 발행이 가능합니다' : '영상 없이 카드들을 인스타 캐러셀로 바로 발행'}
              onClick={async (event) => {
                event.stopPropagation();
                setCarouselLoading(true);
                setVideoMsg(`⏳ 캐러셀 발행 중... (${item.post.id.slice(0, 8)})`);
                try {
                  const result = await onCarousel(item.post.id);
                  if (result?.ok === false) throw new Error(result.error || '캐러셀 발행 실패');
                  if (result?.ok) setVideoMsg('✅ 인스타 캐러셀 발행 완료!');
                  else setVideoMsg('');
                } catch(e) {
                  setVideoMsg('❌ ' + (e.message || '캐러셀 실패'));
                } finally {
                  setCarouselLoading(false);
                }
              }}
            >
              {carouselLoading
                ? <><span className="pr-dot-spin" style={{display:'inline-block',marginRight:6}} />발행 중...</>
                : '🖼 캐러셀 발행 (인스타)'}
            </button>
          )}
        </div>
      )}
      {videoMsg && (
        <div style={{fontSize:12,marginTop:6,color:videoMsg.startsWith('✅')?'#34d399':'#f87171'}}>{videoMsg}</div>
      )}
      {hasPost && !videoLoading && !videoMsg && (
        <div style={{fontSize:11,color:'var(--fg-subtle,#666)'}}>
          ♻️ 재생성 → 카드 확인 → 🎬 영상 생성 순서로 진행하세요
        </div>
      )}
      {previewCard && (
        <div className="sd-preview-backdrop" onClick={() => setPreviewCard(null)}>
          <div className="sd-preview-modal" onClick={(e) => e.stopPropagation()}>
            <button className="sd-preview-close" type="button" onClick={() => setPreviewCard(null)}>x</button>
            <img src={previewCard.src} alt={`card ${previewCard.index + 1}`} className="sd-preview-img" />
          </div>
        </div>
      )}
    </div>
  );
}

// 3 — 썸네일 생성 ← NEW
function StageDetail3({ item, onThumbnailGenerated, onCreateVideo }) {
  const [generating,    setGenerating]    = React.useState(false);
  const [result,        setResult]        = React.useState(null); // { url, prompt, model }
  const [error,         setError]         = React.useState('');
  const [videoLoading,  setVideoLoading]  = React.useState(false);
  const [videoMsg,      setVideoMsg]      = React.useState('');

  const existingThumb = item.thumbUrl;
  const displayThumb  = result?.url || existingThumb;

  async function handleGenerate() {
    const title   = item.post?.title || item.title || '';
    const caption = item.content || '';
    if (!title && !caption) { setError('글 내용이 없습니다.'); return; }

    setGenerating(true);
    setError('');
    try {
      const res  = await fetch('/api/admin/social-videos/generate-thumbnail', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          title,
          caption,
          locale: item.account?.locale || item.account?.metadata?.locale || null,
          language: item.account?.language || item.account?.metadata?.language || null,
          postId: item.post?.id || item.id || null,
          zodiacAnimal: item.post?.metadata?.selected_topic?.zodiac_animal || null,
          force: true,
        }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok || data?.error) throw new Error(data?.error || '썸네일 생성 실패');
      const generated = { url: data.thumbnailImageUrl, storagePath: data.storagePath, prompt: data.prompt, model: data.model };
      setResult(generated);
      await onThumbnailGenerated(item, generated);
    } catch (e) {
      setError(e.message || '알 수 없는 오류');
    } finally {
      setGenerating(false);
    }
  }

  return (
    <div className="sd-wrap">
      <div className="sd-label">저장 배경 썸네일 생성</div>

      <div className="sd-thumb-layout">
        {/* 썸네일 미리보기 */}
        <div className="sd-thumb-preview">
          {displayThumb ? (
            <img src={displayThumb} alt="생성된 썸네일" className="sd-thumb-img" />
          ) : (
            <div className="sd-thumb-placeholder">
              {generating ? (
                <>
                  <div className="sd-thumb-spinner" />
                  <span>썸네일 합성 중...</span>
                  <span style={{fontSize:10,opacity:.6}}>저장 배경에 글자를 합성합니다</span>
                </>
              ) : (
                <>
                  <span style={{fontSize:32}}>🎨</span>
                  <span>아직 썸네일이 없습니다</span>
                </>
              )}
            </div>
          )}
        </div>

        {/* 오른쪽 메타 + 버튼 */}
        <div className="sd-thumb-meta">
          {result?.model && <div className="sd-kv"><span>모델</span><code>{result.model}</code></div>}
          {displayThumb && (
            <div className="sd-kv">
              <span>URL</span>
              <a href={displayThumb} target="_blank" rel="noreferrer" className="sd-link" style={{fontSize:11,wordBreak:'break-all'}}>
                원본 보기 ↗
              </a>
            </div>
          )}

          <div className="sd-actions" style={{marginTop:8}}>
            <button
              className={`btn sm ${generating?'':'primary'}`}
              type="button"
              disabled={generating}
              onClick={handleGenerate}
            >
              {generating ? '생성 중...' : displayThumb ? '🔄 재생성' : '✨ 썸네일 생성'}
            </button>
            {displayThumb && item.post?.id && (
              <button
                className={`btn sm ${videoLoading ? '' : 'primary'}`}
                type="button"
                disabled={videoLoading}
                onClick={async (event) => {
                  event.stopPropagation();
                  if (!onCreateVideo) return;
                  setVideoLoading(true);
                  setVideoMsg(`⏳ 영상 생성 요청 전송 중... (${item.post.id.slice(0, 8)})`);
                  try {
                    const result = await onCreateVideo(item.post.id);
                    if (result?.ok === false) throw new Error(result.error || result.reason || '영상 생성 요청 실패');
                    setVideoMsg('✅ 영상 생성 요청 완료!');
                  } catch(e) {
                    setVideoMsg('❌ ' + (e.message || '요청 실패'));
                  } finally {
                    setVideoLoading(false);
                  }
                }}
              >
                {videoLoading
                  ? <><span className="pr-dot-spin" style={{display:'inline-block',marginRight:6}} />요청 중...</>
                  : '🎬 영상 생성'}
              </button>
            )}
            {videoMsg && (
              <div style={{fontSize:11,marginTop:4,color:videoMsg.startsWith('✅')?'#34d399':'#f87171'}}>{videoMsg}</div>
            )}
          </div>

          <div style={{fontSize:11,color:'var(--fg-subtle,#666)',lineHeight:1.5,marginTop:4}}>
            글 내용을 분석해서 고CTR<br/>문구를 저장 배경 위에 합성합니다.
          </div>
        </div>
      </div>

      {/* 프롬프트 */}
      {result?.prompt && (
        <details style={{marginTop:4}}>
          <summary style={{fontSize:11,color:'var(--fg-subtle,#666)',cursor:'pointer'}}>사용된 프롬프트 보기</summary>
          <pre className="sd-text" style={{marginTop:8,fontSize:11}}>{result.prompt}</pre>
        </details>
      )}

      {error && <div className="pr-error">{error}</div>}
    </div>
  );
}

// 4 — 영상 렌더링
function StageDetail4({ item, onAction, pendingAction }) {
  const job = item.job;
  if (!job) return <div className="sd-wrap"><div className="sd-empty">영상 잡이 없습니다.</div></div>;

  const meta = job.metadata || {};
  const ALL_PLATFORMS = ['youtube', 'tiktok', 'instagram'];
  const publishedUrls = meta.published_urls || {};

  // 항상 세 플랫폼 전부 표시 — 미발행 플랫폼은 기본 선택, 이미 발행된 건 기본 미선택
  const [selPlatforms, setSelPlatforms] = React.useState(() =>
    ALL_PLATFORMS.filter(p => !publishedUrls[p])
  );

  function togglePlatform(p) {
    setSelPlatforms(prev =>
      prev.includes(p) ? prev.filter(x => x !== p) : [...prev, p]
    );
  }

  const thumbUrl = meta.thumbnail_image_url || item.thumbUrl || job.thumbnail_url || null;
  const statusColor = {
    rendering:'#38bdf8', queued:'#38bdf8',
    rendered:'#fbbf24', approved:'#a78bfa', scheduled:'#a78bfa',
    published:'#34d399', failed:'#f87171',
  }[job.status] || '#888';

  const canPublish = ['approved','rendered','failed','published'].includes(job.status) && job.video_url;
  const isBusy = !!pendingAction;
  const busyLabel = pendingAction ? (VIDEO_ACTION_LABELS[pendingAction.action] || '처리 중...') : '';
  const statusHint = job.status === 'approved'
    ? '승인 완료 · 아직 SNS 업로드 전입니다. 아래 🚀 SNS 업로드 시작을 눌러야 POST가 전송됩니다.'
    : job.status === 'publishing'
      ? 'SNS 업로드 요청 전송됨 · 백그라운드에서 발행 중입니다.'
      : '';

  return (
    <div className="sd-wrap">
      <div className="sd-label">영상 렌더링 상세</div>
      <div className="sd-video-layout">
        <div className="sd-media-col">
          {job.video_url ? (
            <video src={job.video_url} controls className="sd-video" poster={thumbUrl||undefined} />
          ) : thumbUrl ? (
            <img src={thumbUrl} alt="썸네일" className="sd-thumb" />
          ) : (
            <div className="sd-no-media">🎬<br/>렌더링 대기 중</div>
          )}
        </div>
        <div className="sd-meta-col">
          <div className="sd-kv"><span>상태</span><span style={{color:statusColor,fontWeight:700}}>{job.status}</span></div>
          {statusHint && <div style={{fontSize:12,color:'#fbbf24',margin:'-4px 0 8px'}}>{statusHint}</div>}
          <div className="sd-kv"><span>잡 ID</span><code style={{fontSize:10}}>{job.id?.slice(0,16)}…</code></div>
          {job.duration_seconds && <div className="sd-kv"><span>길이</span><span>{job.duration_seconds}초</span></div>}
          {meta.image_count && <div className="sd-kv"><span>이미지</span><span>{meta.image_count}장</span></div>}
          {job.title && <div className="sd-kv"><span>제목</span><span style={{fontWeight:600}}>{job.title}</span></div>}

          {/* 플랫폼 선택 체크박스 — 항상 3개 전부 표시 */}
          <div>
              <div style={{fontSize:11,color:'var(--fg-subtle,#666)',marginBottom:6}}>발행 플랫폼 선택</div>
              <div style={{display:'flex',gap:8,flexWrap:'wrap'}}>
                {ALL_PLATFORMS.map(p => {
                  const done = !!publishedUrls[p];
                  const checked = selPlatforms.includes(p);
                  return (
                    <label key={p} style={{display:'flex',alignItems:'center',gap:5,cursor:'pointer',
                      fontSize:12,padding:'4px 10px',borderRadius:6,border:'1px solid',
                      borderColor: checked ? (done?'#34d399':'#a78bfa') : 'var(--border,#333)',
                      background: checked ? (done?'rgba(52,211,153,.1)':'rgba(167,139,250,.1)') : 'transparent',
                      color: checked ? (done?'#34d399':'#a78bfa') : 'var(--fg-subtle,#888)',
                      fontWeight: checked ? 600 : 400,
                    }}>
                      <input type="checkbox" checked={checked} onChange={() => togglePlatform(p)}
                        style={{accentColor: done?'#34d399':'#a78bfa',cursor:'pointer'}} />
                      {PLATFORM_ICONS[p]||p} {p}
                      {done && <span style={{fontSize:10}}>✓</span>}
                    </label>
                  );
                })}
              </div>
            </div>

          <div className="sd-actions">
            {job.status === 'rendered' && (
              <button className="btn sm primary" type="button" disabled={isBusy} onClick={() => onAction(job.id,'approve')}>
                {pendingAction?.action === 'approve' ? busyLabel : '✅ 승인만 하기'}
              </button>
            )}
            {canPublish && (
              <button
                className="btn sm primary"
                type="button"
                disabled={selPlatforms.length === 0 || isBusy}
                onClick={() => onAction(job.id, 'publish', { publishPlatforms: selPlatforms })}
              >
                {pendingAction?.action === 'publish' ? busyLabel : (
                  <>
                    {job.status === 'published' ? '🔁 추가 SNS 업로드' : '🚀 SNS 업로드 시작'}
                    {selPlatforms.length > 0 && selPlatforms.length < ALL_PLATFORMS.length
                      ? ` (${selPlatforms.map(p=>PLATFORM_ICONS[p]||p).join('')})`
                      : ''}
                  </>
                )}
              </button>
            )}
            {job.status === 'failed' && !job.video_url && (
              <button className="btn sm primary" type="button" disabled={isBusy} onClick={() => onAction(job.id,'retry')}>
                {pendingAction?.action === 'retry' ? busyLabel : '🔄 렌더링 재시작'}
              </button>
            )}
            {(job.image_urls?.length || 0) >= 2 && !['publishing','published'].includes(job.status) && (
              <button className="btn sm primary" type="button" disabled={isBusy} onClick={() => onAction(job.id,'carousel')}>
                {pendingAction?.action === 'carousel' ? busyLabel : '🖼 캐러셀 발행 (인스타)'}
              </button>
            )}
            {!['publishing','published'].includes(job.status) && (
              <button className="btn sm ghost" type="button" disabled={isBusy} onClick={() => onAction(job.id,'delete')}>
                {pendingAction?.action === 'delete' ? busyLabel : '🗑 삭제'}
              </button>
            )}
          </div>
          {pendingAction && (
            <div style={{fontSize:12,marginTop:8,color:'#fbbf24'}}>
              {busyLabel} 백그라운드 요청 확인 중입니다.
            </div>
          )}
        </div>
      </div>

      {(meta.youtube_caption || meta.tiktok_caption || meta.instagram_caption) && (
        <div className="sd-captions">
          {[
            { icon:'▶️', label:'YouTube',   val: meta.youtube_caption },
            { icon:'🎵', label:'TikTok',    val: meta.tiktok_caption },
            { icon:'📸', label:'Instagram', val: meta.instagram_caption },
          ].filter(c => c.val).map(c => (
            <div key={c.label} className="sd-caption-block">
              <div className="sd-caption-head">{c.icon} {c.label}</div>
              <pre className="sd-caption-body">{c.val}</pre>
            </div>
          ))}
        </div>
      )}
      {job.last_error && <div className="pr-error">{job.last_error}</div>}
    </div>
  );
}

// 5 — SNS 업로드
function StageDetail5({ item }) {
  const job = item.job;
  const publishedUrls = job?.metadata?.published_urls || {};
  const publishErrors = job?.metadata?.publish_errors || {};
  const platforms = Object.keys(publishedUrls);
  return (
    <div className="sd-wrap">
      <div className="sd-label">SNS 업로드 결과</div>
      {job?.metadata?.youtube_title && (
        <div className="sd-kv"><span>YouTube 제목</span><span>{job.metadata.youtube_title}</span></div>
      )}
      {job?.metadata?.youtube_privacy_status && (
        <div className="sd-kv"><span>공개 상태</span><span>{job.metadata.youtube_privacy_status}</span></div>
      )}
      {job?.metadata?.youtube_thumbnail_mode && (
        <div className="sd-kv">
          <span>썸네일</span>
          <span style={{color:'#34d399'}}>영상 첫 2초에 삽입</span>
        </div>
      )}
      {platforms.length > 0 ? (
        <div className="sd-pub-list">
          {platforms.map(p => (
            <div key={p} className="sd-pub-row">
              <span className="sd-pub-icon">{PLATFORM_ICONS[p]||p}</span>
              <span className="sd-pub-name">{p}</span>
              <a href={publishedUrls[p]} target="_blank" rel="noreferrer" className="sd-link">업로드된 영상 보기 ↗</a>
            </div>
          ))}
        </div>
      ) : (
        <div className="sd-empty">아직 업로드되지 않았습니다.</div>
      )}
      {Object.entries(publishErrors).map(([p,err]) => (
        <div key={p} className="pr-error">{PLATFORM_ICONS[p]||p} 오류: {err}</div>
      ))}
      <div className="sd-meta-row"><span>발행일: {fmtDate(job?.published_at)}</span></div>
    </div>
  );
}

const STAGE_DETAIL_MAP = [StageDetail0, StageDetail1, StageDetail2, StageDetail3, StageDetail4, StageDetail5];

// ── 글로벌 파이프라인 노드 다이어그램 ─────────────────────────────────────
function PipelineDiagram({ items, focusedStage = null }) {
  const stageCounts = STAGES.map((_,i) => ({
    active: items.filter(it => it.stage === i && it.status === 'active').length,
    done:   items.filter(it => it.stage === i && it.status === 'done').length,
    failed: items.filter(it => it.stage === i && it.status === 'failed').length,
  }));

  return (
    <div className="pipeline-diagram">
      {STAGES.map((stage, i) => {
        const c = stageCounts[i];
        const isActive = c.active > 0;
        const hasFailed = c.failed > 0;
        const isFocused = focusedStage === i;
        return (
          <React.Fragment key={stage.id}>
            <div className={`pn-node ${isActive?'pn-active':hasFailed?'pn-failed':'pn-idle'} ${isFocused?'pn-focused':''}`}
                 style={{'--node-color':stage.color}}>
              <div className="pn-icon">{stage.icon}</div>
              <div className="pn-label">{stage.label}</div>
              <div className="pn-sub">{stage.sub}</div>
              <div className="pn-chips">
                {c.active > 0 && <span className="pn-chip active">{c.active} 처리중</span>}
                {c.done   > 0 && <span className="pn-chip done">{c.done} 완료</span>}
                {c.failed > 0 && <span className="pn-chip failed">{c.failed} 오류</span>}
              </div>
              {(isActive || isFocused) && <div className="pn-pulse" />}
            </div>
            {i < STAGES.length - 1 && (
              <div className={`pn-edge ${isActive || isFocused?'pn-edge-active':''}`}>
                <div className="pn-edge-line" />
                <svg className="pn-edge-arrow" width="10" height="14" viewBox="0 0 10 14">
                  <path d="M0 0 L10 7 L0 14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
                </svg>
              </div>
            )}
          </React.Fragment>
        );
      })}
    </div>
  );
}

// ── 개별 파이프라인 행 ─────────────────────────────────────────────────────
function PipelineRow({ item, onCreateVideo, onCarousel, onVideoAction, onThumbnailGenerated, onApprovePost, onPublishPost, onFocusStage, publishingId, videoActions }) {
  const [activeStage, setActiveStage] = React.useState(null);
  const rowPendingAction = item.job?.id ? videoActions?.[item.job.id] : null;

  function toggleStage(i) {
    setActiveStage(prev => {
      const next = prev === i ? null : i;
      onFocusStage?.(next);
      return next;
    });
  }

  function selectStage(i) {
    setActiveStage(i);
    onFocusStage?.(i);
  }

  function closeStage() {
    setActiveStage(null);
    onFocusStage?.(null);
  }

  async function handleVideoAction(jobId, action, options = {}) {
    if (action === 'publish' && !confirm('이 영상을 즉시 발행하시겠습니까?')) return;
    if (action === 'delete'  && !confirm('이 영상 작업을 삭제하시겠습니까?'))  return;
    await onVideoAction(jobId, action, options);
  }

  const DetailComponent = activeStage !== null ? STAGE_DETAIL_MAP[activeStage] : null;

  return (
    <div className={`pr-row ${item.status==='active'?'pr-row-active':''} ${item.status==='failed'?'pr-row-failed':''}`}>

      <div className="pr-header">
        <button
          type="button"
          className={`pr-title-block pr-title-block-btn ${activeStage === item.stage ? 'pr-title-block-open' : ''}`}
          onClick={() => toggleStage(item.stage)}
          title="클릭해서 본문 내용 보기"
        >
          <div>
            <div className="pr-title">{item.title}</div>
            <div className="pr-meta">{fmtDate(item.createdAt)} · 클릭해서 내용 보기 ↓</div>
            {item.autoVideoRequestId && (
              <div className="pr-meta">영상 요청ID: {item.autoVideoRequestId}</div>
            )}
            {item.autoVideoError && (
              <div className="pr-error" style={{marginTop:4}}>{item.autoVideoError}</div>
            )}
          </div>
          <span className="pr-expand-arrow">{activeStage === item.stage ? '▲' : '▼'}</span>
        </button>

        {/* 클릭 가능한 스테이지 도트 */}
        <div className="pr-stages">
          {STAGES.map((stage, i) => {
            const s = stageStatusForItem(item, i);
            const clickable = true;
            const selected  = activeStage === i;
            return (
              <React.Fragment key={stage.id}>
                <button
                  type="button"
                  className={`pr-dot pr-dot-${s} ${selected?'pr-dot-selected':''} ${clickable?'pr-dot-clickable':''}`}
                  style={{'--dot-color': stage.color}}
                  title={`${stage.label}${clickable?' — 클릭해서 상세 보기':''}`}
                  disabled={!clickable}
                  onClick={() => clickable && toggleStage(i)}
                >
                  {s==='done'   && <span className="pr-dot-check">✓</span>}
                  {s==='active' && <span className="pr-dot-spin" />}
                  {s==='failed' && <span>✕</span>}
                </button>
                {i < STAGES.length-1 && (
                  <div className={`pr-line pr-line-${s==='done'?'done':'pending'}`} />
                )}
              </React.Fragment>
            );
          })}
        </div>

        <div className="pr-badge-wrap" style={{display:'flex',alignItems:'center',gap:8,flexShrink:0}}>
          {rowPendingAction && (
            <span className="pr-badge active">{VIDEO_ACTION_LABELS[rowPendingAction.action] || '처리 중...'}</span>
          )}
          {item.status==='active'                && <span className="pr-badge active">렌더링 중</span>}
          {item.status==='done' && item.stage===5 && <span className="pr-badge done">업로드 완료</span>}
          {item.status==='failed' && (
            <>
              <span className="pr-badge failed">오류</span>
              {item.job?.id && (
                <button
                  className="btn sm primary"
                  type="button"
                  style={{fontSize:11,padding:'3px 10px'}}
                  onClick={e => { e.stopPropagation(); handleVideoAction(item.job.id,'retry'); }}
                >
                  🔄 재시작
                </button>
              )}
            </>
          )}
          {item.status==='pending'               && <span className="pr-badge pending">{STAGES[item.stage]?.label}</span>}
        </div>
      </div>

      {/* 단계 상세 패널 */}
      {activeStage !== null && (
        <div className="pr-stage-panel">
          <div className="pr-stage-tabs">
            {STAGES.map((stage, i) => {
              const s = stageStatusForItem(item, i);
              return (
                <button key={stage.id} type="button"
                  className={`pr-stage-tab ${activeStage===i?'active':''} ${s==='pending'?'pr-stage-tab-pending':''}`}
                  style={{'--tab-color': s==='pending' ? '#555' : stage.color}}
                  onClick={() => selectStage(i)}
                >
                  {stage.icon} {stage.label}
                </button>
              );
            })}
            <button type="button" className="pr-stage-close" onClick={closeStage}>✕</button>
          </div>
          <div className="pr-stage-content">
            <DetailComponent
              item={item}
              onAction={handleVideoAction}
              onThumbnailGenerated={onThumbnailGenerated}
              onApprove={onApprovePost}
              onPublish={onPublishPost}
              onCreateVideo={onCreateVideo}
              onCarousel={onCarousel}
              publishingId={publishingId}
              pendingAction={rowPendingAction}
            />
          </div>
        </div>
      )}
    </div>
  );
}

// ── 메인 PipelinePage ──────────────────────────────────────────────────────
function PipelinePage({ state, acc } = {}) {
  const [posts,         setPosts]         = React.useState([]);
  const [jobs,          setJobs]          = React.useState([]);
  const [loading,       setLoading]       = React.useState(true);
  const [message,       setMessage]       = React.useState('');
  const [filter,        setFilter]        = React.useState('all');
  const [focusedStage,  setFocusedStage]  = React.useState(null);
  const [publishingId,  setPublishingId]  = React.useState(() => {
    try {
      const saved = JSON.parse(localStorage.getItem(POST_ACTION_STORAGE_KEY) || '{}');
      return saved?.postId || null;
    } catch {
      return null;
    }
  });
  const [checked,       setChecked]       = React.useState({});
  const [videoActions,  setVideoActions]  = React.useState(() => {
    try { return JSON.parse(localStorage.getItem(VIDEO_ACTION_STORAGE_KEY) || '{}'); } catch { return {}; }
  });
  const selectedAccountId = state?.selectedAccount || acc?.id || '';
  const accountQuery = selectedAccountId ? `&account_id=${encodeURIComponent(selectedAccountId)}` : '';

  function saveVideoActions(next) {
    setVideoActions(next);
    try { localStorage.setItem(VIDEO_ACTION_STORAGE_KEY, JSON.stringify(next)); } catch {}
  }

  function savePostAction(postId) {
    setPublishingId(postId || null);
    try {
      if (postId) {
        localStorage.setItem(POST_ACTION_STORAGE_KEY, JSON.stringify({ postId, startedAt: Date.now() }));
      } else {
        localStorage.removeItem(POST_ACTION_STORAGE_KEY);
      }
    } catch {}
  }

  function reconcilePostAction(nextPosts) {
    let saved = null;
    try { saved = JSON.parse(localStorage.getItem(POST_ACTION_STORAGE_KEY) || 'null'); } catch {}
    if (!saved?.postId) {
      if (publishingId) setPublishingId(null);
      return;
    }

    const post = nextPosts.find(p => p.id === saved.postId);
    const fresh = saved.startedAt && Date.now() - saved.startedAt < VIDEO_ACTION_MAX_AGE_MS;
    const completed = !!post?.external_post_id || ['published', 'failed', 'rejected', 'canceled'].includes(post?.status);
    if (!fresh || !post || completed) {
      savePostAction(null);
      return;
    }
    if (publishingId !== saved.postId) setPublishingId(saved.postId);
  }

  function readVideoActions() {
    try { return JSON.parse(localStorage.getItem(VIDEO_ACTION_STORAGE_KEY) || '{}'); } catch { return {}; }
  }

  function setVideoAction(jobId, action) {
    const current = readVideoActions();
    saveVideoActions({
      ...current,
      [jobId]: { action, startedAt: Date.now() },
    });
  }

  function clearVideoAction(jobId) {
    const next = readVideoActions();
    if (!next[jobId]) return;
    delete next[jobId];
    saveVideoActions(next);
  }

  function reconcileVideoActions(nextJobs) {
    const ids = new Set(nextJobs.map(j => j.id));
    const now = Date.now();
    let changed = false;
    const next = {};
    const current = readVideoActions();
    for (const [jobId, value] of Object.entries(current)) {
      const fresh = value?.startedAt && now - value.startedAt < VIDEO_ACTION_MAX_AGE_MS;
      const job = nextJobs.find(candidate => candidate.id === jobId);
      const age = value?.startedAt ? now - value.startedAt : VIDEO_ACTION_MAX_AGE_MS;
      const waitingForServerStart = age < 8000;
      const stillRunning =
        value?.action === 'publish'
          ? job?.status === 'publishing'
          : value?.action === 'retry'
            ? ['queued', 'rendering'].includes(job?.status)
            : value?.action === 'approve'
              ? job?.status === 'rendered'
              : value?.action === 'delete';

      if (ids.has(jobId) && fresh && (waitingForServerStart || stillRunning)) next[jobId] = value;
      else changed = true;
    }
    if (changed) saveVideoActions(next);
  }
  // 생성된 썸네일 URL 캐시 (postId → url), 페이지 새로고침 전까지 유지
  const [thumbCache,  setThumbCache]  = React.useState(() => {
    try { return JSON.parse(localStorage.getItem('pipeline_thumbs') || '{}'); } catch { return {}; }
  });

  async function handleThumbnailGenerated(item, generated) {
    const postId = item?.id;
    const url = generated?.url;
    const jobId = item?.job?.id;
    if (!postId || !url) return;
    setThumbCache(prev => {
      const next = { ...prev, [postId]: url };
      try { localStorage.setItem('pipeline_thumbs', JSON.stringify(next)); } catch {}
      return next;
    });
    const postRes = await fetch('/api/admin/social-posts', {
      method:'POST',
      headers:{'Content-Type':'application/json'},
      body: JSON.stringify({
        postId,
        action: 'update_thumbnail',
        thumbnail_image_url: url,
        thumbnail_storage_path: generated.storagePath || null,
        thumbnail_prompt: generated.prompt || null,
        thumbnail_model: generated.model || null,
      }),
    });
    const postData = await postRes.json().catch(() => null);
    if (!postRes.ok || postData?.error) throw new Error(postData?.error || '썸네일 저장 실패');
    if (jobId) {
      const res = await fetch('/api/admin/social-videos', {
        method:'POST',
        headers:{'Content-Type':'application/json'},
        body: JSON.stringify({
          jobId,
          action: 'update-thumbnail',
          thumbnail_image_url: url,
          thumbnail_storage_path: generated.storagePath || null,
          thumbnail_prompt: generated.prompt || null,
          thumbnail_model: generated.model || null,
        }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok || data?.error) throw new Error(data?.error || '썸네일 저장 실패');
    }
    load(true);
  }

  async function load(silent = false) {
    if (!silent) setLoading(true);
    try {
      const [postsRes, jobsRes] = await Promise.all([
        fetch(`/api/social/posts?limit=30${accountQuery}`,       { cache:'no-store' }),
        fetch(`/api/admin/social-videos?limit=50${accountQuery}`, { cache:'no-store' }),
      ]);
      const pd = await postsRes.json().catch(() => null);
      const jd = await jobsRes.json().catch(() => null);
      const nextPosts = Array.isArray(pd?.posts) ? pd.posts : [];
      const nextJobs = Array.isArray(jd?.jobs) ? jd.jobs : [];
      setPosts(nextPosts);
      setJobs(nextJobs);
      reconcilePostAction(nextPosts);
      reconcileVideoActions(nextJobs);
    } catch (e) {
      setMessage(e.message || '데이터를 불러오지 못했습니다.');
    } finally {
      if (!silent) setLoading(false);
    }
  }

  React.useEffect(() => { setFocusedStage(null); load(); }, [selectedAccountId]);

  React.useEffect(() => {
    const hasActive = jobs.some(j => ['publishing', 'rendering', 'queued'].includes(j.status));
    const hasSavedActions = Object.keys(videoActions).length > 0;
    if (!hasActive && !publishingId && !hasSavedActions) return;
    const interval = publishingId || hasSavedActions ? 3000 : 5000;
    const t = setInterval(() => load(true), interval);
    return () => clearInterval(t);
  }, [jobs, selectedAccountId, publishingId, videoActions]);

  async function handleCreateVideo(postId) {
    const requestId = `${Date.now().toString(36)}-${postId.slice(0, 8)}`;
    setMessage(`🎬 영상 생성 요청 전송 중... 요청ID ${requestId}`);
    try {
      const res  = await fetch('/api/admin/social-posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ postId, action: 'create_video', requestId }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok || data?.error) throw new Error(data?.error || '영상 생성 실패');
      if (data?.automation?.skipped) {
        setMessage(`⚠️ 스킵됨 (${data.automation.reason}) — ♻️ 카드 재생성 먼저 누르세요`);
        return { ok: false, skipped: true, reason: data.automation.reason };
      }
      setMessage(`✅ 영상 생성 요청 완료! 요청ID ${requestId} — 렌더링 탭에서 진행 상황 확인하세요.`);
      load(true);
      return { ok: true, requestId };
    } catch (e) {
      setMessage('❌ ' + e.message);
      return { ok: false, error: e.message };
    }
  }

  // 영상 대신 카드들을 인스타 캐러셀로 바로 발행(영상 렌더 없이)
  async function handleCreateCarousel(postId) {
    if (!confirm('이 글의 카드들을 영상 없이 인스타그램 캐러셀로 바로 발행하시겠습니까?')) return { ok: false };
    setMessage('🖼 인스타그램 캐러셀 발행 중... (최대 1분 소요)');
    try {
      const res  = await fetch('/api/admin/social-posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ postId, action: 'carousel' }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok || data?.error) throw new Error(data?.error || '캐러셀 발행 실패');
      setMessage(`✅ 캐러셀 발행 완료!${data?.result?.url ? ' ' + data.result.url : ''}`);
      load(true);
      return { ok: true };
    } catch (e) {
      setMessage('❌ ' + e.message);
      return { ok: false, error: e.message };
    }
  }

  async function handleApprovePost(postId) {
    if (!confirm('이 게시글을 승인하고 Threads에 즉시 발행하시겠습니까?')) return;
    savePostAction(postId);
    setMessage('🚀 Threads 발행 중...');
    setFocusedStage(0);
    try {
      const res  = await fetch('/api/admin/social-posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ postId, action: 'publish' }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok || data?.error) throw new Error(data?.error || '발행 실패');
      if (data?.status === 'publishing') {
        setMessage('🚀 Threads 백그라운드 발행 중... 다른 화면에 다녀와도 계속 진행됩니다.');
      } else {
        setMessage('✅ Threads 발행 완료! 카드 이미지 준비됨 — 영상/캐러셀은 원하는 글만 버튼으로 진행하세요');
        savePostAction(null);
        setFocusedStage(1);
      }
      load(true);
    } catch (e) {
      setMessage('❌ ' + e.message);
      savePostAction(null);
    }
  }

  async function handlePublishPost(postId) {
    if (!confirm('이 게시글을 Threads에 즉시 발행하시겠습니까?')) return;
    savePostAction(postId);
    setFocusedStage(0);
    try {
      const res  = await fetch('/api/admin/social-posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ postId, action: 'publish' }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok || data?.error) throw new Error(data?.error || '발행 실패');
      if (data?.status === 'publishing') {
        setMessage('🚀 Threads 백그라운드 발행 중... 다른 화면에 다녀와도 계속 진행됩니다.');
      } else {
        setMessage('🚀 Threads 발행 완료!');
        savePostAction(null);
        setFocusedStage(1);
      }
      load(true);
    } catch (e) {
      setMessage('❌ ' + e.message);
      savePostAction(null);
    }
  }

  async function handleVideoAction(jobId, action, options = {}) {
    if (action === 'delete'  && !confirm('이 영상 작업을 삭제하시겠습니까?'))  return;
    if (action === 'retry'   && !confirm('렌더링을 다시 시작하시겠습니까?'))   return;
    if (action === 'carousel' && !confirm('이 카드들을 인스타그램 캐러셀로 발행하시겠습니까?')) return;
    setVideoAction(jobId, action);
    try {
      if (action === 'publish') setMessage('🚀 발행 중... (최대 30초 소요)');
      if (action === 'retry')   setMessage('🔄 렌더링 재시작 중...');
      if (action === 'delete')  setMessage('🗑 삭제 중... 다른 화면에 갔다 와도 상태가 유지됩니다.');
      if (action === 'carousel') setMessage('🖼 인스타그램 캐러셀 발행 중...');
      const body = { jobId, action, ...(options.publishPlatforms ? { publishPlatforms: options.publishPlatforms } : {}) };
      const res  = await fetch('/api/admin/social-videos', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
      const data = await res.json().catch(() => null);
      if (!res.ok || data?.error) throw new Error(data?.error || '처리 실패');
      if (action === 'publish') {
        const results = data?.results  || {};
        const errors  = data?.errors   || {};
        const skipped = data?.skipped  || {};
        const successes = Object.keys(results);
        const failures  = Object.keys(errors);
        const skips     = Object.keys(skipped);
        if (successes.length > 0) {
          const urls = successes.map(p => `${PLATFORM_ICONS[p]||p} 완료`).join(' ');
          setMessage(`✅ 발행 완료: ${urls}${failures.length > 0 ? ' | ❌ 실패: ' + failures.map(p => `${PLATFORM_ICONS[p]||p} ${errors[p]}`).join(', ') : ''}`);
        } else if (failures.length > 0) {
          const errs = failures.map(p => `${PLATFORM_ICONS[p]||p}: ${errors[p]}`).join(' | ');
          setMessage(`❌ 발행 실패: ${errs}`);
        } else if (skips.length > 0) {
          setMessage(`⚠️ 연동된 플랫폼 없음 — Vercel에 환경변수를 설정하세요 (${skips.join(', ')})`);
        } else {
          setMessage('✅ 발행 완료');
        }
      } else {
        const labels = { approve:'승인', delete:'삭제', retry:'렌더링 재시작', carousel:'캐러셀 발행' };
        setMessage(`✅ ${labels[action] || action} 완료`);
      }
      await load(true);
      clearVideoAction(jobId);
    } catch (e) {
      setMessage('❌ ' + e.message);
      clearVideoAction(jobId);
    }
  }

  const items = buildPipelineItems(posts, jobs, thumbCache, acc);
  const filtered = items.filter(it => {
    if (filter === 'all')     return true;
    if (filter === 'active')  return it.status === 'active';
    if (filter === 'done')    return it.stage === 5;
    if (filter === 'failed')  return it.status === 'failed';
    if (filter === 'pending') return it.status === 'pending';
    return true;
  });
  const counts = {
    all:     items.length,
    active:  items.filter(it => it.status === 'active').length,
    done:    items.filter(it => it.stage === 5).length,
    failed:  items.filter(it => it.status === 'failed').length,
    pending: items.filter(it => it.status === 'pending').length,
  };

  return (
    <div className="pipeline-page">
      <style>{`
        /* ══ Pipeline 전용 스타일 ═════════════════════════════════════════ */
        .pipeline-page{display:grid;gap:24px;padding:0 0 48px}

        /* 다이어그램 */
        .pipeline-diagram{display:flex;align-items:center;background:var(--bg-card,#111);border:1px solid var(--border,#222);border-radius:16px;padding:28px 24px;overflow-x:auto;gap:0}
        .pn-node{position:relative;display:flex;flex-direction:column;align-items:center;gap:6px;min-width:120px;padding:16px 12px;border-radius:12px;border:1.5px solid var(--border,#2a2a2a);background:var(--bg-elevated,#1a1a1a);transition:border-color .3s,box-shadow .3s}
        .pn-node.pn-active{border-color:var(--node-color);box-shadow:0 0 0 3px color-mix(in srgb,var(--node-color) 20%,transparent),0 0 24px color-mix(in srgb,var(--node-color) 15%,transparent)}
        .pn-node.pn-focused{border-color:var(--node-color);box-shadow:0 0 0 4px color-mix(in srgb,var(--node-color) 35%,transparent),0 0 36px color-mix(in srgb,var(--node-color) 28%,transparent);transform:translateY(-2px)}
        .pn-node.pn-failed{border-color:#f87171}
        .pn-icon{font-size:26px;line-height:1}
        .pn-label{font-size:11.5px;font-weight:600;color:var(--fg,#e8e8e8);text-align:center}
        .pn-sub{font-size:9.5px;color:var(--fg-subtle,#666);text-align:center;line-height:1.3}
        .pn-chips{display:flex;flex-wrap:wrap;gap:4px;justify-content:center;margin-top:4px}
        .pn-chip{font-size:10px;font-weight:600;padding:2px 7px;border-radius:999px}
        .pn-chip.active{background:rgba(56,189,248,.15);color:#38bdf8}
        .pn-chip.done{background:rgba(52,211,153,.12);color:#34d399}
        .pn-chip.failed{background:rgba(248,113,113,.12);color:#f87171}
        .pn-pulse{position:absolute;inset:-4px;border-radius:14px;border:2px solid var(--node-color);opacity:0;animation:pn-pulse 1.8s ease-in-out infinite;pointer-events:none}
        @keyframes pn-pulse{0%{opacity:.6;transform:scale(1)}100%{opacity:0;transform:scale(1.08)}}
        .pn-edge{display:flex;align-items:center;flex:1;min-width:20px;color:var(--border,#333)}
        .pn-edge-active{color:#38bdf8}
        .pn-edge-line{flex:1;height:2px;background:currentColor;position:relative;overflow:hidden}
        .pn-edge-active .pn-edge-line::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,transparent 0%,rgba(56,189,248,.9) 50%,transparent 100%);animation:pn-flow 1s linear infinite}
        @keyframes pn-flow{from{transform:translateX(-100%)}to{transform:translateX(100%)}}
        .pn-edge-arrow{flex-shrink:0}

        /* 필터 */
        .pipeline-filter-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
        .pipeline-filter-bar .filter-tab{font-size:12px;padding:5px 14px;border-radius:999px;border:1px solid var(--border,#2a2a2a);background:transparent;color:var(--fg-subtle,#888);cursor:pointer;transition:all .15s}
        .pipeline-filter-bar .filter-tab.active{background:var(--accent,#a3e635);color:#000;border-color:transparent;font-weight:600}
        .filter-count{font-size:10.5px;opacity:.6;margin-left:3px}

        /* 행 */
        .pr-row{border:1px solid var(--border,#222);border-radius:12px;background:var(--bg-card,#111);overflow:hidden;transition:border-color .2s}
        .pr-row-active{border-color:#38bdf8}
        .pr-row-failed{border-color:#f87171}
        .pr-header{display:flex;align-items:center;gap:16px;padding:14px 18px}
        .pr-title-block{display:flex;align-items:center;gap:10px;flex:1;min-width:0}
        .pr-title-block-btn{background:none;border:none;padding:0;cursor:pointer;text-align:left;border-radius:8px;transition:background .15s;display:flex;align-items:center;gap:10px;flex:1;min-width:0}
        .pr-title-block-btn:hover{background:rgba(255,255,255,.04)}
        .pr-title-block-open{background:rgba(163,230,53,.05)}
        .pr-expand-arrow{font-size:10px;color:var(--fg-subtle,#888);flex-shrink:0;margin-left:auto;padding-right:4px}
        .pr-title{font-size:13px;font-weight:500;color:var(--fg,#e8e8e8);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:260px}
        .pr-meta{font-size:11px;color:var(--fg-subtle,#666);margin-top:1px}

        /* 도트 */
        .pr-stages{display:flex;align-items:center;flex-shrink:0}
        .pr-dot{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;border:1.5px solid var(--border,#333);background:var(--bg-elevated,#1a1a1a);color:var(--fg-subtle,#666);flex-shrink:0;cursor:default;padding:0;transition:transform .15s,box-shadow .15s}
        .pr-dot-clickable{cursor:pointer}
        .pr-dot-clickable:hover{transform:scale(1.18)}
        .pr-dot-done{background:color-mix(in srgb,var(--dot-color) 20%,transparent);border-color:var(--dot-color);color:var(--dot-color)}
        .pr-dot-active{background:rgba(56,189,248,.15);border-color:#38bdf8;color:#38bdf8}
        .pr-dot-failed{background:rgba(248,113,113,.15);border-color:#f87171;color:#f87171}
        .pr-dot-selected{box-shadow:0 0 0 3px color-mix(in srgb,var(--dot-color,#a3e635) 40%,transparent);transform:scale(1.18)}
        .pr-dot-check{font-size:10px}
        .pr-dot-spin{width:10px;height:10px;border-radius:50%;border:2px solid rgba(56,189,248,.3);border-top-color:#38bdf8;animation:pr-spin .8s linear infinite}
        @keyframes pr-spin{to{transform:rotate(360deg)}}
        .pr-line{flex:0 0 14px;height:2px}
        .pr-line-done{background:var(--border,#333)}
        .pr-line-pending{background:var(--border,#2a2a2a)}

        /* 배지 */
        .pr-badge-wrap{flex-shrink:0}
        .pr-badge{font-size:10.5px;font-weight:600;padding:3px 10px;border-radius:999px}
        .pr-badge.active{background:rgba(56,189,248,.15);color:#38bdf8}
        .pr-badge.done{background:rgba(52,211,153,.12);color:#34d399}
        .pr-badge.failed{background:rgba(248,113,113,.12);color:#f87171}
        .pr-badge.pending{background:var(--bg-elevated,#1a1a1a);color:var(--fg-subtle,#888);border:1px solid var(--border,#333)}

        /* 단계 상세 패널 */
        .pr-stage-panel{border-top:1px solid var(--border,#222)}
        .pr-stage-tabs{display:flex;align-items:center;gap:2px;padding:10px 18px 0;overflow-x:auto;border-bottom:1px solid var(--border,#222)}
        .pr-stage-tab{font-size:11px;font-weight:600;padding:6px 12px;border-radius:8px 8px 0 0;border:1px solid transparent;background:transparent;color:var(--fg-subtle,#888);cursor:pointer;white-space:nowrap;transition:all .15s;margin-bottom:-1px}
        .pr-stage-tab.active{background:var(--bg-card,#111);color:var(--tab-color,#a3e635);border-color:var(--border,#222);border-bottom-color:var(--bg-card,#111)}
        .pr-stage-tab:hover:not(.active){color:var(--fg,#e8e8e8)}
        .pr-stage-tab-pending{opacity:.45}
        .pr-stage-close{margin-left:auto;font-size:12px;padding:4px 10px;border-radius:6px;border:1px solid var(--border,#2a2a2a);background:transparent;color:var(--fg-subtle,#888);cursor:pointer;margin-bottom:1px}
        .pr-stage-close:hover{color:var(--fg,#e8e8e8)}
        .pr-stage-content{padding:18px}

        /* StageDetail 공통 */
        .sd-wrap{display:grid;gap:14px}
        .sd-label{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--fg-subtle,#666)}
        .sd-text{white-space:pre-wrap;font-size:13px;color:var(--fg,#e8e8e8);line-height:1.6;background:var(--bg-elevated,#1a1a1a);border:1px solid var(--border,#222);border-radius:8px;padding:14px;margin:0;font-family:inherit;max-height:240px;overflow-y:auto}
        .sd-parts{display:grid;gap:10px}
        .sd-part{display:flex;gap:12px}
        .sd-part-num{font-size:10px;font-weight:700;color:var(--fg-subtle,#666);padding-top:4px;flex-shrink:0;min-width:28px}
        .sd-meta-row{display:flex;gap:20px;font-size:11px;color:var(--fg-subtle,#666)}
        .sd-kv{display:flex;gap:10px;align-items:baseline;font-size:12.5px}
        .sd-kv span:first-child{color:var(--fg-subtle,#666);min-width:56px;flex-shrink:0}
        .sd-link{color:#38bdf8;font-size:12.5px;text-decoration:none}
        .sd-link:hover{text-decoration:underline}
        .sd-empty{font-size:12.5px;color:var(--fg-subtle,#666);padding:12px 0}

        /* 카드 이미지 */
        .sd-card-grid{display:flex;gap:12px;flex-wrap:wrap}
        .sd-card-wrap{display:flex;flex-direction:column;align-items:center;gap:6px}
        .sd-card-img{width:85px;height:151px;object-fit:cover;border-radius:8px;border:1px solid var(--border,#2a2a2a);background:var(--bg-elevated,#1a1a1a)}
        .sd-card-preview-btn{border:1px solid var(--border,#333);background:var(--bg-elevated,#1a1a1a);color:var(--fg,#e8e8e8);border-radius:999px;font-size:10px;font-weight:700;padding:3px 9px;cursor:pointer}
        .sd-card-preview-btn:hover{border-color:#34d399;color:#34d399}
        .sd-card-label{font-size:10px;color:var(--fg-subtle,#666);font-weight:600}
        .sd-preview-backdrop{position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.78);display:flex;align-items:center;justify-content:center;padding:24px}
        .sd-preview-modal{position:relative;max-width:min(92vw,540px);max-height:92vh}
        .sd-preview-img{display:block;width:auto;max-width:100%;max-height:88vh;border-radius:12px;background:#fff;box-shadow:0 18px 60px rgba(0,0,0,.55)}
        .sd-preview-close{position:absolute;top:-14px;right:-14px;width:30px;height:30px;border-radius:999px;border:1px solid var(--border,#333);background:#111;color:#fff;font-size:18px;line-height:1;cursor:pointer}

        /* 썸네일 생성 */
        .sd-thumb-layout{display:flex;gap:20px;align-items:flex-start;flex-wrap:wrap}
        .sd-thumb-preview{flex-shrink:0}
        .sd-thumb-img{width:240px;height:auto;max-height:200px;object-fit:contain;background:#000;border-radius:10px;border:1px solid var(--border,#2a2a2a);display:block}
        .sd-thumb-placeholder{width:240px;height:135px;border-radius:10px;border:2px dashed var(--border,#2a2a2a);background:var(--bg-elevated,#1a1a1a);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;font-size:12px;color:var(--fg-subtle,#666);text-align:center}
        .sd-thumb-spinner{width:24px;height:24px;border-radius:50%;border:2px solid rgba(251,146,60,.2);border-top-color:#fb923c;animation:pr-spin .8s linear infinite}
        .sd-thumb-meta{flex:1;display:grid;gap:10px;min-width:160px}

        /* 영상 렌더링 */
        .sd-video-layout{display:flex;gap:20px;align-items:flex-start;flex-wrap:wrap}
        .sd-media-col{flex-shrink:0}
        .sd-video{width:130px;height:231px;object-fit:contain;border-radius:10px;border:1px solid var(--border,#2a2a2a);background:#000;display:block}
        .sd-thumb{width:130px;height:231px;object-fit:contain;border-radius:10px;border:1px solid var(--border,#2a2a2a);background:#000;display:block}
        .sd-no-media{width:130px;height:231px;border-radius:10px;border:1px solid var(--border,#2a2a2a);background:var(--bg-elevated,#1a1a1a);display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:13px;color:var(--fg-subtle,#666);gap:8px;text-align:center}
        .sd-meta-col{flex:1;display:grid;gap:10px;min-width:180px}
        .sd-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px}

        /* 캡션 */
        .sd-captions{display:grid;gap:8px}
        .sd-caption-block{background:var(--bg-elevated,#1a1a1a);border:1px solid var(--border,#222);border-radius:8px;overflow:hidden}
        .sd-caption-body{margin:0;padding:10px 12px;font-size:12px;color:var(--fg,#e8e8e8);white-space:pre-wrap;font-family:inherit;line-height:1.55;max-height:100px;overflow-y:auto}

        /* SNS 업로드 */
        .sd-pub-list{display:grid;gap:8px}
        .sd-pub-row{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--bg-elevated,#1a1a1a);border:1px solid var(--border,#222);border-radius:8px}
        .sd-pub-icon{font-size:18px}
        .sd-pub-name{font-size:13px;font-weight:600;min-width:80px}

        /* 오류 */
        .pr-error{background:rgba(248,113,113,.08);border:1px solid rgba(248,113,113,.25);border-radius:8px;padding:10px 14px;font-size:11.5px;color:#f87171;font-family:monospace}

        /* 빈 상태 / 로딩 */
        .pipeline-empty{text-align:center;padding:64px 24px;color:var(--fg-subtle,#666);font-size:13px}
        .pipeline-empty-icon{font-size:40px;margin-bottom:12px}
        .pipeline-loading{display:flex;align-items:center;justify-content:center;height:160px;color:var(--fg-subtle,#666);font-size:13px;gap:10px}
        .pipeline-spinner{width:18px;height:18px;border-radius:50%;border:2px solid rgba(255,255,255,.1);border-top-color:var(--accent,#a3e635);animation:pr-spin .8s linear infinite}
        .pipeline-header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
        .pipeline-title{font-size:16px;font-weight:700;color:var(--fg,#e8e8e8)}
        .pipeline-subtitle{font-size:12px;color:var(--fg-subtle,#666);margin-top:2px}
        .pipeline-items{display:grid;gap:8px}
        .pipeline-msg{font-size:12px;color:var(--fg-subtle,#888);background:var(--bg-elevated,#1a1a1a);padding:8px 14px;border-radius:8px}
      `}</style>

      <div className="pipeline-header">
        <div>
          <div className="pipeline-title">콘텐츠 파이프라인</div>
          <div className="pipeline-subtitle">
            AI 글 생성 → Threads → 카드 이미지 → 🎨 썸네일 → 영상 렌더 → SNS 업로드
            &nbsp;·&nbsp; 도트 클릭 시 단계별 상세 정보 표시
          </div>
        </div>
        <div style={{display:'flex',gap:8,alignItems:'center'}}>
          {Object.values(checked).filter(Boolean).length > 0 && (
            <button className="btn sm" style={{color:'#f87171',borderColor:'#f87171'}} type="button" onClick={async () => {
              const ids = filtered.filter(it => checked[it.id]).map(it => it.post?.id).filter(Boolean);
              if (!ids.length) return;
              if (!confirm(`선택한 ${ids.length}개 게시글을 삭제하시겠습니까?`)) return;
              await Promise.all(ids.map(id => fetch('/api/admin/social-posts', {
                method:'POST', headers:{'Content-Type':'application/json'},
                body: JSON.stringify({postId:id, action:'delete'})
              })));
              setChecked({});
              load(true);
            }}>
              🗑 {Object.values(checked).filter(Boolean).length}개 삭제
            </button>
          )}
          <button className="btn sm ghost" type="button" onClick={() => load(false)}>↺ 새로고침</button>
        </div>
      </div>

      <PipelineDiagram items={items} focusedStage={focusedStage} />

      {message && <div className="pipeline-msg">{message}</div>}

      <div className="pipeline-filter-bar">
        {[
          { key:'all',     label:'전체' },
          { key:'active',  label:'🔄 처리 중' },
          { key:'done',    label:'✅ 완료' },
          { key:'pending', label:'⏳ 대기' },
          { key:'failed',  label:'❌ 오류' },
        ].map(({ key, label }) => (
          <button key={key} type="button" className={`filter-tab ${filter===key?'active':''}`} onClick={() => setFilter(key)}>
            {label}<span className="filter-count">({counts[key]})</span>
          </button>
        ))}
      </div>

      {loading ? (
        <div className="pipeline-loading"><div className="pipeline-spinner" /><span>파이프라인 불러오는 중...</span></div>
      ) : filtered.length === 0 ? (
        <div className="pipeline-empty">
          <div className="pipeline-empty-icon">🔮</div>
          <div>파이프라인에 항목이 없습니다.</div>
          <div style={{marginTop:6,fontSize:11}}>글쓰기 페이지에서 새 포스트를 생성하면 여기 표시됩니다.</div>
        </div>
      ) : (
        <div className="pipeline-items">
          {filtered.map(item => (
            <div key={item.id} style={{display:'flex',alignItems:'flex-start',gap:8}}>
              <input type="checkbox" style={{marginTop:18,flexShrink:0,cursor:'pointer'}}
                checked={!!checked[item.id]}
                onChange={e => setChecked(s => ({...s, [item.id]: e.target.checked}))}
              />
              <div style={{flex:1,minWidth:0}}>
                <PipelineRow
                  item={item}
                  onCreateVideo={handleCreateVideo}
                  onCarousel={handleCreateCarousel}
                  onVideoAction={handleVideoAction}
                  onThumbnailGenerated={handleThumbnailGenerated}
                  onApprovePost={handleApprovePost}
                  onPublishPost={handlePublishPost}
                  onFocusStage={setFocusedStage}
                  publishingId={publishingId}
                  videoActions={videoActions}
                />
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

window.PipelinePage = PipelinePage;
