// app/chat.jsx — Ribbon 챗봇: 대화 오케스트레이션 훅 + 채팅 UI
// window.RibbonChat 로 노출.

const { useState: useStateC, useEffect: useEffectC, useRef: useRefC } = React;

// ── 인라인 아이콘 (Lucide 스타일, 2px 라운드 스트로크) ──────
const I = {
  send: 'M5 12h14M13 6l6 6-6 6',
  arrowUp: 'M12 19V5M6 11l6-6 6 6',
  copy: 'M9 9h10v10H9zM5 15V5h10',
  refresh: 'M21 12a9 9 0 1 1-3-6.7M21 4v4h-4',
  up: 'M7 11v9M14 4l-2 7h7a2 2 0 0 1 2 2.3l-1 5A2 2 0 0 1 18 20H7',
  down: 'M17 13V4M10 20l2-7H5a2 2 0 0 1-2-2.3l1-5A2 2 0 0 1 7 4h9',
  download: 'M12 3v12M7 11l5 5 5-5M5 21h14',
  mic: 'M12 3a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V6a3 3 0 0 0-3-3zM5 11a7 7 0 0 0 14 0M12 18v3',
  clip: 'M21 11l-8.5 8.5a5 5 0 0 1-7-7L14 4a3.5 3.5 0 0 1 5 5l-8.5 8.5a2 2 0 0 1-3-3L15 6',
  expand: 'M4 14v6h6M20 10V4h-6M14 4h6v6M10 20H4v-6',
  collapse: 'M14 10V4h6M4 14h6v6M10 4v6H4M20 14h-6v6',
  plus: 'M12 5v14M5 12h14',
  x: 'M6 6l12 12M18 6L6 18',
  db: 'M12 3c4.4 0 8 1.3 8 3s-3.6 3-8 3-8-1.3-8-3 3.6-3 8-3zM4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6',
  spark: 'M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z',
  history: 'M3 12a9 9 0 1 0 9-9 9 9 0 0 0-7 3.3M3 4v4h4M12 8v4l3 2',
  flag: 'M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1zM4 22v-7'
};
function Icon({ d, size = 16, color = 'currentColor', sw = 2, style }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color}
    strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round" style={style}>
      {String(d).split('M').filter(Boolean).map((seg, i) => <path key={i} d={'M' + seg} />)}
    </svg>);

}

// 코랄 스파이크 마크 (브랜드 글리프 homage)
function Spike({ size = 22, color = 'var(--accent)' }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill={color}>
      <path d="M12 2l1.3 7.2L20 6.5l-4.5 5.5L22 14l-7-0.5L12 22l-3-8.5L2 14l6.5-2L4 6.5l6.7 2.7z" opacity="0.92" />
    </svg>);

}

// ── 백엔드 연결(실 에이전트) ────────────────────────────────
// serve.ts 가 서빙하면 /api/chat 로 실 LLM+MCP 응답. 정적(file://)으로 열면 실패→mock 폴백.
const API_BASE = '';

// ── 리본 인증 토큰(1단계-①) ─────────────────────────────────
// 리본 ERP 임베드 페이지가 window.REBORN_AUTH_TOKEN 으로 주입한다(로컬 테스트는 localStorage).
// 챗·관리자·문제제기 **모든 /api 호출**이 이 헬퍼를 쓴다 — 하나라도 빠지면 인증 모드에서 401/403.
function rebornToken() {
  if (window.REBORN_AUTH_TOKEN) return window.REBORN_AUTH_TOKEN;
  try { return localStorage.getItem('reborn.auth') || undefined; } catch (e) { return undefined; }
}
/**
 * 토큰 페이로드 해석(서명 검증 아님 — 검증은 **서버만** 한다).
 * base64url JSON 이라 시크릿 없이 읽을 수 있고, UI 가 "누구로 로그인했나 / 만료됐나"를
 * 표시하는 용도로만 쓴다. 여기서 통과해도 서버가 서명을 다시 확인한다.
 */
function decodeToken(tok) {
  try {
    const p = String(tok || '').split('.')[0];
    if (!p) return null;
    const json = atob(p.replace(/-/g, '+').replace(/_/g, '/'));
    const o = JSON.parse(json);
    return (o && typeof o === 'object') ? o : null;
  } catch (e) { return null; }
}
/**
 * 지금 인증된 상태인가. **만료를 반드시 함께 본다** — localStorage 에 만료된 토큰이 남아 있으면
 * "토큰은 있는데 401" 이라는 막다른 상태가 되어 임시 로그인 버튼이 안 보이게 된다.
 */
function hasValidToken() {
  const t = rebornToken();
  if (!t) return false;
  const p = decodeToken(t);
  if (p && typeof p.exp === 'number' && p.exp * 1000 <= Date.now()) return false;
  return true;
}
function setRebornToken(tok) {
  try { localStorage.setItem('reborn.auth', tok); return true; } catch (e) { return false; }
}
function clearRebornToken() {
  try { localStorage.removeItem('reborn.auth'); } catch (e) { /* noop */ }
}
function authHeaders(extra) {
  const t = rebornToken();
  return Object.assign({}, extra || {}, t ? { 'x-reborn-auth': t } : {});
}
/** fetch + 인증 헤더. 응답 그대로 반환(호출측이 status 처리). */
function authFetch(url, opts) {
  const o = Object.assign({}, opts || {});
  o.headers = authHeaders(o.headers);
  return fetch(url, o);
}
window.RibbonAuth = {
  token: rebornToken, headers: authHeaders, fetch: authFetch,
  decode: decodeToken, hasValid: hasValidToken, set: setRebornToken, clear: clearRebornToken,
};

// 스트리밍(NDJSON) 소비: 툴 호출마다 onTool(ev), 진행 알림마다 onProgress(ev) 로 상태를 갱신하고
// 마지막 done 이벤트를 반환. progress 는 **연결 유지 + 정직한 대기**를 위한 것이다 — 원장 조회가
// 길어질 때 무음으로 두면 인그레스가 180초에 끊는다(ALPACA-34).
async function askBackendStream(message, history, onTool, conversationId, onProgress) {
  // 현재 선택 업체(tenant_id)와 언어(locale)를 함께 보낸다 — 백엔드가 업체별 지역·LLM(관리자 설정)과
  // 응답 언어(프롬프트 다국어)를 적용한다.
  const tenantId = (window.RibbonData && window.RibbonData.currentTenantId) || 'thesafe';
  const locale = (window.RibbonData && window.RibbonData.currentLocale) || 'ko';
  const res = await authFetch(API_BASE + '/api/chat', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    // conversation_id(3단계-13): 인증 모드에선 서버가 이 ID 로 저장본 이력을 쓴다(클라이언트 history 불신).
    body: JSON.stringify({ message, history: history || [], tenant_id: tenantId, locale, conversation_id: conversationId }),
  });
  if (!res.ok || !res.body) {
    let msg = 'api ' + res.status;
    try { const j = await res.json(); if (j && j.error) msg = j.error; } catch (e) {}
    const err = new Error(msg);
    err.status = res.status;
    throw err;
  }
  const reader = res.body.getReader();
  const dec = new TextDecoder();
  let buf = '';
  let done = null;
  for (;;) {
    const { value, done: rdone } = await reader.read();
    if (rdone) break;
    buf += dec.decode(value, { stream: true });
    let idx;
    while ((idx = buf.indexOf('\n')) >= 0) {
      const line = buf.slice(0, idx).trim();
      buf = buf.slice(idx + 1);
      if (!line) continue;
      let ev; try { ev = JSON.parse(line); } catch (e) { continue; }
      if (ev.type === 'tool') { onTool && onTool(ev); }
      else if (ev.type === 'progress') { onProgress && onProgress(ev); }
      else if (ev.type === 'done') { done = ev; }
    }
  }
  if (!done) throw new Error('no done event');
  return done; // { text, provider, model, stub, llm_turns, tools, charts }
}

// ── 대화 세션 저장소 (localStorage — 새 대화/닫기 경계로 세션 관리) ──────
// 세션 키는 업체별로 분리(3단계-13) — 업체 전환 시 이전 업체 대화가 이어지지 않는다.
function sessionKey() {
  const t = (window.RibbonData && window.RibbonData.currentTenantId) || 'thesafe';
  return 'ribbon.sessions.v1.' + t;
}
function loadSessions() { try { const a = JSON.parse(localStorage.getItem(sessionKey()) || '[]'); return Array.isArray(a) ? a : []; } catch { return []; } }
function persistSessions(list) { try { localStorage.setItem(sessionKey(), JSON.stringify(list.slice(0, 50))); } catch (e) {} }
// 로그아웃/세션만료(401) 시 로컬 대화 제거(3단계-13) — 다음 사용자에게 이전 대화가 보이지 않게.
function clearLocalSessions() {
  try {
    for (let i = localStorage.length - 1; i >= 0; i--) {
      const k = localStorage.key(i);
      if (k && k.indexOf('ribbon.sessions.v1') === 0) localStorage.removeItem(k);
    }
  } catch (e) {}
}
function newSessionId() { return 's' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); }
function relTime(ts, locale) {
  const loc = locale || (window.RibbonData && window.RibbonData.currentLocale) || 'ko';
  const T = (k, ...a) => window.RibbonI18n.t(loc, k, ...a);
  const s = Math.floor((Date.now() - (ts || 0)) / 1000);
  if (s < 60) return T('rel_now'); if (s < 3600) return T('rel_min', Math.floor(s / 60));
  if (s < 86400) return T('rel_hour', Math.floor(s / 3600)); return T('rel_day', Math.floor(s / 86400));
}
function assistantText(m) {
  if (m.text) return m.text;
  const blocks = (m.scenario && m.scenario.answer) || [];
  return blocks.filter((b) => b.type === 'markdown' || b.type === 'text').map((b) => b.md).join('\n');
}
function firstUserText(messages) { const u = messages.find((m) => m.role === 'user'); return u ? u.text : ''; }
// '#' 방향성 지침 접두어(ALPACA-39) — 서버(serve.ts DIRECTIVE_PREFIX)와 **동일 문자열** 유지.
const DIRECTIVE_PREFIX = '[사용자 방향성 지침 — 이후 모든 답변에 반영하라]';

// LLM 에 넘길 이전 대화(맥락) — user/assistant 최종 텍스트.
// 어시스턴트 턴에는 화면에 그려진 블록 요약("[화면 출력] …")을 이어 붙인다 — 달력·표는 텍스트에
// 없어서, 이게 없으면 다음 턴이 "저 달력" 같은 지시어를 해석하지 못한다.
// '#' 지침(directive) 턴은 접두어를 붙여 상시 지시임을 표시한다(개발 모드 — 인증 모드는 서버 이력이 담당).
function historyFrom(messages) {
  return messages
    .map((m) => {
      if (m.role === 'user') {
        const t = (m.text || '').trim();
        return { role: 'user', content: m.directive ? DIRECTIVE_PREFIX + ' ' + t : t };
      }
      const note = m.scenario && m.scenario.history_note;
      return { role: 'assistant', content: (assistantText(m) + (note ? '\n\n' + note : '')).trim() };
    })
    .filter((x) => x.content);
}

// 실 응답 → 기존 렌더러가 소비하는 scenario 형태로 변환.
// 텍스트(마크다운) 뒤에 백엔드가 실데이터로 만든 차트 블록(kpi/line/bar/table…)을 이어 붙인다.
function backendToScenario(data) {
  const locale = (window.RibbonData && window.RibbonData.currentLocale) || 'ko';
  const toolNames = [...new Set((data.tools || []).map((t) => t.name).filter(Boolean))];
  const charts = Array.isArray(data.charts) ? data.charts : [];
  const answer = [{ type: 'markdown', md: data.text || window.RibbonI18n.t(locale, 'empty_reply') }, ...charts];
  return {
    real: true,
    answer,
    sources: toolNames.length ? toolNames : undefined,
    followups: window.RibbonI18n.chips(locale).slice(0, 3),
    // 화면 출력 기억 — 서버가 만든 "[화면 출력] …" 요약. 화면엔 안 그리고 다음 턴 이력에만 싣는다.
    history_note: data.history_note || undefined,
    meta: { provider: data.provider, model: data.model, stub: data.stub, llm_turns: data.llm_turns },
  };
}

// ── 대화 오케스트레이션 훅 ──────────────────────────────────
function useConversation() {
  const [messages, setMessages] = useStateC([]);
  const [busy, setBusy] = useStateC(false);
  const [status, setStatus] = useStateC(null); // {steps, idx, label, source, external}
  const [spotlight, setSpotlight] = useStateC(null);
  const [reportMsg, setReportMsg] = useStateC(null); // 문제제기 모달 대상 메시지(null=닫힘)
  const [sessions, setSessions] = useStateC(loadSessions);
  const [activeId, setActiveId] = useStateC(newSessionId());
  const sid = useRefC(activeId); // 저장/삭제 로직이 참조하는 canonical id
  const timers = useRefC([]);

  function clearTimers() {timers.current.forEach(clearTimeout);timers.current = [];}
  useEffectC(() => clearTimers, []);

  // 자동 저장: 메시지가 바뀔 때마다 현재 세션을 '최근 대화'에 upsert(비어있으면 스킵).
  useEffectC(() => {
    if (messages.length === 0) return;
    const sess = { id: sid.current, title: firstUserText(messages) || '새 대화', ts: Date.now(), messages };
    setSessions((prev) => {
      const next = [sess, ...prev.filter((s) => s.id !== sess.id)].slice(0, 50);
      persistSessions(next);
      return next;
    });
  }, [messages]);

  // 실 백엔드 경로(기본, 스트리밍). 툴 호출마다 상태창을 실시간 갱신. 실패 시 mock 폴백.
  async function runReal(text, history) {
    const locale = (window.RibbonData && window.RibbonData.currentLocale) || 'ko';
    const T = (k, ...a) => window.RibbonI18n.t(locale, k, ...a);
    setBusy(true);
    setSpotlight(null);
    setStatus({ steps: [{ label: T('understanding'), source: '알파카', external: false }], idx: 0 });
    try {
      const steps = []; // 툴 호출을 누적 → 진행바가 단계별로 전진.
      // live 는 "지금 무슨 단계이고 몇 초 지났나" — 서버가 5초마다 보낸다. 단계명이 실려 오므로
      // 긴 원장 조회도 "매출 조회 중… 18초" 로 보인다(스피너만 도는 대기를 없앤다).
      let live = null;
      const paint = () => {
        if (steps.length) setStatus({ steps: steps.slice(), idx: steps.length - 1, live });
        else if (live) setStatus({ steps: [{ label: live.label, source: '알파카', external: false }], idx: 0, live });
      };
      const data = await askBackendStream(text, history, (ev) => {
        steps.push({ label: ev.label || T('calling', ev.name), source: ev.server ? ev.server + ' MCP' : 'MCP', external: true });
        live = null; // 새 단계가 시작됐으니 경과 표시는 초기화
        paint();
      }, sid.current, (pev) => {
        live = { label: pev.label, elapsed_s: pev.elapsed_s || 0 };
        paint();
      });
      const scenario = backendToScenario(data);
      setStatus(null);
      setBusy(false);
      setMessages((m) => [...m, { id: 'a' + Date.now(), role: 'assistant', scenario, text: data.text, feedback: null }]);
    } catch (e) {
      setStatus(null);
      setBusy(false);
      // 목업 폴백 제거(1단계-④): 서버에 붙어 있는 상태(http/https)에서는 절대 가짜 답을
      // 만들지 않는다 — 인증 실패(401/403/400)는 그 사유를, 그 외 장애는 원장 조회 불가 안내를
      // 그대로 보여준다. matchScenario 데모는 디자인 작업용 file:// 로 열었을 때만 동작한다.
      if (window.location.protocol === 'file:') {
        const scenario = window.RibbonData.matchScenario(text);
        const txt = (scenario.answer || []).filter((b) => b.type === 'text' || b.type === 'markdown').map((b) => b.md).join('\n');
        setMessages((m) => [...m, { id: 'a' + Date.now(), role: 'assistant', scenario, text: '(목업 데모 — 실데이터 아님)\n\n' + txt, feedback: null, fallback: true }]);
        return;
      }
      if (e && e.status === 401) clearLocalSessions(); // 세션 만료 — 로컬 대화 제거(3단계-13)
      const reason = (e && e.status === 401) ? '로그인이 필요합니다 — 리본에 다시 로그인해 주세요.'
        : (e && e.status === 403) ? '접근 권한이 없습니다 — ' + (e.message || '관리자에게 문의하세요.')
        : (e && e.status === 400) ? '요청이 올바르지 않습니다 — ' + (e.message || '')
        : '현재 리본 데이터를 조회할 수 없습니다. 잠시 후 다시 시도해 주세요. 문제가 계속되면 관리자에게 문의하세요.';
      const errScenario = { id: 'error', answer: [{ type: 'markdown', md: reason }], sources: [], followups: [] };
      setMessages((m) => [...m, { id: 'a' + Date.now(), role: 'assistant', scenario: errScenario, text: reason, feedback: null, error: true }]);
    }
  }

  function send(text) {
    const v = (text || '').trim();
    if (!v || busy) return;
    // '#' 방향성 피드백(ALPACA-39): 답변을 만들지 않는다 — 지침 등록 + 문제제기 접수.
    if (v.startsWith('#')) { sendDirective(v.replace(/^#+\s*/, '')); return; }
    const hist = historyFrom(messages); // 현재 메시지 추가 전의 이전 턴 = 맥락
    setMessages((m) => [...m, { id: 'u' + Date.now(), role: 'user', text: v }]);
    runReal(v, hist);
  }

  // '#' 방향성 지침(ALPACA-39): ① 서버에 지침 턴 저장(인증 모드 이력 — /api/chat directive)
  // ② 같은 내용을 문제제기로 접수(마지막 응답 + 대화록 첨부 — 개선 추적용) ③ 확인 말풍선.
  // 이후 턴부터: 개발 모드는 historyFrom 의 접두어, 인증 모드는 서버 이력이 지침을 LLM 에 전달한다.
  async function sendDirective(text) {
    const v = (text || '').trim();
    if (!v) return;
    const authFetch = (u, o) => (window.RibbonAuth ? window.RibbonAuth.fetch(u, o) : fetch(u, o));
    const lastAssistant = [...messages].reverse().find((x) => x.role !== 'user');
    const transcript = historyFrom(messages);
    setMessages((m) => [...m, { id: 'd' + Date.now(), role: 'user', directive: true, text: v }]);
    // ① 서버 이력 저장(+ack) — 실패해도 로컬 지침은 유지된다(개발 모드 경로가 커버).
    let ack = '방향성으로 반영했습니다 — 이후 답변부터 적용됩니다.';
    try {
      const locale = (window.RibbonData && window.RibbonData.currentLocale) || 'ko';
      const res = await authFetch('/api/chat', {
        method: 'POST', headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ message: v, directive: true, conversation_id: sid.current, locale,
          tenant_id: (window.RibbonData && window.RibbonData.currentTenantId) || undefined }),
      });
      const line = (await res.text()).trim().split('\n')[0];
      const ev = JSON.parse(line || '{}');
      if (ev && ev.text) ack = ev.text;
    } catch (e) { /* 지침은 로컬 이력으로도 반영된다 — 서버 저장 실패는 치명 아님 */ }
    // ② 문제제기 접수 — 개선 루프(process-feedback)가 소비한다.
    let fbNote = '';
    try {
      let name = '';
      try { name = localStorage.getItem('ribbon.reporter.name') || ''; } catch (e) {}
      const res = await authFetch('/api/feedback', {
        method: 'POST', headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          user_name: name || '채팅 # 피드백',
          content: '[# 방향성 피드백] ' + v,
          tenant_id: (window.RibbonData && window.RibbonData.currentTenantId) || undefined,
          session_id: sid.current,
          reported_message: lastAssistant ? assistantText(lastAssistant) : undefined,
          transcript,
          attachments: [],
        }),
      });
      const j = await res.json().catch(() => ({}));
      fbNote = res.ok && j.id ? ' (문제제기 접수 ' + j.id + ')' : ' (문제제기 전송 실패 — 지침만 반영됨)';
    } catch (e) { fbNote = ' (문제제기 전송 실패 — 지침만 반영됨)'; }
    setMessages((m) => [...m, {
      id: 'da' + Date.now(), role: 'assistant', directiveAck: true,
      text: '📌 ' + ack + fbNote,
      scenario: { answer: [{ type: 'markdown', md: '📌 ' + ack + fbNote }], followups: [] },
      feedback: null,
    }]);
  }

  function regenerate() {
    if (busy) return;
    let lui = -1;
    for (let i = messages.length - 1; i >= 0; i--) if (messages[i].role === 'user') { lui = i; break; }
    if (lui < 0) return;
    const lastUser = messages[lui];
    const hist = historyFrom(messages.slice(0, lui));
    setMessages((m) => {
      const copy = [...m];
      if (copy[copy.length - 1]?.role === 'assistant') copy.pop();
      return copy;
    });
    runReal(lastUser.text, hist);
  }

  function setFeedback(id, val) {
    setMessages((m) => m.map((x) => x.id === id ? { ...x, feedback: x.feedback === val ? null : val } : x));
  }

  // 새 대화: 현재 세션은 자동저장돼 있으니 새 id 로 비운다.
  function reset() {
    clearTimers();
    const nid = newSessionId();
    sid.current = nid; setActiveId(nid);
    setMessages([]); setBusy(false); setStatus(null); setSpotlight(null);
  }

  // 저장 세션 열기 — 메시지 복원 + 그 세션으로 이어서 대화(맥락 포함).
  function loadSession(sess) {
    if (!sess) return;
    clearTimers();
    sid.current = sess.id; setActiveId(sess.id);
    setMessages(Array.isArray(sess.messages) ? sess.messages : []);
    setBusy(false); setStatus(null); setSpotlight(null);
  }

  // 저장 세션 삭제. 현재 열려있는 세션이면 새 대화로 전환.
  function deleteSession(id) {
    setSessions((prev) => {
      const next = prev.filter((s) => s.id !== id);
      persistSessions(next);
      return next;
    });
    if (id === sid.current) reset();
  }

  return {
    messages, busy, status, spotlight, sessions, activeId,
    send, regenerate, setFeedback, reset, loadSession, deleteSession, setSpotlight,
    // 문제제기(피드백) 모달 — 대상 메시지를 넣으면 열리고 null 로 닫는다.
    reportMsg, openReport: setReportMsg, closeReport: () => setReportMsg(null),
  };
}

// ── MCP 미니 상태 표시줄 ────────────────────────────────────
function McpStatusBar({ status }) {
  const T = window.RibbonI18n.useT();
  if (!status) return null;
  const step = status.steps[status.idx];
  // 진행 알림이 와 있으면 그 라벨이 더 구체적이다(브리핑 안쪽 단계까지 나온다).
  const liveLabel = (status.live && status.live.label) || step.label;
  const liveSecs = status.live && status.live.elapsed_s > 0 ? status.live.elapsed_s : 0;
  // 스트리밍이라 총 단계 수를 미리 알 수 없다 → "N/전체" 대신 진행한 단계 수만(카운트 업),
  // 진행바는 불확정(인디터미네이트) 애니메이션으로 표시(가짜 100% 방지). 사용자 지적 2026-07-05.
  const color = step.external ? 'var(--color-teal)' : 'var(--accent)';
  return (
    <div style={{ borderBottom: '1px solid var(--border-hairline)', background: 'var(--surface-soft)', padding: '8px 16px' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
        <span className="rb-spin" style={{ width: 13, height: 13, borderRadius: '50%', flexShrink: 0,
          border: '2px solid var(--border-hairline)', borderTopColor: color }} />
        <span style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--text-strong)' }}>
          {liveLabel}{liveSecs ? ' · ' + liveSecs + '초' : ''}
        </span>
        {step.external &&
        <span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.6px', color: 'var(--color-teal)',
          background: 'rgba(93,184,114,0.14)', padding: '2px 7px', borderRadius: 'var(--radius-pill)', textTransform: 'uppercase' }}>MCP</span>
        }
        <span style={{ marginLeft: 'auto', fontSize: 11, color: 'var(--text-faint)', fontFamily: 'var(--font-mono)' }}>{step.source}</span>
        <span style={{ fontSize: 11, color: 'var(--text-muted)', fontWeight: 600 }}>{T('step_suffix', status.idx + 1)}</span>
      </div>
      <div style={{ position: 'relative', height: 2, background: 'var(--border-hairline)', borderRadius: 2, marginTop: 8, overflow: 'hidden' }}>
        <div className="rb-indet" style={{ background: color }} />
      </div>
    </div>);

}

// ── 사용자 / 어시스턴트 메시지 ──────────────────────────────
function UserMsg({ text, directive }) {
  // '#' 지침 말풍선은 일반 질문과 구분(테두리 강조 + 📌) — "질문이 아니라 방향성"임이 보이게.
  return (
    <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
      <div style={{ maxWidth: '82%', background: 'var(--surface-card-strong)', color: 'var(--text-heading)',
        border: directive ? '1px solid var(--accent)' : 'none',
        borderRadius: '14px 14px 4px 14px', padding: '10px 14px', fontFamily: 'var(--font-sans)', fontSize: 14.5, lineHeight: 1.5 }}>
        {directive ? '📌 ' : ''}{text}</div>
    </div>);

}

function ActionBtn({ icon, label, active, activeColor, onClick }) {
  const [hov, setHov] = useStateC(false);
  return (
    <button onClick={onClick} onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)} title={label}
    style={{ display: 'inline-flex', alignItems: 'center', gap: 5, height: 28, padding: '0 9px', borderRadius: 'var(--radius-sm)',
      border: 'none', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontSize: 12, fontWeight: 500,
      background: hov ? 'var(--surface-card)' : 'transparent',
      color: active ? activeColor || 'var(--accent)' : 'var(--text-muted)', transition: 'background .12s, color .12s' }}>
      <Icon d={icon} size={14} />{label && <span>{label}</span>}
    </button>);

}

function Sources({ items }) {
  const T = window.RibbonI18n.useT();
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 7, flexWrap: 'wrap', marginTop: 4 }}>
      <Icon d={I.db} size={12} color="var(--text-faint)" />
      <span style={{ fontSize: 11.5, color: 'var(--text-faint)', fontWeight: 500 }}>{T('sources')}</span>
      {items.map((s, i) =>
      <span key={i} style={{ fontSize: 11.5, fontWeight: 500, color: 'var(--text-muted)',
        background: 'var(--surface-soft)', border: '1px solid var(--border-hairline)', padding: '2px 9px', borderRadius: 'var(--radius-pill)' }}>{s}</span>
      )}
    </div>);

}

function AssistantMsg({ msg, conv, onChip, opts = {} }) {
  const { Block } = window.RibbonCharts;
  const T = window.RibbonI18n.useT();
  const sc = msg.scenario;
  const [toast, setToast] = useStateC(null);
  function copy() {
    const txt = sc.answer.filter((b) => b.type === 'text' || b.type === 'markdown').map((b) => b.md.replace(/\*\*/g, '')).join('\n\n');
    navigator.clipboard && navigator.clipboard.writeText(txt);
    setToast(T('copied'));setTimeout(() => setToast(null), 1400);
  }
  function exportChart() {setToast(T('exported'));setTimeout(() => setToast(null), 1600);}
  return (
    <div style={{ display: 'flex', gap: 11 }}>
      <div style={{ flexShrink: 0, width: 30, height: 30, borderRadius: 'var(--radius-full)', background: 'var(--surface-card)',
        display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: 2 }}><Spike size={17} /></div>
      <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 14 }}>
        {sc.answer.map((b, i) => <Block key={i} block={b} />)}
        {sc.sources && opts.showSources !== false && <Sources items={sc.sources} />}
        {/* 메시지 액션 */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap', marginTop: -2 }}>
          <ActionBtn icon={I.copy} label={T('act_copy')} onClick={copy} />
          <ActionBtn icon={I.refresh} label={T('act_regen')} onClick={() => conv.regenerate()} />
          <ActionBtn icon={I.download} label={T('act_export')} onClick={exportChart} />
          <div style={{ width: 1, height: 16, background: 'var(--border-hairline)', margin: '0 4px' }} />
          <ActionBtn icon={I.up} active={msg.feedback === 'up'} activeColor="var(--color-success)" onClick={() => conv.setFeedback(msg.id, 'up')} />
          <ActionBtn icon={I.down} active={msg.feedback === 'down'} activeColor="var(--color-error)" onClick={() => conv.setFeedback(msg.id, 'down')} />
          <ActionBtn icon={I.flag} label={T('act_report')} onClick={() => conv.openReport(msg)} />
          {toast && <span style={{ marginLeft: 8, fontSize: 11.5, color: 'var(--color-success)', fontWeight: 600 }}>{toast}</span>}
        </div>
        {/* 후속 질문 칩 */}
        {sc.followups && sc.followups.length > 0 &&
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 7 }}>
            {sc.followups.map((f, i) =>
          <button key={i} onClick={() => onChip(f)} className="rb-chip"
          style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '7px 12px', borderRadius: 'var(--radius-pill)',
            border: '1px solid var(--border-hairline)', background: 'var(--surface-page)', cursor: 'pointer',
            fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: 500, color: 'var(--text-body)' }}>
                <Icon d={I.spark} size={12} color="var(--accent)" />{f}</button>
          )}
          </div>
        }
      </div>
    </div>);

}

// ── 능동 브리핑 카드(2026-07-14) ────────────────────────────
// 사장님이 화면을 열자마자 **묻기 전에** 어제 이상징후를 먼저 보여준다.
// 판정은 서버(/api/briefing)가 한 것을 그대로 표시한다 — 프런트는 계산하지 않는다.
const SEV = {
  critical: { label: '즉시 조치', color: 'var(--color-error)', bg: 'rgba(198,69,69,0.08)' },
  warning: { label: '주의', color: '#e83667', bg: 'rgba(232,54,103,0.09)' },
  info: { label: '참고', color: 'var(--text-muted)', bg: 'var(--surface-soft)' },
};
function BriefingCard({ onChip }) {
  const [b, setB] = useStateC(null); // null=로딩, false=권한없음/실패
  useEffectC(() => {
    let alive = true;
    const f = (window.RibbonAuth ? window.RibbonAuth.fetch : fetch);
    f('/api/briefing')
      .then((r) => (r.ok ? r.json() : null))
      .then((j) => { if (alive) setB(j || false); })
      .catch(() => { if (alive) setB(false); });
    return () => { alive = false; };
  }, []);
  if (!b || !b.signals) return null; // 로딩·권한없음·실패 시 조용히 숨긴다(빈 카드는 노이즈)
  const sigs = b.signals;
  const crit = sigs.filter((s) => s.severity === 'critical').length;
  return (
    <div style={{ border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)',
      background: 'var(--surface-page)', padding: '14px 15px', marginBottom: 20 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: sigs.length ? 11 : 0 }}>
        <Icon d="M12 9v4M12 17h.01M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
          size={14} color={crit ? 'var(--color-error)' : 'var(--text-faint)'} />
        <span style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-heading)' }}>
          {sigs.length ? `어제(${b.date}) 이상징후 ${sigs.length}건` : `어제(${b.date}) 특이사항 없습니다`}
        </span>
        {b.skipped && b.skipped.length > 0 &&
          <span title={b.skipped.join(' · ')} style={{ fontSize: 11, color: 'var(--text-faint)', marginLeft: 'auto' }}>
            판정 보류 {b.skipped.length}건</span>}
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {sigs.map((s, i) => {
          const sv = SEV[s.severity] || SEV.info;
          return (
            <button key={i} onClick={() => s.followup && onChip(s.followup)} disabled={!s.followup}
              style={{ textAlign: 'left', display: 'flex', gap: 9, alignItems: 'flex-start', padding: '9px 11px',
                borderRadius: 'var(--radius-md)', border: '1px solid var(--border-hairline)', background: sv.bg,
                cursor: s.followup ? 'pointer' : 'default', fontFamily: 'var(--font-sans)', width: '100%' }}>
              <span style={{ flexShrink: 0, fontSize: 10, fontWeight: 700, color: sv.color, padding: '2px 6px',
                borderRadius: 'var(--radius-pill)', border: `1px solid ${sv.color}`, marginTop: 1, whiteSpace: 'nowrap' }}>{sv.label}</span>
              <span style={{ minWidth: 0 }}>
                <span style={{ display: 'block', fontSize: 13, fontWeight: 650, color: 'var(--text-heading)', lineHeight: 1.4 }}>{s.title}</span>
                <span style={{ display: 'block', fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.5, marginTop: 2 }}>{s.detail}</span>
                {s.action && <span style={{ display: 'block', fontSize: 12, color: 'var(--text-body)', lineHeight: 1.5, marginTop: 3 }}>→ {s.action}</span>}
              </span>
            </button>);
        })}
      </div>
    </div>);
}

// ── 시작 화면 (인사 + 추천 질문) ────────────────────────────
function Welcome({ onChip }) {
  const locale = window.RibbonI18n.useLocale();
  const T = window.RibbonI18n.useT();
  const chips = window.RibbonI18n.chips(locale);
  return (
    <div style={{ padding: '8px 4px 4px' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
        <div style={{ width: 40, height: 40, borderRadius: 'var(--radius-full)', background: 'var(--surface-card)',
          display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Spike size={24} /></div>
      </div>
      <h2 style={{ fontFamily: 'var(--font-display)', fontSize: 27, letterSpacing: '-0.5px', color: 'var(--text-heading)', margin: '0 0 6px', fontWeight: 400 }}>
        {T('welcome_title')}</h2>
      <p style={{ fontFamily: 'var(--font-sans)', fontSize: 14, color: 'var(--text-muted)', margin: '0 0 22px', lineHeight: 1.55 }}>
        {T('welcome_sub')}</p>
      <BriefingCard onChip={onChip} />
      <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-faint)', marginBottom: 10, letterSpacing: '0.3px' }}>{T('suggested')}</div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
        {chips.map((c, i) =>
        <button key={i} onClick={() => onChip(c)} className="rb-chip rb-starter"
        style={{ textAlign: 'left', display: 'flex', alignItems: 'center', gap: 9, padding: '12px 13px', borderRadius: 'var(--radius-md)',
          border: '1px solid var(--border-hairline)', background: 'var(--surface-page)', cursor: 'pointer',
          fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 500, color: 'var(--text-body)', lineHeight: 1.4 }}>
            <Icon d={I.spark} size={14} color="var(--accent)" style={{ flexShrink: 0 }} />{c}</button>
        )}
      </div>
    </div>);

}

// ── 입력 바 (음성 / 첨부 / 전송) ────────────────────────────
function ChatInput({ onSend, busy }) {
  const T = window.RibbonI18n.useT();
  const [val, setVal] = useStateC('');
  const [listening, setListening] = useStateC(false);
  const [attached, setAttached] = useStateC(false);
  const ref = useRefC(null);
  function submit() {if (!val.trim() || busy) return;onSend(val);setVal('');setAttached(false);if (ref.current) ref.current.style.height = 'auto';}
  function key(e) {if (e.key === 'Enter' && !e.shiftKey) {e.preventDefault();submit();}}
  function grow(e) {e.target.style.height = 'auto';e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px';setVal(e.target.value);}
  // '#' 지침 모드(ALPACA-39): 입력이 #으로 시작하면 컴포저 색이 바뀐다 — 질문이 아니라 방향성 피드백임을 표시.
  const directiveMode = val.trimStart().startsWith('#');
  return (
    <div style={{ padding: '12px 16px 14px', borderTop: '1px solid var(--border-hairline)', background: 'var(--surface-page)' }}>
      {attached &&
      <div style={{ display: 'inline-flex', alignItems: 'center', gap: 7, marginBottom: 8, padding: '5px 10px', borderRadius: 'var(--radius-sm)',
        background: 'var(--surface-soft)', border: '1px solid var(--border-hairline)', fontSize: 12, color: 'var(--text-body)' }}>
          <Icon d={I.clip} size={13} color="var(--text-muted)" />6월_가동률_리포트.xlsx
          <button onClick={() => setAttached(false)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 0, marginLeft: 2 }}><Icon d={I.x} size={12} color="var(--text-faint)" /></button>
        </div>
      }
      {directiveMode &&
        <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, marginBottom: 6, padding: '3px 10px',
          borderRadius: 'var(--radius-pill)', background: 'rgba(30,168,231,0.12)', color: 'var(--accent)', fontSize: 11.5, fontWeight: 700 }}>
          📌 방향성 피드백 모드 — 답변이 아니라 개선 지침으로 반영되고 문제제기로도 접수됩니다
        </div>}
      <div style={{ display: 'flex', alignItems: 'flex-end', gap: 8,
        background: directiveMode ? 'rgba(30,168,231,0.06)' : 'var(--surface-page)',
        border: directiveMode ? '1px solid var(--accent)' : '1px solid var(--border-hairline)',
        borderRadius: 'var(--radius-lg)', padding: '8px 8px 8px 12px', transition: 'border-color .15s, background .15s' }}>
        <button onClick={() => setAttached(true)} title="첨부" style={{ flexShrink: 0, width: 32, height: 32, borderRadius: 'var(--radius-full)',
          border: 'none', background: 'transparent', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <Icon d={I.clip} size={18} color="var(--text-muted)" /></button>
        <textarea ref={ref} rows={1} value={val} onChange={grow} onKeyDown={key} placeholder={T('input_placeholder')}
        style={{ flex: 1, resize: 'none', border: 'none', outline: 'none', background: 'transparent', fontFamily: 'var(--font-sans)',
          fontSize: 14, lineHeight: 1.5, color: 'var(--text-heading)', maxHeight: 120, padding: '5px 0' }} />
        <button onClick={() => setListening((l) => !l)} title="음성 입력" className={listening ? 'rb-pulse' : ''}
        style={{ flexShrink: 0, width: 32, height: 32, borderRadius: 'var(--radius-full)', border: 'none', cursor: 'pointer',
          background: listening ? 'var(--accent)' : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <Icon d={I.mic} size={18} color={listening ? '#fff' : 'var(--text-muted)'} /></button>
        <button onClick={submit} disabled={!val.trim() || busy} title="전송"
        style={{ flexShrink: 0, width: 32, height: 32, borderRadius: 'var(--radius-full)', border: 'none',
          cursor: val.trim() && !busy ? 'pointer' : 'default', display: 'flex', alignItems: 'center', justifyContent: 'center',
          background: val.trim() && !busy ? 'var(--accent)' : 'var(--surface-card-strong)', transition: 'background .15s' }}>
          <Icon d={I.arrowUp} size={17} color={val.trim() && !busy ? '#fff' : 'var(--text-faint)'} /></button>
      </div>
      <div style={{ textAlign: 'center', fontSize: 10.5, color: 'var(--text-faint)', marginTop: 8 }}>{T('disclaimer')}</div>
    </div>);

}

// ── 메시지 스레드 (스크롤 영역) ─────────────────────────────
function ChatThread({ conv, onChip, opts = {} }) {
  const T = window.RibbonI18n.useT();
  const endRef = useRefC(null);
  useEffectC(() => {if (endRef.current) endRef.current.scrollTop = endRef.current.scrollHeight;}, [conv.messages.length, conv.status]);
  return (
    <div ref={endRef} className="rb-thread" style={{ flex: 1, overflowY: 'auto', padding: '20px 18px' }}>
      {conv.messages.length === 0 && !conv.busy ?
      <Welcome onChip={onChip} /> :

      <div className="rb-msgs" style={{ display: 'flex', flexDirection: 'column', gap: 22, maxWidth: 720, margin: '0 auto', fontWeight: "600" }}>
          {conv.messages.map((m) => m.role === 'user' ?
        <UserMsg key={m.id} text={m.text} directive={m.directive} /> :
        <AssistantMsg key={m.id} msg={m} conv={conv} onChip={onChip} opts={opts} />)}
          {conv.busy && conv.status &&
        <div style={{ display: 'flex', gap: 11 }}>
              <div style={{ flexShrink: 0, width: 30, height: 30, borderRadius: 'var(--radius-full)', background: 'var(--surface-card)',
            display: 'flex', alignItems: 'center', justifyContent: 'center' }} className="rb-breathe"><Spike size={17} /></div>
              <div style={{ fontSize: 13.5, color: 'var(--text-muted)', paddingTop: 6, fontStyle: 'italic' }}>{T('analyzing')}</div>
            </div>
        }
        </div>
      }
    </div>);

}

window.RibbonChat = { useConversation, McpStatusBar, ChatThread, ChatInput, Icon, Spike, I, relTime, historyFrom };