/**
 * Home23 Dashboard Chat
 *
 * Native chat tile connecting to the agent loop via the bridge endpoint.
 * Renders in tile, overlay, or standalone mode.
 *
 * Conversation/streaming state is mirrored to chatState (home23-chat-state.mjs)
 * so newer view components (menu, overlay, standalone sidebar) can subscribe to
 * changes without reaching into this file's internals. The `let` variables
 * below remain the working copy; call _syncState() after mutating them.
 */

import { chatState } from './home23-chat-state.mjs';

// Expose for classic-script consumers (home23-dashboard.js, home23-chat.html
// onload handler) and for in-browser debugging.
if (typeof window !== 'undefined') {
  window.chatState = chatState;
}

const CHAT_API = '/home23/api/chat';
const CHAT_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
let chatAgent = null;
let chatAgents = [];
let chatModels = {};
let chatModel = null;
let chatStreaming = false;
let activeTurnId = null;
let activeChatId = null;
let activeCursor = -1;
let activeEventSource = null;
// UI render state per turn — reset when a turn starts, reused by both live streaming and reconnect.
let currentTurnCtx = null;
let chatConversationId = null;  // current conversation ID
let chatConversations = [];     // list of all conversations

// ── Image attachment state ──
let pendingAttachments = []; // Array<{ id, file, dataUrl }>

const ATTACH_MAX_IMAGES = 6;
const ATTACH_MAX_BYTES = 10 * 1024 * 1024;
const ATTACH_ALLOWED_MIME = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);

function readFileAsDataURL(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = () => reject(reader.error);
    reader.readAsDataURL(file);
  });
}

function dataUrlToBase64(dataUrl) {
  const i = dataUrl.indexOf(',');
  return i >= 0 ? dataUrl.slice(i + 1) : dataUrl;
}

async function ingestAttachmentFiles(files) {
  for (const file of files) {
    if (pendingAttachments.length >= ATTACH_MAX_IMAGES) {
      console.warn('[chat] attachment cap reached, dropping', file.name);
      break;
    }
    if (!ATTACH_ALLOWED_MIME.has(file.type)) {
      console.warn('[chat] unsupported mime, dropping', file.type, file.name);
      continue;
    }
    if (file.size > ATTACH_MAX_BYTES) {
      console.warn('[chat] image too big, dropping', file.size, file.name);
      continue;
    }
    try {
      const dataUrl = await readFileAsDataURL(file);
      pendingAttachments.push({
        id: `att-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
        file,
        dataUrl,
      });
    } catch (err) {
      console.warn('[chat] failed to read attachment', err);
    }
  }
  renderAttachmentTray();
}

function removeAttachment(id) {
  pendingAttachments = pendingAttachments.filter(a => a.id !== id);
  renderAttachmentTray();
}

function clearAttachments() {
  pendingAttachments = [];
  renderAttachmentTray();
}

function renderAttachmentTray() {
  const tray = document.getElementById('chat-attach-tray');
  if (!tray) return;
  if (pendingAttachments.length === 0) {
    tray.hidden = true;
    tray.innerHTML = '';
    return;
  }
  tray.hidden = false;
  tray.innerHTML = pendingAttachments.map(a => `
    <div class="h23-chat-attach-thumb">
      <img src="${a.dataUrl}" alt="${a.file.name || 'attachment'}" />
      <button class="h23-chat-attach-thumb-remove" data-att-id="${a.id}" aria-label="Remove">&times;</button>
    </div>
  `).join('');
  tray.querySelectorAll('.h23-chat-attach-thumb-remove').forEach(btn => {
    btn.addEventListener('click', () => removeAttachment(btn.dataset.attId));
  });
}
let chatPersistTimer = null;
let chatPersistenceBound = false;
let chatCurrentAgentName = null;

/** Push the local `let` vars into chatState. Call after any mutation block. */
function _syncState() {
  chatState.set({
    agent: chatAgent,
    model: chatModel,
    provider: chatAgent?.provider ?? null,
    conversationId: chatConversationId,
    conversations: chatConversations,
    streaming: chatStreaming,
    activeTurnId,
    activeCursor,
    turnCtx: currentTurnCtx,
  });
}

// ── Init ──

async function initChat(mode) {
  // Load agents
  try {
    const res = await fetch('/home23/api/settings/agents');
    const data = await res.json();
    chatAgents = data.agents || [];
    chatCurrentAgentName = data.currentAgent || null;
  } catch { chatAgents = []; }

  // Load models catalog
  try {
    const res = await fetch('/home23/api/settings/models');
    const data = await res.json();
    chatModels = data.providers || {};
  } catch { chatModels = {}; }

  // Determine initial agent (URL param or this dashboard's agent)
  const urlParams = new URLSearchParams(window.location.search);
  const urlAgent = urlParams.get('agent');
  const initialAgent = (urlAgent && chatAgents.find(a => a.name === urlAgent))
    || (chatCurrentAgentName && chatAgents.find(a => a.name === chatCurrentAgentName))
    || chatAgents.find(a => a.isPrimary)
    || chatAgents[0];

  if (!initialAgent) {
    const empty = document.getElementById('chat-messages');
    if (empty) empty.innerHTML = '<div class="h23-chat-empty">No agents configured. Create one in Settings.</div>';
    return;
  }

  bindChatPersistence();

  // Mount the shared message-list + input subtree into the active slot. This
  // is the single DOM we move between tile and overlay via appendChild —
  // moving the same nodes preserves event bindings and mid-stream state
  // across expand/collapse.
  mountSharedChatNodes();

  renderAgentSelectors(initialAgent.name);
  await switchAgent(initialAgent.name, { preferRestore: true });

  // Model selector — use agent's current model (may have been changed in settings)
  chatModel = initialAgent.model;
  populateModelSelect(initialAgent.provider, initialAgent.model);

  // Input bindings — one input now (shared). 'overlay' binding removed; the
  // same #chat-input serves both modes since we move the node between slots.
  bindInput('chat-input', 'chat-send-btn', '');

  // Expand / close / standalone
  const expandBtn = document.getElementById('chat-expand-btn');
  if (expandBtn) expandBtn.addEventListener('click', openOverlay);
  const closeBtn = document.getElementById('chat-overlay-close-btn');
  if (closeBtn) closeBtn.addEventListener('click', closeOverlay);

  // Standalone button moved to ⋯ menu ("Open in new tab"); kept the
  // handler in handleMenuAction. No separate toolbar button needed.

  // Backdrop click (on overlay wrapper but outside the panel) → close.
  const overlay = document.getElementById('chat-overlay');
  overlay?.addEventListener('click', (e) => {
    if (e.target === overlay) closeOverlay();
  });
  // Esc key to close overlay.
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && overlay?.classList.contains('open')) closeOverlay();
  });

  // ⋯ menu + agent pill + model pill (replaces the old + / ☰ / model-select
  // button row).
  setupMoreMenu();
  setupAgentPill();
  setupModelPill();

  // Keep tile's agent pill + overlay title in sync with state. Subscribe AND
  // apply the current snapshot immediately — switchAgent() may have already
  // fired 'agent:switch' before we got here, and without an initial paint
  // the header reads "Loading…" forever.
  const renderAgentHeader = (snap) => {
    const nameEl = document.getElementById('chat-agent-name');
    const avatarEl = document.getElementById('chat-agent-avatar');
    const title = snap.agent?.displayName || snap.agent?.agentName || snap.agent?.name || '…';
    if (nameEl) nameEl.textContent = title;
    if (avatarEl) avatarEl.textContent = String(title).trim().slice(0, 1).toUpperCase() || '●';
    const overlayTitle = document.getElementById('chat-overlay-title-label');
    if (overlayTitle) overlayTitle.textContent = `Talk to ${title}`;
  };
  chatState.on('agent:switch', renderAgentHeader);
  renderAgentHeader(chatState.get());
}

/** Wire the ⋯ more-menu — click button toggles the popover near it; clicking
 *  outside or pressing Esc closes; menu item clicks dispatch to handlers. */
function setupMoreMenu() {
  const menu = document.getElementById('chat-more-menu');
  if (!menu) return;

  function openAt(anchor) {
    if (!anchor) return;
    const rect = anchor.getBoundingClientRect();
    menu.style.position = 'fixed';
    menu.style.top = `${rect.bottom + 4}px`;
    menu.style.right = `${Math.max(8, window.innerWidth - rect.right)}px`;
    menu.hidden = false;
    anchor.setAttribute('aria-expanded', 'true');
  }
  function closeAll() {
    menu.hidden = true;
    for (const id of ['chat-more-btn', 'chat-overlay-more-btn']) {
      const b = document.getElementById(id);
      if (b) b.setAttribute('aria-expanded', 'false');
    }
  }

  for (const id of ['chat-more-btn', 'chat-overlay-more-btn']) {
    const btn = document.getElementById(id);
    if (!btn) continue;
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      if (menu.hidden) openAt(btn); else closeAll();
    });
  }

  menu.addEventListener('click', (e) => {
    const item = e.target.closest('[data-action]');
    if (!item) return;
    closeAll();
    handleMenuAction(item.dataset.action);
  });

  document.addEventListener('click', (e) => {
    if (menu.hidden) return;
    if (menu.contains(e.target)) return;
    if (e.target.closest('#chat-more-btn, #chat-overlay-more-btn')) return;
    closeAll();
  });
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && !menu.hidden) closeAll();
  });
}

function handleMenuAction(action) {
  if (action === 'new-conversation') {
    newConversation();
  } else if (action === 'toggle-conversations') {
    toggleConversationList();
  } else if (action === 'open-standalone') {
    cacheHistory();
    window.open(`/home23/chat?agent=${chatAgent?.agentName || ''}`, '_blank');
  }
}

/**
 * Reusable list popover. Renders `items` as clickable rows anchored near
 * `anchor`; calls onSelect(item) when a row is clicked. Closes on outside
 * click or Esc. One popover instance lives at body level and is reused.
 */
function openListPopover({ anchor, items, onSelect, emptyText = 'Nothing to show' }) {
  if (!anchor) return;
  let pop = document.getElementById('chat-list-popover');
  if (!pop) {
    pop = document.createElement('div');
    pop.id = 'chat-list-popover';
    pop.className = 'h23-chat-menu h23-chat-list-popover';   // reuses menu styling
    pop.setAttribute('role', 'menu');
    document.body.appendChild(pop);
  } else {
    pop.className = 'h23-chat-menu h23-chat-list-popover';
  }
  if (!items || items.length === 0) {
    pop.innerHTML = `<div style="padding:10px 12px;color:var(--text-muted);font-size:13px;font-style:italic;">${escapeHtml(emptyText)}</div>`;
  } else {
    pop.innerHTML = items.map((it, i) => `
      <button class="h23-chat-menu-item" data-i="${i}" role="menuitem" type="button">
        ${it.label ? escapeHtml(it.label) : escapeHtml(String(it))}${
          it.hint ? `<span style="color:var(--text-muted);font-size:11px;margin-left:8px;">${escapeHtml(it.hint)}</span>` : ''
        }${it.active ? ' <span style="color:var(--accent-color,#0a84ff);margin-left:6px;">●</span>' : ''}
      </button>
    `).join('');
  }
  const rect = anchor.getBoundingClientRect();
  const viewportGap = 8;
  const menuGap = 4;
  const maxMenuHeight = 420;
  const belowSpace = Math.max(0, window.innerHeight - rect.bottom - viewportGap - menuGap);
  const aboveSpace = Math.max(0, rect.top - viewportGap - menuGap);
  const placeAbove = belowSpace < 180 && aboveSpace > belowSpace;
  const availableSpace = placeAbove ? aboveSpace : belowSpace;
  const constrainedHeight = Math.max(140, Math.min(maxMenuHeight, availableSpace || maxMenuHeight));

  pop.style.position = 'fixed';
  pop.style.maxHeight = `${constrainedHeight}px`;
  pop.style.overflowY = 'auto';
  pop.style.overscrollBehavior = 'contain';
  pop.style.left = `${Math.max(8, Math.min(rect.left, window.innerWidth - 260))}px`;
  pop.style.right = '';
  pop.hidden = false;
  const renderedHeight = Math.min(pop.scrollHeight, constrainedHeight);
  pop.style.top = placeAbove
    ? `${Math.max(viewportGap, rect.top - renderedHeight - menuGap)}px`
    : `${Math.min(rect.bottom + menuGap, window.innerHeight - renderedHeight - viewportGap)}px`;

  const close = () => {
    pop.hidden = true;
    document.removeEventListener('click', onDocClick, true);
    document.removeEventListener('keydown', onKey, true);
  };
  const onDocClick = (e) => {
    if (pop.contains(e.target) || anchor.contains(e.target)) return;
    close();
  };
  const onKey = (e) => { if (e.key === 'Escape') close(); };
  // Defer attaching doc listeners to avoid the current click event immediately closing.
  setTimeout(() => {
    document.addEventListener('click', onDocClick, true);
    document.addEventListener('keydown', onKey, true);
  }, 0);

  pop.onclick = (e) => {
    const btn = e.target.closest('[data-i]');
    if (!btn) return;
    const i = Number(btn.dataset.i);
    close();
    if (typeof onSelect === 'function') onSelect(items[i]);
  };
}

function showModelPicker(anchorOverride) {
  const select = document.getElementById('chat-model-select');
  if (!select) return;
  const current = select.value || chatModel || '';
  const items = Array.from(select.options)
    .map(o => o.value)
    .filter(Boolean)
    .map(v => ({ label: v, active: v === current }));
  // Prefer the model pill (tile) as anchor; fall back to the ⋯ button.
  const anchor = anchorOverride
    || document.getElementById('chat-model-pill')
    || document.getElementById('chat-more-btn')
    || document.getElementById('chat-overlay-more-btn');
  openListPopover({
    anchor,
    items,
    emptyText: 'No models available for this provider.',
    onSelect: (it) => {
      select.value = it.label;
      select.dispatchEvent(new Event('change', { bubbles: true }));
    },
  });
}

/** Model pill: compact button next to the agent pill that shows the current
 *  model and opens the model picker on click. Label syncs to chatState.model
 *  changes so Settings-driven changes also reflect here. */
function setupModelPill() {
  const pill = document.getElementById('chat-model-pill');
  if (!pill) return;
  pill.addEventListener('click', () => showModelPicker(pill));

  const renderLabel = (snap) => {
    const label = document.getElementById('chat-model-pill-label');
    if (!label) return;
    const raw = snap.model || chatModel || '';
    label.textContent = shortenModelName(raw) || 'model';
    pill.title = raw ? `Model: ${raw} — click to change` : 'Change model';
  };
  chatState.on('change', renderLabel);
  renderLabel(chatState.get());
}

/** Compact display name for the pill. Strips common provider prefixes and
 *  truncates date-stamped suffixes so "claude-opus-4-7-20260101" fits. */
function shortenModelName(model) {
  if (!model) return '';
  const trimmed = String(model).trim();
  // Drop trailing -YYYYMMDD date suffix (common on Claude model IDs).
  const withoutDate = trimmed.replace(/-\d{8}$/, '');
  // Cap at 22 chars with ellipsis — the pill has limited width.
  return withoutDate.length > 22 ? withoutDate.slice(0, 21) + '…' : withoutDate;
}

/** Clicking the agent pill opens an inline list popover of agents. */
function setupAgentPill() {
  const pill = document.getElementById('chat-agent-pill');
  if (!pill) return;
  pill.addEventListener('click', () => {
    if (!Array.isArray(chatAgents) || chatAgents.length === 0) return;
    const currentName = chatAgent?.agentName;
    const items = chatAgents.map(a => ({
      label: a.displayName || a.name,
      hint: a.isPrimary ? 'primary' : '',
      active: a.name === currentName,
      _name: a.name,
    }));
    openListPopover({
      anchor: pill,
      items,
      emptyText: 'No agents configured.',
      onSelect: (it) => {
        if (it?._name && it._name !== currentName) switchAgent(it._name, { preferRestore: false });
      },
    });
  });
}

function bindInput(inputId, btnId, source) {
  const input = document.getElementById(inputId);
  const btn = document.getElementById(btnId);
  if (input) {
    input.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        sendMessage(source);
      }
    });
    input.addEventListener('input', () => {
      input.style.height = 'auto';
      input.style.height = Math.min(input.scrollHeight, 120) + 'px';
    });
    input.addEventListener('paste', (e) => {
      const items = Array.from(e.clipboardData?.items || []);
      const files = items
        .filter(it => it.kind === 'file' && it.type.startsWith('image/'))
        .map(it => it.getAsFile())
        .filter(f => !!f);
      if (files.length > 0) {
        e.preventDefault();
        ingestAttachmentFiles(files);
      }
    });
  }
  if (btn) btn.addEventListener('click', () => sendMessage(source));

  // Image attachments — file picker
  const attachBtn = document.getElementById('chat-attach-btn');
  const attachInput = document.getElementById('chat-attach-input');
  if (attachBtn && attachInput && !attachBtn.dataset.bound) {
    attachBtn.addEventListener('click', () => attachInput.click());
    attachInput.addEventListener('change', () => {
      const files = Array.from(attachInput.files || []);
      ingestAttachmentFiles(files);
      attachInput.value = '';
    });
    attachBtn.dataset.bound = 'true';
  }

  // Drag and drop onto the input area
  const inputArea = document.getElementById('chat-input-area') || document.querySelector('.sh-input-bar');
  const dropOverlay = document.getElementById('chat-drop-overlay');
  if (inputArea && dropOverlay && !inputArea.dataset.dropBound) {
    let dragDepth = 0;
    inputArea.addEventListener('dragenter', (e) => {
      if (!Array.from(e.dataTransfer?.types || []).includes('Files')) return;
      dragDepth++;
      dropOverlay.hidden = false;
    });
    inputArea.addEventListener('dragover', (e) => { e.preventDefault(); });
    inputArea.addEventListener('dragleave', () => {
      dragDepth = Math.max(0, dragDepth - 1);
      if (dragDepth === 0) dropOverlay.hidden = true;
    });
    inputArea.addEventListener('drop', (e) => {
      e.preventDefault();
      dragDepth = 0;
      dropOverlay.hidden = true;
      const files = Array.from(e.dataTransfer?.files || []).filter(f => f.type.startsWith('image/'));
      if (files.length > 0) ingestAttachmentFiles(files);
    });
    // Suppress browser default of opening dropped images outside the input area
    window.addEventListener('dragover', (e) => e.preventDefault());
    window.addEventListener('drop', (e) => {
      if (!inputArea.contains(e.target)) e.preventDefault();
    });
    inputArea.dataset.dropBound = 'true';
  }
}

function renderAgentSelectors(selectedName) {
  const options = chatAgents.map(agent =>
    `<option value="${agent.name}" ${agent.name === selectedName ? 'selected' : ''}>${agent.displayName || agent.name}${agent.isPrimary ? ' (primary)' : ''}</option>`
  ).join('');

  document.querySelectorAll('.h23-chat-agent-select').forEach(select => {
    select.innerHTML = options;
    select.value = selectedName;
    select.title = select.selectedOptions[0]?.textContent || '';
    if (!select.dataset.bound) {
      select.addEventListener('change', () => switchAgent(select.value));
      select.dataset.bound = 'true';
    }
  });
}

function populateModelSelect(provider, currentModel) {
  // Collect all models across providers
  const allModels = [];
  for (const [provName, cfg] of Object.entries(chatModels)) {
    for (const m of (cfg.defaultModels || [])) {
      allModels.push({ provider: provName, model: m });
    }
  }

  const options = allModels.map(modelEntry =>
    `<option value="${modelEntry.model}" data-provider="${modelEntry.provider}" title="${modelEntry.model}" ${modelEntry.model === currentModel ? 'selected' : ''}>${formatModelLabel(modelEntry.model)}</option>`
  ).join('');

  document.querySelectorAll('.h23-chat-model-select').forEach(select => {
    select.innerHTML = options;
    if (currentModel) {
      select.value = currentModel;
    }
    select.title = select.selectedOptions[0]?.value || currentModel || '';

    if (!select.dataset.bound) {
      select.addEventListener('change', async () => {
        chatModel = select.value;
        const selectedOpt = select.selectedOptions[0];
        const selectedProvider = selectedOpt?.dataset?.provider || '';
        syncModelSelectors(chatModel);
        // Push into chatState so the model pill label (and any other
        // subscribers) re-render with the new model.
        _syncState();

        if (chatAgent?.agentName) {
          try {
            await fetch(`/home23/api/settings/agents/${chatAgent.agentName}`, {
              method: 'PUT',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ model: chatModel, provider: selectedProvider }),
            });
          } catch (err) {
            console.warn('Failed to persist model change:', err);
          }
        }
      });
      select.dataset.bound = 'true';
    }
  });

  syncModelSelectors(currentModel);
}

function syncModelSelectors(modelName) {
  if (!modelName) return;

  document.querySelectorAll('.h23-chat-model-select').forEach(select => {
    select.value = modelName;
    select.title = select.selectedOptions[0]?.value || modelName;
  });
}

function formatModelLabel(modelName) {
  return modelName.length > 28 ? `${modelName.slice(0, 25)}…` : modelName;
}

// ── Slash Commands ──

const SLASH_COMMANDS = {
  '/new': { description: 'Start a fresh conversation', handler: cmdNew },
  '/clear': { description: 'Clear chat history', handler: cmdNew },
  '/stop': { description: 'Stop the current agent run', handler: () => stopChat() },
  '/help': { description: 'Show available commands', handler: cmdHelp },
};

function handleSlashCommand(text, source) {
  const cmd = text.split(/\s/)[0].toLowerCase();
  const handler = SLASH_COMMANDS[cmd];
  if (!handler) {
    const containerId = 'chat-messages';
    appendError(`Unknown command: ${cmd}. Type /help for available commands.`, containerId);
    return true;
  }
  handler.handler(text, source);
  return true;
}

function cmdNew(text, source) {
  newConversation();
}

function cmdHelp(text, source) {
  const containerId = 'chat-messages';
  const lines = Object.entries(SLASH_COMMANDS)
    .map(([cmd, info]) => `**${cmd}** — ${info.description}`)
    .join('\n');
  appendMessage('assistant', 'Available commands:\n\n' + lines, containerId);
  scrollContainer(containerId);
}

// ── Agent Switching ──

async function switchAgent(name, options = {}) {
  const { preferRestore = false } = options;
  if (chatAgent) cacheHistory();

  try {
    const res = await fetch(`${CHAT_API}/config/${name}`);
    chatAgent = await res.json();
  } catch (err) {
    console.error('Failed to load agent config:', err);
    return;
  }

  renderAgentSelectors(name);
  const overlayTitle = document.getElementById('chat-overlay-title-label');
  if (overlayTitle) {
    overlayTitle.textContent = `Talk to ${chatAgent.displayName || chatAgent.agentName || name}`;
  }

  // Reset model to agent's default
  chatModel = null;
  const agentData = chatAgents.find(a => a.name === name);
  if (agentData) {
    chatModel = agentData.model;
    populateModelSelect(agentData.provider, agentData.model);
  }

  // Load conversation list first — we'll use it to decide whether to resume.
  await loadConversationList(name);
  if (preferRestore && restoreChatState(name)) {
    renderConversationList();
    resetSendButtons();
    return;
  }

  // Prefer resuming the most recent conversation for this agent over silently
  // starting a fresh one. Without this, every agent switch creates an empty
  // chatId that the user has to manually back out of.
  const latest = Array.isArray(chatConversations) && chatConversations.length > 0
    ? chatConversations[0]
    : null;
  if (latest && latest.id) {
    chatConversationId = latest.id;
    await loadHistory(name, latest.id);
    renderConversationList();
  } else {
    newConversation();
    await loadHistory(name);
  }
  _syncState();
}

// ── History ──

async function loadHistory(agentName, conversationId) {
  const container = document.getElementById('chat-messages');
  if (!container) return;

  const convParam = conversationId ? `&conversation=${conversationId}` : '';
  try {
    const res = await fetch(`${CHAT_API}/history/${agentName}?limit=100${convParam}`);
    const data = await res.json();
    container.innerHTML = '';
    if (data.messages && data.messages.length > 0) {
      data.messages.forEach(m => appendMessage(m.role, m.content));
    } else {
      container.innerHTML = '<div class="h23-chat-empty">Start a conversation with your agent.</div>';
    }
    scheduleChatPersist();
    scrollToBottom();
  } catch (err) {
    console.error('Failed to load history:', err);
    container.innerHTML = '<div class="h23-chat-empty">Start a conversation with your agent.</div>';
    scheduleChatPersist();
  }
  await resumePendingTurns();
}

async function resumePendingTurns() {
  if (!chatAgent?.bridgePort || !chatConversationId) return;
  try {
    const bridgeBase = `http://${window.location.hostname}:${chatAgent.bridgePort}`;
    const res = await fetch(`${bridgeBase}/api/chat/pending?chatId=${encodeURIComponent(chatConversationId)}`);
    if (!res.ok) return;
    const data = await res.json();
    const pending = data.pending || [];
    if (pending.length === 0) return;

    const turn = pending[pending.length - 1];

    const containerId = 'chat-messages';
    currentTurnCtx = {
      containerId,
      responseEl: null,
      currentResponse: '',
      thinkingEl: null,
      currentThinking: '',
    };
    activeTurnId = turn.turn_id;
    activeChatId = chatConversationId;
    activeCursor = -1;
    chatStreaming = true;

    // Button swap to Stop while resuming — single shared send button.
    const sendBtn = document.getElementById('chat-send-btn');
    if (sendBtn) { sendBtn.innerHTML = '&#9632;'; sendBtn.disabled = false; sendBtn.onclick = stopChat; sendBtn.title = 'Stop'; sendBtn.style.background = 'var(--accent-red)'; }

    openTurnStream({ bridgeBase, chatId: activeChatId, turnId: turn.turn_id, cursor: -1 });
  } catch (err) {
    console.warn('[chat] pending-turn resume failed', err);
  }
}

// ── Conversations ──

function newConversation() {
  chatConversationId = `dashboard-${chatAgent?.agentName || 'agent'}-${Date.now()}`;
  const container = document.getElementById('chat-messages');
  if (container) container.innerHTML = '<div class="h23-chat-empty">Start a conversation with your agent.</div>';
  // Highlight active in list
  updateConversationListHighlight();
  scheduleChatPersist();
  _syncState();
}

async function loadConversationList(agentName) {
  try {
    const res = await fetch(`${CHAT_API}/conversations/${agentName || chatAgent?.agentName}`);
    const data = await res.json();
    chatConversations = data.conversations || [];
  } catch { chatConversations = []; }
  renderConversationList();
  _syncState();
}

function renderConversationList() {
  const list = document.getElementById('chat-conv-list');
  if (!list) return;

  if (chatConversations.length === 0) {
    list.innerHTML = '<div style="padding:8px 12px;font-size:12px;color:var(--text-muted);font-style:italic;">No previous conversations</div>';
    return;
  }

  list.innerHTML = chatConversations.map(c => {
    const date = new Date(c.lastActivity);
    const timeStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ' ' +
                    date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true });
    const isActive = c.id === chatConversationId;
    const sourceIcon = c.source === 'telegram' ? '&#9992; ' : c.source === 'dashboard' ? '&#128172; ' : '';
    const sourceLabel = c.source && c.source !== 'dashboard' ? `<span style="color:var(--text-muted);font-size:10px;text-transform:uppercase;">${c.source}</span> &middot; ` : '';
    return `
      <div class="h23-chat-conv-item ${isActive ? 'active' : ''}" onclick="openConversation('${c.id}')" title="${c.preview}">
        <div class="h23-chat-conv-preview">${sourceIcon}${escapeHtml(c.preview)}</div>
        <div class="h23-chat-conv-meta">${sourceLabel}${timeStr} &middot; ${c.messageCount} msgs</div>
      </div>
    `;
  }).join('');
}

function updateConversationListHighlight() {
  document.querySelectorAll('.h23-chat-conv-item').forEach(el => el.classList.remove('active'));
  // New conversation won't match any existing item — that's correct
}

async function openConversation(convId) {
  chatConversationId = convId;
  await loadHistory(chatAgent?.agentName, convId);
  renderConversationList();
  scheduleChatPersist();
  _syncState();
}

function toggleConversationList() {
  const panel = document.getElementById('chat-conv-panel');
  if (panel) {
    panel.classList.toggle('open');
    if (panel.classList.contains('open') && chatAgent) {
      loadConversationList(chatAgent.agentName);
    }
  }
}

// ── Send Message ──

async function sendMessage(source) {
  const inputId = 'chat-input';
  const input = document.getElementById(inputId);
  if (!input || !chatAgent) return;

  const text = input.value.trim();
  if ((!text && pendingAttachments.length === 0) || chatStreaming) return;

  // Snapshot attachments for this turn before clearing the tray.
  const turnAttachments = pendingAttachments.slice();
  clearAttachments();

  input.value = '';
  input.style.height = 'auto';
  scheduleChatPersist();

  // Handle slash commands (slash commands cannot carry attachments)
  if (text.startsWith('/')) {
    handleSlashCommand(text, source);
    return;
  }

  const empty = document.querySelector('.h23-chat-empty');
  if (empty) empty.remove();

  // Determine which messages container to use
  const containerId = 'chat-messages';
  appendUserMessage(text, turnAttachments.map(a => a.dataUrl), containerId);
  scrollContainer(containerId);

  chatStreaming = true;
  scheduleChatPersist();
  // Swap send → stop button — single shared button.
  const sendBtn = document.getElementById('chat-send-btn');
  if (sendBtn) { sendBtn.innerHTML = '&#9632;'; sendBtn.disabled = false; sendBtn.onclick = stopChat; sendBtn.title = 'Stop'; sendBtn.style.background = 'var(--accent-red)'; }

  // New turn protocol flow
  currentTurnCtx = {
    containerId,
    responseEl: null,
    currentResponse: '',
    thinkingEl: null,
    currentThinking: '',
  };
  activeChatId = chatConversationId;
  activeCursor = -1;
  _syncState();

  const bridgeBase = `http://${window.location.hostname}:${chatAgent.bridgePort}`;

  let turnId;
  try {
    const imagesPayload = turnAttachments.map(a => ({
      data: dataUrlToBase64(a.dataUrl),
      mimeType: a.file.type,
      fileName: a.file.name,
    }));
    const res = await fetch(`${bridgeBase}/api/chat/turn`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        chatId: activeChatId,
        message: text,
        ...(imagesPayload.length > 0 ? { images: imagesPayload } : {}),
      }),
    });

    if (res.status === 409) {
      const data = await res.json();
      turnId = data.turn_id;
      console.warn('[chat] resuming in-flight turn', turnId);
    } else if (!res.ok) {
      let errBody = '';
      try { errBody = (await res.json()).error || ''; } catch {}
      throw new Error(`turn start failed (${res.status}) ${errBody}`);
    } else {
      const data = await res.json();
      turnId = data.turn_id;
    }
  } catch (err) {
    appendError('Connection failed: ' + err.message, containerId);
    chatStreaming = false;
    resetSendButtons();
    scheduleChatPersist();
    return;
  }

  activeTurnId = turnId;
  openTurnStream({ bridgeBase, chatId: activeChatId, turnId, cursor: -1 });
}

function dispatchLegacyEvent(event, ctx) {
  const { containerId } = ctx;
  if (event.type === 'text' || event.type === 'response_chunk') {
    ctx.thinkingEl = null;
    ctx.currentThinking = '';
    ctx.currentResponse += event.text || event.chunk || '';
    if (!ctx.responseEl) {
      ctx.responseEl = appendMessage('assistant', ctx.currentResponse, containerId);
    } else {
      ctx.responseEl.innerHTML = renderMarkdown(ctx.currentResponse);
      scheduleChatPersist();
    }
    scrollContainer(containerId);
  } else if (event.type === 'thinking') {
    ctx.responseEl = null;
    ctx.currentResponse = '';
    ctx.currentThinking += event.content || event.message || '';
    if (!ctx.thinkingEl) {
      ctx.thinkingEl = appendThinking(ctx.currentThinking, containerId);
    } else {
      ctx.thinkingEl.textContent = ctx.currentThinking;
      scheduleChatPersist();
    }
    scrollContainer(containerId);
  } else if (event.type === 'tool_start') {
    ctx.thinkingEl = null; ctx.currentThinking = '';
    ctx.responseEl = null; ctx.currentResponse = '';
    appendToolCard(event.tool || event.name, event.args, 'running', containerId);
    scrollContainer(containerId);
  } else if (event.type === 'tool_complete' || event.type === 'tool_result') {
    updateToolCard(event.tool || event.name, event.result || event.summary || event.output, true);
    scrollContainer(containerId);
  } else if (event.type === 'media') {
    appendMedia(event.mediaType, event.path, event.caption, containerId);
    scrollContainer(containerId);
  } else if (event.type === 'subagent_result') {
    appendMessage('assistant', `**[Sub-agent]** ${event.task}\n\n${event.result}`, containerId);
    scrollContainer(containerId);
  }
}

function openTurnStream({ bridgeBase, chatId, turnId, cursor }) {
  // Close any existing stream
  if (activeEventSource) { try { activeEventSource.close(); } catch {} activeEventSource = null; }

  const url = `${bridgeBase}/api/chat/stream?chatId=${encodeURIComponent(chatId)}&turn_id=${encodeURIComponent(turnId)}&cursor=${cursor}`;
  const es = new EventSource(url);
  activeEventSource = es;

  es.onmessage = (msg) => {
    if (msg.data === '[DONE]') {
      es.close();
      if (activeEventSource === es) activeEventSource = null;
      finalizeTurn(null);
      return;
    }
    let record;
    try { record = JSON.parse(msg.data); } catch { return; }

    if (record.type === 'event') {
      activeCursor = record.seq;
      if (currentTurnCtx) dispatchLegacyEvent(record.data, currentTurnCtx);
    } else if (record.type === 'turn' && record.status !== 'pending') {
      es.close();
      if (activeEventSource === es) activeEventSource = null;
      finalizeTurn(record);
    }
  };

  es.onerror = () => {
    // Don't auto-reconnect here — visibilitychange handles resume; avoid duplicate streams.
    try { es.close(); } catch {}
    if (activeEventSource === es) activeEventSource = null;
  };
}

function finalizeTurn(finalEnvelope) {
  if (finalEnvelope && finalEnvelope.status === 'error') {
    appendError(finalEnvelope.error || 'Error', (currentTurnCtx && currentTurnCtx.containerId) || 'chat-messages');
  } else if (finalEnvelope && finalEnvelope.status === 'stopped') {
    // quiet — the stop button already reflected the user action
  } else if (finalEnvelope && finalEnvelope.status === 'orphaned') {
    appendError('Previous turn was interrupted — try resending.', (currentTurnCtx && currentTurnCtx.containerId) || 'chat-messages');
  }

  chatStreaming = false;
  activeTurnId = null;
  activeChatId = null;
  activeCursor = -1;
  currentTurnCtx = null;
  resetSendButtons();
  cacheHistory();
  scheduleChatPersist();
  _syncState();
}

function resetSendButtons() {
  const sendBtn = document.getElementById('chat-send-btn');
  if (sendBtn) { sendBtn.innerHTML = '&#9654;'; sendBtn.disabled = false; sendBtn.onclick = () => sendMessage(''); sendBtn.title = 'Send'; sendBtn.style.background = ''; }
}

async function stopChat() {
  if (activeEventSource) { try { activeEventSource.close(); } catch {} activeEventSource = null; }
  if (chatAgent?.bridgePort && activeChatId) {
    try {
      await fetch(`http://${window.location.hostname}:${chatAgent.bridgePort}/api/chat/stop-turn`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ chatId: activeChatId }),
      });
    } catch { /* bridge might be unreachable */ }
  }
  // finalizeTurn will be called when the stream closes with a 'stopped' envelope,
  // but if the stream was already closed, clean up here too.
  if (!activeTurnId) {
    chatStreaming = false;
    resetSendButtons();
    scheduleChatPersist();
    _syncState();
  }
}

// ── DOM Helpers ──

function appendMedia(mediaType, filePath, caption, containerId) {
  const container = document.getElementById(containerId || 'chat-messages');
  if (!container) return;
  const div = document.createElement('div');
  div.className = 'h23-chat-msg assistant';
  if (mediaType === 'image') {
    // Serve image via the dashboard — the path is on the server filesystem
    // Use a data endpoint or serve from tempDir
    div.innerHTML = `
      <div style="margin:4px 0;">
        <img src="/home23/api/media?path=${encodeURIComponent(filePath)}"
             style="max-width:100%;border-radius:8px;border:1px solid var(--glass-border);"
             alt="${escapeHtml(caption || 'Generated image')}"
             onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
        <span style="display:none;color:var(--text-muted);font-size:12px;">Image: ${escapeHtml(filePath)}</span>
      </div>
      ${caption ? `<div style="font-size:12px;color:var(--text-muted);margin-top:4px;">${escapeHtml(caption)}</div>` : ''}
    `;
  } else {
    div.textContent = `[${mediaType}: ${filePath}]${caption ? ' — ' + caption : ''}`;
  }
  container.appendChild(div);
  scheduleChatPersist();
}

function renderMarkdown(text) {
  // Simple markdown: code blocks, inline code, bold, italic, lists, paragraphs
  let html = escapeHtml(text);

  // Code blocks: ```...```
  html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) =>
    `<pre><code>${code.trim()}</code></pre>`
  );

  // Inline code: `...`
  html = html.replace(/`([^`]+)`/g, '<code>$1</code>');

  // Bold: **...**
  html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');

  // Italic: *...*
  html = html.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');

  // Unordered lists: lines starting with - or *
  html = html.replace(/^([- *]) (.+)$/gm, '<li>$2</li>');
  html = html.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');

  // Paragraphs: double newlines
  html = html.replace(/\n\n+/g, '</p><p>');
  html = html.replace(/\n/g, '<br>');
  html = `<p>${html}</p>`;

  // Clean up empty paragraphs
  html = html.replace(/<p>\s*<\/p>/g, '');

  return html;
}

function appendMessage(role, content, containerId) {
  const container = document.getElementById(containerId || 'chat-messages');
  if (!container) return null;
  const div = document.createElement('div');
  div.className = `h23-chat-msg ${role}`;
  if (role === 'assistant') {
    div.innerHTML = renderMarkdown(content);
  } else {
    div.textContent = content;
  }
  container.appendChild(div);
  scheduleChatPersist();
  return div;
}

function appendUserMessage(text, imageDataUrls, containerId) {
  const container = document.getElementById(containerId || 'chat-messages');
  if (!container) return null;
  const div = document.createElement('div');
  div.className = 'h23-chat-msg user';
  if (imageDataUrls && imageDataUrls.length > 0) {
    const wrap = document.createElement('div');
    wrap.className = 'h23-chat-msg-images';
    for (const url of imageDataUrls) {
      const img = document.createElement('img');
      img.src = url;
      img.alt = 'attachment';
      wrap.appendChild(img);
    }
    div.appendChild(wrap);
  }
  if (text) {
    const textNode = document.createElement('div');
    textNode.textContent = text;
    div.appendChild(textNode);
  }
  container.appendChild(div);
  scheduleChatPersist();
  return div;
}

function appendThinking(text, containerId) {
  const container = document.getElementById(containerId || 'chat-messages');
  if (!container) return null;
  const div = document.createElement('div');
  div.className = 'h23-chat-thinking';
  div.textContent = text;
  container.appendChild(div);
  scheduleChatPersist();
  return div;
}

function appendToolCard(name, args, status, containerId) {
  const container = document.getElementById(containerId || 'chat-messages');
  if (!container) return;
  const div = document.createElement('div');
  div.className = 'h23-chat-tool';
  div.dataset.toolName = name;

  let argsPreview = '';
  if (args) {
    try {
      argsPreview = typeof args === 'string' ? args : JSON.stringify(args, null, 2);
      if (argsPreview.length > 1200) argsPreview = argsPreview.slice(0, 1200) + '\n... [input truncated]';
    } catch { argsPreview = String(args); }
  }

  div.innerHTML = `
    <div class="h23-chat-tool-header">
      <span class="h23-chat-tool-name">${escapeHtml(name)}</span>
      <span class="h23-chat-tool-status">${status === 'running' ? 'running...' : 'done'}</span>
    </div>
    ${argsPreview ? `<div class="h23-chat-tool-args">${escapeHtml(argsPreview)}</div>` : ''}
    <div class="h23-chat-tool-result"></div>
  `;
  container.appendChild(div);
  scheduleChatPersist();
}

function updateToolCard(name, result, success) {
  const cards = document.querySelectorAll(`.h23-chat-tool[data-tool-name="${name}"]`);
  const card = cards[cards.length - 1];
  if (!card) return;

  const statusEl = card.querySelector('.h23-chat-tool-status');
  if (statusEl) {
    statusEl.textContent = success ? 'done' : 'error';
    statusEl.className = `h23-chat-tool-status ${success ? 'done' : 'error'}`;
  }

  if (result) {
    const resultEl = card.querySelector('.h23-chat-tool-result');
    if (resultEl) {
      let text = typeof result === 'string' ? result : JSON.stringify(result);
      if (text.length > 4000) text = text.slice(0, 4000) + '\n... [result truncated]';
      resultEl.textContent = text;
    }
  }
  scheduleChatPersist();
}

function appendError(text, containerId) {
  const container = document.getElementById(containerId || 'chat-messages');
  if (!container) return;
  const div = document.createElement('div');
  div.className = 'h23-chat-error';
  div.textContent = text;
  container.appendChild(div);
  scheduleChatPersist();
}

function scrollToBottom() {
  scrollContainer('chat-messages');
}

function scrollContainer(containerId) {
  const container = document.getElementById(containerId || 'chat-messages');
  if (container) container.scrollTop = container.scrollHeight;
}

function escapeHtml(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

function cacheHistory() {
  if (!chatAgent) return;
  const container = getActiveChatContainer();
  if (!container) return;
  const messages = extractMessageHistory(container);

  try {
    localStorage.setItem(`home23:chat:${chatAgent.agentName}`, JSON.stringify(messages.slice(-50)));
    localStorage.setItem(getChatSessionKey(chatAgent.agentName), JSON.stringify({
      agentName: chatAgent.agentName,
      conversationId: chatConversationId,
      html: container.innerHTML,
      streaming: chatStreaming,
      savedAt: Date.now(),
      tileInput: document.getElementById('chat-input')?.value || '',
      // overlayInput removed — single shared input lives in the shared DOM
    }));
  } catch {
    // LocalStorage may be unavailable or full; keep chat functional.
  }
}

// ── Overlay ──

/**
 * Mount the shared message-list + input subtree into the active slot. Called
 * once at init. The shared node is the single source of truth for rendered
 * messages and the live input — on expand/collapse we physically relocate it
 * via appendChild rather than copying innerHTML.
 */
function mountSharedChatNodes() {
  const existing = document.getElementById('chat-shared');
  if (existing) return existing;
  const tpl = document.getElementById('chat-shared-template');
  if (!tpl || !tpl.content) return null;
  // Prefer the standalone slot if on the standalone page; otherwise tile.
  const dest = document.getElementById('chat-slot-standalone')
    || document.getElementById('chat-slot-tile');
  if (!dest) return null;
  const node = tpl.content.firstElementChild.cloneNode(true);
  dest.appendChild(node);
  return node;
}

function openOverlay() {
  const overlay = document.getElementById('chat-overlay');
  const shared = document.getElementById('chat-shared');
  const overlaySlot = document.getElementById('chat-slot-overlay');
  if (!overlay || !shared || !overlaySlot) return;

  // Move (not clone) the shared DOM into the overlay slot. appendChild
  // relocates the existing node, preserving all event bindings and any
  // in-flight streaming handlers pointed at #chat-messages.
  overlaySlot.appendChild(shared);

  overlay.classList.add('open');
  document.getElementById('chat-input')?.focus();
  scrollToBottom();
  scheduleChatPersist();
}

function closeOverlay() {
  const overlay = document.getElementById('chat-overlay');
  const shared = document.getElementById('chat-shared');
  const tileSlot = document.getElementById('chat-slot-tile');
  if (!overlay) return;

  overlay.classList.remove('open');

  // Move shared DOM back to the tile slot (or standalone, whichever exists).
  const dest = tileSlot || document.getElementById('chat-slot-standalone');
  if (shared && dest) dest.appendChild(shared);

  scrollToBottom();
  scheduleChatPersist();
}

function bindChatPersistence() {
  if (chatPersistenceBound) return;

  const persist = () => cacheHistory();
  window.addEventListener('beforeunload', persist);
  window.addEventListener('pagehide', persist);
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') persist();
  });

  chatPersistenceBound = true;
}

function scheduleChatPersist() {
  if (chatPersistTimer) return;
  chatPersistTimer = setTimeout(() => {
    chatPersistTimer = null;
    cacheHistory();
  }, 150);
}

function getChatSessionKey(agentName) {
  return `home23:chat:session:${agentName}`;
}

function getActiveChatContainer() {
  // The shared #chat-messages node moves between slots on expand/collapse;
  // a single getElementById reaches it wherever it currently lives.
  return document.getElementById('chat-messages');
}

function extractMessageHistory(container) {
  const messages = [];
  container.querySelectorAll('.h23-chat-msg').forEach(el => {
    messages.push({
      role: el.classList.contains('user') ? 'user' : 'assistant',
      content: el.textContent,
    });
  });
  return messages;
}

function restoreChatState(agentName) {
  try {
    const raw = localStorage.getItem(getChatSessionKey(agentName));
    if (!raw) return false;

    const saved = JSON.parse(raw);
    if (!saved || saved.agentName !== agentName) return false;
    if (!saved.savedAt || (Date.now() - saved.savedAt) > CHAT_SESSION_TTL_MS) {
      localStorage.removeItem(getChatSessionKey(agentName));
      return false;
    }

    chatConversationId = saved.conversationId || `dashboard-${agentName}-${Date.now()}`;

    const messagesEl = document.getElementById('chat-messages');
    const html = saved.html || '<div class="h23-chat-empty">Start a conversation with your agent.</div>';
    if (messagesEl) messagesEl.innerHTML = html;

    const input = document.getElementById('chat-input');
    if (input) input.value = saved.tileInput || '';

    if (saved.streaming && messagesEl) {
      appendError('Response interrupted by refresh.', 'chat-messages');
    }

    chatStreaming = false;
    scrollToBottom();
    _syncState();
    return true;
  } catch {
    return false;
  }
}

// ── Reconnect on tab visibility ──

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState !== 'visible') return;
  if (!activeTurnId || !activeChatId || activeEventSource) return;
  if (!chatAgent?.bridgePort) return;

  const bridgeBase = `http://${window.location.hostname}:${chatAgent.bridgePort}`;
  console.log('[chat] tab visible — resuming stream from cursor', activeCursor);
  openTurnStream({ bridgeBase, chatId: activeChatId, turnId: activeTurnId, cursor: activeCursor });
});

// Expose classic-script entry points (home23-dashboard.js calls initChat and
// closeOverlay via typeof checks; the standalone HTML calls initChat('standalone')).
// Module scope is not global, so bind explicitly.
if (typeof window !== 'undefined') {
  window.initChat = initChat;
  window.closeOverlay = closeOverlay;
  window.openConversation = openConversation;  // called from inline onclick in conv-list HTML
  window.newConversation = newConversation;
  window.openOverlay = openOverlay;
  window.stopChat = stopChat;
  window.sendMessage = sendMessage;
  window.toggleConversationList = toggleConversationList;
}
