/**
 * Flynn Chat Page
 *
 * Session selector, message input, streaming tool events,
 * markdown-rendered responses, slash commands, and web search.
 */

/* global hljs */
import { renderSafeMarkdown } from '../lib/markdown.js';

let _currentSession = null;
let _sending = false;
let _cancelling = false;
let _searchMode = false;
let _slashPopupIndex = -1;
let _elements = {};
let _pendingAttachments = [];
let _sessionSort = 'recent';

// ── Slash Command Definitions ───────────────────────────────

const SLASH_COMMANDS = [
  { name: '/help',    desc: 'Show available commands' },
  { name: '/reset',   desc: 'Reset session' },
  { name: '/compact', desc: 'Compact context' },
  { name: '/usage',   desc: 'Show token usage' },
  { name: '/status',  desc: 'Show system health' },
  { name: '/model',   desc: 'Show current model' },
  { name: '/stop',   desc: 'Stop active response' },
  { name: '/cancel',   desc: 'Alias for /stop' },
  { name: '/approvals', desc: 'List pending guarded actions' },
  { name: '/approve', desc: 'Approve latest or by id' },
  { name: '/deny', desc: 'Deny latest or by id' },
];

// ── Helpers ─────────────────────────────────────────────────

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

function highlightCode() {
  if (typeof hljs !== 'undefined') {
    document.querySelectorAll('#chat-messages pre code').forEach(block => {
      hljs.highlightElement(block);
    });
  }
}

function formatMessageTimestamp(timestamp) {
  const date = new Date(typeof timestamp === 'number' ? timestamp : Date.now());
  if (Number.isNaN(date.getTime())) {
    return '';
  }

  const now = new Date();
  const isSameDay = now.getFullYear() === date.getFullYear()
    && now.getMonth() === date.getMonth()
    && now.getDate() === date.getDate();

  const timeText = new Intl.DateTimeFormat(undefined, {
    hour: 'numeric',
    minute: '2-digit',
  }).format(date);

  if (isSameDay) {
    return timeText;
  }

  const dateText = new Intl.DateTimeFormat(undefined, {
    month: 'short',
    day: 'numeric',
  }).format(date);
  return `${dateText}, ${timeText}`;
}

function createTimestampEl(role, timestamp) {
  const ts = document.createElement('div');
  const alignClass = role === 'user' ? 'self-end text-right' : 'self-start text-left';
  ts.className = `px-1 text-[11px] leading-none text-zinc-600 select-none ${alignClass}`;
  ts.textContent = formatMessageTimestamp(timestamp);
  return ts;
}

function formatSessionLastActive(timestamp) {
  if (typeof timestamp !== 'number' || !Number.isFinite(timestamp)) {
    return 'no activity';
  }

  const date = new Date(timestamp);
  if (Number.isNaN(date.getTime())) {
    return 'no activity';
  }

  const now = new Date();
  const sameDay = now.getFullYear() === date.getFullYear()
    && now.getMonth() === date.getMonth()
    && now.getDate() === date.getDate();

  if (sameDay) {
    return `today ${new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' }).format(date)}`;
  }

  return new Intl.DateTimeFormat(undefined, {
    month: 'short',
    day: 'numeric',
    hour: 'numeric',
    minute: '2-digit',
  }).format(date);
}

function sortSessions(sessions, mode) {
  const copy = [...sessions];
  if (mode === 'messages') {
    copy.sort((a, b) => {
      const byMessages = (b.messageCount ?? 0) - (a.messageCount ?? 0);
      if (byMessages !== 0) {return byMessages;}
      return String(a.id ?? '').localeCompare(String(b.id ?? ''));
    });
    return copy;
  }

  if (mode === 'name') {
    copy.sort((a, b) => String(a.id ?? '').localeCompare(String(b.id ?? '')));
    return copy;
  }

  // Default: most recent activity first, then message count, then name.
  copy.sort((a, b) => {
    const aTs = typeof a.lastMessageAt === 'number' ? a.lastMessageAt : 0;
    const bTs = typeof b.lastMessageAt === 'number' ? b.lastMessageAt : 0;
    const byTime = bTs - aTs;
    if (byTime !== 0) {return byTime;}
    const byMessages = (b.messageCount ?? 0) - (a.messageCount ?? 0);
    if (byMessages !== 0) {return byMessages;}
    return String(a.id ?? '').localeCompare(String(b.id ?? ''));
  });
  return copy;
}

function createMessageEl(role, content, timestamp = Date.now()) {
  const wrapper = document.createElement('div');

  const roleClasses = {
    user: 'flex flex-col gap-1.5 max-w-[85%] md:max-w-[75%] self-end group',
    assistant: 'flex flex-col gap-1.5 max-w-[85%] md:max-w-[75%] self-start group',
    system: 'flex flex-col gap-1.5 self-stretch max-w-full group',
    error: 'flex flex-col gap-1.5 max-w-[85%] md:max-w-[75%] self-start group',
  };
  wrapper.className = roleClasses[role] || roleClasses.assistant;

  const div = document.createElement('div');

  const messageClasses = {
    user: 'rounded-lg px-3.5 py-2.5 text-sm leading-relaxed break-words whitespace-pre-wrap bg-blue-500/15 border border-blue-500/25 text-zinc-50',
    assistant: 'rounded-lg px-3.5 py-2.5 text-sm leading-relaxed break-words whitespace-pre-wrap bg-zinc-900 border border-zinc-800 text-zinc-50',
    system: 'rounded-lg px-3.5 py-2.5 leading-relaxed break-words whitespace-pre-wrap bg-zinc-800 text-zinc-400 border border-zinc-700 text-xs',
    error: 'rounded-lg px-3.5 py-2.5 text-sm leading-relaxed break-words whitespace-pre-wrap bg-red-500/15 border border-red-500/30 text-zinc-50',
  };
  div.className = messageClasses[role] || messageClasses.assistant;
  div.dataset.messageBody = 'true';
  // Keep role as a data attribute for querySelector compatibility
  div.dataset.role = role;

  if (role === 'assistant' || role === 'system') {
    div.innerHTML = renderSafeMarkdown(content);
    setTimeout(highlightCode, 0);
  } else {
    div.textContent = content;
  }
  wrapper.appendChild(div);
  wrapper.appendChild(createTimestampEl(role, timestamp));

  // Add action buttons (copy for all, edit for user) outside the message box
  if (role !== 'system') {
    wrapper.appendChild(createMessageActions(role));
  }

  return wrapper;
}

function createToolEventEl(event, data) {
  const group = document.createElement('div');
  group.className = 'border border-zinc-800 rounded-md my-1 overflow-hidden';

  const header = document.createElement('div');
  header.className = 'flex items-center gap-2 px-3 py-1.5 bg-zinc-800 cursor-pointer text-xs text-zinc-500 hover:text-zinc-400 select-none';

  if (event === 'tool_start') {
    header.innerHTML = `<span class="spinner"></span> <strong>${escapeHtml(data.tool)}</strong>`;
  } else if (event === 'tool_end') {
    const icon = data.result?.success ? '&#10003;' : '&#10007;';
    const cls = data.result?.success ? 'text-green-500' : 'text-red-500';
    header.innerHTML = `<span class="${cls}">${icon}</span> <strong>${escapeHtml(data.tool)}</strong>`;
  }

  header.addEventListener('click', () => {
    body.classList.toggle('open');
  });

  const body = document.createElement('div');
  body.className = 'tool-event-body px-3 py-2 text-xs text-zinc-400 bg-zinc-900 whitespace-pre-wrap break-all max-h-48 overflow-y-auto font-mono';

  if (event === 'tool_start' && data.args) {
    body.textContent = JSON.stringify(data.args, null, 2);
  } else if (event === 'tool_end' && data.result) {
    body.textContent = data.result.output || data.result.error || '(no output)';
  }

  group.appendChild(header);
  group.appendChild(body);
  return group;
}

function scrollToBottom() {
  const msgs = _elements.messages;
  if (msgs) {
    msgs.scrollTop = msgs.scrollHeight;
  }
}

function isSupportedImageMime(mimeType) {
  return mimeType === 'image/jpeg'
    || mimeType === 'image/png'
    || mimeType === 'image/gif'
    || mimeType === 'image/webp';
}

function readFileAsBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onerror = () => reject(new Error('Failed to read file'));
    reader.onload = () => {
      const result = String(reader.result || '');
      // result is like: data:<mime>;base64,<payload>
      const comma = result.indexOf(',');
      if (comma === -1) {
        reject(new Error('Unexpected file encoding'));
        return;
      }
      resolve(result.slice(comma + 1));
    };
    reader.readAsDataURL(file);
  });
}

function renderPendingAttachments() {
  const el = _elements.attachments;
  if (!el) {return;}

  el.innerHTML = '';
  if (!_pendingAttachments.length) {
    el.classList.add('hidden');
    return;
  }

  el.classList.remove('hidden');
  for (let i = 0; i < _pendingAttachments.length; i++) {
    const att = _pendingAttachments[i];
    const chip = document.createElement('div');
    chip.className = 'inline-flex items-center gap-2 px-2.5 py-1 rounded-full bg-zinc-900 border border-zinc-800 text-zinc-400 text-xs';

    const name = document.createElement('span');
    name.className = 'max-w-[220px] overflow-hidden text-ellipsis whitespace-nowrap';
    name.textContent = att.filename || 'attachment';

    const rm = document.createElement('button');
    rm.className = 'text-zinc-500 hover:text-zinc-50 cursor-pointer text-base leading-none appearance-none border-0 bg-transparent';
    rm.type = 'button';
    rm.title = 'Remove attachment';
    rm.textContent = '\u00d7';
    rm.addEventListener('click', () => {
      _pendingAttachments.splice(i, 1);
      renderPendingAttachments();
    });

    chip.appendChild(name);
    chip.appendChild(rm);
    el.appendChild(chip);
  }
}

function clearPendingAttachments() {
  _pendingAttachments = [];
  if (_elements.fileInput) {
    _elements.fileInput.value = '';
  }
  renderPendingAttachments();
}

// ── Message Action Buttons ──────────────────────────────────

function getMessageText(el) {
  const messageBody = el?.dataset?.messageBody === 'true'
    ? el
    : el?.closest?.('.group')?.querySelector?.('[data-message-body="true"]');
  if (!messageBody) {
    return '';
  }
  // For user messages, textContent is the raw text.
  // For assistant/system messages rendered as markdown, extract plain text.
  return (messageBody.textContent || '').trim();
}

function createMessageActions(role) {
  const bar = document.createElement('div');
  bar.className = 'flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity';

  // Copy button — all messages
  const copyBtn = document.createElement('button');
  copyBtn.className = 'inline-flex items-center justify-center w-6 h-6 rounded text-zinc-500 hover:text-zinc-50 hover:bg-zinc-800 transition-colors';
  copyBtn.title = 'Copy';
  copyBtn.innerHTML = COPY_ICON;
  copyBtn.addEventListener('click', () => {
    const msgEl = bar.closest('.group')?.querySelector('[data-message-body="true"]');
    if (!msgEl) {return;}
    const text = getMessageText(msgEl);
    navigator.clipboard.writeText(text).then(() => {
      copyBtn.innerHTML = CHECK_ICON;
      copyBtn.classList.add('text-green-500');
      setTimeout(() => {
        copyBtn.innerHTML = COPY_ICON;
        copyBtn.classList.remove('text-green-500');
      }, 1500);
    });
  });
  bar.appendChild(copyBtn);

  // Edit button — user messages only
  if (role === 'user') {
    const editBtn = document.createElement('button');
    editBtn.className = 'inline-flex items-center justify-center w-6 h-6 rounded text-zinc-500 hover:text-zinc-50 hover:bg-zinc-800 transition-colors';
    editBtn.title = 'Edit';
    editBtn.innerHTML = EDIT_ICON;
    editBtn.addEventListener('click', () => {
      const msgEl = bar.closest('.group')?.querySelector('[data-message-body="true"]');
      if (!msgEl) {return;}
      const text = getMessageText(msgEl);
      const input = _elements.input;
      if (input) {
        input.value = text;
        input.focus();
        // Trigger auto-resize
        input.style.height = 'auto';
        input.style.height = Math.min(input.scrollHeight, 150) + 'px';
      }
    });
    bar.appendChild(editBtn);
  }

  return bar;
}

// ── Search Mode ─────────────────────────────────────────────

function setSearchMode(active) {
  _searchMode = active;
  const btn = _elements.searchBtn;
  const input = _elements.input;
  if (!btn || !input) {return;}

  if (active) {
    btn.classList.add('active');
    input.placeholder = 'What do you want to search for?';
  } else {
    btn.classList.remove('active');
    input.placeholder = 'Type a message...';
  }
  input.focus();
}

// ── Slash Popup ─────────────────────────────────────────────

function getFilteredCommands(text) {
  const prefix = text.toLowerCase();
  return SLASH_COMMANDS.filter(c => c.name.startsWith(prefix));
}

function showSlashPopup(filtered) {
  const popup = _elements.slashPopup;
  if (!popup) {return;}

  popup.innerHTML = '';
  if (filtered.length === 0) {
    hideSlashPopup();
    return;
  }

  for (let i = 0; i < filtered.length; i++) {
    const item = document.createElement('div');
    item.className = 'flex items-baseline gap-3 px-3 py-2 cursor-pointer transition-colors' + (i === _slashPopupIndex ? ' bg-zinc-800' : '');
    item.innerHTML = `<span class="text-blue-500 font-semibold text-sm min-w-[80px]">${escapeHtml(filtered[i].name)}</span><span class="text-zinc-500 text-xs">${escapeHtml(filtered[i].desc)}</span>`;
    item.addEventListener('click', () => {
      selectSlashCommand(filtered[i].name);
    });
    // Touch-friendly: handle pointerenter for hover on mobile
    item.addEventListener('pointerenter', () => {
      _slashPopupIndex = i;
      updatePopupSelection(filtered);
    });
    popup.appendChild(item);
  }
  popup.classList.remove('hidden');
}

function hideSlashPopup() {
  const popup = _elements.slashPopup;
  if (popup) {popup.classList.add('hidden');}
  _slashPopupIndex = -1;
}

function updatePopupSelection(_filtered) {
  const popup = _elements.slashPopup;
  if (!popup) {return;}
  const items = popup.children;
  for (let i = 0; i < items.length; i++) {
    if (i === _slashPopupIndex) {
      items[i].classList.add('bg-zinc-800');
    } else {
      items[i].classList.remove('bg-zinc-800');
    }
  }
}

function selectSlashCommand(name) {
  const input = _elements.input;
  if (!input) {return;}
  input.value = name;
  hideSlashPopup();
  input.focus();
}

function handleSlashPopupInput() {
  const input = _elements.input;
  if (!input) {return;}
  const text = input.value;

  // Show popup only when text starts with / and is at most a single word (the command itself)
  if (text.startsWith('/') && !text.includes(' ')) {
    const filtered = getFilteredCommands(text);
    // Clamp selection index
    if (_slashPopupIndex >= filtered.length) {_slashPopupIndex = filtered.length - 1;}
    showSlashPopup(filtered);
  } else {
    hideSlashPopup();
  }
}

// ── Slash Command Handlers ──────────────────────────────────

function parseSlashCommand(text) {
  const trimmed = text.trim();
  if (!trimmed.startsWith('/')) {return null;}

  const parts = trimmed.split(/\s+/);
  const cmd = parts[0].toLowerCase();
  const args = parts.slice(1).join(' ');

  switch (cmd) {
    case '/help':    return { type: 'help' };
    case '/reset':   return { type: 'reset' };
    case '/compact': return { type: 'compact' };
    case '/usage':   return { type: 'usage' };
    case '/status':  return { type: 'status' };
    case '/model':   return { type: 'model', args };
    case '/stop': return { type: 'stop' };
    case '/cancel': return { type: 'cancel' };
    case '/approvals': return { type: 'approvals' };
    case '/approve': return { type: 'approve', args };
    case '/deny': return { type: 'deny', args };
    default:         return null;
  }
}

function showSystemMessage(content) {
  _elements.messages.appendChild(createMessageEl('system', content, Date.now()));
  scrollToBottom();
}

async function executeAgentSlashCommand(client, command, commandArgs = '') {
  const normalizedArgs = (commandArgs ?? '').trim();
  const message = normalizedArgs ? `/${command} ${normalizedArgs}` : `/${command}`;
  const metadata = {
    isCommand: true,
    command,
    ...(normalizedArgs ? { commandArgs: normalizedArgs } : {}),
  };

  const stream = client.stream('agent.send', { message, metadata });
  const done = await stream.result;
  return done?.content ?? done?.text ?? '';
}

async function handleSlashCommand(cmd, client) {
  switch (cmd.type) {
    case 'help': {
      const lines = [
        '**Available Commands**',
        '',
        '| Command | Description |',
        '|---------|-------------|',
        '| `/help` | Show this help |',
        '| `/reset` | Reset the current session |',
        '| `/compact` | Ask the agent to compact context |',
        '| `/usage` | Show token usage stats |',
        '| `/status` | Show system health |',
        '| `/model [tier|provider]` | Show or set model tier/provider |',
        '| `/stop` | Stop active response |',
        '| `/cancel` | Alias for `/stop` |',
        '| `/approvals` | List pending guarded actions |',
        '| `/approve [id]` | Approve latest pending or specific id |',
        '| `/deny [id] [reason]` | Deny latest pending or specific id |',
        '',
        'Type `/` to see autocomplete suggestions.',
      ];
      showSystemMessage(lines.join('\n'));
      return true;
    }

    case 'reset': {
      try {
        const result = await executeAgentSlashCommand(client, 'reset');
        _elements.messages.innerHTML = '';
        showSystemMessage(result || 'Session reset.');
      } catch (err) {
        showSystemMessage(`Failed to reset: ${err.message}`);
      }
      return true;
    }

    case 'compact': {
      try {
        const result = await executeAgentSlashCommand(client, 'compact');
        showSystemMessage(result || 'Compaction requested.');
      } catch (err) {
        showSystemMessage(`Failed to compact: ${err.message}`);
      }
      return true;
    }

    case 'usage': {
      try {
        const result = await executeAgentSlashCommand(client, 'usage');
        showSystemMessage(result || 'No usage data available.');
      } catch (err) {
        showSystemMessage(`Failed to fetch usage: ${err.message}`);
      }
      return true;
    }

    case 'status': {
      try {
        const result = await executeAgentSlashCommand(client, 'status');
        showSystemMessage(result || 'Status unavailable.');
      } catch (err) {
        showSystemMessage(`Failed to fetch status: ${err.message}`);
      }
      return true;
    }

    case 'model': {
      try {
        const result = await executeAgentSlashCommand(client, 'model', cmd.args ?? '');
        showSystemMessage(result || 'Model info unavailable.');
      } catch (err) {
        showSystemMessage(`Failed to fetch model info: ${err.message}`);
      }
      return true;
    }
    case 'stop':
    case 'cancel': {
      try {
        const result = await executeAgentSlashCommand(client, 'stop');
        showSystemMessage(result || 'Cancellation requested.');
      } catch (err) {
        showSystemMessage(`Failed to stop: ${err.message}`);
      }
      return true;
    }

    case 'approvals': {
      try {
        const result = await executeAgentSlashCommand(client, 'approvals');
        showSystemMessage(result || 'No pending approvals.');
      } catch (err) {
        showSystemMessage(`Failed to list approvals: ${err.message}`);
      }
      return true;
    }

    case 'approve': {
      try {
        const result = await executeAgentSlashCommand(client, 'approve', cmd.args ?? '');
        showSystemMessage(result || 'Approved.');
      } catch (err) {
        showSystemMessage(`Failed to approve: ${err.message}`);
      }
      return true;
    }

    case 'deny': {
      try {
        const result = await executeAgentSlashCommand(client, 'deny', cmd.args ?? '');
        showSystemMessage(result || 'Denied.');
      } catch (err) {
        showSystemMessage(`Failed to deny: ${err.message}`);
      }
      return true;
    }

    default:
      return false;
  }
}

// ── Session Management ──────────────────────────────────────

async function loadSessions(client) {
  const select = _elements.sessionSelect;
  if (!select) {return;}

  try {
    const result = await client.call('sessions.list');
    const sessions = sortSessions(result.sessions ?? [], _sessionSort);

    // Preserve current selection
    const current = _currentSession;
    select.innerHTML = '';

    if (sessions.length === 0) {
      const opt = document.createElement('option');
      opt.value = '';
      opt.textContent = 'No sessions';
      select.appendChild(opt);
    } else {
      for (const s of sessions) {
        const opt = document.createElement('option');
        opt.value = s.id;
        const msgCount = s.messageCount ?? 0;
        const msgLabel = msgCount === 1 ? '1 msg' : `${msgCount} msgs`;
        opt.textContent = `${s.id} · ${msgLabel} · ${formatSessionLastActive(s.lastMessageAt)}`;
        if (s.id === current) {opt.selected = true;}
        select.appendChild(opt);
      }
    }

    // Update current session
    _currentSession = select.value || null;
  } catch {
    // Ignore — sessions may not be available
  }
}

async function loadHistory(client) {
  const msgs = _elements.messages;
  if (!msgs || !_currentSession) {return;}

  msgs.innerHTML = '';

  try {
    const result = await client.call('sessions.history', { sessionId: _currentSession });
    const messages = result.messages ?? [];

    for (const msg of messages) {
      const role = msg.role ?? 'assistant';
      const content = msg.content ?? msg.text ?? '';
      msgs.appendChild(createMessageEl(role, content, msg.timestamp ?? Date.now()));
    }

    scrollToBottom();
  } catch {
    msgs.innerHTML = '<div class="text-center py-12 px-6 text-zinc-500 text-sm">Could not load history</div>';
  }
}

// ── Send Message ────────────────────────────────────────────

async function sendMessage(client, overrideText) {
  const input = _elements.input;
  const rawText = (overrideText ?? input?.value ?? '').trim();
  const hasText = Boolean(rawText);
  const hasAttachments = Boolean(_pendingAttachments.length > 0);
  if ((!hasText && !hasAttachments) || _sending) {return;}

  // Check for slash commands first (text only)
  if (hasText) {
    const cmd = parseSlashCommand(rawText);
    if (cmd) {
      if (!overrideText) {input.value = '';}
      hideSlashPopup();
      const handled = await handleSlashCommand(cmd, client);
      if (handled) {return;}
      // If not fully handled (e.g. /compact), fall through to send as message
    }
  }

  _sending = true;
  _cancelling = false;
  updateSendButton();
  if (!overrideText && input) {input.value = '';}

  // Apply search mode prefix
  let messageText = rawText;
  if (_searchMode && hasText && !rawText.startsWith('/')) {
    messageText = `Search the web for: ${rawText}`;
    setSearchMode(false);
  }

  const userDisplay = hasText
    ? rawText
    : `Sent ${_pendingAttachments.length} attachment(s)`;
  _elements.messages.appendChild(createMessageEl('user', userDisplay, Date.now()));
  scrollToBottom();

  // Create placeholder for assistant response
  const placeholder = document.createElement('div');
  placeholder.className = 'rounded-lg px-3.5 py-2.5 text-sm leading-relaxed break-words whitespace-pre-wrap bg-zinc-900 border border-zinc-800 text-zinc-50 streaming-cursor';
  placeholder.innerHTML = '<span class="text-zinc-500">Thinking...</span>';
  _elements.messages.appendChild(placeholder);
  scrollToBottom();

  try {
    const stream = client.stream('agent.send', { message: messageText, attachments: _pendingAttachments });

    stream.on('tool_start', (data) => {
      const el = createToolEventEl('tool_start', data);
      _elements.messages.insertBefore(el, placeholder);
      scrollToBottom();
    });

    stream.on('tool_end', (data) => {
      // Replace the last tool_start spinner with completion marker
      const events = _elements.messages.querySelectorAll('.border.border-zinc-800.rounded-md');
      const last = events[events.length - 1];
      if (last) {
        const header = last.firstElementChild;
        if (header && data.tool) {
          const icon = data.result?.success !== false ? '&#10003;' : '&#10007;';
          const cls = data.result?.success !== false ? 'text-green-500' : 'text-red-500';
          header.innerHTML = `<span class="${cls}">${icon}</span> <strong>${escapeHtml(data.tool)}</strong>`;
        }
        // Add result body
        const body = last.lastElementChild;
        if (body && data.result) {
          body.textContent = data.result.output || data.result.error || '(no output)';
        }
      }
      scrollToBottom();
    });

    stream.on('context_warning', (data) => {
      const note = document.createElement('div');
      note.className = 'rounded-lg px-3.5 py-2.5 text-sm leading-relaxed break-words whitespace-pre-wrap bg-zinc-900 border border-zinc-800 text-zinc-50';
      const text = data?.message || 'Context usage is getting high.';
      note.innerHTML = renderSafeMarkdown(`> ${text}`);
      _elements.messages.insertBefore(note, placeholder);
      scrollToBottom();
    });

    const done = await stream.result;
    const content = done?.content ?? done?.text ?? '(no response)';
    const assistantMessage = createMessageEl('assistant', content, Date.now());
    placeholder.replaceWith(assistantMessage);
  } catch (err) {
    const errorMessage = createMessageEl('error', `Error: ${err.message}`, Date.now());
    placeholder.replaceWith(errorMessage);
  } finally {
    _sending = false;
    _cancelling = false;
    updateSendButton();
    clearPendingAttachments();
    scrollToBottom();
  }
}

function updateSendButton() {
  if (!_elements.sendBtn) {
    return;
  }
  if (_sending) {
    _elements.sendBtn.disabled = _cancelling;
    _elements.sendBtn.textContent = _cancelling ? 'Stopping...' : 'Stop';
    return;
  }
  _elements.sendBtn.disabled = false;
  _elements.sendBtn.textContent = 'Send';
}

async function cancelActiveRun(client) {
  if (!_sending || _cancelling) {
    return;
  }
  _cancelling = true;
  updateSendButton();
  try {
    await client.call('agent.cancel', {});
  } catch (err) {
    showSystemMessage(`Cancel failed: ${err.message}`);
    _cancelling = false;
    updateSendButton();
  }
}

// ── Search SVG Icon ─────────────────────────────────────────

const SEARCH_ICON = '<svg class="w-3.5 h-3.5 fill-current shrink-0" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M8.5 3a5.5 5.5 0 0 1 4.38 8.82l4.15 4.15a.75.75 0 0 1-1.06 1.06l-4.15-4.15A5.5 5.5 0 1 1 8.5 3zm0 1.5a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" fill="currentColor"/></svg>';
const ATTACH_ICON = '<svg class="w-3.5 h-3.5 fill-current shrink-0" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M7.5 4.75A3.75 3.75 0 0 1 11.25 1h.5A3.25 3.25 0 0 1 15 4.25V12a4.5 4.5 0 0 1-9 0V6.75a2.75 2.75 0 0 1 5.5 0V12a1.25 1.25 0 0 1-2.5 0V6.75a.75.75 0 0 0-1.5 0V12a2.75 2.75 0 0 0 5.5 0V6.75A4.25 4.25 0 0 0 5 6.75V12a6 6 0 0 0 12 0V4.25A4.75 4.75 0 0 0 11.75-.5h-.5A5.25 5.25 0 0 0 6 4.75v8a.75.75 0 0 0 1.5 0z" fill="currentColor"/></svg>';
const COPY_ICON = '<svg class="w-3.5 h-3.5 fill-current" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z" fill="currentColor"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" fill="currentColor"/></svg>';
const CHECK_ICON = '<svg class="w-3.5 h-3.5 fill-current" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" fill="currentColor"/></svg>';
const EDIT_ICON = '<svg class="w-3.5 h-3.5 fill-current" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm1.414 1.06a.25.25 0 0 0-.354 0L3.463 11.1l-.47 1.64 1.64-.47 8.61-8.61a.25.25 0 0 0 0-.354Z" fill="currentColor"/></svg>';

// ── Page Export ──────────────────────────────────────────────

export const ChatPage = {
  async render(el, client) {
    el.innerHTML = `
      <div class="flex flex-col h-[calc(100vh-6rem)] md:h-[calc(100vh-3rem)] max-w-3xl">
        <div class="flex items-center gap-3 pb-3 border-b border-zinc-800 mb-3 flex-wrap">
          <select id="chat-session-select" class="bg-zinc-900 text-zinc-50 border border-zinc-800 rounded-lg px-3 py-1.5 text-sm outline-none focus:border-blue-500"></select>
          <select id="chat-session-sort" class="bg-zinc-900 text-zinc-50 border border-zinc-800 rounded-lg px-3 py-1.5 text-sm outline-none focus:border-blue-500" title="Sort sessions">
            <option value="recent">Sort: Recent</option>
            <option value="messages">Sort: Most messages</option>
            <option value="name">Sort: Name</option>
          </select>
          <button id="chat-new-session" class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors">+ New</button>
          <button id="chat-load-history" class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors">History</button>
        </div>
        <div class="flex-1 overflow-y-auto flex flex-col gap-3 py-3" id="chat-messages"></div>
        <div class="flex items-center gap-2 py-2 flex-wrap">
          <button id="chat-search" class="btn-action inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-zinc-800 text-zinc-400 border border-zinc-700 hover:text-zinc-50 hover:border-zinc-600 transition-colors cursor-pointer select-none" title="Search the web">
            ${SEARCH_ICON}
            <span>Search</span>
          </button>
          <button id="chat-attach" class="btn-action inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-zinc-800 text-zinc-400 border border-zinc-700 hover:text-zinc-50 hover:border-zinc-600 transition-colors cursor-pointer select-none" title="Attach image">
            ${ATTACH_ICON}
            <span>Attach</span>
          </button>
          <input id="chat-file" type="file" accept="image/png,image/jpeg,image/gif,image/webp" multiple class="hidden" />
          <div id="chat-attachments" class="inline-flex items-center gap-1.5 flex-wrap hidden"></div>
        </div>
        <div class="relative">
          <div id="slash-popup" class="absolute bottom-full left-0 right-0 mb-1 bg-zinc-900 border border-zinc-800 rounded-lg max-h-60 overflow-y-auto z-50 shadow-lg shadow-black/30 hidden"></div>
          <div class="flex gap-2 pt-3 border-t border-zinc-800">
            <textarea id="chat-input" class="flex-1 bg-zinc-900 text-zinc-50 border border-zinc-800 rounded-lg px-3 py-2.5 text-sm outline-none resize-none min-h-[42px] max-h-[150px] leading-relaxed focus:border-blue-500 transition-colors" placeholder="Type a message..." rows="1"></textarea>
            <button id="chat-send" class="px-4 py-2.5 bg-blue-500 text-zinc-950 font-semibold text-sm rounded-lg hover:opacity-85 disabled:opacity-40 disabled:cursor-not-allowed transition-opacity self-end">Send</button>
          </div>
        </div>
      </div>
    `;

    _elements = {
      sessionSelect: el.querySelector('#chat-session-select'),
      sessionSort: el.querySelector('#chat-session-sort'),
      messages: el.querySelector('#chat-messages'),
      input: el.querySelector('#chat-input'),
      sendBtn: el.querySelector('#chat-send'),
      searchBtn: el.querySelector('#chat-search'),
      attachBtn: el.querySelector('#chat-attach'),
      fileInput: el.querySelector('#chat-file'),
      attachments: el.querySelector('#chat-attachments'),
      slashPopup: el.querySelector('#slash-popup'),
    };

    // Load sessions into dropdown
    await loadSessions(client);

    // Event: session change
    _elements.sessionSelect.addEventListener('change', () => {
      _currentSession = _elements.sessionSelect.value || null;
    });

    _elements.sessionSort.addEventListener('change', () => {
      _sessionSort = _elements.sessionSort.value || 'recent';
      loadSessions(client);
    });

    // Event: new session
    el.querySelector('#chat-new-session').addEventListener('click', async () => {
      try {
        const result = await client.call('sessions.create');
        _currentSession = result.sessionId;
        await loadSessions(client);
        _elements.messages.innerHTML = '';
      } catch (err) {
        _elements.messages.innerHTML = `<div class="text-center py-12 px-6 text-zinc-500 text-sm">Failed to create session: ${err.message}</div>`;
      }
    });

    // Event: load history
    el.querySelector('#chat-load-history').addEventListener('click', () => {
      loadHistory(client);
    });

    // Event: search button toggle
    _elements.searchBtn.addEventListener('click', () => {
      setSearchMode(!_searchMode);
    });

    // Event: attach button
    _elements.attachBtn.addEventListener('click', () => {
      if (_elements.fileInput) {_elements.fileInput.click();}
    });

    // Event: file input change
    _elements.fileInput.addEventListener('change', async () => {
      const files = Array.from(_elements.fileInput.files ?? []);
      if (files.length === 0) {return;}

      const MAX_BYTES = 5 * 1024 * 1024;
      for (const file of files) {
        if (!isSupportedImageMime(file.type)) {
          showSystemMessage(`Unsupported file type: ${file.type || file.name}`);
          continue;
        }
        if (file.size > MAX_BYTES) {
          showSystemMessage(`File too large (max 5MB): ${file.name}`);
          continue;
        }

        try {
          const data = await readFileAsBase64(file);
          _pendingAttachments.push({ mimeType: file.type, data, filename: file.name });
        } catch (err) {
          showSystemMessage(`Failed to attach ${file.name}: ${err.message}`);
        }
      }

      renderPendingAttachments();
    });

    // Event: send message
    _elements.sendBtn.addEventListener('click', () => {
      if (_sending) {
        void cancelActiveRun(client);
        return;
      }
      void sendMessage(client);
    });

    // Event: keyboard in textarea
    _elements.input.addEventListener('keydown', (e) => {
      const popup = _elements.slashPopup;
      const isPopupVisible = popup && !popup.classList.contains('hidden');

      // Handle slash popup navigation
      if (isPopupVisible) {
        const text = _elements.input.value;
        const filtered = getFilteredCommands(text);

        if (e.key === 'ArrowDown') {
          e.preventDefault();
          _slashPopupIndex = Math.min(_slashPopupIndex + 1, filtered.length - 1);
          updatePopupSelection(filtered);
          return;
        }
        if (e.key === 'ArrowUp') {
          e.preventDefault();
          _slashPopupIndex = Math.max(_slashPopupIndex - 1, 0);
          updatePopupSelection(filtered);
          return;
        }
        if ((e.key === 'Enter' || e.key === 'Tab') && _slashPopupIndex >= 0 && _slashPopupIndex < filtered.length) {
          e.preventDefault();
          selectSlashCommand(filtered[_slashPopupIndex].name);
          return;
        }
        if (e.key === 'Escape') {
          e.preventDefault();
          hideSlashPopup();
          return;
        }
      }

      // Enter to send (Shift+Enter for newline)
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        hideSlashPopup();
        if (_sending) {
          void cancelActiveRun(client);
          return;
        }
        void sendMessage(client);
      }
    });

    // Event: input changes for slash popup + auto-resize
    _elements.input.addEventListener('input', () => {
      // Auto-resize textarea
      const ta = _elements.input;
      ta.style.height = 'auto';
      ta.style.height = Math.min(ta.scrollHeight, 150) + 'px';

      // Slash command popup
      handleSlashPopupInput();
    });

    // Dismiss slash popup on outside click
    el.addEventListener('click', (e) => {
      if (!e.target.closest('.relative')) {
        hideSlashPopup();
      }
    });

    // If there's a current session, show welcome
    if (!_currentSession) {
      _elements.messages.innerHTML = '<div class="text-center py-12 px-6 text-zinc-500 text-sm">Select a session or create a new one to start chatting</div>';
    }
    updateSendButton();
  },

  teardown() {
    _currentSession = null;
    _sending = false;
    _cancelling = false;
    _searchMode = false;
    _slashPopupIndex = -1;
    _sessionSort = 'recent';
    _elements = {};
    _pendingAttachments = [];
  },
};
