/* ═══════════════════════════════════════════════════════
   13  로컬 LLM 채팅
   ══════════════════════════════════════════════════════ */

// ── 상태 ───────────────────────────────────────────────
let llmSessions = JSON.parse(localStorage.getItem('llmSessions') || '[]');
let llmActiveSessionId = localStorage.getItem('llmActiveSessionId') || '';
let llmAbortController = null;
let llmIsStreaming = false;
let llmType = localStorage.getItem('llmType') || 'vllm';
if (llmType === 'ollama') {
  llmType = 'vllm';
  localStorage.setItem('llmType', 'vllm');
}
let llmUseTools = localStorage.getItem('llmUseTools') === 'true';
let llmAgentMode = localStorage.getItem('llmAgentMode') === 'true';
let llmMcpTools = [];

// ── Phase 5.5: welcome 화면 추천 질문 ───────────────────
const LLM_STARTER_CHIPS = [
  '지금 활성 알람을 보여줘',
  'Unit A의 24시간 운전 상황을 요약해줘',
  'FIC-6113.PV 최근 1시간 추이',
  '오늘 발생한 디지털 이벤트 정리',
  'P-6201 펌프의 정비 이력',
  '이번 주 보고서를 작성해줘',
  '냉각수 펌프 토출 압력 태그를 찾아줘'
];

function llmUseChip(btn) {
  const input = document.getElementById('llm-input');
  if (!input) return;
  input.value = btn.textContent;
  input.focus();
  input.style.height = 'auto';
  input.style.height = Math.min(input.scrollHeight, 150) + 'px';
}

// ── 초기화 (탭 진입 시) ──────────────────────────────
paneInit.llmchat = async function() {
  // llm-type-select 를 JS 변수(llmType)와 동기화 (HTML 기본값 ollama 보정)
  const typeSel = document.getElementById('llm-type-select');
  if (typeSel) typeSel.value = llmType;

  llmRenderSessionList();
  llmLoadActiveSession();
  await llmLoadModels();   // 모델 목록 먼저 채운 뒤
  llmLoadConfigToUI();     // llm-model.json 값 반영
  llmLoadMcpTools();
};

// ── 세션 관리 ──────────────────────────────────────────
function llmCreateSession() {
  const id = 'llm_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
  const session = {
    id,
    title: '새 대화',
    model: '',
    systemPrompt: '',
    messages: [],
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString()
  };
  llmSessions.unshift(session);
  llmActiveSessionId = id;
  llmSaveSessions();
  llmRenderSessionList();
  llmRenderMessages();
  return session;
}

function llmNewSession() {
  llmCreateSession();
  document.getElementById('llm-input').focus();
}

function llmSwitchSession(id) {
  llmActiveSessionId = id;
  localStorage.setItem('llmActiveSessionId', id);
  llmRenderSessionList();
  llmRenderMessages();
}

function llmDeleteSession(id, e) {
  e.stopPropagation();
  if (!confirm('이 대화를 삭제하시겠습니까?')) return;
  llmSessions = llmSessions.filter(s => s.id !== id);
  llmSaveSessions();
  if (llmActiveSessionId === id) {
    llmActiveSessionId = llmSessions.length > 0 ? llmSessions[0].id : '';
    localStorage.setItem('llmActiveSessionId', llmActiveSessionId);
  }
  llmRenderSessionList();
  llmRenderMessages();
}

function llmGetActiveSession() {
  return llmSessions.find(s => s.id === llmActiveSessionId) || null;
}

function llmSaveSessions() {
  localStorage.setItem('llmSessions', JSON.stringify(llmSessions));
  localStorage.setItem('llmActiveSessionId', llmActiveSessionId);
}

function llmSaveSessionMeta() {
  const sess = llmGetActiveSession();
  if (!sess) return;
  sess.model = document.getElementById('llm-model-select').value;
  sess.systemPrompt = document.getElementById('llm-system-prompt').value.trim();
  sess.updatedAt = new Date().toISOString();
  llmSaveSessions();
  llmRenderSessionList();
}

// ── 세션 목록 렌더링 ───────────────────────────────────
function llmRenderSessionList() {
  const el = document.getElementById('llm-session-list');
  if (!el) return;

  if (llmSessions.length === 0) {
    el.innerHTML = '<div class="llm-empty">대화가 없습니다.<br>+ 버튼을 눌러 새 대화를 시작하세요.</div>';
    return;
  }

  el.innerHTML = llmSessions.map(s => {
    const isActive = s.id === llmActiveSessionId;
    const title = esc(s.title || '제목 없음');
    return `
      <div class="llm-session-item ${isActive ? 'active' : ''}" onclick="llmSwitchSession('${s.id}')">
        <span class="llm-sess-title" title="${title}">${title}</span>
        <span class="llm-sess-del" onclick="llmDeleteSession('${s.id}', event)" title="삭제">×</span>
      </div>
    `;
  }).join('');
}

// ── 메시지 렌더링 ──────────────────────────────────────
function llmRenderMessages() {
  const el = document.getElementById('llm-messages');
  if (!el) return;

  const sess = llmGetActiveSession();
  if (!sess || sess.messages.length === 0) {
    const chips = LLM_STARTER_CHIPS.map(q =>
      `<button class="llm-chip" type="button" onclick="llmUseChip(this)">${esc(q)}</button>`
    ).join('');
    el.innerHTML = `
      <div class="llm-welcome">
        <div class="llm-welcome-icon">💬</div>
        <div class="llm-welcome-text">새 대화를 시작하세요</div>
        <div class="llm-welcome-hint">모델을 선택하고 메시지를 입력하세요. 또는 아래 추천 질문을 클릭하세요.</div>
        <div class="llm-chip-row">${chips}</div>
      </div>
    `;
    if (sess) {
      const sel = document.getElementById('llm-model-select');
      if (sel && sess.model) sel.value = sess.model;
      const ta = document.getElementById('llm-system-prompt');
      if (ta && sess.systemPrompt) ta.value = sess.systemPrompt;
    }
    return;
  }

  const sel = document.getElementById('llm-model-select');
  if (sel && sess.model) sel.value = sess.model;
  const ta = document.getElementById('llm-system-prompt');
  if (ta && sess.systemPrompt) ta.value = sess.systemPrompt;

  const summaryBanner = sess.summary
    ? `<div class="llm-summary-card" onclick="this.classList.toggle('open')" title="클릭하여 펼치기/접기">
         <div class="llm-summary-head">📋 이전 대화 요약 (${sess.summarizedUpTo || 0}개 메시지 압축)</div>
         <div class="llm-summary-body">${esc(sess.summary)}</div>
       </div>`
    : '';
  const upTo = sess.summarizedUpTo || 0;
  el.innerHTML = summaryBanner + sess.messages.slice(upTo).map(m => {
    const role = m.role === 'user' ? 'user' : 'assistant';
    const avatar = role === 'user' ? 'U' : 'AI';
    const content = llmFormatMessage(m.content);
    const toolCards = (role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0)
      ? llmRenderToolCardsHtml(m.toolCalls)
      : '';
    return `
      <div class="llm-msg ${role}">
        <div class="llm-msg-avatar">${avatar}</div>
        <div class="llm-msg-bubble">${toolCards}${content}</div>
      </div>
    `;
  }).join('');

  el.scrollTop = el.scrollHeight;
}

// ── 메시지 포맷팅 (placeholder 패턴으로 esc 순서 보장) ──
function llmFormatMessage(text) {
  const blocks = [];
  text = text.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, __, code) => {
    blocks.push(`<pre><code>${esc(code.trim())}</code></pre>`);
    return `\x00B${blocks.length - 1}\x00`;
  });
  text = text.replace(/`([^`]+)`/g, (_, code) => {
    blocks.push(`<code>${esc(code)}</code>`);
    return `\x00B${blocks.length - 1}\x00`;
  });
  text = esc(text).replace(/\n/g, '<br>');
  text = text.replace(/\x00B(\d+)\x00/g, (_, i) => blocks[+i]);
  return llmLinkKbCitations(text);
}

/* ── Phase 5: KB 인용 → 다운로드 링크 치환 ─────────────── */
let llmKbDocMap = {};   // title → docId (search_kb 결과로 누적)

function llmRegisterKbHits(hits) {
  if (!Array.isArray(hits)) return;
  for (const h of hits) {
    if (h && h.title && h.doc_id) llmKbDocMap[h.title] = h.doc_id;
  }
}

function llmLinkKbCitations(html) {
  const titles = Object.keys(llmKbDocMap);
  if (titles.length === 0) return html;
  // 긴 제목부터 매칭 (부분문자열 겹침 회피)
  titles.sort((a, b) => b.length - a.length);
  for (const title of titles) {
    const docId = llmKbDocMap[title];
    const safeTitle = esc(title).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const re = new RegExp(safeTitle, 'g');
    html = html.replace(re,
      `<a class="kb-cite-link" href="/api/kb/download/${docId}" target="_blank" rel="noopener">${esc(title)} ⬇</a>`);
  }
  return html;
}

/* ── Phase 5: 툴 실행 카드 ─────────────────────────────── */
function llmEnsureStreamingMsgEl() {
  let msgEl = document.getElementById('llm-streaming-msg');
  if (msgEl) return msgEl;
  const messagesEl = document.getElementById('llm-messages');
  msgEl = document.createElement('div');
  msgEl.className = 'llm-msg assistant';
  msgEl.id = 'llm-streaming-msg';
  msgEl.innerHTML = `
    <div class="llm-msg-avatar">AI</div>
    <div class="llm-msg-bubble"></div>
  `;
  messagesEl.appendChild(msgEl);
  return msgEl;
}

function llmEnsureToolCardContainer() {
  const msgEl = llmEnsureStreamingMsgEl();
  let cont = msgEl.querySelector('.llm-tool-cards');
  if (!cont) {
    cont = document.createElement('div');
    cont.className = 'llm-tool-cards';
    const bubble = msgEl.querySelector('.llm-msg-bubble');
    msgEl.insertBefore(cont, bubble);
  }
  return cont;
}

function llmAppendToolCard(id, name, args) {
  const cont = llmEnsureToolCardContainer();
  const argSummary = llmSummarizeArgs(args);
  const card = document.createElement('div');
  card.className = 'llm-tool-card running';
  card.dataset.toolId = id;
  card.innerHTML = `
    <div class="llm-tool-head" onclick="this.parentElement.classList.toggle('open')">
      <span class="llm-tool-icon">⚙</span>
      <span class="llm-tool-name">${esc(name)}</span>
      <span class="llm-tool-args mono">${esc(argSummary)}</span>
      <span class="llm-tool-status">실행 중…</span>
    </div>
    <div class="llm-tool-body">
      <div class="placeholder">결과 대기 중…</div>
    </div>
  `;
  cont.appendChild(card);
  const messagesEl = document.getElementById('llm-messages');
  if (messagesEl) messagesEl.scrollTop = messagesEl.scrollHeight;
}

// 소프트 타임아웃 초과 — 1회 연장 진행 중 알림
function llmMarkToolCardExtending(id, softSec, extSec) {
  const cont = document.querySelector('.llm-tool-cards');
  if (!cont) return;
  const card = cont.querySelector(`.llm-tool-card[data-tool-id="${CSS.escape(id)}"]`);
  if (!card) return;
  card.classList.remove('running');
  card.classList.add('extending');
  const st = card.querySelector('.llm-tool-status');
  if (st) st.textContent = `응답 지연 — 연장 중 (${softSec}s → ${extSec}s)`;
}

function llmUpdateToolCard(id, name, ok, preview, length, payload) {
  const cont = document.querySelector('.llm-tool-cards');
  if (!cont) return;
  const card = cont.querySelector(`.llm-tool-card[data-tool-id="${CSS.escape(id)}"]`);
  if (!card) return;
  card.classList.remove('running', 'extending');
  card.classList.add(ok ? 'ok' : 'err');
  const st = card.querySelector('.llm-tool-status');
  if (st) st.textContent = ok ? `완료 · ${length}자` : '실패';
  const body = card.querySelector('.llm-tool-body');
  if (body) body.innerHTML = llmRenderToolPayload(name, ok, preview, payload);

  // search_kb 결과면 인용 매핑 등록
  if (ok && name === 'search_kb') {
    try {
      const parsed = JSON.parse(payload);
      if (parsed.success && Array.isArray(parsed.hits)) llmRegisterKbHits(parsed.hits);
    } catch {}
  }
}

// 영구 보존: sess.messages[*].toolCalls 배열 → 정적 HTML로 재렌더링
function llmRenderToolCardsHtml(toolCalls) {
  const cards = toolCalls.map(tc => {
    const ok = tc.ok;
    const cls = ok === null || ok === undefined ? 'running' : (ok ? 'ok' : 'err');
    const statusText = ok === null || ok === undefined
      ? '실행 중…'
      : (ok ? `완료${typeof tc.length === 'number' ? ' · ' + tc.length + '자' : ''}` : '실패');
    const argSummary = llmSummarizeArgs(tc.args);
    const body = (ok === null || ok === undefined)
      ? '<div class="placeholder">결과 대기 중…</div>'
      : llmRenderToolPayload(tc.name, ok, tc.preview, tc.payload);
    const toolId = tc.id ? `data-tool-id="${esc(tc.id)}"` : '';
    return `
      <div class="llm-tool-card ${cls}" ${toolId}>
        <div class="llm-tool-head" onclick="this.parentElement.classList.toggle('open')">
          <span class="llm-tool-icon">⚙</span>
          <span class="llm-tool-name">${esc(tc.name || '')}</span>
          <span class="llm-tool-args mono">${esc(argSummary)}</span>
          <span class="llm-tool-status">${esc(statusText)}</span>
        </div>
        <div class="llm-tool-body">${body}</div>
      </div>
    `;
  }).join('');
  return `<div class="llm-tool-cards">${cards}</div>`;
}

function llmSummarizeArgs(args) {
  if (!args) return '';
  if (typeof args === 'string') {
    return args.length > 100 ? args.slice(0, 100) + '…' : args;
  }
  try {
    const s = JSON.stringify(args);
    return s.length > 100 ? s.slice(0, 100) + '…' : s;
  } catch { return ''; }
}

function llmRenderToolPayload(name, ok, preview, payload) {
  if (!ok) return `<div class="llm-tool-err">${esc(preview || '오류')}</div>`;
  // JSON 응답이면 표/시계열 자동 렌더 시도
  try {
    const j = JSON.parse(payload || preview);
    // search_kb 형태
    if (Array.isArray(j.hits)) return llmRenderKbHits(j.hits);
    // run_sql/query_with_nl 형태: {success, columns:[], data:[{...}, ...]}
    if (j.success && Array.isArray(j.columns) && Array.isArray(j.data)) {
      const ts = llmDetectTimeSeries(j.data);
      const sparkHtml = ts ? llmBuildSparklineHtml(j.data, ts) : '';
      return sparkHtml + llmRenderTable(j.columns, j.data);
    }
    // query_pv_history 형태: {success, data:[{tag_name, timestamp, value}, ...]}
    if (j.success && Array.isArray(j.data) && j.data.length > 0 && typeof j.data[0] === 'object') {
      const cols = Object.keys(j.data[0]);
      const ts = llmDetectTimeSeries(j.data);
      const sparkHtml = ts ? llmBuildSparklineHtml(j.data, ts) : '';
      return sparkHtml + llmRenderTable(cols, j.data);
    }
  } catch {}
  return `<pre class="llm-tool-raw">${esc((preview || '').slice(0, 800))}</pre>`;
}

// 시계열 감지: data가 {timestamp|recorded_at|ts} + {value|pv|val} 컬럼을 가지면 키 페어 반환
function llmDetectTimeSeries(data) {
  if (!Array.isArray(data) || data.length < 3) return null;
  const sample = data[0];
  if (!sample || typeof sample !== 'object') return null;
  const timeKeys = ['timestamp', 'recorded_at', 'ts', 'time', 'datetime'];
  const valueKeys = ['value', 'pv', 'val', 'fieldvalue', 'sp', 'op'];
  const timeKey = timeKeys.find(k => k in sample);
  const valueKey = valueKeys.find(k => k in sample && !isNaN(parseFloat(sample[k])));
  if (!timeKey || !valueKey) return null;
  // 유효 데이터 수 체크 (timestamp 파싱 가능 + numeric value)
  let valid = 0;
  for (const r of data) {
    const t = new Date(r[timeKey]).getTime();
    const v = parseFloat(r[valueKey]);
    if (!isNaN(t) && !isNaN(v)) valid++;
    if (valid >= 3) return { timeKey, valueKey };
  }
  return null;
}

// uPlot은 비동기 DOM 부착 후 생성해야 하므로, container를 placeholder로 만들고
// requestAnimationFrame에서 차트를 그린다.
function llmBuildSparklineHtml(data, keys) {
  if (typeof uPlot === 'undefined') return '';
  const id = 'llm-spark-' + (++_llmSparkSeq);
  // 데이터를 캐시에 보관 → 비동기 렌더 시 참조
  _llmSparkCache[id] = { data, keys };
  // 다음 프레임에 부착되도록 예약
  requestAnimationFrame(() => llmMountSparkline(id));
  return `<div class="llm-sparkline-box"><div class="llm-sparkline-label">📈 시계열 추세 (${data.length}건)</div><div id="${id}" class="llm-sparkline-chart"></div></div>`;
}

let _llmSparkSeq = 0;
const _llmSparkCache = {};

function llmMountSparkline(id) {
  const container = document.getElementById(id);
  if (!container) return;
  const entry = _llmSparkCache[id];
  if (!entry) return;
  delete _llmSparkCache[id];
  if (typeof uPlot === 'undefined') return;

  const { data, keys } = entry;
  const pairs = [];
  for (const r of data) {
    const t = new Date(r[keys.timeKey]).getTime() / 1000;
    const v = parseFloat(r[keys.valueKey]);
    if (!isNaN(t) && !isNaN(v)) pairs.push([t, v]);
  }
  if (pairs.length < 2) return;
  pairs.sort((a, b) => a[0] - b[0]);
  const xs = pairs.map(p => p[0]);
  const ys = pairs.map(p => p[1]);

  const opts = {
    width: Math.min(container.clientWidth || 360, 600),
    height: 90,
    cursor: { show: true, points: { show: true } },
    select: { show: false },
    legend: { show: false },
    scales: { x: { time: true } },
    axes: [
      { stroke: '#888', size: 22, grid: { show: false } },
      { stroke: '#888', size: 36, grid: { stroke: 'rgba(255,255,255,0.05)' } }
    ],
    series: [
      {},
      {
        label: keys.valueKey,
        stroke: 'rgb(96, 165, 250)',
        width: 1.4,
        points: { show: false }
      }
    ]
  };
  try {
    new uPlot(opts, [xs, ys], container);
  } catch (e) {
    container.innerHTML = `<div class="placeholder">차트 렌더 실패</div>`;
  }
}

function llmRenderTable(columns, data) {
  if (!data || data.length === 0) return '<div class="placeholder">결과 0건</div>';
  const limit = Math.min(data.length, 50);
  const ths = columns.map(c => `<th>${esc(c)}</th>`).join('');
  const rows = data.slice(0, limit).map(row => {
    const tds = columns.map(c => {
      const v = row[c];
      return `<td>${v == null ? '' : esc(String(v))}</td>`;
    }).join('');
    return `<tr>${tds}</tr>`;
  }).join('');
  const more = data.length > limit ? `<div class="llm-tool-more">…나머지 ${data.length - limit}건 생략</div>` : '';
  return `<div class="llm-tool-tbl-wrap"><table class="llm-tool-tbl"><thead><tr>${ths}</tr></thead><tbody>${rows}</tbody></table></div>${more}`;
}

function llmRenderKbHits(hits) {
  if (!hits || hits.length === 0) return '<div class="placeholder">검색 결과 0건</div>';
  llmRegisterKbHits(hits);
  return '<div class="llm-kb-hits">' + hits.slice(0, 8).map(h => {
    const score = (h.score || 0).toFixed(3);
    const link = h.doc_id
      ? `<a href="/api/kb/download/${h.doc_id}" target="_blank" rel="noopener">${esc(h.title || '(제목없음)')} ⬇</a>`
      : esc(h.title || '(제목없음)');
    const loc = h.locator ? ` · <span class="mono">${esc(h.locator)}</span>` : '';
    const snippet = (h.text || '').slice(0, 200).replace(/\s+/g, ' ');
    return `<div class="llm-kb-hit">
      <div class="llm-kb-head"><span class="mono">${score}</span> ${link}${loc}</div>
      <div class="llm-kb-snip">${esc(snippet)}…</div>
    </div>`;
  }).join('') + '</div>';
}

// ── 모델 목록 로드 ─────────────────────────────────────
async function llmLoadModels() {
  const prefix = llmType === 'vllm' ? '/api/ollama/vllm' : '/api/ollama';
  const label = llmType === 'vllm' ? 'vLLM' : 'Ollama';
  const sel = document.getElementById('llm-model-select');
  if (!sel) return;

  try {
    const d = await api('GET', `${prefix}/models`);
    sel.innerHTML = '<option value="">-- 모델을 선택하세요 --</option>';

    if (d.success && d.models) {
      d.models.forEach(m => {
        const opt = document.createElement('option');
        opt.value = m;
        opt.textContent = m;
        sel.appendChild(opt);
      });
    }

    // vLLM: llm-model.json 을 항상 단일 선택 기준으로 반영
    if (llmType === 'vllm') {
      try {
        const cfg = await api('GET', '/api/llm/config');
        if (cfg.success && cfg.vllmModel) {
          if (![...sel.options].some(o => o.value === cfg.vllmModel)) {
            const opt = document.createElement('option');
            opt.value = cfg.vllmModel;
            opt.textContent = cfg.vllmModel;
            sel.appendChild(opt);
          }
          sel.value = cfg.vllmModel;
        }
      } catch (_) {}
    }

    const dot = document.getElementById('llm-conn-status');
    if (dot) {
      dot.className = d.success ? 'llm-conn-dot connected' : 'llm-conn-dot error';
      dot.title = d.success ? `${label} 연결됨` : `${label} 연결 실패`;
    }
  } catch (e) {
    const dot = document.getElementById('llm-conn-status');
    if (dot) {
      dot.className = 'llm-conn-dot error';
      dot.title = `${label} 연결 실패: ` + e.message;
    }
  }
}

async function llmOnTypeChange() {
  llmType = document.getElementById('llm-type-select').value;
  localStorage.setItem('llmType', llmType);
  await llmLoadModels();   // 목록 채운 뒤
  llmLoadConfigToUI();     // llm-model.json 값 반영 (vLLM 전환 시도 포함)
  llmLoadMcpTools();
}

async function llmLoadMcpTools() {
  const useTools = document.getElementById('llm-use-tools');
  const toolsRow = document.getElementById('llm-tools-row');
  const agentRow = document.getElementById('llm-agent-row');
  const agentChk = document.getElementById('llm-agent-mode');
  if (llmType !== 'vllm') {
    if (useTools) useTools.checked = false;
    if (toolsRow) toolsRow.style.display = 'none';
    if (agentRow) agentRow.style.display = 'none';
    llmUseTools = false;
    localStorage.setItem('llmUseTools', 'false');
    return;
  }
  if (toolsRow) toolsRow.style.display = '';
  // 에이전트 모드 토글은 MCP 도구가 활성일 때만 의미 있음
  if (agentRow) agentRow.style.display = (useTools && useTools.checked) ? '' : 'none';
  if (agentChk) agentChk.checked = !!llmAgentMode;
  if (useTools) {
    llmUseTools = useTools.checked;
    localStorage.setItem('llmUseTools', llmUseTools);
  }
  if (!llmUseTools) { llmMcpTools = []; return; }
  try {
    const d = await api('GET', '/api/text-to-sql/tools');
    if (d.success && d.tools) {
      llmMcpTools = d.tools.map(t => ({
        type: 'function',
        function: {
          name: t.Name || t.name,
          description: t.Description || t.description || '',
          parameters: t.InputSchema || t.inputSchema || { type: 'object', properties: {} }
        }
      }));
    } else {
      llmMcpTools = [];
    }
  } catch {
    llmMcpTools = [];
  }
}

function llmToggleTools() {
  llmUseTools = document.getElementById('llm-use-tools').checked;
  localStorage.setItem('llmUseTools', llmUseTools);
  const agentRow = document.getElementById('llm-agent-row');
  if (agentRow) agentRow.style.display = (llmType === 'vllm' && llmUseTools) ? '' : 'none';
}

function llmToggleAgentMode() {
  const el = document.getElementById('llm-agent-mode');
  llmAgentMode = el && el.checked;
  localStorage.setItem('llmAgentMode', llmAgentMode ? 'true' : 'false');
}

/* ── Phase 7.2: 대화 요약 ─────────────────────────────────
   sess.messages 길이가 임계값을 넘으면 오래된 절반을 LLM에 요약 요청 → sess.summary 보관.
   요약된 만큼 sess.summarizedUpTo (메시지 인덱스, exclusive)를 진전시킨다.
   전송 시: messages는 sess.messages.slice(summarizedUpTo, -1), systemPrompt에 요약 prepend. */
const LLM_MAX_HISTORY = 20;
const LLM_SUMMARY_KEEP = 10; // 요약 후 유지할 최근 메시지 수

async function llmEnsureSummary(sess, model) {
  if (!sess) return;
  const total = sess.messages.length;
  const upTo = sess.summarizedUpTo || 0;
  // 활성 메시지 수 (요약되지 않은) 가 임계를 넘으면 요약
  if (total - upTo <= LLM_MAX_HISTORY) return;

  // 요약 대상: [upTo, total - LLM_SUMMARY_KEEP)  ← 마지막 placeholder는 호출 시점에 아직 없음 (caller 호출 순서 주의)
  const targetEnd = Math.max(upTo + 1, total - LLM_SUMMARY_KEEP);
  if (targetEnd <= upTo) return;

  const oldMsgs = sess.messages.slice(upTo, targetEnd)
    .filter(m => m && m.content && (m.role === 'user' || m.role === 'assistant'))
    .map(m => ({ role: m.role, content: m.content }));
  if (oldMsgs.length < 2) return;

  // 기존 summary가 있다면 함께 넘겨 누적 요약
  const merged = sess.summary
    ? [{ role: 'system', content: '[이전 요약]\n' + sess.summary }, ...oldMsgs]
    : oldMsgs;

  try {
    const r = await api('POST', '/api/ollama/summarize', { model, messages: merged });
    if (r && r.success && r.summary) {
      sess.summary = r.summary;
      sess.summarizedUpTo = targetEnd;
      llmSaveSessions();
    }
  } catch (e) {
    // 요약 실패는 무시 (다음 턴에 재시도)
    console.warn('summarize failed:', e);
  }
}

// ── 메시지 전송 (스트리밍) ─────────────────────────────
async function llmSend() {
  const input = document.getElementById('llm-input');
  const text = input.value.trim();
  if (!text || llmIsStreaming) return;

  let sess = llmGetActiveSession();
  if (!sess) {
    sess = llmCreateSession();
  }

  const model = document.getElementById('llm-model-select').value;
  if (!model) {
    alert('모델을 선택하세요.');
    return;
  }

  const systemPrompt = document.getElementById('llm-system-prompt').value.trim();

  // 첫 번째 메시지 시 세션 제목 자동 갱신
  if (sess.messages.length === 0 && sess.title === '새 대화') {
    sess.title = text.slice(0, 30) + (text.length > 30 ? '…' : '');
  }

  sess.messages.push({ role: 'user', content: text });
  input.value = '';
  input.style.height = 'auto';
  llmSaveSessions();
  llmRenderMessages();
  llmRenderSessionList();

  sess.model = model;
  sess.systemPrompt = systemPrompt;

  // Phase 7.2: 임계 초과 시 오래된 메시지 요약 (assistant placeholder 추가 전에 실행)
  await llmEnsureSummary(sess, model);

  const assistantMsg = { role: 'assistant', content: '', toolCalls: [] };
  sess.messages.push(assistantMsg);

  llmIsStreaming = true;
  llmUpdateButtons();
  llmRenderMessages();

  const messagesEl = document.getElementById('llm-messages');
  const typingDiv = document.createElement('div');
  typingDiv.className = 'llm-msg assistant';
  typingDiv.id = 'llm-streaming-msg';
  typingDiv.innerHTML = `
    <div class="llm-msg-avatar">AI</div>
    <div class="llm-msg-bubble">
      <div class="llm-typing"><span></span><span></span><span></span></div>
    </div>
  `;
  messagesEl.appendChild(typingDiv);
  messagesEl.scrollTop = messagesEl.scrollHeight;

  llmAbortController = new AbortController();

  try {
    const prefix = llmType === 'vllm' ? '/api/ollama/vllm' : '/api/ollama';
    const upTo = sess.summarizedUpTo || 0;
    const activeMsgs = sess.messages.slice(upTo, -1);
    let effectiveSystemPrompt = systemPrompt || '';
    if (sess.summary) {
      const prefixBlock = `[이전 대화 요약 — 사용자에게 보이지 않음]\n${sess.summary}`;
      effectiveSystemPrompt = effectiveSystemPrompt
        ? `${prefixBlock}\n\n${effectiveSystemPrompt}`
        : prefixBlock;
    }
    const requestBody = {
      model,
      messages: activeMsgs,
      systemPrompt: effectiveSystemPrompt || undefined
    };
    if (llmType === 'vllm' && llmUseTools && llmMcpTools.length > 0) {
      requestBody.tools = llmMcpTools;
      if (llmAgentMode) requestBody.agentMode = true;
    }
    const res = await fetch(`${prefix}/chat/stream`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody),
      signal: llmAbortController.signal
    });

    if (!res.ok) {
      throw new Error(`HTTP ${res.status}`);
    }

    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    let streamDone = false;

    while (true) {
      if (streamDone) break;
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });

      const parts = buffer.split('\n\n');
      buffer = parts.pop() || '';

      for (const part of parts) {
        if (streamDone) break;
        const lines = part.split('\n');
        let eventType = 'message';
        let eventData = '';
        for (const line of lines) {
          if (line.startsWith('event: ')) {
            eventType = line.slice(7).trim();
          } else if (line.startsWith('data: ')) {
            eventData = line.slice(6);
          }
        }

        if (eventType === 'error') {
          throw new Error(eventData || '스트리밍 오류');
        }
        if (eventType === 'done') {
          streamDone = true;
          break;
        }

        if (eventType === 'tool_start') {
          try {
            const t = JSON.parse(eventData);
            llmAppendToolCard(t.id, t.name, t.args);
            assistantMsg.toolCalls.push({
              id: t.id, name: t.name, args: t.args,
              ok: null, preview: null, length: null, payload: null
            });
          } catch {}
          continue;
        }

        if (eventType === 'tool_extending') {
          try {
            const t = JSON.parse(eventData);
            llmMarkToolCardExtending(t.id, t.softTimeoutSec, t.extendedTimeoutSec);
            const tc = assistantMsg.toolCalls.find(x => x.id === t.id);
            if (tc) {
              tc.extended = true;
              tc.softTimeoutSec = t.softTimeoutSec;
              tc.extendedTimeoutSec = t.extendedTimeoutSec;
            }
          } catch {}
          continue;
        }

        if (eventType === 'tool_result') {
          try {
            const t = JSON.parse(eventData);
            llmUpdateToolCard(t.id, t.name, t.ok, t.preview, t.length, t.payload);
            const tc = assistantMsg.toolCalls.find(x => x.id === t.id);
            if (tc) {
              tc.ok = t.ok;
              tc.preview = t.preview;
              tc.length = t.length;
              tc.payload = t.payload;
            } else {
              assistantMsg.toolCalls.push({
                id: t.id, name: t.name, args: null,
                ok: t.ok, preview: t.preview, length: t.length, payload: t.payload
              });
            }
          } catch {}
          continue;
        }

        // 일반 message 이벤트
        if (eventData && eventData !== '{}') {
          try {
            const json = JSON.parse(eventData);
            if (json.message && json.message.content) {
              assistantMsg.content += json.message.content;
              llmUpdateStreamingMessage(assistantMsg.content);
            } else if (json.response) {
              assistantMsg.content += json.response;
              llmUpdateStreamingMessage(assistantMsg.content);
            }
          } catch {
          }
        }
      }
    }

    sess.updatedAt = new Date().toISOString();
    llmSaveSessions();
    llmRenderMessages();
    llmRenderSessionList();

  } catch (e) {
    if (e.name === 'AbortError') {
      if (assistantMsg.content || (assistantMsg.toolCalls && assistantMsg.toolCalls.length > 0)) {
        sess.updatedAt = new Date().toISOString();
        llmSaveSessions();
        llmRenderMessages();
      } else {
        sess.messages.pop();
        llmSaveSessions();
        llmRenderMessages();
      }
    } else {
      assistantMsg.content = `❌ 오류: ${e.message}`;
      sess.messages.pop();
      sess.messages.push(assistantMsg);
      sess.updatedAt = new Date().toISOString();
      llmSaveSessions();
      llmRenderMessages();
    }
  } finally {
    llmIsStreaming = false;
    llmAbortController = null;
    llmUpdateButtons();
  }
}

function llmUpdateStreamingMessage(content) {
  let msgEl = document.getElementById('llm-streaming-msg');
  if (!msgEl) {
    const messagesEl = document.getElementById('llm-messages');
    msgEl = document.createElement('div');
    msgEl.className = 'llm-msg assistant';
    msgEl.id = 'llm-streaming-msg';
    msgEl.innerHTML = `
      <div class="llm-msg-avatar">AI</div>
      <div class="llm-msg-bubble"></div>
    `;
    messagesEl.appendChild(msgEl);
  }

  const bubble = msgEl.querySelector('.llm-msg-bubble');
  if (bubble) {
    bubble.innerHTML = llmFormatMessage(content);
  }

  const messagesEl = document.getElementById('llm-messages');
  if (messagesEl) {
    messagesEl.scrollTop = messagesEl.scrollHeight;
  }
}

function llmStop() {
  if (llmAbortController) {
    llmAbortController.abort();
  }
}

function llmUpdateButtons() {
  const sendBtn = document.getElementById('llm-send-btn');
  const stopBtn = document.getElementById('llm-stop-btn');
  if (sendBtn) sendBtn.disabled = llmIsStreaming;
  if (stopBtn) stopBtn.style.display = llmIsStreaming ? 'inline-flex' : 'none';
}

// ── 입력 키 처리 ───────────────────────────────────────
function llmInputKeydown(e) {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    llmSend();
  }
  const ta = e.target;
  setTimeout(() => {
    ta.style.height = 'auto';
    ta.style.height = Math.min(ta.scrollHeight, 150) + 'px';
  }, 0);
}

// ── 세션 초기화 ────────────────────────────────────────
function llmClearSession() {
  const sess = llmGetActiveSession();
  if (!sess) return;
  if (!confirm('현재 대화의 메시지를 모두 지우시겠습니까?')) return;
  sess.messages = [];
  sess.updatedAt = new Date().toISOString();
  llmSaveSessions();
  llmRenderMessages();
  llmRenderSessionList();
}

// ── 설정 ───────────────────────────────────────────────
function llmToggleSettings() {
  const panel = document.getElementById('llm-settings-panel');
  if (panel) {
    const opening = panel.classList.contains('hidden');
    panel.classList.toggle('hidden');
    if (opening) llmLoadConfigToUI();
  }
}

async function llmSaveConfig() {
  const host = document.getElementById('llm-host').value.trim() || 'localhost';
  const port = parseInt(document.getElementById('llm-port').value) || 11434;
  try {
    const d = await api('POST', '/api/ollama/config', { host, port });
    if (d.success) {
      alert('설정 저장 완료. 변경 사항 적용을 위해 페이지를 새로고침하세요.');
    } else {
      alert('설정 저장 실패: ' + (d.error || '알 수 없는 오류'));
    }
  } catch (e) {
    alert('설정 저장 실패: ' + e.message);
  }
}

async function llmTestConnection() {
  const prefix = llmType === 'vllm' ? '/api/ollama/vllm' : '/api/ollama';
  const label = llmType === 'vllm' ? 'vLLM' : 'Ollama';
  try {
    const d = await api('GET', `${prefix}/ping`);
    const dot = document.getElementById('llm-conn-status');
    if (dot) {
      dot.className = d.success ? 'llm-conn-dot connected' : 'llm-conn-dot error';
      dot.title = d.success ? `${label} 연결됨` : `${label} 연결 실패: ${d.error || ''}`;
    }
    alert(d.success ? `${label} 연결 성공!` : `${label} 연결 실패: ${d.error || ''}`);
  } catch (e) {
    alert(`${label} 연결 테스트 실패: ` + e.message);
  }
}

function llmLoadConfigToUI() {
  api('GET', '/api/ollama/config').then(d => {
    if (d.success) {
      const hostEl = document.getElementById('llm-host');
      const portEl = document.getElementById('llm-port');
      if (hostEl && d.host) hostEl.value = d.host;
      if (portEl && d.port) portEl.value = d.port;
    }
  }).catch(() => {});

  api('GET', '/api/llm/config').then(d => {
    if (d.success && d.vllmModel) {
      const sel = document.getElementById('llm-model-select');
      if (!sel) return;
      // llm-model.json 값이 드롭다운 옵션에 없으면 직접 추가
      if (![...sel.options].some(o => o.value === d.vllmModel)) {
        const opt = document.createElement('option');
        opt.value = d.vllmModel;
        opt.textContent = d.vllmModel;
        sel.appendChild(opt);
      }
      sel.value = d.vllmModel;
    }
  }).catch(() => {});
}

function llmLoadActiveSession() {
  if (llmActiveSessionId && !llmSessions.find(s => s.id === llmActiveSessionId)) {
    llmActiveSessionId = '';
    localStorage.setItem('llmActiveSessionId', '');
  }
}

// ── 전체 내보내기 ──────────────────────────────────────
function llmExportAll() {
  if (llmSessions.length === 0) {
    alert('내보낼 대화가 없습니다.');
    return;
  }
  const text = llmSessions.map(s => {
    const header = `=== ${s.title} (${new Date(s.updatedAt).toLocaleString('ko-KR')}) ===`;
    const msgs = s.messages.map(m => `[${m.role}] ${m.content}`).join('\n\n');
    return `${header}\n\n${msgs}`;
  }).join('\n\n' + '='.repeat(50) + '\n\n');

  const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `llm-chat-${new Date().toISOString().slice(0, 10)}.txt`;
  a.click();
  URL.revokeObjectURL(url);
}
