// builder.js
/* global setStatus */

// Builder state management - must be defined before any code uses it
const BUILDER_STATE_KEY = 'builder_state';

function loadBuilderState() {
  try {
    const saved = localStorage.getItem(BUILDER_STATE_KEY);
    if (saved) {
      return JSON.parse(saved);
    }
  } catch (e) {
    console.warn('Failed to load builder state:', e);
  }
  return null;
}

function saveBuilderState(state) {
  try {
    localStorage.setItem(BUILDER_STATE_KEY, JSON.stringify(state));
  } catch (e) {
    console.warn('Failed to save builder state:', e);
  }
}

// Initialize builderState with persisted data
const savedState = loadBuilderState();
const builderState = savedState || {
  mode: 'plan',
  planApproved: false,
  lastUserRequest: '',
  lastPlanText: '',
  pluginPrompt: '',
  subsequentPrompt: '',
  externalTestingEnabled: false
};

// Auto-save builderState changes to localStorage
const builderStateProxy = new Proxy(builderState, {
  set(target, prop, value) {
    target[prop] = value;
    saveBuilderState(target);
    return true;
  }
});

// Replace builderState reference in global scope with proxy
window.builderState = builderStateProxy;

// Function to clear builder state (for new sessions, logout, etc.)
window.clearBuilderState = function() {
  const preservedPluginPrompt = builderState.pluginPrompt || '';
  const preservedSubsequentPrompt = builderState.subsequentPrompt || '';
  const resetState = {
    mode: 'plan',
    planApproved: false,
    lastUserRequest: '',
    lastPlanText: '',
    pluginPrompt: preservedPluginPrompt,
    subsequentPrompt: preservedSubsequentPrompt
  };
  Object.assign(builderState, resetState);
  saveBuilderState(builderState);
  console.log('[BUILDER] Builder state cleared');
};

// Update build mode UI - switch to OpenCode immediately for uploaded apps
function updateBuildModeUI(modeOverride) {
  const mode = modeOverride || (builderState && builderState.mode) || 'plan';

  const modeIcon = document.getElementById('mode-icon');
  const modeText = document.getElementById('mode-text');
  const modeDescription = document.getElementById('mode-description');

  if (modeIcon && modeText && modeDescription) {
    if (mode === 'build') {
      modeIcon.innerHTML = `
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
        </svg>
      `;
      modeText.textContent = 'OpenCode';
      modeDescription.textContent = 'Imported apps open directly in coding mode';
    } else {
      modeIcon.innerHTML = `
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <path d="M9 11l3 3L22 4"></path>
          <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
        </svg>
      `;
      modeText.textContent = 'Planning';
      modeDescription.textContent = 'Discuss and refine your app plan';
    }
  }

  // Always show model selector above message input for both plan and build modes
  if (el.modelSelectWrap) {
    el.modelSelectWrap.style.display = 'inline-flex';
  }

  if (typeof window.updateUsageProgressBar === 'function') {
    window.updateUsageProgressBar();
  }
}

async function proceedWithBuild(planContent) {
  if (!planContent) return;
  pendingPlanContent = planContent;
  
  // Always ensure model selector is visible
  if (el.modelSelectWrap) {
    el.modelSelectWrap.style.display = 'inline-flex';
  }
  
  // Show confirmation modal before proceeding
  if (el.confirmBuildModal) {
    el.confirmBuildModal.style.display = 'flex';
  } else {
    // Fallback to confirm if modal not found for some reason
    const confirmBuild = confirm("Are you sure you want to proceed with this plan? This will start the build process.");
    if (confirmBuild) await executeBuild(planContent);
  }
}

async function executeBuild(planContent) {
  console.log('executeBuild called with planContent:', planContent ? planContent.substring(0, 100) + '...' : 'null');
  // Ensure external testing is still allowed if enabled
  if (builderState.externalTestingEnabled) {
    await loadUsageSummary();
    const summary = state.usageSummary?.externalTesting || null;
    const limit = summary ? summary.limit : null;
    const used = summary ? summary.used : 0;
    if (Number.isFinite(limit) && used >= limit) {
      // show modal and abort
      const modal = document.getElementById('external-testing-limit-modal');
      if (modal) modal.style.display = 'flex';
      setStatus('External testing limit reached. Disable external testing or upgrade your plan.');
      return;
    }
  }

  builderState.mode = 'build';
  builderState.planApproved = true;
  updateBuildModeUI();

  // Get the user-selected model (preserved across model list refreshes)
  let selectedModel = state.selectedModelId || (el.modelSelect && el.modelSelect.value);
  if (!selectedModel) {
    setStatus('Select a model configured by your admin');
    alert('Please select a model before proceeding with the build.');
    return;
  }

  console.log('Selected model for build:', selectedModel);
  console.log('Current session ID:', state.currentSessionId);

  // Construct the build prompt - replace {{USER_REQUEST}} with actual user request
  let promptTemplate = builderState.pluginPrompt || '';
  const userRequest = builderState.lastUserRequest || '';
  const session = state.sessions.find(s => s.id === state.currentSessionId);
  const pluginSlug = (session && session.pluginSlug) || 'plugin-name';
  const pluginName = (session && session.pluginName) || `Plugin Compass ${(session && session.title) || 'Plugin'}`;
  promptTemplate = promptTemplate.replace('{{USER_REQUEST}}', userRequest)
                                  .replace(/{{PLUGIN_SLUG}}/g, pluginSlug)
                                  .replace(/{{PLUGIN_NAME}}/g, pluginName);
  const buildPrompt = promptTemplate + '\n\nAPPROVED PLAN:\n' + planContent;

  // Send to opencode
  try {
    state.currentCli = 'opencode';
    if (el.cliSelect) el.cliSelect.value = 'opencode';

    // Show loading indicator with "building" text
    showLoadingIndicator('building');

    console.log('Sending build message to opencode...');
    console.log('Current session opencodeSessionId:', session?.opencodeSessionId);

    const buildPayload = {
      content: buildPrompt,
      displayContent: "**Starting Build Process...**",
      model: selectedModel,
      cli: 'opencode',
      isProceedWithBuild: true,
      planContent: planContent,
      externalTestingEnabled: !!builderState.externalTestingEnabled
    };
    // Preserve opencodeSessionId for session continuity
    if (session && session.opencodeSessionId) {
      buildPayload.opencodeSessionId = session.opencodeSessionId;
      console.log('[BUILD] Preserving opencodeSessionId:', session.opencodeSessionId);
    }

    const response = await api(`/api/sessions/${state.currentSessionId}/messages`, {
      method: 'POST',
      body: JSON.stringify(buildPayload),
    });

    console.log('Build message response:', response);

    // Start streaming if message was created
    if (response.message && response.message.id) {
      console.log('Starting stream for message:', response.message.id);
      streamMessage(state.currentSessionId, response.message.id);
    } else {
      console.warn('No message ID in response:', response);
    }

    await refreshCurrentSession();
    await loadUsageSummary();
    console.log('Build process initiated successfully');
  } catch (e) {
    console.error('Failed to start build:', e);
    const msg = (e && e.message) ? e.message : 'Unknown error';
    if (msg && msg.toLowerCase().includes('external wp cli testing')) {
      // Show upgrade modal
      const modal = document.getElementById('external-testing-limit-modal');
      if (modal) modal.style.display = 'flex';
      setStatus(msg, true);
    } else {
      alert('Failed to start build: ' + msg);
    }

    builderState.mode = 'plan'; // Revert
    updateBuildModeUI();
    hideLoadingIndicator();
    
    // Update usage on error
    loadUsageSummary().catch(err => {
      console.warn('[USAGE] Usage update after build error failed:', err.message);
    });
  }
}

// Redo a proceed-with-build message - skips confirmation, rebuilds prompt properly
async function redoProceedWithBuild(planContent, model) {
  if (!planContent) {
    setStatus('Cannot redo: no plan content available');
    return;
  }

  setStatus('Redoing build process...');
  builderState.mode = 'build';
  builderState.planApproved = true;
  updateBuildModeUI();

  // Use provided model or get from dropdown
  let selectedModel = model;
  if (!selectedModel || selectedModel === 'default') {
    selectedModel = state.selectedModelId || (el.modelSelect && el.modelSelect.value);
  }
  if (!selectedModel) {
    setStatus('Select a model configured by your admin');
    return;
  }

  // Construct the build prompt - replace {{USER_REQUEST}} with actual user request
  let promptTemplate = builderState.pluginPrompt || '';
  const userRequest = builderState.lastUserRequest || '';
  const session = state.sessions.find(s => s.id === state.currentSessionId);
  const pluginSlug = (session && session.pluginSlug) || 'plugin-name';
  const pluginName = (session && session.pluginName) || `Plugin Compass ${(session && session.title) || 'Plugin'}`;
  promptTemplate = promptTemplate.replace('{{USER_REQUEST}}', userRequest)
                                  .replace(/{{PLUGIN_SLUG}}/g, pluginSlug)
                                  .replace(/{{PLUGIN_NAME}}/g, pluginName);
  const buildPrompt = promptTemplate + '\n\nAPPROVED PLAN:\n' + planContent;

  try {
    state.currentCli = 'opencode';
    if (el.cliSelect) el.cliSelect.value = 'opencode';

    // Show loading indicator with "building" text
    showLoadingIndicator('building');

    const buildPayload = {
      content: buildPrompt,
      displayContent: "**Retrying Build Process...**",
      model: selectedModel,
      cli: 'opencode',
      isProceedWithBuild: true,
      planContent: planContent
    };
    // Preserve opencodeSessionId for session continuity
    if (session && session.opencodeSessionId) {
      buildPayload.opencodeSessionId = session.opencodeSessionId;
      console.log('[REDO] Preserving opencodeSessionId:', session.opencodeSessionId);
    }

    const response = await api(`/api/sessions/${state.currentSessionId}/messages`, {
      method: 'POST',
      body: JSON.stringify(buildPayload),
    });

    // Start streaming for the new message
    if (response.message && response.message.id) {
      streamMessage(state.currentSessionId, response.message.id);
    }

    await refreshCurrentSession();
    await loadUsageSummary();
    setStatus('Build process restarted');
  } catch (e) {
    setStatus('Failed to redo build: ' + e.message);
    builderState.mode = 'plan';
    updateBuildModeUI();
    hideLoadingIndicator();
    
    // Update usage on error
    loadUsageSummary().catch(err => {
      console.warn('[USAGE] Usage update after redo error failed:', err.message);
    });
  }
}

// Redo a message - handles OpenRouter, Opencode, and Proceed-with-Build cases
async function redoMessage(msg, session) {
  const isOpencodeMessage = msg.cli === 'opencode';
  const isProceedWithBuild = msg.isProceedWithBuild || (msg.displayContent && msg.displayContent.includes('Starting Build Process'));
  const isOpenRouterMessage = !isOpencodeMessage || msg.phase === 'plan';

  setStatus('Preparing to redo...');

  // Case 1: Proceed with Build - undo first, then rebuild with plan content
  if (isProceedWithBuild) {
    // Check if this is the first opencode message (nothing to undo yet)
    const messageIndex = session.messages.findIndex(m => m.id === msg.id);
    const hasPreviousOpencodeMessages = session.messages
      .slice(0, messageIndex)
      .some(m => m.cli === 'opencode');

    // Check if any previous messages had output (files were changed)
    const hasPreviousOutput = session.messages
      .slice(0, messageIndex)
      .some(m => m.cli === 'opencode' && (m.reply || m.partialOutput));

    // Only undo if there are previous opencode messages AND they produced output
    if (hasPreviousOpencodeMessages && hasPreviousOutput) {
      try {
        await api(`/api/sessions/${session.id}/messages/${msg.id}/undo`, {
          method: 'POST',
        });
        setStatus('Undo complete, rebuilding...');
      } catch (err) {
        console.warn('Undo failed, continuing with redo:', err.message);
      }
    } else {
      console.log('No previous output to undo - skipping undo, proceeding directly to rebuild');
      setStatus('Starting build...');
    }

    // Get the plan content - either stored on the message or try to extract from content
    let planContent = msg.planContent;
    if (!planContent && msg.content) {
      // Try to extract plan from the content (after "APPROVED PLAN:" marker)
      const planMatch = msg.content.match(/APPROVED PLAN:\s*([\s\S]*)/);
      if (planMatch) {
        planContent = planMatch[1].trim();
      }
    }

    if (planContent) {
      // Use null to force redoProceedWithBuild to use the currently selected model
      // instead of the original message's model
      await redoProceedWithBuild(planContent, null);
    } else {
      setStatus('Cannot redo: plan content not available');
    }
    return;
  }

  // Case 2: Regular Opencode message (not proceed-with-build) - use /redo command in the same session
  if (isOpencodeMessage) {
    setStatus('Redoing with OpenCode...');
    
    // Show loading indicator with "building" text
    showLoadingIndicator('building');
    
    try {
      // Use the /redo endpoint to redo in the same OpenCode session
      await api(`/api/sessions/${session.id}/messages/${msg.id}/redo`, {
        method: 'POST',
      });
      
      setStatus('Redo command sent, waiting for response...');
      
      // Refresh the session to get the updated state
      await refreshCurrentSession();
      await loadUsageSummary();
      setStatus('Redo complete');
    } catch (err) {
      setStatus('Redo failed: ' + err.message);
      console.error('Redo failed:', err);
      hideLoadingIndicator();
      
      // Update usage on error
      loadUsageSummary().catch(loadErr => {
        console.warn('[USAGE] Usage update after redo error failed:', loadErr.message);
      });
    }
    return;
  }

  // Case 3: OpenRouter (plan) message - resend to /api/plan
  if (isOpenRouterMessage) {
    setStatus('Resending to OpenRouter...');
    // Find the original user message (the content before it was processed)
    const userContent = msg.displayContent || msg.content || builderState.lastUserRequest || '';

    await sendPlanMessage(userContent);
    setStatus('Plan message resent');
    return;
  }

  setStatus('Unable to redo this message type');
}


const state = {

  sessions: [],
  sessionsLoaded: false,
  currentSessionId: null,
  models: [],
  modelsSignature: null,
  selectedModelId: null,
  pollingTimer: null,
  cliOptions: ['opencode'],
  currentCli: 'opencode',
  activeStreams: new Map(), // Track active SSE connections
  opencodeStatus: null,
  userId: null,
  accountPlan: 'hobby',
  isAdmin: false,
  usageSummary: null,
  todos: [], // Current todos from OpenCode
  currentMessageId: null, // Track which message we're displaying todos for
  isSending: false, // Track if a message is currently being sent
  currentSendingMessageId: null, // Track the ID of the message being sent for cancellation
};

// Expose state for builder.html
window.state = state;

const TOKENS_TO_WORD_RATIO = window.TOKENS_TO_WORD_RATIO || 0.75;

// Message input caching constants
const MESSAGE_INPUT_CACHE_KEY = 'builder_message_input';
const MESSAGE_INPUT_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // Cache for 24 hours

// Save message input to localStorage
function cacheMessageInput(content) {
  if (!el?.messageInput) return;
  try {
    const data = {
      content: content || '',
      timestamp: Date.now()
    };
    localStorage.setItem(MESSAGE_INPUT_CACHE_KEY, JSON.stringify(data));
  } catch (e) {
    console.warn('Failed to cache message input:', e);
  }
}

// Restore message input from localStorage
function restoreMessageInput() {
  if (!el?.messageInput) return false;
  try {
    const raw = localStorage.getItem(MESSAGE_INPUT_CACHE_KEY);
    if (!raw) return false;

    const data = JSON.parse(raw);
    if (!data || !data.content) return false;

    // Check if cache is expired (older than 24 hours)
    if (Date.now() - data.timestamp > MESSAGE_INPUT_CACHE_MAX_AGE_MS) {
      localStorage.removeItem(MESSAGE_INPUT_CACHE_KEY);
      return false;
    }

    // Restore the content
    el.messageInput.value = data.content;

    // Adjust textarea height for restored content
    el.messageInput.style.height = 'auto';
    el.messageInput.style.height = (el.messageInput.scrollHeight) + 'px';

    console.log('[BUILDER-CACHE] Restored cached message input:', {
      contentLength: data.content.length,
      age: Date.now() - data.timestamp
    });

    return true;
  } catch (e) {
    console.warn('Failed to restore message input:', e);
    return false;
  }
}

// Clear cached message input
function clearMessageInputCache() {
  try {
    localStorage.removeItem(MESSAGE_INPUT_CACHE_KEY);
    console.log('[BUILDER-CACHE] Cleared message input cache');
  } catch (e) {
    console.warn('Failed to clear message input cache:', e);
  }
}

function syncAdminVisibility() {
  const adminOnly = document.querySelectorAll('[data-admin-only]');
  adminOnly.forEach((node) => {
    node.style.display = state.isAdmin ? '' : 'none';
  });
}

async function detectAdminSession() {
  try {
    // Ensure credentials sent so admin session cookie is included
    const res = await fetch('/api/admin/me', { credentials: 'same-origin' });
    state.isAdmin = res.ok;
  } catch (_) {
    state.isAdmin = false;
  }
  syncAdminVisibility();
}

function isFreePlan() {
  return (state.accountPlan || '').toLowerCase() === 'hobby';
}

function formatPlanLabel(plan) {
  const normalized = (plan || 'hobby').toLowerCase();
  const pretty = normalized.charAt(0).toUpperCase() + normalized.slice(1);
  return `${pretty} plan`;
}

// Expose for builder.html
window.formatPlanLabel = formatPlanLabel;

function applyAccountPlan(plan) {
  const normalized = (plan || 'hobby').toLowerCase();
  state.accountPlan = normalized;
  const badge = document.getElementById('user-badge');
  if (badge) badge.dataset.plan = normalized;
  const planEl = document.getElementById('user-plan');
  if (planEl) planEl.textContent = formatPlanLabel(normalized);

  // Hide upgrade header button for enterprise users
  const upgradeHeaderBtn = document.getElementById('upgrade-header-btn');
  if (upgradeHeaderBtn) {
    upgradeHeaderBtn.style.display = normalized === 'enterprise' ? 'none' : 'flex';
  }

  // Hide upgrade button in token limit modal for enterprise and professional users
  const tokenLimitUpgrade = document.getElementById('token-limit-upgrade');
  if (tokenLimitUpgrade) {
    tokenLimitUpgrade.style.display = (normalized === 'enterprise' || normalized === 'professional') ? 'none' : 'flex';
  }

  syncUploadButtonState();
  applyPlanModelLock();
}

// Expose for builder.html
window.applyAccountPlan = applyAccountPlan;

const tokensToFriendly = window.tokensToFriendly || function tokensToFriendlyLocal(limit) {
  const usage = Math.round(Math.max(0, limit || 0) * TOKENS_TO_WORD_RATIO);
  if (!usage) return '—';
  if (usage < 10_000) return `≈ ${usage.toLocaleString()} usage`;
  return `≈ ${(usage / 1000).toFixed(1)}k usage`;
};

function updateUsageProgressBar(summary = state.usageSummary) {
  if (!el.usageMeterFill || !el.usageMeterTrack || !el.usageMeterPercent || !el.usageMeterTitle) {
    console.warn('[USAGE] Usage meter DOM elements not found');
    return;
  }

  const used = summary?.used || 0;
  const limit = summary?.limit || 0;
  const remaining = summary?.remaining || (limit - used);
  const plan = summary?.plan || 'hobby';
  const percent = limit > 0 ? Math.max(0, Math.min(100, parseFloat(((used / limit) * 100).toFixed(1)))) : null;
  const nearOut = percent !== null && percent >= 90;

  console.log('[USAGE] Updating progress bar:', { used, limit, remaining, plan, percent });

  el.usageMeterTitle.textContent = 'Token Usage';

  if (percent === null) {
    el.usageMeterPercent.textContent = '—';
    el.usageMeterFill.style.width = '0%';
    el.usageMeterTrack.setAttribute('aria-valuenow', '0');
    el.usageMeterTrack.title = '';
    el.usageMeterTrack.style.background = 'rgba(0, 128, 96, 0.12)';
    return;
  }

  const percentText = `${percent}% used${nearOut ? ' • almost out' : ''}`;
  const remainingText = `${remaining.toLocaleString()} remaining`;

  el.usageMeterPercent.innerHTML = `${percentText}<br><span style="font-size:10px; font-weight:500;">${remainingText}</span>`;
  el.usageMeterFill.style.width = `${percent}%`;

  if (nearOut) {
    el.usageMeterFill.style.background = 'linear-gradient(135deg, #fcd34d, #f59e0b)';
  } else {
    el.usageMeterFill.style.background = 'linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark))';
  }

  el.usageMeterTrack.setAttribute('aria-valuenow', String(percent));
  el.usageMeterTrack.title = `${used.toLocaleString()} / ${limit.toLocaleString()} tokens`;

  const actionsDiv = el.usageMeterFill.closest('.usage-meter')?.querySelector('.usage-meter-actions');
  const hintSpan = actionsDiv?.querySelector('.usage-hint');
  if (hintSpan) {
    if (plan === 'enterprise') {
      hintSpan.textContent = 'You are on the top tier';
    } else {
      hintSpan.textContent = 'Need more runway?';
    }
  }

  // Hide upgrade link for enterprise users
  const upgradeLinks = actionsDiv?.querySelectorAll('a[href^="/upgrade"]');
  upgradeLinks?.forEach(link => {
    link.style.display = plan === 'enterprise' ? 'none' : 'inline-flex';
  });

  console.log('[USAGE] Progress bar updated successfully');
}

window.updateUsageProgressBar = updateUsageProgressBar;

async function loadUsageSummary() {
  try {
    const data = await api(`/api/account/usage?_t=${Date.now()}`);
    console.log('[USAGE] API response:', data);
    if (data?.summary) {
      state.usageSummary = data.summary;
      console.log('[USAGE] Usage summary loaded:', {
        month: data.summary.month,
        plan: data.summary.plan,
        used: data.summary.used,
        limit: data.summary.limit,
        percent: data.summary.percent
      });
      console.log('[USAGE] DOM elements found:', {
        usageMeterTitle: !!el.usageMeterTitle,
        usageMeterPercent: !!el.usageMeterPercent,
        usageMeterFill: !!el.usageMeterFill,
        usageMeterTrack: !!el.usageMeterTrack
      });
      updateUsageProgressBar(state.usageSummary);
      if (typeof window.updateExternalTestingUI === 'function') {
        try { updateExternalTestingUI(); } catch (e) { console.warn('external testing UI update failed', e); }
      }
      if (typeof window.checkTokenLimitAndShowModal === 'function') {
        setTimeout(() => window.checkTokenLimitAndShowModal(), 500);
      }
    } else {
      console.warn('[USAGE] No summary in usage response:', data);
    }
  } catch (err) {
    setAdminStatus(`usage fetch failed: ${err?.message || String(err)}`);
    console.error('[USAGE] Failed to fetch usage summary:', err);
  }
}

// Expose for builder.html
window.loadUsageSummary = loadUsageSummary;

// --- External testing UI helpers ---
function updateExternalTestingUI() {
  const elToggle = document.getElementById('external-testing-toggle');
  const elUsage = document.getElementById('external-testing-usage');
  const infoBtn = document.getElementById('external-testing-info');
  if (!elToggle || !elUsage) return;

  const et = state.usageSummary?.externalTesting || null;
  if (!et) {
    elUsage.textContent = 'Not configured';
    elToggle.disabled = true;
    elToggle.checked = false;
    builderState.externalTestingEnabled = false;
    saveBuilderState(builderState);
    return;
  }

  const used = Number(et.used || 0);
  const limit = Number.isFinite(Number(et.limit)) ? et.limit : 'unlimited';
  elUsage.textContent = Number.isFinite(Number(et.limit)) ? `${used} / ${limit}` : `${used} / ∞`;

  if (typeof builderState.externalTestingEnabled === 'boolean') {
    elToggle.checked = !!builderState.externalTestingEnabled;
  } else {
    elToggle.checked = false;
    builderState.externalTestingEnabled = false;
    saveBuilderState(builderState);
  }

  if (infoBtn) {
    infoBtn.onclick = (e) => {
      e.preventDefault();
      alert('External WP tests run a series of WP-CLI checks on an external WordPress site. Tests are counted against your monthly allowance.');
    };
  }

  elToggle.addEventListener('change', async (e) => {
    const wantOn = e.target.checked === true;
    if (!wantOn) {
      builderState.externalTestingEnabled = false;
      saveBuilderState(builderState);
      elToggle.checked = false;
      return;
    }

    // Re-check usage before enabling
    await loadUsageSummary();
    const summary = state.usageSummary?.externalTesting || null;
    const limit = summary ? summary.limit : null;
    const used = summary ? summary.used : 0;
    if (Number.isFinite(limit) && used >= limit) {
      // show modal suggesting upgrade
      const modal = document.getElementById('external-testing-limit-modal');
      if (modal) modal.style.display = 'flex';
      elToggle.checked = false;
      return;
    }

    builderState.externalTestingEnabled = true;
    saveBuilderState(builderState);
    elToggle.checked = true;
  });
}

(function wireExternalTestingModal() {
  const modal = document.getElementById('external-testing-limit-modal');
  if (!modal) return;
  const closeBtn = document.getElementById('external-testing-limit-close');
  const cancelBtn = document.getElementById('external-testing-limit-cancel');
  const upgradeBtn = document.getElementById('external-testing-limit-upgrade');
  const upgradeHeaderBtn = document.getElementById('upgrade-header-btn');
  const closeModal = () => { modal.style.display = 'none'; };
  if (closeBtn) closeBtn.addEventListener('click', closeModal);
  if (cancelBtn) cancelBtn.addEventListener('click', closeModal);
  if (upgradeBtn) {
    upgradeBtn.addEventListener('click', () => {
      closeModal();
      if (upgradeHeaderBtn) upgradeHeaderBtn.click(); else window.location.href = '/topup';
    });
  }
})();

// Expose to global scope
window.updateExternalTestingUI = updateExternalTestingUI;


function checkTokenLimitAndShowModal() {
  const remaining = state.usageSummary?.remaining || 0;
  if (remaining <= 5000) {
    const modal = document.getElementById('token-limit-modal');
    if (modal && modal.style.display !== 'flex') {
      modal.style.display = 'flex';
    }
  }
}

window.checkTokenLimitAndShowModal = checkTokenLimitAndShowModal;

// Poll usage more aggressively when OpenCode is running
let usagePollingInterval = null;

function startUsagePolling() {
  if (usagePollingInterval) clearInterval(usagePollingInterval);
  usagePollingInterval = setInterval(() => {
    loadUsageSummary().catch(err => {
      console.warn('[USAGE] Polling failed:', err);
    });
  }, 60000); // Poll every 60 seconds
  console.log('[USAGE] Started usage polling');
}

function stopUsagePolling() {
  if (usagePollingInterval) {
    clearInterval(usagePollingInterval);
    usagePollingInterval = null;
    console.log('[USAGE] Stopped usage polling');
  }
}

// Expose for builder.html
window.startUsagePolling = startUsagePolling;
window.stopUsagePolling = stopUsagePolling;

async function buyBoost(tier) {
  const res = await api('/api/account/boost', { method: 'POST', body: JSON.stringify(tier ? { tier } : {}) });
  if (res?.summary) {
    state.usageSummary = res.summary;
    updateUsageProgressBar(state.usageSummary);
    setStatus('Extra AI energy added to your plan');
  }
}

// Cached account fetch to avoid duplicate/competing requests
let _cachedAccountPromise = null;
function getAccountInfo(forceRefresh = false) {
  if (!forceRefresh && _cachedAccountPromise) return _cachedAccountPromise;
  _cachedAccountPromise = api('/api/account').catch((err) => {
    // Clear cache on error so future attempts can retry
    _cachedAccountPromise = null;
    throw err;
  });
  return _cachedAccountPromise;
}
// Expose for legacy inline scripts
window.getAccountInfo = getAccountInfo;

async function loadAccountPlanWithRetry(attempt = 1) {
  const maxRetries = 3;
  const baseTimeoutMs = 10000; // 10 seconds timeout

  console.log(`[BUILDER-PLAN] loadAccountPlanWithRetry called, attempt ${attempt}/${maxRetries}`);

  try {
    // Start fetching account info with longer timeout
    const accountPromise = getAccountInfo();

    // Race against a configurable timeout
    const timeoutPromise = new Promise((resolve) => {
      setTimeout(() => {
        console.log(`[BUILDER-PLAN] Timeout reached after ${baseTimeoutMs}ms (attempt ${attempt})`);
        resolve(null);
      }, baseTimeoutMs);
    });

    const data = await Promise.race([accountPromise, timeoutPromise]);

    if (!data) {
      // Timed out - apply a safe default so the UI moves on, and continue fetching in background
      console.log(`[BUILDER-PLAN] Account fetch timed out, applying default plan (attempt ${attempt})`);
      applyAccountPlan(state.accountPlan || 'hobby');
      syncUploadButtonState();

      if (attempt < maxRetries) {
        // Retry with exponential backoff
        const delayMs = 2000 * Math.pow(2, attempt - 1);
        console.log(`[BUILDER-PLAN] Scheduling retry in ${delayMs}ms...`);
        setTimeout(() => {
          loadAccountPlanWithRetry(attempt + 1).catch(err => {
            console.error(`[BUILDER-PLAN] Retry ${attempt} failed:`, err.message);
          });
        }, delayMs);
      } else {
        console.warn(`[BUILDER-PLAN] All ${maxRetries} retries exhausted, using default plan`);
      }

      accountPromise.then((fullData) => {
        if (fullData?.account?.plan) {
          console.log(`[BUILDER-PLAN] Background fetch completed, applying plan:`, fullData.account.plan);
          applyAccountPlan(fullData.account.plan);
        }
        if (fullData?.account?.tokenUsage) {
          state.usageSummary = fullData.account.tokenUsage;
          updateUsageProgressBar(state.usageSummary);
          console.log(`[BUILDER-PLAN] Usage summary updated from background fetch`);
        } else {
          loadUsageSummary().catch(() => {});
        }
      }).catch((err) => {
        setAdminStatus(`account fetch failed: ${err?.message || String(err)}`);
        console.error(`[BUILDER-PLAN] Background account fetch failed:`, err.message);
      });

      return;
    }

    if (data?.account?.plan) {
      console.log(`[BUILDER-PLAN] Account plan loaded successfully:`, data.account.plan);
      applyAccountPlan(data.account.plan);
      syncUploadButtonState();
      if (data.account.tokenUsage) {
        state.usageSummary = data.account.tokenUsage;
        updateUsageProgressBar(state.usageSummary);
      } else {
        await loadUsageSummary();
      }
    } else {
      console.warn(`[BUILDER-PLAN] No account plan in response, using default`);
      applyAccountPlan(state.accountPlan || 'hobby');
    }
  } catch (err) {
    setAdminStatus(`account fetch failed: ${err?.message || String(err)}`);
    console.error(`[BUILDER-PLAN] Account fetch error (attempt ${attempt}):`, {
      error: err.message,
      stack: err.stack
    });

    if (attempt < maxRetries) {
      const delayMs = 2000 * Math.pow(2, attempt - 1);
      console.log(`[BUILDER-PLAN] Scheduling retry in ${delayMs}ms...`);
      await new Promise(resolve => setTimeout(resolve, delayMs));
      return loadAccountPlanWithRetry(attempt + 1);
    }

    console.error(`[BUILDER-PLAN] All ${maxRetries} retries exhausted, using default plan`);
    applyAccountPlan(state.accountPlan || 'hobby');
    syncUploadButtonState();
  }
}

// Legacy wrapper for backward compatibility
async function loadAccountPlan() {
  return loadAccountPlanWithRetry(1);
}

let modelPreviewDropdown = null;
let modelPreviewAttached = false;
let modelPreviewHandler = null;
let customDropdownOpen = false;

function toggleCustomDropdown() {
  if (!el.modelSelectDropdown) return;
  customDropdownOpen = !customDropdownOpen;
  el.modelSelectDropdown.style.display = customDropdownOpen ? 'block' : 'none';
  if (el.modelSelectBtn) {
    el.modelSelectBtn.setAttribute('aria-expanded', customDropdownOpen ? 'true' : 'false');
  }
}

function closeCustomDropdown() {
  if (!el.modelSelectDropdown) return;
  customDropdownOpen = false;
  el.modelSelectDropdown.style.display = 'none';
  if (el.modelSelectBtn) {
    el.modelSelectBtn.setAttribute('aria-expanded', 'false');
  }
}

function showBlurredModelPreviewInline() {
  if (!isFreePlan()) return;
  if (modelPreviewDropdown) {
    try { modelPreviewDropdown.remove(); } catch (_) { }
    modelPreviewDropdown = null;
  }
  const wrap = document.getElementById('model-select-wrap') || (el.modelSelectBtn && el.modelSelectBtn.parentElement);
  if (!wrap) return;
  // Ensure the wrapper is positioned for absolute dropdown
  wrap.style.position = wrap.style.position || 'relative';

  const dropdown = document.createElement('div');
  dropdown.className = 'model-preview-dropdown';
  dropdown.setAttribute('role', 'menu');
  dropdown.setAttribute('aria-label', 'Available models (blurred)');

  const title = document.createElement('div');
  title.style.fontWeight = '700';
  title.style.marginBottom = '6px';
  title.textContent = 'Models are auto-selected on the hobby plan';
  const subtitle = document.createElement('div');
  subtitle.style.color = '#6b7280';
  subtitle.style.fontSize = '13px';
  subtitle.style.marginBottom = '12px';
  subtitle.textContent = 'Upgrade to choose a specific model. Here are the available options:';

  const list = document.createElement('div');
  list.style.display = 'grid';
  list.style.gap = '8px';
  (state.models || []).forEach((m) => {
    const row = document.createElement('div');
    row.className = 'model-preview-item';
    row.textContent = m.label || m.name || m.id || 'Model';
    list.appendChild(row);
  });
  if (!list.children.length) {
    const placeholder = document.createElement('div');
    placeholder.className = 'model-preview-item';
    placeholder.textContent = 'Admin has not published models yet';
    list.appendChild(placeholder);
  }

  // Add upgrade link at bottom
  const upgradeContainer = document.createElement('div');
  upgradeContainer.style.marginTop = '16px';
  upgradeContainer.style.paddingTop = '12px';
  upgradeContainer.style.borderTop = '1px solid #e5e7eb';
  const upgradeLink = document.createElement('a');
  upgradeLink.href = '/upgrade?source=builder_model';
  upgradeLink.style.color = '#008060';
  upgradeLink.style.textDecoration = 'none';
  upgradeLink.style.fontWeight = '700';
  upgradeLink.style.fontSize = '14px';
  upgradeLink.style.display = 'flex';
  upgradeLink.style.alignItems = 'center';
  upgradeLink.style.gap = '6px';
  upgradeLink.textContent = 'Upgrade Plan to Access All Models';

  const upgradeIcon = document.createElement('svg');
  upgradeIcon.setAttribute('width', '16');
  upgradeIcon.setAttribute('height', '16');
  upgradeIcon.setAttribute('viewBox', '0 0 24 24');
  upgradeIcon.setAttribute('fill', 'none');
  upgradeIcon.setAttribute('stroke', 'currentColor');
  upgradeIcon.setAttribute('stroke-width', '2');
  upgradeIcon.setAttribute('stroke-linecap', 'round');
  upgradeIcon.setAttribute('stroke-linejoin', 'round');

  const upgradePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  upgradePath.setAttribute('d', 'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5zM2 12l10 5 10-5');
  upgradeIcon.appendChild(upgradePath);

  upgradeLink.appendChild(upgradeIcon);
  upgradeContainer.appendChild(upgradeLink);

  dropdown.appendChild(title);
  dropdown.appendChild(subtitle);
  dropdown.appendChild(list);
  dropdown.appendChild(upgradeContainer);
  wrap.appendChild(dropdown);
  modelPreviewDropdown = dropdown;

  // Close dropdown when clicking outside
  const onDocClick = (e) => {
    if (!dropdown.contains(e.target) && !wrap.contains(e.target)) {
      try { dropdown.remove(); } catch (_) { }
      modelPreviewDropdown = null;
      document.removeEventListener('click', onDocClick);
    }
  };
  // Delay attaching the listener to avoid immediately closing when opening
  setTimeout(() => document.addEventListener('click', onDocClick), 0);
}

function applyPlanModelLock() {
  if (!el.modelSelect) return;
  if (!isFreePlan()) {
    // Paid plan: enable normal model selection
    if (modelPreviewAttached && modelPreviewHandler) {
      if (el.modelSelectBtn) {
        el.modelSelectBtn.removeEventListener('click', modelPreviewHandler);
      }
      modelPreviewHandler = null;
      modelPreviewAttached = false;
    }
    el.modelSelect.dataset.locked = '';
    state.selectedModelId = el.modelSelect.value || state.selectedModelId;
    renderModelIcon(el.modelSelect.value);
    updateModelSelectDisplay(el.modelSelect.value);
    return;
  }

  // Hobby (free) plan: allow model selection but ensure 'auto' is available
  // Only add 'auto' if it doesn't exist - don't clear existing options
  const hasAutoOption = Array.from(el.modelSelect.options).some(opt => opt.value === 'auto');
  if (!hasAutoOption) {
    const opt = document.createElement('option');
    opt.value = 'auto';
    opt.textContent = 'Auto (admin managed)';
    el.modelSelect.appendChild(opt);
  }

  // Only set to 'auto' if current value is empty or invalid to avoid overwriting user selection
  let valueToDisplay = el.modelSelect.value;
  if (!valueToDisplay || valueToDisplay === '') {
    el.modelSelect.value = 'auto';
    el.modelSelect.selectedIndex = 0;
    valueToDisplay = 'auto';
  }

  state.selectedModelId = valueToDisplay;
  updateModelSelectDisplay(valueToDisplay);

  el.modelSelect.dataset.locked = 'true';

  // Attach click handler to show blurred preview (but NOT by default)
  if (!modelPreviewAttached && el.modelSelectBtn) {
    modelPreviewHandler = (e) => {
      e.preventDefault();
      e.stopPropagation();
      // Show the blurred inline preview on click, not by default
      try { showBlurredModelPreviewInline(); } catch (_) { }
    };
    el.modelSelectBtn.addEventListener('click', modelPreviewHandler);
    modelPreviewAttached = true;
  }

  renderModelIcon(valueToDisplay !== 'auto' ? valueToDisplay : null);
}

const el = {
  sessionList: null, // Not used in builder
  chatArea: document.getElementById('chat-area'),
  chatTitle: document.getElementById('chat-title'),
  sessionId: document.getElementById('session-id'),
  sessionModel: document.getElementById('session-model'),
  sessionPending: document.getElementById('session-pending'),
  queueIndicator: document.getElementById('queue-indicator'),
  cliSelect: document.getElementById('cli-select'),
  modelSelect: document.getElementById('model-select'),
  modelSelectBtn: document.getElementById('model-select-btn'),
  modelSelectDropdown: document.getElementById('model-select-dropdown'),
  modelSelectOptions: document.getElementById('model-select-options'),
  modelSelectText: document.getElementById('model-select-text'),
  modelSelectMultiplier: document.getElementById('model-select-multiplier'),
  modelIcon: document.getElementById('model-icon'),
  modelSelectWrap: document.getElementById('model-select-wrap'),
  customModelLabel: document.getElementById('custom-model-label'),
  customModelInput: document.getElementById('custom-model-input'),
  newChat: document.getElementById('new-chat'),
  historyBtn: document.getElementById('history-btn'),
  historyModal: document.getElementById('history-modal'),
  historyList: document.getElementById('history-list'),
  historyEmpty: document.getElementById('history-empty'),
  historyClose: document.getElementById('history-close'),
  messageInput: document.getElementById('message-input'),
  // sendBtn: document.getElementById('send-btn'), // Removed
  miniSendBtn: document.getElementById('mini-send-btn'),
  uploadMediaBtn: document.getElementById('upload-media-btn'),
  uploadMediaInput: document.getElementById('upload-media-input'),
  attachmentPreview: document.getElementById('attachment-preview'),
  statusLine: document.getElementById('status-line'),
  statusLineAdmin: document.getElementById('status-line-admin'),
  usageMeterTitle: document.getElementById('usage-meter-title'),
  usageMeterPercent: document.getElementById('usage-meter-percent'),
  usageMeterFill: document.getElementById('usage-meter-fill'),
  usageMeterTrack: document.getElementById('usage-meter-track'),
  quickButtons: document.querySelectorAll('[data-quick]'),
  confirmBuildModal: document.getElementById('confirm-build-modal'),
  confirmBuildProceed: document.getElementById('confirm-build-proceed'),
  confirmBuildCancel: document.getElementById('confirm-build-cancel'),
  confirmBuildClose: document.getElementById('confirm-build-close'),
};

console.log('Builder DOM elements initialized:', {
  uploadMediaBtn: el.uploadMediaBtn,
  uploadMediaInput: el.uploadMediaInput,
  messageInput: el.messageInput
});

const pendingAttachments = [];

function isPaidPlanClient() {
  return !isFreePlan();
}

function currentModelSupportsMedia() {
  const selectedModelId = state.selectedModelId || el.modelSelect?.value;
  if (!selectedModelId) return false;
  
  // When model is 'auto', check if any available model supports media
  if (selectedModelId === 'auto') {
    // Check if any model in the list supports media
    return state.models.some((m) => m.supportsMedia === true);
  }
  
  const model = state.models.find((m) => (m.id || m.name || m) === selectedModelId);
  return model?.supportsMedia === true;
}

function bytesToFriendly(bytes) {
  const n = Number(bytes || 0);
  if (!Number.isFinite(n) || n <= 0) return '0 B';
  if (n < 1024) return `${n} B`;
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
  return `${(n / (1024 * 1024)).toFixed(2)} MB`;
}

function renderAttachmentPreview() {
  if (!el.attachmentPreview) return;
  if (!pendingAttachments.length) {
    el.attachmentPreview.style.display = 'none';
    el.attachmentPreview.innerHTML = '';
    return;
  }
  el.attachmentPreview.style.display = 'flex';
  el.attachmentPreview.innerHTML = '';
  pendingAttachments.forEach((att, idx) => {
    const chip = document.createElement('div');
    chip.className = 'attachment-chip';
    const img = document.createElement('img');
    img.className = 'attachment-thumb';
    img.alt = att.name || 'image';
    img.src = att.previewUrl || '';
    const meta = document.createElement('div');
    meta.className = 'attachment-meta';
    const name = document.createElement('div');
    name.className = 'name';
    name.textContent = att.name || 'image';
    const size = document.createElement('div');
    size.className = 'size';
    size.textContent = `${att.type || 'image'} • ${bytesToFriendly(att.size || 0)}`;
    meta.appendChild(name);
    meta.appendChild(size);
    const remove = document.createElement('button');
    remove.className = 'attachment-remove';
    remove.type = 'button';
    remove.textContent = 'Remove';
    remove.onclick = () => {
      try {
        const removed = pendingAttachments.splice(idx, 1);
        if (removed[0] && removed[0].previewUrl && removed[0].previewUrl.startsWith('blob:')) {
          URL.revokeObjectURL(removed[0].previewUrl);
        }
      } catch (_) { }
      renderAttachmentPreview();
    };
    chip.appendChild(img);
    chip.appendChild(meta);
    chip.appendChild(remove);
    el.attachmentPreview.appendChild(chip);
  });
}

async function fileToCompressedWebpAttachment(file) {
  const maxDim = 1600;
  const quality = 0.8;
  const mime = (file && file.type) ? file.type : 'application/octet-stream';
  if (!file || !mime.startsWith('image/')) throw new Error('Only images are supported');

  let bitmap;
  try {
    bitmap = await createImageBitmap(file);
  } catch (_) {
    bitmap = null;
  }
  if (!bitmap) {
    const dataUrl = await new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onerror = () => reject(new Error('Failed to read image'));
      reader.onload = () => resolve(String(reader.result || ''));
      reader.readAsDataURL(file);
    });
    const base64 = dataUrl.split(',')[1] || '';
    return { name: file.name || 'image', type: mime, data: base64, size: Math.floor((base64.length * 3) / 4), previewUrl: dataUrl };
  }

  const scale = Math.min(1, maxDim / Math.max(bitmap.width, bitmap.height));
  const width = Math.max(1, Math.round(bitmap.width * scale));
  const height = Math.max(1, Math.round(bitmap.height * scale));
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d', { alpha: false });
  ctx.drawImage(bitmap, 0, 0, width, height);
  const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/webp', quality));
  const outBlob = blob || file;

  const dataUrl = await new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onerror = () => reject(new Error('Failed to read compressed image'));
    reader.onload = () => resolve(String(reader.result || ''));
    reader.readAsDataURL(outBlob);
  });
  const base64 = dataUrl.split(',')[1] || '';
  const previewUrl = URL.createObjectURL(outBlob);
  return { name: file.name || 'image', type: 'image/webp', data: base64, size: outBlob.size || Math.floor((base64.length * 3) / 4), previewUrl };
}

function syncUploadButtonState() {
  if (!el.uploadMediaBtn) return;
  const isPaid = isPaidPlanClient();
  const modelsLoaded = state.models && state.models.length > 0;
  const modelSupportsMedia = modelsLoaded ? currentModelSupportsMedia() : true; // If models not loaded yet, assume supported for paid plans
  const allowed = isPaid && modelSupportsMedia;
  el.uploadMediaBtn.style.display = isPaid ? 'flex' : 'none';
  el.uploadMediaBtn.style.opacity = modelsLoaded ? (modelSupportsMedia ? '1' : '0.5') : '1';
  el.uploadMediaBtn.style.cursor = modelsLoaded ? (modelSupportsMedia ? 'pointer' : 'not-allowed') : 'pointer';
  
  // Update title based on why button is disabled
  if (!isPaid) {
    el.uploadMediaBtn.title = 'Upload media (available on Professional/Enterprise plans)';
  } else if (!modelsLoaded) {
    el.uploadMediaBtn.title = 'Attach images';
  } else if (!modelSupportsMedia) {
    el.uploadMediaBtn.title = 'Current model does not support media. Select a different model.';
  } else {
    el.uploadMediaBtn.title = 'Attach images';
  }
}

// Consolidated with change listener at the bottom of the file

// Set up custom dropdown button click handler
if (el.modelSelectBtn) {
  el.modelSelectBtn.addEventListener('click', (e) => {
    if (isFreePlan()) {
      // For free plans, check if user has manually selected a non-auto model
      // If yes, allow them to change it via dropdown
      // If no, show blurred preview
      if (el.modelSelect.value !== 'auto') {
        toggleCustomDropdown();
      } else {
        // Let the modelPreviewHandler handle it (shows blurred preview)
        if (modelPreviewHandler) {
          modelPreviewHandler(e);
        }
      }
      return;
    }
    // Toggle dropdown for paid plans
    toggleCustomDropdown();
  });
}

// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
  if (customDropdownOpen && el.modelSelectDropdown && !el.modelSelectDropdown.contains(e.target) && !el.modelSelectBtn.contains(e.target)) {
    closeCustomDropdown();
  }
});

let pendingPlanContent = null;

function cyrb53(str, seed = 0) {
  let h1 = 0xdeadbeef ^ seed;
  let h2 = 0x41c6ce57 ^ seed;
  for (let i = 0, ch; i < str.length; i++) {
    ch = str.charCodeAt(i);
    h1 = Math.imul(h1 ^ ch, 2654435761);
    h2 = Math.imul(h2 ^ ch, 1597334677);
  }
  h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
  h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
  return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}

function computeAccountId(email) {
  const normalized = (email || '').trim().toLowerCase();
  if (!normalized) return '';
  const hash = cyrb53(normalized);
  return `acct-${hash.toString(16)}`;
}

function getDeviceUserId() {
  try {
    const existing = localStorage.getItem('wordpress_plugin_ai_user_id');
    if (existing) return existing;
    const uuidPart = crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 12);
    const generated = `user-${uuidPart}`;
    localStorage.setItem('wordpress_plugin_ai_user_id', generated);
    return generated;
  } catch (_) {
    const fallbackPart = (typeof crypto !== 'undefined' && crypto.randomUUID)
      ? crypto.randomUUID()
      : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
    return `user-${fallbackPart}`;
  }
}

function resolveUserId() {
  try {
    const keys = ['wordpress_plugin_ai_user', 'shopify_ai_user'];
    for (const key of keys) {
      const stored = localStorage.getItem(key);
      if (stored) {
        const parsed = JSON.parse(stored);
        if (parsed && parsed.email) {
          const accountId = parsed.accountId || computeAccountId(parsed.email);
          if (accountId && (!parsed.accountId || parsed.accountId !== accountId)) {
            try { localStorage.setItem(key, JSON.stringify({ ...parsed, accountId })); } catch (_) { }
          }
          return accountId;
        }
      }
    }
  } catch (_) { /* ignore */ }
  return '';
}

state.userId = resolveUserId();

async function hydrateUserIdFromServerSession() {
  if (state.userId) return state.userId;
  try {
    const res = await fetch('/api/me', { credentials: 'same-origin' });
    if (!res.ok) return '';
    const data = await res.json().catch(() => null);
    const serverUserId = data?.user?.id || '';
    if (serverUserId) {
      state.userId = serverUserId;
      return serverUserId;
    }
  } catch (_) {
    // ignore
  }
  return '';
}

(async () => {
  if (!state.userId) {
    await hydrateUserIdFromServerSession();
  }

  // We keep the legacy chat_user cookie in sync when we can, but we no longer
  // hard-require localStorage identity for authenticated sessions.
  if (state.userId) {
    try {
      document.cookie = `chat_user=${encodeURIComponent(state.userId)}; path=/; SameSite=Lax`;
    } catch (_) { /* ignore */ }
  }

  // If neither localStorage nor server-session auth is present, send them to login.
  if (!state.userId) {
    const next = encodeURIComponent(window.location.pathname + window.location.search);
    window.location.href = `/login?next=${next}`;
    return;
  }

  detectAdminSession();

  // Load usage summary on page load
  loadUsageSummary().catch(err => {
    console.warn('[USAGE] Initial loadUsageSummary failed:', err.message);
  }).then(() => {
    try { if (typeof window.updateExternalTestingUI === 'function') window.updateExternalTestingUI(); } catch (e) { console.warn('updateExternalTestingUI failed on init', e); }
  });
})();

function setAdminStatus(msg) {
  if (!el.statusLineAdmin) return;
  el.statusLineAdmin.textContent = msg || '';
}

function classifyStatusMessage(msg) {
  const text = (msg || '').toString();
  const lower = text.toLowerCase();

  if (!text) return { userText: '', adminText: '' };

  if (lower.startsWith('no models configured')) {
    return {
      userText: 'No models are configured. Please contact support.',
      adminText: text,
    };
  }

  if (lower.startsWith('model load failed:')) {
    return {
      userText: 'Models are currently unavailable. Please contact support.',
      adminText: text,
    };
  }

  if (lower.startsWith('planning failed:')) {
    return {
      userText: 'Planning is currently unavailable. Please contact support.',
      adminText: text,
    };
  }

  // Surface missing provider API keys to the user with actionable text
  if (lower.includes('missing provider api keys') || lower.includes('no configured planning providers')) {
    // Try to extract provider list from the message if present
    const m = text.match(/Missing provider API keys:\s*([^\n\r]+)/i);
    const providers = m ? m[1].trim() : null;
    return {
      userText: providers
        ? `Planning unavailable: missing API keys for ${providers}. Please set the environment variables (e.g. GROQ_API_KEY) or configure providers in Admin.`
        : 'Planning unavailable: missing provider API keys. Please check server configuration.',
      adminText: text,
    };
  }

  if (lower.includes('openrouter api key') || lower.includes('openrouter request failed')) {
    return {
      userText: 'Planning is currently unavailable. Please contact support.',
      adminText: text,
    };
  }

  if (lower.startsWith('warning: opencode cli not available')) {
    return {
      userText: 'Builder service is currently unavailable. Please contact support.',
      adminText: text,
    };
  }

  if (lower.includes('proper prefixing') ||
      lower.includes('tool call format') ||
      lower.includes('tool call prefix') ||
      lower.includes('session terminated') ||
      lower.includes('early termination')) {
    return {
      userText: 'Connection interrupted. Resuming...',
      adminText: `Early termination detected: ${text}`,
      type: 'warning'
    };
  }

  return { userText: text, adminText: '' };
}

function setStatus(msg) {
  const { userText, adminText } = classifyStatusMessage(msg);
  if (el.statusLine) el.statusLine.textContent = userText || '';
  setAdminStatus(adminText);
}

// Expose for builder.html
window.setStatus = setStatus;

async function api(path, options = {}) {
  const headers = {
    'Content-Type': 'application/json',
    ...(state.userId ? { 'X-User-Id': state.userId } : {}),
    ...(options.headers || {}),
  };
  const res = await fetch(path, {
    headers,
    credentials: 'same-origin',
    ...options,
  });
  const text = await res.text();
  let json;
  try {
    json = text ? JSON.parse(text) : {};
  } catch (parseErr) {
    console.error('[API] JSON parse error:', parseErr.message, 'Response:', text.substring(0, 300));
    throw new Error(`Invalid JSON response from server: ${parseErr.message}`);
  }
  if (!res.ok) {
    const err = new Error(json.error || res.statusText);
    if (json.stdout) err.stdout = json.stdout;
    if (json.stderr) err.stderr = json.stderr;
    throw err;
  }
  return json;
}

// Expose for builder.html
window.api = api;

function populateCliSelect() {
  if (!el.cliSelect) return;
  el.cliSelect.innerHTML = '';
  state.cliOptions.forEach((cli) => {
    const opt = document.createElement('option');
    opt.value = cli;
    opt.textContent = cli.toUpperCase();
    el.cliSelect.appendChild(opt);
  });
  el.cliSelect.value = state.currentCli;
}

// No-op for builder - session list not used
function renderSessions() {
  // Builder doesn't render session list, but we still render history
  renderHistoryList();
}

function formatSessionTime(value) {
  if (!value) return '—';
  const dt = new Date(value);
  if (Number.isNaN(dt.getTime())) return '—';
  return dt.toLocaleString(undefined, {
    month: 'short',
    day: 'numeric',
    hour: 'numeric',
    minute: '2-digit'
  });
}

function getSessionPreview(session) {
  const messages = Array.isArray(session.messages) ? session.messages : [];
  if (!messages.length) return 'No messages yet.';
  const last = messages[messages.length - 1];
  const preview = (last.reply || last.displayContent || last.content || '').trim();
  return preview ? preview.slice(0, 140) : 'No messages yet.';
}

function renderHistoryList() {
  if (!el.historyList || !el.historyEmpty) return;
  
  let sessions = Array.isArray(state.sessions) ? state.sessions.slice() : [];
  
  // Get current app ID from current session (not from URL since that's handled separately now)
  const currentSession = state.sessions.find(s => s.id === state.currentSessionId);
  let currentAppId = currentSession?.appId || null;
  
  // If we have a current appId, filter to show only chats for this app
  if (currentAppId) {
    sessions = sessions.filter(s => s.appId === currentAppId);
  }
  
  sessions.sort((a, b) => new Date(b.updatedAt || b.createdAt || 0) - new Date(a.updatedAt || a.createdAt || 0));
  
  el.historyList.innerHTML = '';
  
  if (!sessions.length) {
    el.historyEmpty.style.display = 'block';
    el.historyEmpty.textContent = 'No past chats';
    return;
  }
  
  el.historyEmpty.style.display = 'none';

  sessions.forEach((session) => {
    const item = document.createElement('div');
    item.className = 'history-item';
    if (session.id === state.currentSessionId) {
      item.classList.add('active');
    }

    const content = document.createElement('div');
    content.style.flex = '1';

    const title = document.createElement('div');
    title.className = 'history-title';
    title.textContent = session.title || 'Untitled chat';

    const preview = document.createElement('div');
    preview.className = 'history-preview';
    preview.textContent = getSessionPreview(session);

    content.appendChild(title);
    content.appendChild(preview);

    const meta = document.createElement('div');
    meta.className = 'history-meta';
    meta.textContent = formatSessionTime(session.updatedAt || session.createdAt);

    item.appendChild(content);
    item.appendChild(meta);

    item.addEventListener('click', async (e) => {
      e.preventDefault();
      e.stopPropagation();
      
      if (session.id === state.currentSessionId) {
        console.log('[HISTORY] Clicked on current session, just closing modal');
        closeHistoryModal();
        return;
      }
      
      try {
        console.log('[HISTORY] Clicked on session:', { id: session.id, title: session.title });
        console.log('[HISTORY] Current session ID before select:', state.currentSessionId);
        await selectSession(session.id);
        console.log('[HISTORY] Session selected successfully, new currentSessionId:', state.currentSessionId);
        console.log('[HISTORY] Closing modal');
        closeHistoryModal();
      } catch (err) {
        console.error('[HISTORY] Failed to select session:', err);
        setStatus('Failed to load chat: ' + err.message);
      }
    });

    el.historyList.appendChild(item);
  });
}

async function openHistoryModal() {
  // Refresh sessions from server to ensure we have the latest chats
  await loadSessions().catch(err => console.warn('Failed to refresh sessions for history:', err.message));
  renderHistoryList();
  if (el.historyModal) el.historyModal.style.display = 'flex';
}

function closeHistoryModal() {
  if (el.historyModal) el.historyModal.style.display = 'none';
}

function scrollChatToBottom(force = false) {
  if (!el.chatArea) return;
  const target = el.chatArea;
  
  const doScroll = () => {
    if (!target) return;
    target.scrollTop = target.scrollHeight;
  };

  // Use multiple techniques to ensure scroll happens after content is rendered
  // 1. Immediate scroll attempt
  doScroll();
  
  // 2. After short delays to allow DOM updates
  setTimeout(() => { doScroll(); }, 10);
  setTimeout(() => { doScroll(); }, 50);
  setTimeout(() => { doScroll(); }, 100);
  setTimeout(() => { doScroll(); }, 250);
  setTimeout(() => { doScroll(); }, 500);
  
  // 3. Longer delay for content to fully settle (especially on page load)
  setTimeout(() => { doScroll(); }, 1000);
  setTimeout(() => { doScroll(); }, 2000);
  
  // 4. Use requestAnimationFrame for smooth scrolling
  requestAnimationFrame(() => {
    doScroll();
    requestAnimationFrame(() => {
      doScroll();
      requestAnimationFrame(() => {
        doScroll();
      });
    });
  });

  // 5. Wait for ALL images to load before scrolling
  const images = target.querySelectorAll('img');
  let imagesToLoad = 0;
  
  images.forEach((img) => {
    if (!img.complete) {
      imagesToLoad++;
      img.addEventListener('load', () => {
        imagesToLoad--;
        doScroll();
        // Scroll again after a short delay to ensure layout is updated
        setTimeout(() => doScroll(), 100);
      }, { once: true });
      
      img.addEventListener('error', () => {
        imagesToLoad--;
        doScroll();
      }, { once: true });
    }
  });
  
  // If there are images loading, scroll again after they all complete
  if (imagesToLoad > 0) {
    const checkImagesLoaded = setInterval(() => {
      if (imagesToLoad === 0) {
        clearInterval(checkImagesLoaded);
        doScroll();
        setTimeout(() => doScroll(), 100);
        setTimeout(() => doScroll(), 500);
      }
    }, 100);
    
    // Stop checking after 5 seconds to avoid infinite interval
    setTimeout(() => clearInterval(checkImagesLoaded), 5000);
  }
  
  // 6. Use ResizeObserver to detect when content height changes
  if (typeof ResizeObserver !== 'undefined') {
    const resizeObserver = new ResizeObserver((entries) => {
      for (const entry of entries) {
        if (entry.target === target) {
          doScroll();
        }
      }
    });
    resizeObserver.observe(target);
    
    // Stop observing after a while
    setTimeout(() => resizeObserver.disconnect(), 3000);
  }
  
  // 7. Also scroll when any content finishes rendering (using MutationObserver)
  if (typeof MutationObserver !== 'undefined') {
    const observer = new MutationObserver(() => {
      doScroll();
    });
    observer.observe(target, { childList: true, subtree: true });
    // Stop observing after a while to avoid memory leaks
    setTimeout(() => observer.disconnect(), 3000);
  }
}

window.scrollChatToBottom = scrollChatToBottom;

function renderSessionMeta(session) {
  if (!session) {
    console.error('renderSessionMeta called with null/undefined session');
    return;
  }
  if (el.sessionId) el.sessionId.textContent = session.id ?? '-';
  if (el.sessionModel) el.sessionModel.textContent = session.model ? `${session.cli ?? 'opencode'} / ${session.model}` : '-';
  if (el.sessionPending) el.sessionPending.textContent = session.pending ?? 0;
  if (el.queueIndicator) {
    el.queueIndicator.textContent = session.pending ? `${session.pending} queued` : 'Idle';
    el.queueIndicator.style.borderColor = session.pending ? 'var(--accent)' : 'var(--border)';
  }
  
  // Update chat title while preserving the badge
  if (el.chatTitle) {
    const badge = el.chatTitle.querySelector('.shopify-badge');
    const titleText = session.title || 'New Project';
    if (badge) {
      // Clone badge to preserve it, clear title, add text, then re-add badge
      const badgeClone = badge.cloneNode(true);
      el.chatTitle.textContent = titleText;
      el.chatTitle.appendChild(badgeClone);
    } else {
      el.chatTitle.textContent = titleText;
    }
  }
  
  if (session.cli && el.cliSelect && el.cliSelect.value !== session.cli) {
    el.cliSelect.value = session.cli;
    state.currentCli = session.cli;
  }
  // Don't update model if user just changed it manually
  if (session.model && !userJustChangedModel) {
    console.log(`[renderSessionMeta] Server model: ${session.model}, Current UI model: ${el.modelSelect.value}, userJustChangedModel: false - syncing from server`);
    if (!isFreePlan()) {
      // For paid plans: only update if current value is empty/default, preserve user selection
      const currentValue = el.modelSelect.value;
      const isDefaultOrEmpty = !currentValue || currentValue === 'default' || currentValue === '';
      const isDifferentModel = currentValue !== session.model;
      
      // Only overwrite if user hasn't selected something specific
      if ((isDefaultOrEmpty || !currentValue) && isDifferentModel) {
        programmaticModelChange = true;
        el.modelSelect.value = session.model;
        state.selectedModelId = session.model;
        updateModelSelectDisplay(session.model);
        setTimeout(() => { programmaticModelChange = false; }, 100);
      }
    } else {
      // Hobby plan shows Auto, but preserve user selection if they've manually selected a non-auto model
      // Only update if the current value is 'auto' to avoid overwriting user selections
      if (el.modelSelect.value === 'auto' && session.model !== 'auto') {
        programmaticModelChange = true;
        el.modelSelect.value = session.model;
        state.selectedModelId = session.model;
        updateModelSelectDisplay(session.model);
        setTimeout(() => { programmaticModelChange = false; }, 100);
      } else if (el.modelSelect.value !== 'auto' && session.model === 'auto') {
        // Server has 'auto', but user has selected a specific model - keep user selection
        // Don't overwrite it
      }
    }
  }
}

// Helper functions for loading indicator
function showLoadingIndicator(type) {
  // Remove any existing loading indicator first
  hideLoadingIndicator();

  const loadingDiv = document.createElement('div');
  loadingDiv.className = 'loading-indicator animate-in';
  loadingDiv.id = 'loading-indicator';
  loadingDiv.style.cssText = 'display: flex; flex-direction: column; align-items: flex-start; padding: 16px; background: var(--shopify-bg); border-radius: 8px; margin: 8px 0;';

  // Create status container
  const statusContainer = document.createElement('div');
  statusContainer.style.cssText = 'display: flex; align-items: center; gap: 12px; margin-bottom: 8px;';

  // Create animated dots
  const dotsContainer = document.createElement('div');
  dotsContainer.style.cssText = 'display: flex; gap: 4px;';
  
  for (let i = 0; i < 3; i++) {
    const dot = document.createElement('span');
    dot.style.cssText = `width: 8px; height: 8px; background: var(--shopify-green); border-radius: 50%; animation: loadingDot 1.4s ease-in-out ${i * 0.2}s infinite;`;
    dotsContainer.appendChild(dot);
  }

  // Create status text
  const statusText = document.createElement('span');
  statusText.className = 'loading-status-text';
  statusText.style.cssText = 'font-weight: 600; color: var(--shopify-text); font-size: 14px;';
  
  // Set appropriate text based on type
  if (type === 'planning') {
    statusText.textContent = 'Starting planning process';
  } else {
    statusText.textContent = 'Starting build process';
  }

  statusContainer.appendChild(dotsContainer);
  statusContainer.appendChild(statusText);

  // Create detailed info text
  const detailText = document.createElement('span');
  detailText.className = 'loading-detail-text';
  detailText.style.cssText = 'font-size: 12px; color: var(--shopify-text-secondary); margin-left: 32px;';
  
  if (type === 'planning') {
    detailText.textContent = 'Analyzing your request and creating a development plan...';
  }

  // Add animation keyframes if not already added
  if (!document.getElementById('loading-animations')) {
    const style = document.createElement('style');
    style.id = 'loading-animations';
    style.textContent = `
      @keyframes loadingDot {
        0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
        40% { transform: scale(1); opacity: 1; }
      }
      @keyframes pulse {
        0%, 100% { opacity: 1; }
        50% { opacity: 0.7; }
      }
    `;
    document.head.appendChild(style);
  }

  loadingDiv.appendChild(statusContainer);
  loadingDiv.appendChild(detailText);

  el.chatArea.appendChild(loadingDiv);

  // Scroll to bottom to show the loading indicator
  el.chatArea.scrollTop = el.chatArea.scrollHeight;
}

// Expose for builder.html
window.showLoadingIndicator = showLoadingIndicator;

function hideLoadingIndicator() {
  const existing = document.getElementById('loading-indicator');
  if (existing) {
    existing.remove();
  }
}

// Expose for builder.html
window.hideLoadingIndicator = hideLoadingIndicator;

function renderMessages(session) {
  // Helper function to strip markdown formatting from user input
  function stripMarkdownFormatting(text) {
    if (!text || typeof text !== 'string') return text;
    let cleaned = text;
    cleaned = cleaned.replace(/\*\*([^*]+)\*\*/g, '$1');
    cleaned = cleaned.replace(/\*\*/g, '');
    cleaned = cleaned.replace(/^##\s+/gm, '');
    cleaned = cleaned.replace(/##/g, '');
    return cleaned;
  }

  // Don't clear the loading indicator if it exists
  const loadingIndicator = document.getElementById('loading-indicator');
  // Store reference before clearing, and remove animation class to prevent bounce on re-render
  if (loadingIndicator) {
    loadingIndicator.classList.remove('animate-in');
    // Instead of removing from DOM, just hide it temporarily
    // This prevents layout shifts when re-adding the element
    loadingIndicator.style.visibility = 'hidden';
    loadingIndicator.style.position = 'absolute';
  }

  // Remove all message elements while preserving the loading indicator
  // This prevents the loading indicator from being removed from the DOM
  const messageElements = el.chatArea.querySelectorAll('.message');
  messageElements.forEach(el => el.remove());
  const emptyMessage = el.chatArea.querySelector('.empty-message');
  if (emptyMessage) emptyMessage.remove();

  if (!session.messages || !session.messages.length) {
    // Remove any existing empty message
    const existingEmpty = el.chatArea.querySelector('.empty-message');
    if (existingEmpty) existingEmpty.remove();
    
    if (!loadingIndicator) {
      const emptyDiv = document.createElement('div');
      emptyDiv.className = 'muted empty-message';
      emptyDiv.textContent = 'Send a message to start the conversation.';
      el.chatArea.appendChild(emptyDiv);
    } else {
      // Restore loading indicator visibility if it was there
      loadingIndicator.style.visibility = '';
      loadingIndicator.style.position = '';
    }
    // Ensure loading indicator is at the end if it exists
    if (loadingIndicator && loadingIndicator.parentNode !== el.chatArea) {
      el.chatArea.appendChild(loadingIndicator);
    }
    scrollChatToBottom();
    return;
  }

  // Find the latest message (excluding background continuations)
  const latestMessage = [...session.messages].reverse().find(msg => !msg.isBackgroundContinuation);

  session.messages.forEach((msg) => {
    if (msg.isBackgroundContinuation) return;

    const status = msg.status || 'done';

    const userCard = document.createElement('div');
    userCard.className = 'message user';
    const userMeta = document.createElement('div');
    userMeta.className = 'meta';
    // Hide model badge and status for plan messages (OpenRouter messages)
    const isPlanMessage = (msg.cli !== 'opencode') || (msg.phase === 'plan');
    const modelBadge = '';
    const statusChip = isPlanMessage ? '' : `<span class="status-chip ${status}">${status}</span>`;
    userMeta.innerHTML = `
      <span>You</span>
      ${modelBadge}
      ${statusChip}
    `;
    const userBody = document.createElement('div');
    userBody.className = 'body';
    const contentToRender = isPlanMessage ? stripMarkdownFormatting(msg.displayContent || msg.content || '') : (msg.displayContent || msg.content || '');
    userBody.appendChild(renderContentWithTodos(contentToRender));
    userCard.appendChild(userMeta);
    userCard.appendChild(userBody);
    // Attachments
    if (Array.isArray(msg.attachments) && msg.attachments.length) {
      const attachWrap = document.createElement('div');
      attachWrap.className = 'attachments';
      msg.attachments.forEach((a) => {
        if (a && a.url && (a.type || '').startsWith('image/')) {
          const img = document.createElement('img');
          img.className = 'attachment-image';
          img.src = a.url;
          img.alt = a.name || 'image';
          img.style.maxWidth = '150px';
          img.style.display = 'block';
          img.style.marginTop = '8px';
          attachWrap.appendChild(img);
        }
      });
      userCard.appendChild(attachWrap);
    }
    el.chatArea.appendChild(userCard);

    const isOpencodeMsg = msg.cli === 'opencode' || msg.phase === 'build';
    const isLatestMessage = latestMessage && latestMessage.id === msg.id;
    const isServerRestartError = msg.error === 'Server restart took too long. Please refresh and try again.';
    const shouldShowUndoRedo = isLatestMessage && (status === 'done' || status === 'error' || status === 'cancelled') && (isOpencodeMsg || isServerRestartError);
    const shouldRenderAssistantCard = msg.reply || msg.error || (status === 'running' && msg.partialOutput) || shouldShowUndoRedo;

    if (shouldRenderAssistantCard) {
      console.log('[RenderMessages] Creating assistant card for message:', {
        id: msg.id,
        cli: msg.cli,
        phase: msg.phase,
        status: status,
        hasReply: !!msg.reply,
        hasError: !!msg.error,
        hasPartialOutput: !!msg.partialOutput
      });
      // Don't hide loading indicator during streaming - only hide when completely done
      // This allows the spinner to continue until the OpenCode session is fully complete
      const assistantCard = document.createElement('div');
      assistantCard.className = 'message assistant' + (status === 'cancelled' ? ' cancelled' : '');
      const assistantMeta = document.createElement('div');
      assistantMeta.className = 'meta';
      const msgCliLabel = (msg.cli || session.cli || 'opencode');
      // For OpenCode messages, don't show provider/model label in builder
      // Only show CLI label for non-OpenCode messages (e.g., OpenRouter)
      if (msgCliLabel !== 'opencode') {
        const cliSpan = document.createElement('span');
        cliSpan.textContent = String(msgCliLabel).toUpperCase();
        assistantMeta.appendChild(cliSpan);
      }
      const rawBtn = document.createElement('button');
      rawBtn.className = 'ghost';
      rawBtn.style.marginLeft = '8px';
      rawBtn.textContent = 'Plugin Compass';
      assistantMeta.appendChild(rawBtn);

      // Add Undo/Redo buttons - show for latest message when done, errored, or cancelled
      if (shouldShowUndoRedo) {
        const undoBtn = document.createElement('button');
        undoBtn.className = 'ghost';
        undoBtn.style.marginLeft = '8px';
        undoBtn.textContent = '↺ Undo';
          undoBtn.onclick = async () => {
          undoBtn.disabled = true;
          undoBtn.textContent = '↺ Undoing...';
          try {
            // Clear todos before undoing
            clearTodos();
            await api(`/api/sessions/${session.id}/messages/${msg.id}/undo`, {
              method: 'POST',
            });
            await refreshCurrentSession();
            setStatus('Undo complete');
          } catch (err) {
            setStatus('Undo failed: ' + err.message);
          } finally {
            undoBtn.disabled = false;
            undoBtn.textContent = '↺ Undo';
          }
        };
        assistantMeta.appendChild(undoBtn);

        const redoBtn = document.createElement('button');
        redoBtn.className = 'ghost';
        redoBtn.style.marginLeft = '8px';
        redoBtn.textContent = '↻ Redo';
          redoBtn.onclick = async () => {
          redoBtn.disabled = true;
          redoBtn.textContent = '↻ Redoing...';
          try {
            // Clear todos before redoing
            clearTodos();
            await redoMessage(msg, session);
          } catch (err) {
            setStatus('Redo failed: ' + err.message);
          } finally {
            redoBtn.disabled = false;
            redoBtn.textContent = '↻ Redo';
          }
        };
        assistantMeta.appendChild(redoBtn);
      }


      const assistantBody = document.createElement('div');
      assistantBody.className = 'body';
      assistantBody.appendChild(renderContentWithTodos(msg.reply || msg.partialOutput || msg.opencodeSummary || ''));
      assistantCard.appendChild(assistantMeta);
      assistantCard.appendChild(assistantBody);

      // Check if this is an OpenRouter (plan) message - show "Proceed with Build" on ALL OpenRouter messages, not just the first
      const isOpenRouterMessage = (msg.cli !== 'opencode') || (msg.phase === 'plan') || (msg.model && !msg.model.includes('opencode') && msg.cli !== 'opencode');
      const hasReply = msg.reply || msg.partialOutput;
      const isPlanPhase = msg.phase === 'plan';
      const isBuildPhase = msg.phase === 'build';

      // Check if the plan message is asking for clarification (not ready to build yet)
      const replyContent = msg.reply || msg.partialOutput || '';
      const clarificationPatterns = [
        /To create a complete plan[,.\s\w]*I need clarification/i,
        /need clarification on a few points/i,
        /before I can create a plan/i,
        /I have a few questions/i,
        /need more (?:details|information|context)/i,
        /need some clarification/i,
        /could you clarify/i,
        /I need to ask/i
      ];
      const needsClarification = clarificationPatterns.some(pattern => pattern.test(replyContent));

      if (isOpenRouterMessage && hasReply && (isPlanPhase || isBuildPhase) && !needsClarification) {
        const proceedBtn = document.createElement('button');
        proceedBtn.className = 'primary';
        proceedBtn.style.marginTop = '12px';
        proceedBtn.style.width = '100%';
        proceedBtn.textContent = 'Proceed with Build';
        proceedBtn.onclick = () => proceedWithBuild(replyContent);
        assistantBody.appendChild(proceedBtn);
      }

      // Add Download ZIP button for OpenCode messages that are finished and are the latest message
      const isOpenCodeMessage = (msg.cli === 'opencode') || (msg.phase === 'build');
      if (isOpenCodeMessage && hasReply && status === 'done' && isLatestMessage) {
        const downloadBtn = document.createElement('button');
        downloadBtn.className = 'primary';
        downloadBtn.style.marginTop = '12px';
        downloadBtn.style.width = '100%';
        downloadBtn.textContent = 'Download ZIP';
        downloadBtn.onclick = () => {
          const modal = document.getElementById('export-modal');
          if (modal) modal.style.display = 'flex';
        };
        assistantBody.appendChild(downloadBtn);
      }

      if (msg.error) {
        const err = document.createElement('div');
        err.className = 'body';
        err.style.color = 'var(--danger)';
        err.textContent = msg.error;
        assistantCard.appendChild(err);
      }
      if ((!msg.reply || !msg.reply.length) && (!msg.partialOutput || !msg.partialOutput.length) && msg.opencodeSummary) {
        const summary = document.createElement('div');
        summary.className = 'body';
        summary.style.color = 'var(--muted)';
        summary.textContent = `Opencode output: ${msg.opencodeSummary}`;
        assistantCard.appendChild(summary);
      }

      // Render todos if they exist on the message
      if (msg.todos && Array.isArray(msg.todos) && msg.todos.length > 0) {
        const todoContainer = renderStructuredTodos(msg.todos);
        if (todoContainer) {
          assistantCard.appendChild(todoContainer);
        }
      }

      const rawPre = document.createElement('pre');
      rawPre.className = 'raw-output muted';
      rawPre.style.display = 'none';
      rawPre.textContent = [(msg.partialOutput || ''), (msg.opencodeSummary || '')].filter(Boolean).join('\n\n');
      assistantCard.appendChild(rawPre);
      rawBtn.addEventListener('click', () => { rawPre.style.display = rawPre.style.display === 'none' ? 'block' : 'none'; });
      el.chatArea.appendChild(assistantCard);

      // Approve & Build button removed - keeping users in plan mode only
    }
  });

  // Restore loading indicator visibility after all messages are rendered
  // Only restore if the latest message is not done or error
  const latestMsg = session.messages?.length > 0 
    ? [...session.messages].reverse().find(msg => !msg.isBackgroundContinuation)
    : null;
  const shouldShowLoading = latestMsg && latestMsg.status !== 'done' && latestMsg.status !== 'error' && latestMsg.status !== 'cancelled';
  
  if (loadingIndicator && shouldShowLoading) {
    loadingIndicator.style.visibility = '';
    loadingIndicator.style.position = '';
    // Ensure loading indicator is at the end of chat area
    if (loadingIndicator.parentNode !== el.chatArea) {
      el.chatArea.appendChild(loadingIndicator);
    } else {
      // Move to end to ensure it's after all messages
      el.chatArea.appendChild(loadingIndicator);
    }
  } else if (loadingIndicator && !shouldShowLoading) {
    // Remove the loading indicator if message is done/error
    loadingIndicator.remove();
  }

  scrollChatToBottom();

  updateExportButtonVisibility(session);
}

function updateExportButtonVisibility(session) {
  const exportZipBtn = document.getElementById('export-zip-btn');
  if (!exportZipBtn) return;

  if (!session || !session.messages || !session.messages.length) {
    exportZipBtn.style.display = 'none';
    return;
  }

  // Show the export button when the session contains at least one completed
  // OpenCode/build message with output. Relying on session.status caused
  // the button to disappear in some cases when the session-level status
  // wasn't set yet even though messages were complete. Inspect messages
  // directly for a more reliable check.
  const msgs = Array.isArray(session.messages) ? session.messages.slice().reverse() : [];
  const hasExportable = msgs.find((msg) => {
    if (msg.isBackgroundContinuation) return false;
    const isOpencode = (msg.cli === 'opencode') || (msg.phase === 'build');
    const isFinished = msg.status === 'done' || msg.status === 'error';
    const hasOutput = !!(msg.reply || msg.partialOutput);
    return isOpencode && isFinished && hasOutput;
  });

  exportZipBtn.style.display = hasExportable ? '' : 'none';
}

// Expose for builder.html
window.renderMessages = renderMessages;

// Helper: render text content and convert markdown task-list lines to actual checkboxes
function renderContentWithTodos(text) {
  const wrapper = document.createElement('div');
  if (!text) return document.createTextNode('');
  const processedText = String(text)
    .replace(/:\s*(?=[A-Z])/g, ':\n\n')
    .replace(/([.])\s+(?=[A-Z])/g, '$1\n\n');
  const lines = processedText.split(/\r?\n/);
  let currentList = null;
  let inCodeBlock = false;
  let codeBuffer = [];

  for (const line of lines) {
    if (line.trim().startsWith('```')) {
      if (inCodeBlock) {
        const pre = document.createElement('pre');
        pre.className = 'code-block';
        pre.style.background = '#1e1e1e';
        pre.style.color = '#d4d4d4';
        pre.style.padding = '12px';
        pre.style.borderRadius = '8px';
        pre.style.overflowX = 'auto';
        pre.style.fontFamily = 'monospace';
        pre.style.fontSize = '13px';
        pre.style.margin = '10px 0';
        pre.textContent = codeBuffer.join('\n');
        wrapper.appendChild(pre);
        codeBuffer = [];
        inCodeBlock = false;
      } else {
        inCodeBlock = true;
      }
      continue;
    }

    if (inCodeBlock) {
      codeBuffer.push(line);
      continue;
    }

    const taskMatch = line.match(/^\s*[-*]\s*\[( |x|X)\]\s*(.*)$/);
    if (taskMatch) {
      if (!currentList) { currentList = document.createElement('ul'); wrapper.appendChild(currentList); }
      const li = document.createElement('li');
      const label = document.createElement('label');
      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      checkbox.disabled = true;
      checkbox.checked = !!taskMatch[1].trim();
      label.appendChild(checkbox);
      label.appendChild(document.createTextNode(' ' + taskMatch[2]));
      li.appendChild(label);
      currentList.appendChild(li);
    } else {
      if (currentList) currentList = null;
      const p = document.createElement('div');
      p.style.marginBottom = '8px';
      p.textContent = line;
      wrapper.appendChild(p);
    }
  }
  return wrapper;
}

// Render structured todos with status and priority
function renderStructuredTodos(todos) {
  if (!todos || !Array.isArray(todos) || todos.length === 0) {
    return null;
  }

  const container = document.createElement('div');
  container.className = 'todo-container';
  container.style.marginTop = '16px';
  container.style.marginBottom = '16px';
  container.style.padding = '16px';
  container.style.background = '#f8fffc';
  container.style.border = '1px solid rgba(0, 128, 96, 0.2)';
  container.style.borderRadius = '12px';

  const header = document.createElement('div');
  header.style.display = 'flex';
  header.style.alignItems = 'center';
  header.style.gap = '8px';
  header.style.marginBottom = '12px';
  header.style.fontWeight = '600';
  header.style.color = 'var(--shopify-green)';
  header.style.fontSize = '14px';
  header.innerHTML = `
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <path d="M9 11l3 3L22 4"></path>
      <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
    </svg>
    Tasks (${todos.length})
  `;
  container.appendChild(header);

  const list = document.createElement('div');
  list.style.display = 'flex';
  list.style.flexDirection = 'column';
  list.style.gap = '8px';

  todos.forEach((todo) => {
    const item = document.createElement('div');
    item.className = `todo-item todo-status-${todo.status}`;
    item.style.display = 'flex';
    item.style.alignItems = 'flex-start';
    item.style.gap = '10px';
    item.style.padding = '10px 12px';
    item.style.borderRadius = '8px';
    item.style.background = '#fff';
    item.style.border = '1px solid var(--border)';
    item.style.transition = 'all 0.2s ease';

    // Status icon
    const statusIcon = document.createElement('span');
    statusIcon.className = 'todo-status-icon';
    statusIcon.style.flexShrink = '0';
    statusIcon.style.width = '20px';
    statusIcon.style.height = '20px';
    statusIcon.style.display = 'flex';
    statusIcon.style.alignItems = 'center';
    statusIcon.style.justifyContent = 'center';
    statusIcon.style.fontSize = '14px';

    // Set icon and color based on status
    switch (todo.status) {
      case 'completed':
        statusIcon.innerHTML = '✓';
        statusIcon.style.color = '#10b981';
        item.style.borderColor = 'rgba(16, 185, 129, 0.3)';
        item.style.background = 'rgba(16, 185, 129, 0.05)';
        break;
      case 'in_progress':
        statusIcon.innerHTML = '●';
        statusIcon.style.color = '#f59e0b';
        item.style.borderColor = 'rgba(245, 158, 11, 0.3)';
        item.style.background = 'rgba(245, 158, 11, 0.05)';
        break;
      case 'cancelled':
        statusIcon.innerHTML = '✗';
        statusIcon.style.color = '#6b7280';
        item.style.opacity = '0.6';
        item.style.textDecoration = 'line-through';
        break;
      case 'pending':
      default:
        statusIcon.innerHTML = '○';
        statusIcon.style.color = '#9ca3af';
        break;
    }

    const content = document.createElement('span');
    content.className = 'todo-content';
    content.style.flex = '1';
    content.style.fontSize = '14px';
    content.style.lineHeight = '1.4';
    content.style.color = 'var(--ink)';
    content.textContent = todo.content || '';

    // Priority badge
    if (todo.priority && todo.priority !== 'medium') {
      const priorityBadge = document.createElement('span');
      priorityBadge.className = `todo-priority-${todo.priority}`;
      priorityBadge.style.fontSize = '10px';
      priorityBadge.style.fontWeight = '700';
      priorityBadge.style.textTransform = 'uppercase';
      priorityBadge.style.letterSpacing = '0.02em';
      priorityBadge.style.padding = '2px 6px';
      priorityBadge.style.borderRadius = '4px';
      priorityBadge.style.flexShrink = '0';

      if (todo.priority === 'high') {
        priorityBadge.textContent = 'High';
        priorityBadge.style.background = '#fee2e2';
        priorityBadge.style.color = '#dc2626';
      } else if (todo.priority === 'low') {
        priorityBadge.textContent = 'Low';
        priorityBadge.style.background = '#dbeafe';
        priorityBadge.style.color = '#2563eb';
      }

      content.appendChild(document.createTextNode(' '));
      content.appendChild(priorityBadge);
    }

    item.appendChild(statusIcon);
    item.appendChild(content);
    list.appendChild(item);
  });

  container.appendChild(list);
  return container;
}

// Clear todos from the UI
function clearTodos() {
  state.todos = [];
  state.currentMessageId = null;
  const existingContainer = document.querySelector('.todo-container');
  if (existingContainer) {
    existingContainer.remove();
  }
  console.log('[TODOS] Cleared todos from UI');
}

// Update todos in the UI
function updateTodos(todos, messageId) {
  if (!todos || !Array.isArray(todos) || todos.length === 0) {
    return;
  }

  // Only update if this is for the current message or a new message
  if (state.currentMessageId && state.currentMessageId !== messageId) {
    console.log('[TODOS] Ignoring todos for different message', { current: state.currentMessageId, received: messageId });
    return;
  }

  state.todos = todos;
  state.currentMessageId = messageId;

  // Remove existing todo container
  const existingContainer = document.querySelector('.todo-container');
  if (existingContainer) {
    existingContainer.remove();
  }

  // Find the latest assistant message to append todos to
  const assistantMessages = document.querySelectorAll('.message.assistant');
  if (assistantMessages.length === 0) {
    console.log('[TODOS] No assistant message found to attach todos to');
    return;
  }

  const latestMessage = assistantMessages[assistantMessages.length - 1];
  const todoContainer = renderStructuredTodos(todos);
  if (todoContainer) {
    latestMessage.appendChild(todoContainer);
    console.log('[TODOS] Updated todos in UI', { count: todos.length, messageId });
  }
}

function renderModelIcon(selectedValue) {
  if (!el.modelIcon) return;
  if (!selectedValue) {
    el.modelIcon.src = '';
    el.modelIcon.style.display = 'none';
    el.modelIcon.title = '';
    return;
  }
  const model = state.models.find((m) => (m.id || m.name || m) === selectedValue);
  if (!model || !model.icon) {
    el.modelIcon.src = '';
    el.modelIcon.style.display = 'none';
    el.modelIcon.title = model ? (model.label || model.name || selectedValue) : '';
    return;
  }
  el.modelIcon.src = model.icon;
  el.modelIcon.alt = model.label || model.name || selectedValue;
  el.modelIcon.title = model.label || model.name || selectedValue;
  el.modelIcon.style.display = 'inline-block';
}

function updateModelSelectDisplay(selectedValue) {
  if (!el.modelSelectText) return;

  const model = state.models.find((m) => (m.id || m.name || m) === selectedValue);

  if (selectedValue === 'auto') {
    el.modelSelectText.textContent = 'Auto (admin managed)';
    if (el.modelSelectMultiplier) { el.modelSelectMultiplier.textContent = ''; el.modelSelectMultiplier.style.display = 'none'; }
    renderModelIcon(null);
  } else if (model) {
    el.modelSelectText.textContent = model.label || model.name || selectedValue;
    if (el.modelSelectMultiplier) { el.modelSelectMultiplier.textContent = `${model.multiplier || 1}x`; el.modelSelectMultiplier.style.display = 'inline-block'; }
    renderModelIcon(selectedValue);
  } else {
    el.modelSelectText.textContent = 'Select model';
    if (el.modelSelectMultiplier) { el.modelSelectMultiplier.textContent = ''; el.modelSelectMultiplier.style.display = 'none'; }
    renderModelIcon(null);
  }
}

function renderCustomDropdownOptions() {
  if (!el.modelSelectOptions) {
    console.warn('[renderCustomDropdownOptions] el.modelSelectOptions not found!');
    return;
  }
  el.modelSelectOptions.innerHTML = '';
  
  console.log(`[renderCustomDropdownOptions] Rendering ${state.models ? state.models.length : 0} models, current selection: ${state.selectedModelId || el.modelSelect.value}`);

  if (!state.models || state.models.length === 0) {
    const placeholder = document.createElement('div');
    placeholder.className = 'model-option disabled';
    placeholder.textContent = 'No models available';
    el.modelSelectOptions.appendChild(placeholder);
    return;
  }

  const currentSelection = state.selectedModelId || el.modelSelect.value;

  state.models.forEach((model) => {
    const option = document.createElement('div');
    option.className = 'model-option';
    option.dataset.value = model.id || model.name || model;
    option.setAttribute('role', 'option');

    // Add icon if available
    if (model.icon) {
      const icon = document.createElement('img');
      icon.src = model.icon;
      icon.alt = model.label || model.name || model.name;
      option.appendChild(icon);
    }

    // Add text
    const text = document.createElement('span');
    text.className = 'model-option-text';
    text.textContent = model.label || model.name || model.id || model;
    option.appendChild(text);

    // Add multiplier badge (e.g., "1x", "2x")
    const mult = document.createElement('span');
    mult.className = 'model-option-multiplier';
    mult.textContent = `${model.multiplier || 1}x`;
    mult.title = `Usage rate: ${model.multiplier || 1}x`;
    option.appendChild(mult);

    // Mark as selected if matches current selection
    const modelId = model.id || model.name || model;
    if (currentSelection === modelId) {
      option.classList.add('selected');
      console.log(`[renderCustomDropdownOptions] Marked model as selected: ${modelId}`);
    }

    // Click handler
    option.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      console.log(`[renderCustomDropdownOptions] Model clicked: ${modelId}`);
      selectModel(modelId);
    });

    el.modelSelectOptions.appendChild(option);
  });
}

// Flag to track if model selection is programmatic (not user-triggered)
let programmaticModelChange = false;

// Flag to track when user manually changed model (prevent server from overwriting)
// This persists longer to survive polling cycles - cleared when user stops interacting
let userJustChangedModel = false;
let userModelChangeTimer = null;

function markUserModelChange() {
  userJustChangedModel = true;
  // Clear any existing timer
  if (userModelChangeTimer) {
    clearTimeout(userModelChangeTimer);
    userModelChangeTimer = null;
  }
  // Set timer to clear the flag - this gives time for polling to complete
  userModelChangeTimer = setTimeout(() => {
    console.log(`[markUserModelChange] Timer expired, clearing userJustChangedModel (was true for 3s)`);
    userJustChangedModel = false;
    userModelChangeTimer = null;
  }, 3000); // 3 seconds to survive polling cycles
}

function selectModel(modelId) {
  // This is a user-triggered change via the custom dropdown (even though we dispatch a synthetic change event).
  console.log(`[selectModel] User selected model: ${modelId}, setting userJustChangedModel=true`);
  markUserModelChange();
  state.selectedModelId = modelId;

  programmaticModelChange = true;
  el.modelSelect.value = modelId;
  updateModelSelectDisplay(modelId);
  closeCustomDropdown();

  // Update selected state in options
  const options = el.modelSelectOptions.querySelectorAll('.model-option');
  options.forEach((opt) => {
    if (opt.dataset.value === modelId) {
      opt.classList.add('selected');
    } else {
      opt.classList.remove('selected');
    }
  });

  // Manually trigger change event to sync with server
  el.modelSelect.dispatchEvent(new Event('change'));

  // Reset flag after event is dispatched - but let the timer handle final clearing
  setTimeout(() => {
    programmaticModelChange = false;
  }, 100);
}

async function loadModels(cli = state.currentCli || 'opencode') {
  try {
    const previousSelection = state.selectedModelId || el.modelSelect?.value || '';
    const previousSignature = state.modelsSignature;

    state.currentCli = cli;
    if (el.cliSelect && el.cliSelect.value !== cli) el.cliSelect.value = cli;

    const data = await api(`/api/models?cli=${encodeURIComponent(cli)}`);
    const nextModels = data.models || [];
    const nextSignature = JSON.stringify(nextModels.map((m) => {
      if (!m) return { id: '', label: '', icon: '', mult: 1 };
      if (typeof m === 'string') return { id: m, label: m, icon: '', mult: 1 };
      const id = m.id || m.name || '';
      return {
        id,
        label: m.label || m.name || id,
        icon: m.icon || '',
        mult: m.multiplier || 1,
      };
    }));

    state.models = nextModels;
    state.modelsSignature = nextSignature;

    const selectionToRestore = state.selectedModelId || el.modelSelect?.value || previousSelection;

    console.log(`[loadModels] Loaded ${state.models.length} models for CLI "${cli}"`, {
      modelsChanged: previousSignature !== nextSignature,
      previousSelection,
      selectionToRestore,
      modelsCount: state.models.length,
    });

    el.modelSelect.innerHTML = '';

    const optionValues = new Set();

    // For free plans, populate custom dropdown for preview but keep select as "auto"
    if (isFreePlan()) {
      el.modelSelect.disabled = false;
      renderModelIcon(null);
      setStatus('');

      state.models.forEach((m) => {
        const option = document.createElement('option');
        option.value = m.id || m.name || m;
        option.textContent = m.label || m.name || m.id || m;
        if (m.icon) option.dataset.icon = m.icon;
        el.modelSelect.appendChild(option);
        optionValues.add(option.value);
      });

      // Try to keep whatever the user previously selected (e.g. during async reloads)
      // But don't overwrite if user just changed it manually
      if (selectionToRestore && optionValues.has(selectionToRestore) && !userJustChangedModel) {
        el.modelSelect.value = selectionToRestore;
      }

      if (state.models.length > 0) {
        renderCustomDropdownOptions();
      }

      // For free plans, don't call applyPlanModelLock() here to avoid overwriting user selection
      // The lock will be applied later when needed
      // Only update state.selectedModelId if user hasn't just changed it
      if (!userJustChangedModel) {
        state.selectedModelId = el.modelSelect.value || state.selectedModelId;
        updateModelSelectDisplay(state.selectedModelId);
      } else {
        // User just changed model, ensure display is in sync
        updateModelSelectDisplay(el.modelSelect.value || state.selectedModelId);
      }
      updateUsageProgressBar();
      return;
    }

    if (!state.models.length) {
      el.modelSelect.disabled = true;
      state.selectedModelId = null;
      renderModelIcon(null);
      setStatus('No models configured. Ask an admin to add models in the admin panel.');
      applyPlanModelLock();
      updateUsageProgressBar();
      return;
    }

    el.modelSelect.disabled = false;

    state.models.forEach((m) => {
      const option = document.createElement('option');
      option.value = m.id || m.name || m;
      option.textContent = m.label || m.name || m.id || m;
      if (m.icon) option.dataset.icon = m.icon;
      el.modelSelect.appendChild(option);
      optionValues.add(option.value);
    });

    const activeSessionModel = state.currentSessionId
      ? (state.sessions.find(s => s.id === state.currentSessionId)?.model || '')
      : '';

    const preferred = selectionToRestore || activeSessionModel;

    // Don't overwrite user's selection if they just changed it
    if (userJustChangedModel) {
      console.log(`[loadModels] User just changed model (userJustChangedModel=true), preserving current selection: ${el.modelSelect.value}`);
      // Still update state.selectedModelId to match current UI
      state.selectedModelId = el.modelSelect.value || state.selectedModelId;
    } else if (preferred && optionValues.has(preferred)) {
      el.modelSelect.value = preferred;
    } else if (activeSessionModel && optionValues.has(activeSessionModel)) {
      el.modelSelect.value = activeSessionModel;
    } else if (state.models.length > 0) {
      el.modelSelect.value = state.models[0].id || state.models[0].name || state.models[0] || 'default';
    }

    // Preserve the user's model selection across reloads
    const finalSelection = el.modelSelect.value || state.selectedModelId;
    state.selectedModelId = finalSelection;

    // Update the display first, then render options to ensure synchronization
    updateModelSelectDisplay(finalSelection);
    renderCustomDropdownOptions();

    setStatus('');
    applyPlanModelLock();
    updateUsageProgressBar();
  } catch (error) {
    setStatus(`Model load failed: ${error.message}`);
    state.models = [];
    state.modelsSignature = null;
    state.selectedModelId = null;
    el.modelSelect.innerHTML = '';
    const opt = document.createElement('option');
    opt.value = '';
    opt.textContent = 'No models available';
    opt.disabled = true;
    opt.selected = true;
    el.modelSelect.appendChild(opt);
    el.modelSelect.disabled = true;
    renderModelIcon(null);
    applyPlanModelLock();
    updateUsageProgressBar();
  }
}

// Load sessions list (builder-specific) with retry logic and detailed logging
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 1000;

async function loadSessionsWithRetry(attempt = 1) {
  console.log(`[BUILDER-SESSIONS] loadSessionsWithRetry called, attempt ${attempt}/${MAX_RETRIES}`);
  try {
    console.log(`[BUILDER-SESSIONS] Loading sessions from API... (attempt ${attempt})`);
    const data = await api('/api/sessions');
    console.log(`[BUILDER-SESSIONS] Sessions API response:`, { 
      sessionCount: data.sessions?.length || 0,
      attempt,
      timestamp: new Date().toISOString()
    });
    state.sessions = data.sessions || [];
    state.sessionsLoaded = true;

    // Adopt CLI from sessions if not set
    if (state.sessions.length && !state.currentCli) {
      state.currentCli = state.sessions[0].cli || 'opencode';
      if (el.cliSelect) el.cliSelect.value = state.currentCli;
      console.log(`[BUILDER-SESSIONS] Adopted CLI from first session:`, state.currentCli);
    }

    renderSessions();
    console.log(`[BUILDER-SESSIONS] Sessions rendered, count:`, state.sessions.length);

    // If no current session, select the first available session
    if (!state.currentSessionId && state.sessions.length) {
      console.log(`[BUILDER-SESSIONS] No current session, selecting first available:`, state.sessions[0].id);
      await selectSession(state.sessions[0].id);
    } else if (!state.currentSessionId && !state.sessions.length) {
      console.log(`[BUILDER-SESSIONS] No sessions available after loading`);
      // Don't create a session here - let the sendMessage function handle that
    }
    // If state.currentSessionId is already set, use it (this is the normal case for plugin builder)

    return { success: true, sessionCount: state.sessions.length };
  } catch (err) {
    console.error(`[BUILDER-SESSIONS] Failed to load sessions (attempt ${attempt}/${MAX_RETRIES}):`, {
      error: err.message,
      stack: err.stack,
      timestamp: new Date().toISOString()
    });

    if (attempt < MAX_RETRIES) {
      const delayMs = BASE_DELAY_MS * Math.pow(2, attempt - 1); // Exponential backoff
      console.log(`[BUILDER-SESSIONS] Retrying in ${delayMs}ms...`);
      await new Promise(resolve => setTimeout(resolve, delayMs));
      return loadSessionsWithRetry(attempt + 1);
    }

    // All retries exhausted - don't block initialization, but log the failure
    console.error(`[BUILDER-SESSIONS] All ${MAX_RETRIES} retries exhausted, proceeding with empty sessions`);
    state.sessions = state.sessions || [];
    state.sessionsLoaded = true;
    return { success: false, sessionCount: 0, error: err.message };
  }
}

// Legacy wrapper for backward compatibility
async function loadSessions() {
  return loadSessionsWithRetry(1);
}

// Expose for builder.html
window.loadSessions = loadSessions;

// Load current session data (fetch only the single session to avoid redundant work)
async function loadCurrentSession() {
  if (!state.currentSessionId) return;
  try {
    const { session } = await api(`/api/sessions/${state.currentSessionId}`);

    // Update sessions array with fresh session data
    const idx = state.sessions.findIndex((s) => s.id === session.id);
    if (idx >= 0) state.sessions[idx] = session;
    else state.sessions.push(session);

    renderSessionMeta(session);
    renderMessages(session);

    // Set up streaming for any running messages
    const running = (session.messages || []).filter((m) => m.status === 'running' || m.status === 'queued');
    running.forEach(msg => {
      if (!state.activeStreams.has(msg.id)) {
        streamMessage(session.id, msg.id);
      }
    });

    // Adjust polling
    if (running.length > 0) setPollingInterval(2000);
    else setPollingInterval(5000);

    return session;
  } catch (error) {
    setStatus(error.message);
    return null;
  }
}

function restorePlanStateFromSession(session) {
  if (!session) return;

  const planMessages = Array.isArray(session.messages)
    ? session.messages.filter((m) => m.phase === 'plan')
    : [];
  const latestPlan = planMessages[planMessages.length - 1];

  // Only restore lastPlanText from session if not already set in persisted state
  if (!builderState.lastPlanText) {
    if (session.planSummary) {
      builderState.lastPlanText = session.planSummary;
    } else if (latestPlan) {
      builderState.lastPlanText = latestPlan.reply || latestPlan.partialOutput || latestPlan.content || '';
    }
  }

  // Only restore lastUserRequest from session if not already set in persisted state
  if (!builderState.lastUserRequest) {
    if (session.planUserRequest) {
      builderState.lastUserRequest = session.planUserRequest;
    } else if (latestPlan?.content) {
      builderState.lastUserRequest = latestPlan.content;
    }
  }

  // Only restore planApproved from session if:
  // 1. Not already true in persisted state (don't downgrade)
  // 2. Session explicitly indicates plan is approved
  if (!builderState.planApproved && typeof session.planApproved !== 'undefined') {
    builderState.planApproved = !!session.planApproved;
  }
}

function applyEntryMode(session) {
  if (!session) return;
  const isUploaded = session.entryMode === 'opencode' || session.source === 'upload';
  
  // For uploaded apps, always use build mode
  if (isUploaded) {
    builderState.mode = 'build';
    builderState.planApproved = true;
    state.currentCli = 'opencode';
    if (el.cliSelect) el.cliSelect.value = 'opencode';
    updateBuildModeUI('build');
    return;
  }
  
  // Check if app has EVER had build messages sent (plan was approved and executed)
  // This means any session with this appId has messages with cli='opencode'
  const appHasBeenBuilt = state.sessions.some(s => 
    s.appId === session.appId && 
    s.messages && 
    s.messages.some(m => m.cli === 'opencode')
  );
  
  // Set mode based on app build history to ensure correct behavior when switching
  // between apps (ignoring persisted state which may be stale from previous app)
  if (appHasBeenBuilt) {
    // App has been built before - all chats should use build mode
    builderState.mode = 'build';
    builderState.planApproved = true;
    updateBuildModeUI('build');
  } else {
    // App has never been built - start in plan mode
    builderState.mode = 'plan';
    builderState.planApproved = false;
    updateBuildModeUI('plan');
  }
}

async function selectSession(id) {
  console.log('[BUILDER] Selecting session:', id);
  state.currentSessionId = id;
  await selectSessionById(id);
}

// Expose for builder.html
window.selectSessionById = async function(id) {
  console.log('[BUILDER] selectSessionById called with ID:', id);
  console.log('[BUILDER] Available sessions:', state.sessions.map(s => ({ id: s.id, title: s.title })));
  console.log('[BUILDER] Current session ID before selection:', state.currentSessionId);
  console.log('[BUILDER] Chat area exists:', !!el.chatArea, 'in DOM:', el.chatArea && document.contains(el.chatArea));
  
  const session = state.sessions.find(s => s.id === id);
  if (!session) {
    console.warn('[BUILDER] Session not found for ID:', id);
    throw new Error('Session not found');
  }
  state.currentSessionId = session.id;
  
  console.log('[BUILDER] Session selected by ID:', { id: session.id, appId: session.appId, title: session.title });
  state.currentCli = session.cli || 'opencode';
  if (el.cliSelect) el.cliSelect.value = state.currentCli;
  
  try {
    await loadModels(state.currentCli);
  } catch (err) {
    console.warn('[BUILDER] Failed to load models:', err);
  }
  
  restorePlanStateFromSession(session);
  applyEntryMode(session);
  
  console.log('[BUILDER] About to refresh current session from server...');
  // Refresh session data from server and get fresh data
  const freshSession = await refreshCurrentSession();
  
  console.log('[BUILDER] Session refresh complete, freshSession:', freshSession ? 'yes' : 'no');
  
  // Ensure that UI is updated with the new session
  if (freshSession) {
    console.log('[BUILDER] Rendering UI with fresh session data');
    renderSessionMeta(freshSession);
    renderMessages(freshSession);
    // Wait for content to fully render before scrolling
    // Use multiple delays to catch content that loads asynchronously
    setTimeout(() => scrollChatToBottom(), 100);
    setTimeout(() => scrollChatToBottom(), 500);
    setTimeout(() => scrollChatToBottom(), 1000);
  } else {
    // If refresh failed, use the local session data instead
    console.warn('[BUILDER] Using local session data for UI render');
    renderSessionMeta(session);
    renderMessages(session);
    // Wait for content to fully render before scrolling
    setTimeout(() => scrollChatToBottom(), 100);
    setTimeout(() => scrollChatToBottom(), 500);
    setTimeout(() => scrollChatToBottom(), 1000);
  }
  
  // Update URL to reflect the new session (but don't reload page)
  const url = new URL(window.location);
  url.searchParams.set('session', session.id);
  window.history.replaceState({}, '', url);
  console.log('[BUILDER] Updated URL to:', url.href);
  
  console.log('[BUILDER] Session selection complete, new currentSessionId:', state.currentSessionId);
  return session;
};

// Expose for builder.html
window.selectSession = selectSession;

// Set up SSE stream for a message
function streamMessage(sessionId, messageId) {
  // Close existing stream if any
  if (state.activeStreams.has(messageId)) {
    const existing = state.activeStreams.get(messageId);
    existing.close();
    state.activeStreams.delete(messageId);
  }

  const url = `/api/sessions/${sessionId}/messages/${messageId}/stream`;
  const eventSource = new EventSource(url);

  eventSource.onopen = () => {
    console.log('SSE stream opened for message', messageId);
  };

  eventSource.onmessage = async (event) => {
    try {
      const data = JSON.parse(event.data);

      // Update the session with streaming data
      const session = state.sessions.find(s => s.id === sessionId);
      if (!session) return;

      const message = session.messages.find(m => m.id === messageId);
      if (!message) return;

      if (data.type === 'server-restart') {
        // Server is restarting, notify user and prepare for reconnection
        message.status = 'queued';
        setStatus('Server restarting, your session will be restored...');
        renderMessages(session);
        // Keep the connection open to allow automatic reconnection
        return;
      } else if (data.type === 'start') {
        message.status = 'running';
        setStatus('OpenCode is responding...');
        startUsagePolling(); // Start aggressive polling when OpenCode starts
        // Keep loading indicator spinning - don't hide when OpenCode starts
        // Clear todos when a new message starts
        clearTodos();
      } else if (data.type === 'chunk') {
        // Update partial output immediately
        message.partialOutput = data.filtered || data.partialOutput || data.content;
        message.outputType = data.outputType;
        message.partialUpdatedAt = data.timestamp;
        message.status = 'running';

        // Keep loading indicator spinning during streaming - don't hide on first chunk
        // Update loading indicator text to show we're now building
        const loadingIndicator = document.getElementById('loading-indicator');
        if (loadingIndicator) {
          const statusText = loadingIndicator.querySelector('.loading-status-text');
          const detailText = loadingIndicator.querySelector('.loading-detail-text');
          if (statusText && statusText.textContent === 'Starting build process') {
            statusText.textContent = 'Building plugin';
          }
          if (detailText) {
            detailText.textContent = 'Generating code and files...';
          }
        }

        // Re-render messages to show new content immediately
        renderMessages(session);
        setStatus('Streaming response...');
      } else if (data.type === 'todos') {
        // Handle todo updates from OpenCode
        if (data.todos && Array.isArray(data.todos)) {
          message.todos = data.todos;
          updateTodos(data.todos, messageId);
          console.log('[TODOS] Received todo update via SSE', { count: data.todos.length, messageId });
        }
      } else if (data.type === 'health') {
        // Sync status from server heartbeat
        if (data.status && message.status !== data.status) {
          console.log('Syncing message status from health event', { 
            messageId, 
            oldStatus: message.status, 
            newStatus: data.status 
          });
          message.status = data.status;
          renderMessages(session);
        }
      } else if (data.type === 'complete') {
        message.reply = data.content;
        message.status = 'done';
        message.finishedAt = data.timestamp;
        message.outputType = data.outputType;
        message.opencodeExitCode = data.exitCode;
        eventSource.close();
        state.activeStreams.delete(messageId);

        // Clear todos when message completes (to start fresh for next message)
        clearTodos();
        if (message.todos && Array.isArray(message.todos)) {
          message.todos = [];
        }

        renderMessages(session);
        setStatus('Complete');

        // Update usage meter immediately when opencode completes
        stopUsagePolling(); // Stop aggressive polling

        // Immediate usage update for better UX
        await loadUsageSummary().catch(err => {
          console.warn('[USAGE] Immediate usage update failed, will retry:', err.message);
        });

        // Final update after delay to ensure server persistence
        setTimeout(() => {
          loadUsageSummary().catch(err => {
            console.warn('[USAGE] Final usage update failed:', err.message);
          });
        }, 2000);

        // Restart polling after message completes to keep usage meter updating
        startUsagePolling();

        // Hide loading indicator only when message is confirmed as done
        if (message.status === 'done') {
          hideLoadingIndicator();
        }

        // Auto-scroll to bottom after message completes
        scrollChatToBottom();

        // Update session list (no-op in builder)
        renderSessions();
      } else if (data.type === 'error') {
        message.error = data.error || 'Unknown error';
        message.reply = data.content || message.partialOutput || '';
        message.status = 'error';
        message.finishedAt = data.timestamp;
        message.outputType = data.outputType;
        message.opencodeExitCode = data.exitCode;

        eventSource.close();
        state.activeStreams.delete(messageId);

        // Clear todos on error
        clearTodos();
        if (message.todos && Array.isArray(message.todos)) {
          message.todos = [];
        }

        renderMessages(session);
        scrollChatToBottom();

        if (!message.isBackgroundContinuation) {
          setStatus('Error: ' + (data.error || 'Unknown error'));
        }

        stopUsagePolling();

        // Update usage on error too
        await loadUsageSummary().catch(err => {
          console.warn('[USAGE] Usage update after error failed:', err.message);
        });
        setTimeout(() => {
          loadUsageSummary().catch(err => {
            console.warn('[USAGE] Final usage update after error failed:', err.message);
          });
        }, 2000);

        // Restart polling after message completes/errors to keep usage meter updating
        startUsagePolling();

        // Hide loading indicator only when message is confirmed as error
        if (message.status === 'error') {
          hideLoadingIndicator();
        }
        renderSessions();
      }
    } catch (err) {
      console.error('Failed to parse SSE message', err);
    }
  };

  eventSource.onerror = async (err) => {
    console.error('SSE error', err);
    eventSource.close();
    state.activeStreams.delete(messageId);

    // Check if server is restarting by examining message status
    const session = state.sessions.find(s => s.id === sessionId);
    const message = session?.messages.find(m => m.id === messageId);
    
    if (message && message.status === 'queued') {
      // Check if this is a plan message (don't set error for plan messages, just log)
      const isPlanMessage = message.phase === 'plan' || message.cli !== 'opencode';
      
      if (isPlanMessage) {
        // For plan messages, just log and don't modify the message
        console.log('[SSE] Plan message encountered server restart, preserving original content', { messageId, sessionId });
        hideLoadingIndicator();
        setStatus('Service temporarily unavailable. Please try again in a moment.');
        return;
      }
      
      // For OpenCode messages, poll for reconnection
      console.log('Server restart detected, attempting reconnection...', { messageId, sessionId });
      let reconnectAttempts = 0;
      const maxReconnectAttempts = 30; // 30 seconds max
      const reconnectInterval = setInterval(async () => {
        reconnectAttempts++;
        try {
          const { session: freshSession } = await api(`/api/sessions/${sessionId}`);
          const freshMessage = freshSession?.messages?.find(m => m.id === messageId);
          
          if (freshMessage && freshMessage.status !== 'queued') {
            clearInterval(reconnectInterval);
            const idx = state.sessions.findIndex(s => s.id === sessionId);
            if (idx !== -1) state.sessions[idx] = freshSession;
            renderSessionMeta(freshSession);
            renderMessages(freshSession);
            scrollChatToBottom();
            setStatus('Reconnected to server');
            console.log('Successfully reconnected after server restart');
          }
        } catch (e) {
          // Server still down
          if (reconnectAttempts >= maxReconnectAttempts) {
            clearInterval(reconnectInterval);
            if (message) {
              message.status = 'error';
              message.error = 'Server restart took too long. Please refresh and try again.';
              renderMessages(session);
              scrollChatToBottom();
            }
            setStatus('Server reconnection failed');
          }
        }
      }, 1000);
      return;
    }

    // When SSE connection fails, determine if it's a user action or server issue
    // Check if page is visible - if not, likely user left page
    const pageVisible = !document.hidden;
    const userLeftPage = !pageVisible;
    
    console.log('SSE connection failed', {
      messageId,
      sessionId,
      pageVisible,
      userLeftPage,
      messageStatus: message?.status
    });

    // Stop usage polling and update usage for non-restart failures
    stopUsagePolling();
    await loadUsageSummary().catch(err => {
      console.warn('[USAGE] Usage update after SSE error failed:', err.message);
    });
    // Restart polling to keep usage meter updated
    startUsagePolling();

    // First, refresh the session to get the latest state from the server
    // This handles the case where the message actually completed but the
    // client missed the final event (e.g., user slept their device)
    (async () => {
      try {
        const { session: freshSession } = await api(`/api/sessions/${sessionId}`);
        const freshMessage = freshSession?.messages?.find(m => m.id === messageId);

        if (freshMessage && (freshMessage.status === 'done' || freshMessage.status === 'error')) {
          console.log('Message completed on server, displaying final state', {
            messageId,
            status: freshMessage.status,
            exitCode: freshMessage.opencodeExitCode,
            tokenExtractionFailed: freshMessage.tokenExtractionFailed,
            potentiallyIncomplete: freshMessage.potentiallyIncomplete
          });
          // Message is actually done, just refresh to show the final state
          const idx = state.sessions.findIndex(s => s.id === sessionId);
          if (idx !== -1) state.sessions[idx] = freshSession;
          renderSessionMeta(freshSession);
          renderMessages(freshSession);

          // Hide loading indicator only when message is confirmed as done or error
          if (freshMessage.status === 'done' || freshMessage.status === 'error') {
            hideLoadingIndicator();
          }

          // Detect server-side errors that need recovery
          // This includes:
          // 1. Token extraction failure (indicates server-side issue)
          // 2. Message marked as potentially incomplete
          // 3. Actual OpenCode errors with non-zero exit codes
          const tokenExtractionFailed = freshMessage.tokenExtractionFailed || false;
          const potentiallyIncomplete = freshMessage.potentiallyIncomplete || false;
          const partialOutputLength = freshMessage.partialOutput?.length || 0;
          const hasSubstantialOutput = partialOutputLength > 500;
          const isTinyOutput = partialOutputLength > 0 && partialOutputLength <= 50;
          const connectionErrorCodes = [null, undefined, 'ECONNRESET', 'ETIMEDOUT', 'ENOTCONN'];
          const isConnectionError = connectionErrorCodes.includes(freshMessage.opencodeExitCode) ||
                                    (typeof freshMessage.opencodeExitCode === 'string' &&
                                     freshMessage.opencodeExitCode.includes('timeout'));

          // Check if this is a server-side error that needs continuation
          // Server-side errors (token extraction failure) should trigger continuation even if page is not visible
          // because server-side processing continues independently of client visibility
          const isServerError = tokenExtractionFailed || potentiallyIncomplete;
          const isOpencodeError = freshMessage.status === 'error' &&
                                freshMessage.opencodeExitCode &&
                                freshMessage.opencodeExitCode !== 0;

          if ((isServerError && !hasSubstantialOutput) ||
              (pageVisible && isOpencodeError && !isConnectionError && !hasSubstantialOutput) ||
              (pageVisible && isTinyOutput && !isConnectionError)) {
            console.log('Server-side error detected, triggering automatic continuation', {
              messageId,
              status: freshMessage.status,
              isServerError,
              tokenExtractionFailed,
              potentiallyIncomplete,
              isOpencodeError,
              exitCode: freshMessage.opencodeExitCode,
              partialOutputLength,
              isTinyOutput,
              hasSubstantialOutput,
              isConnectionError,
              pageVisible
            });
            // Only trigger continuation if page is visible to avoid UI issues when user left
            if (pageVisible) {
              setTimeout(() => handleEarlyTermination(sessionId, messageId), 500);
            }
          } else if (!pageVisible) {
            console.log('Page not visible, skipping automatic UI actions (server-side processing continues)', {
              messageId,
              pageVisible,
              tokenExtractionFailed,
              potentiallyIncomplete
            });
          }
          return;
        }

        // If message is still running on server, just poll - don't auto-retry
        console.log('Message still running on server, will continue polling', {
          messageId,
          status: freshMessage?.status
        });
        setTimeout(() => refreshCurrentSession(), 1000);
      } catch (e) {
        console.error('Failed to check server state, will poll for updates', e);
        setTimeout(() => refreshCurrentSession(), 1000);
      }
    })();
  };

  state.activeStreams.set(messageId, eventSource);
}

async function handleEarlyTermination(sessionId, messageId) {
  const session = state.sessions.find(s => s.id === sessionId);
  const message = session?.messages?.find(m => m.id === messageId);

  if (!message) {
    console.log('handleEarlyTermination: message not found', { messageId });
    return;
  }

  // Allow handling of messages marked as 'done' but potentially incomplete
  // This catches cases where token extraction failed but message was still marked done
  const isNormalCompletion = message.status === 'done' && !message.potentiallyIncomplete;
  const isError = message.status === 'error';

  if (isNormalCompletion || isError) {
    console.log('handleEarlyTermination: skipping - message is already completed or in error state', {
      messageId,
      status: message.status,
      potentiallyIncomplete: message.potentiallyIncomplete
    });
    return;
  }

  console.log('Handling early termination', {
    messageId,
    partialLength: message.partialOutput?.length,
    status: message.status,
    potentiallyIncomplete: message.potentiallyIncomplete,
    tokenExtractionFailed: message.tokenExtractionFailed
  });

  // Don't trigger client-side model fallback - let the server handle fallback through its proper chain
  // The server fallback sequence: preferred model -> all configured models -> default -> ultimate backup model
  // Client-side fallback was bypassing this chain and jumping straight to the ultimate backup model
  console.log('Early termination detected, letting server handle fallback', {
    messageId,
    status: message.status
  });
  setTimeout(() => refreshCurrentSession(), 1000);
}

function checkFallbackCriteria(message) {
  // Only trigger fallback on actual opencode errors with non-zero exit codes
  // Don't trigger on partial output or SSE disconnections
  const isActualError = message.status === 'error';
  const hasErrorExitCode = message.opencodeExitCode && message.opencodeExitCode !== 0;

  // Check for token extraction failure (server-side error)
  // This indicates a problem with streaming/tracking, not with user leaving the page
  const tokenExtractionFailed = message.tokenExtractionFailed || false;
  const potentiallyIncomplete = message.potentiallyIncomplete || false;
  const isServerError = tokenExtractionFailed || potentiallyIncomplete;

  // Check if message has substantial output (indicates model was working fine)
  const partialOutputLength = message.partialOutput?.length || 0;
  const hasSubstantialOutput = partialOutputLength > 500;
  const isTinyOutput = partialOutputLength > 0 && partialOutputLength <= 50;

  // Don't trigger if message has substantial output - model was working fine
  if (hasSubstantialOutput) {
    console.log('Skipping fallback - message has substantial output', {
      messageId: message.id,
      partialOutputLength,
      status: message.status
    });
    return false;
  }

  // Check for connection errors that shouldn't trigger fallback
  const connectionErrorCodes = [null, undefined, 'ECONNRESET', 'ETIMEDOUT', 'ENOTCONN'];
  const isConnectionError = connectionErrorCodes.includes(message.opencodeExitCode) ||
                            (typeof message.opencodeExitCode === 'string' &&
                             message.opencodeExitCode.includes('timeout'));

  // Don't trigger on connection errors (user left page, network issues)
  if (isConnectionError) {
    console.log('Skipping fallback - connection error detected', {
      messageId: message.id,
      exitCode: message.opencodeExitCode,
      isConnectionError
    });
    return false;
  }

  console.log('Checking fallback criteria', {
    messageId: message.id,
    status: message.status,
    exitCode: message.opencodeExitCode,
    isActualError,
    hasErrorExitCode,
    tokenExtractionFailed,
    potentiallyIncomplete,
    isServerError,
    hasSubstantialOutput,
    partialOutputLength,
    isTinyOutput,
    isConnectionError
  });

  // Trigger fallback for:
  // 1. Actual opencode errors with non-zero exit codes
  // 2. Server-side token extraction failures (indicates streaming issue)
  // 3. Tiny outputs (<= 50 chars) which commonly indicate incomplete responses
  return (isActualError && hasErrorExitCode) || isServerError || isTinyOutput;
}

async function triggerModelFallback(messageId, session) {
  const message = session.messages.find(m => m.id === messageId);
  if (!message) return;

  if (message.failoverAttempts?.length >= 2) {
    console.log('Max fallback attempts reached', { messageId });
    setTimeout(() => refreshCurrentSession(), 1000);
    return;
  }

  const currentModel = message.model;
  const backupModel = getBackupModel(currentModel);

  if (!backupModel) {
    console.log('No backup model available', { currentModel });
    setTimeout(() => refreshCurrentSession(), 1000);
    return;
  }

  console.log('Switching to backup model', {
    messageId,
    from: currentModel,
    to: backupModel
  });

  const continuationContent = 'continue to ensure that the previous request is fully completed';

  try {
    // Preserve the opencodeSessionId to continue in the same session
    // Priority order: session.opencodeSessionId > message.opencodeSessionId > session.initialOpencodeSessionId
    const preservedSessionId = session?.opencodeSessionId || message.opencodeSessionId || session?.initialOpencodeSessionId;

    const payload = {
      content: continuationContent,
      displayContent: '',
      model: backupModel,
      cli: 'opencode',
      isContinuation: true,
      isBackgroundContinuation: true,
      originalMessageId: messageId
    };

    // Preserve the opencodeSessionId for session continuity
    if (preservedSessionId) {
      payload.opencodeSessionId = preservedSessionId;
      console.log('[FALLBACK] Preserving opencodeSessionId:', preservedSessionId);
    } else {
      console.warn('[FALLBACK] No opencodeSessionId available for continuation');
    }
    
    // Preserve workspaceDir for file location continuity
    if (session.workspaceDir) {
      payload.workspaceDir = session.workspaceDir;
    }
    
    console.log('Triggering continuation with preserved context', {
      messageId,
      sessionId: session.id,
      opencodeSessionId: payload.opencodeSessionId,
      workspaceDir: payload.workspaceDir,
      model: backupModel
    });

    const response = await api(`/api/sessions/${session.id}/messages`, {
      method: 'POST',
      body: JSON.stringify(payload)
    });

    if (response.message) {
      streamMessage(session.id, response.message.id);
      message.status = 'superseded';
      message.supersededBy = response.message.id;
    }
  } catch (error) {
    console.error('Failed to trigger fallback', error);
    setTimeout(() => refreshCurrentSession(), 1000);
  }
}

async function handleServerCutOff(session, message) {
  // This function is called when a message transitions from 'running' to 'queued'
  // indicating that the server-side OpenCode session was cut off
  
  // Don't handle if message is already done or has substantial output
  if (message.status === 'done' || message.status === 'error') {
    console.log('[SERVER_CUTOFF] Message already finished, skipping', message.id);
    return;
  }
  
  // Check if message has substantial partial output (likely near completion)
  const partialOutputLength = (message.partialOutput || '').length;
  if (partialOutputLength > 1000) {
    console.log('[SERVER_CUTOFF] Message has substantial output, likely near completion', {
      messageId: message.id,
      partialOutputLength
    });
    return;
  }
  
  console.log('[SERVER_CUTOFF] Automatically continuing implementation after server cut off', {
    messageId: message.id,
    sessionId: session.id,
    partialOutputLength
  });
  
  // Preserve the opencodeSessionId to continue in the same session
  // Priority order: session.opencodeSessionId > message.opencodeSessionId > session.initialOpencodeSessionId
  const preservedSessionId = session?.opencodeSessionId || message.opencodeSessionId || session?.initialOpencodeSessionId;
  
  const payload = {
    content: 'continue to ensure that the previous request is fully completed',
    displayContent: '',
    model: message.model || session.model,
    cli: 'opencode',
    isContinuation: true,
    isBackgroundContinuation: true, // Don't show this to user
    isServerCutOffContinuation: true,
    originalMessageId: message.id
  };
  
  // Preserve the opencodeSessionId for session continuity
  if (preservedSessionId) {
    payload.opencodeSessionId = preservedSessionId;
    console.log('[SERVER_CUTOFF] Preserving opencodeSessionId:', preservedSessionId);
  } else {
    console.warn('[SERVER_CUTOFF] No opencodeSessionId available for continuation');
  }
  
  // Preserve workspaceDir for file location continuity
  if (session.workspaceDir) {
    payload.workspaceDir = session.workspaceDir;
  }
  
  try {
    const response = await api(`/api/sessions/${session.id}/messages`, {
      method: 'POST',
      body: JSON.stringify(payload)
    });
    
    if (response.message) {
      console.log('[SERVER_CUTOFF] Continuation message created', response.message.id);
      // Start streaming the continuation message
      streamMessage(session.id, response.message.id);
      // Mark original message as superseded
      message.status = 'superseded';
      message.supersededBy = response.message.id;
    }
  } catch (error) {
    console.error('[SERVER_CUTOFF] Failed to create continuation message', error);
  }
}

function getBackupModel(currentModel) {
  const normalize = (m) => (m && (m.id || m.name || m)) ? String(m.id || m.name || m).trim() : '';
  const configuredModels = (state.models || []).map(normalize).filter(Boolean);
  const current = (currentModel || '').trim();

  const preferredBackup = normalize(window.providerLimits?.opencodeBackupModel || '');
  
  // If we have a preferred backup model, use it if it's different from current
  // Don't require it to be in configured models since OpenCode CLI can access models directly
  if (preferredBackup && preferredBackup !== current) {
    return preferredBackup;
  }

  if (!configuredModels.length) return null;

  // If current model is auto/default or not in list, pick the first configured model
  if (!current || current === 'auto' || current === 'default' || !configuredModels.includes(current)) {
    return configuredModels[0];
  }

  // Pick the first different model as backup
  const fallback = configuredModels.find((m) => m !== current);
  return fallback || configuredModels[0];
}

async function refreshCurrentSession() {
  if (!state.currentSessionId) return;
  try {
    const { session } = await api(`/api/sessions/${state.currentSessionId}`);

    // Preserve optimistic "temp-" messages that may have been added locally
    const activeStatuses = new Set(['running', 'queued']);
    const completedStatuses = new Set(['done', 'error']);
    const old = state.sessions.find((s) => s.id === session.id);
    const oldStatuses = new Map();
    if (old && Array.isArray(old.messages)) {
      old.messages.forEach((msg) => {
        if (activeStatuses.has(msg.status)) {
          oldStatuses.set(msg.id, msg.status);
        }
      });
    }
    
    // Detect messages that transitioned from 'running' to 'queued' - this indicates server cut off
    const transitionedToQueued = [];
    if (old && Array.isArray(old.messages) && Array.isArray(session.messages)) {
      session.messages.forEach((newMsg) => {
        const oldMsg = old.messages.find(m => m.id === newMsg.id);
        if (oldMsg && oldMsg.status === 'running' && newMsg.status === 'queued') {
          console.log('[DETECTION] Message transitioned from running to queued - server cut off detected', {
            messageId: newMsg.id,
            oldStatus: oldMsg.status,
            newStatus: newMsg.status
          });
          transitionedToQueued.push(newMsg);
        }
      });
    }
    
    const tempMsgs = (old && Array.isArray(old.messages)) ? old.messages.filter(m => String(m.id).startsWith('temp-')) : [];
    if (tempMsgs.length) {
      session.messages = session.messages || [];
      const existingIds = new Set((session.messages || []).map((m) => m.id));
      // Append any temp messages that the server hasn't returned yet
      tempMsgs.forEach((m) => {
        if (!existingIds.has(m.id)) session.messages.push(m);
      });

      // De-duplicate: if server already returned a real (non-temp) message with the same content,
      // drop the temp message to avoid duplicates
      const realContents = new Set((session.messages || []).filter(m => !String(m.id).startsWith('temp-')).map(m => (m.displayContent || m.content || '').trim()));
      session.messages = (session.messages || []).filter(m => {
        if (String(m.id).startsWith('temp-')) {
          return !realContents.has((m.displayContent || m.content || '').trim());
        }
        return true;
      });

      // Ensure messages are time-ordered
      session.messages.sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
    }

    const idx = state.sessions.findIndex((s) => s.id === session.id);
    if (idx === -1) state.sessions.unshift(session);
    else state.sessions[idx] = session;
    renderSessions();
    renderSessionMeta(session);
    renderMessages(session);

    const completedMessage = oldStatuses.size > 0 && (session.messages || []).some((msg) => (
      oldStatuses.has(msg.id) && completedStatuses.has(msg.status)
    ));
    if (completedMessage) {
      loadUsageSummary().catch((err) => {
        console.warn('[USAGE] Usage update after completion failed:', err.message);
      });
    }
    
    // Handle messages that transitioned to queued - indicates server cut off
    if (transitionedToQueued.length > 0) {
      for (const msg of transitionedToQueued) {
        console.log('[DETECTION] Handling server cut off for message', msg.id);
        await handleServerCutOff(session, msg);
      }
    }

    // Set up streaming for any running messages that don't have streams yet
    const running = (session.messages || []).filter((m) => m.status === 'running' || m.status === 'queued');
    running.forEach(msg => {
      if (!state.activeStreams.has(msg.id)) {
        streamMessage(session.id, msg.id);
      }
    });

    // Adjust polling - slower when using SSE
    if (running.length > 0) setPollingInterval(2000);
    else setPollingInterval(5000);

    // Return the session so callers can access fresh data
    return session;
  } catch (error) {
    setStatus(error.message);
    console.error('Failed to refresh session:', error);
    return null;
  }
}

// Expose for builder.html
window.refreshCurrentSession = refreshCurrentSession;

function setPollingInterval(intervalMs) {
  if (!intervalMs) return;
  if (state.pollingInterval === intervalMs) return;
  if (state.pollingTimer) clearInterval(state.pollingTimer);
  state.pollingInterval = intervalMs;
  state.pollingTimer = setInterval(refreshCurrentSession, state.pollingInterval);
}

async function createSession(options = {}) {
  const cli = el.cliSelect ? el.cliSelect.value : state.currentCli || 'opencode';
  state.currentCli = cli;
  // Sessions are used for both planning (OpenRouter) and building (OpenCode).
  // Planning should work even before a model is selected/configured, so fall back
  // to a server-side default model instead of blocking session creation.
  const model = state.selectedModelId || (el.modelSelect && el.modelSelect.value) || (isFreePlan() ? 'auto' : 'default');
  state.selectedModelId = model || state.selectedModelId;
  const payload = { model, cli };
  
  // When creating a new chat within an existing app, preserve the app title
  if (options.appId && options.reuseAppId) {
    const currentSession = state.sessions.find(s => s.id === state.currentSessionId);
    if (currentSession && currentSession.title) {
      payload.title = currentSession.title;
    }
  }
  
  if (options.title) payload.title = options.title;
  if (options.appId) {
    payload.appId = options.appId;
    payload.reuseAppId = true;
  }
  let session;
  try {
    const data = await api('/api/sessions', {
      method: 'POST',
      body: JSON.stringify(payload),
    });
    session = data.session;
  } catch (err) {
    setStatus(err.message || 'Unable to create app');
    throw err;
  }
  
  // Clear builder state for new session (this also clears persisted state)
  if (typeof window.clearBuilderState === 'function') {
    window.clearBuilderState();
  } else {
    // Fallback if clearBuilderState not yet defined
    builderState.mode = 'plan';
    builderState.planApproved = false;
    builderState.lastUserRequest = '';
    builderState.lastPlanText = '';
  }
  
  state.sessions.unshift(session);
  renderSessions();
  // Render session meta immediately with newly created session
  renderSessionMeta(session);
  
  // Set current session ID directly instead of calling selectSession
  // This avoids refreshing from server which might not have the new session yet
  state.currentSessionId = session.id;
  
  // Set up CLI and model from the session
  state.currentCli = session.cli || 'opencode';
  if (el.cliSelect) el.cliSelect.value = state.currentCli;
  
  // Load models for the session's CLI
  await loadModels(state.currentCli);
  
  // Restore builder state from the new session
  restorePlanStateFromSession(session);
  applyEntryMode(session);
  
  // Clear the chat area for the new session
  if (el.chatArea) {
    el.chatArea.innerHTML = '<div class="muted empty-message">Send a message to start the conversation.</div>';
  }
  
  // Scroll to bottom of chat
  scrollChatToBottom();
}

// Expose for builder.html
window.createSession = createSession;

// Helper function to ensure we have a valid current session
async function ensureCurrentSession() {
  console.log('[BUILDER] ensureCurrentSession called:', { 
    currentSessionId: state.currentSessionId, 
    sessionsCount: state.sessions?.length || 0 
  });
  
  if (state.currentSessionId) {
    // Check if the current session still exists in our sessions array
    const existingSession = state.sessions?.find(s => s.id === state.currentSessionId);
    if (existingSession) {
      console.log('[BUILDER] Current session is valid:', state.currentSessionId);
      return existingSession;
    } else {
      console.log('[BUILDER] Current session ID exists but session not found in local array');
    }
  }
  
  // Try to use an existing session from the sessions array
  if (state.sessions && state.sessions.length > 0) {
    console.log('[BUILDER] Selecting first available session:', state.sessions[0].id);
    await selectSession(state.sessions[0].id);
    return state.sessions[0];
  }
  
  // No sessions exist, create a new one
  console.log('[BUILDER] No sessions available, creating new session');
  await createSession();
  return state.sessions[0];
}

// Expose for builder.html
window.ensureCurrentSession = ensureCurrentSession;

async function checkOpencodeStatus() {
  try {
    const status = await api('/api/opencode/status');
    state.opencodeStatus = status;

    if (!status.available) {
      setStatus(`Warning: OpenCode CLI not available - ${status.error || 'unknown error'}`);
    }

    return status;
  } catch (error) {
    console.error('Failed to check opencode status', error);
    return null;
  }
}

// Message sending prevention constants
const LAST_SENT_MESSAGE_KEY = 'builder_last_sent_message';
const LAST_SENT_MAX_AGE_MS = 5000; // Prevent resending same message within 5 seconds

function getLastSentMessage() {
  try {
    const raw = localStorage.getItem(LAST_SENT_MESSAGE_KEY);
    if (!raw) return null;
    const data = JSON.parse(raw);
    if (!data || !data.content) return null;
    // Check if cache is expired
    if (Date.now() - data.timestamp > LAST_SENT_MAX_AGE_MS) {
      localStorage.removeItem(LAST_SENT_MESSAGE_KEY);
      return null;
    }
    return data;
  } catch (e) {
    return null;
  }
}

function setLastSentMessage(content) {
  try {
    const data = {
      content: content,
      timestamp: Date.now()
    };
    localStorage.setItem(LAST_SENT_MESSAGE_KEY, JSON.stringify(data));
  } catch (e) {
    console.warn('Failed to set last sent message:', e);
  }
}

async function sendMessage() {
  const content = el.messageInput.value.trim();
  if (!content && !pendingAttachments.length) return;

  // Prevent duplicate sends: check if this exact message was recently sent
  const lastSent = getLastSentMessage();
  if (lastSent && lastSent.content === content) {
    console.log('[BUILDER] Ignoring duplicate message send attempt - message was recently sent');
    el.messageInput.value = '';
    clearMessageInputCache();
    return;
  }

  const displayContent = content;
  const cli = el.cliSelect ? el.cliSelect.value : (state.currentCli || 'opencode');
  state.currentCli = cli;

  el.messageInput.value = '';
  el.messageInput.style.height = 'auto';
  clearMessageInputCache();

  // Track this message as sent to prevent duplicates
  setLastSentMessage(content);

  const model = state.selectedModelId || el.modelSelect?.value || (isFreePlan() ? 'auto' : 'default');
  if (!model) {
    setStatus('Select a model configured by your admin');
    return;
  }

  const remainingTokens = state.usageSummary?.remaining || 0;
  if (remainingTokens <= 5000) {
    const modal = document.getElementById('token-limit-modal');
    if (modal) {
      modal.style.display = 'flex';
      if (el.miniSendBtn) el.miniSendBtn.disabled = false;
      return;
    }
  }

  // Set sending state and update button
  state.isSending = true;
  updateSendButtonState();

  // Ensure we have a valid current session before proceeding
  if (!state.currentSessionId) {
    try {
      if (state.sessions && state.sessions.length > 0) {
        console.log('[BUILDER] No current session ID, using existing session:', state.sessions[0].id);
        await selectSession(state.sessions[0].id);
      } else {
        console.log('[BUILDER] No sessions available, creating new session');
        await createSession();
      }
    } catch (err) {
      console.error('[BUILDER] Failed to establish session:', err);
      setStatus('Failed to establish session: ' + (err.message || 'Unknown error'));
      if (el.miniSendBtn) el.miniSendBtn.disabled = false;
      return;
    }
  }

  if (!state.currentSessionId) {
    console.error('[BUILDER] Still no session ID after session establishment attempts');
    setStatus('Unable to establish session. Please refresh and try again.');
    if (el.miniSendBtn) el.miniSendBtn.disabled = false;
    return;
  }

  builderState.lastUserRequest = builderState.lastUserRequest || content;

  const tempMessageId = 'temp-' + Date.now();
  const currentSession = state.sessions.find(s => s.id === state.currentSessionId);
  if (currentSession) {
    currentSession.messages = currentSession.messages || [];
    currentSession.messages.push({
      id: tempMessageId,
      content,
      displayContent,
      attachments: [...pendingAttachments],
      model,
      cli,
      status: 'queued',
      createdAt: new Date().toISOString()
    });
    renderMessages(currentSession);
  }

  setStatus('Sending...');
  showLoadingIndicator('building');

  try {
    let messageContent = content;

//    if (builderState.mode === 'build' && cli === 'opencode' && !content.includes('proceed with build')) {
//      const subsequentPromptTemplate = builderState.subsequentPrompt || '';
//      const userRequest = builderState.lastUserRequest || content;
//      const pluginSlug = (currentSession && currentSession.pluginSlug) || 'plugin-name';
//      const pluginName = (currentSession && currentSession.pluginName) || `Plugin Compass ${(currentSession && currentSession.title) || 'Plugin'}`;
//      const promptWithRequest = subsequentPromptTemplate.replace('{{USER_REQUEST}}', userRequest);
//      const promptWithSlug = promptWithRequest.replace(/{{PLUGIN_SLUG}}/g, pluginSlug).replace(/{{PLUGIN_NAME}}/g, pluginName);
//      messageContent = promptWithSlug + '\n\n' + content;
//    }

    const payload = {
      content: messageContent,
      displayContent,
      model,
      cli,
      attachments: pendingAttachments.length ? pendingAttachments : undefined,
      externalTestingEnabled: !!builderState.externalTestingEnabled // Send feature toggle state to server
    };
    // Preserve opencodeSessionId to continue in the same session
    if (currentSession && currentSession.opencodeSessionId) {
      payload.opencodeSessionId = currentSession.opencodeSessionId;
      console.log('[BUILDER] Preserving opencodeSessionId:', currentSession.opencodeSessionId);
    }

    const response = await api(`/api/sessions/${state.currentSessionId}/messages`, {
      method: 'POST',
      body: JSON.stringify(payload),
    });

    // Track the message ID being sent for potential cancellation
    if (response?.message?.id) {
      state.currentSendingMessageId = response.message.id;
      console.log('[BUILDER] Tracking message for cancellation:', response.message.id);
    }

    // Clear attachments after successful send
    pendingAttachments.length = 0;
    renderAttachmentPreview();

    // Start SSE streaming immediately so users see output without waiting for polling.
    if (response?.message?.id) {
      streamMessage(state.currentSessionId, response.message.id);
    } else {
      // No message id (unexpected) - check message status before hiding spinner
      setTimeout(() => {
        const session = state.sessions.find(s => s.id === state.currentSessionId);
        if (session) {
          const msg = session.messages.find(m => m.id === tempMessageId);
          if (msg && (msg.status === 'done' || msg.status === 'error')) {
            hideLoadingIndicator();
          } else if (!msg) {
            hideLoadingIndicator();
          }
        } else {
          hideLoadingIndicator();
        }
      }, 100);
      
      // Update usage even in unexpected cases
      loadUsageSummary().catch(err => {
        console.warn('[USAGE] Usage update after unexpected case failed:', err.message);
      });
    }

    await refreshCurrentSession();
  } catch (error) {
    setStatus(error.message || 'Failed to send');

    // Update usage on error
    loadUsageSummary().catch(err => {
      console.warn('[USAGE] Usage update after send error failed:', err.message);
    });

    // Hide loading indicator after checking message status
    setTimeout(() => {
      const session = state.sessions.find(s => s.id === state.currentSessionId);
      if (session) {
        const msg = session.messages.find(m => m.id === tempMessageId);
        if (msg && (msg.status === 'done' || msg.status === 'error')) {
          hideLoadingIndicator();
        } else if (!msg) {
          // Temp message was already removed, safe to hide
          hideLoadingIndicator();
        }
      } else {
        // No session found, safe to hide
        hideLoadingIndicator();
      }
    }, 100);

    // Remove the temp message on error
    if (currentSession) {
      currentSession.messages = (currentSession.messages || []).filter(m => m.id !== tempMessageId);
      renderMessages(currentSession);
    }
  } finally {
    // Reset sending state when message completes, is cancelled, or errors out
    // Only reset if we're not currently in a cancellation (cancelMessage handles that)
    if (state.isSending && state.currentSendingMessageId) {
      const session = state.sessions.find(s => s.id === state.currentSessionId);
      const messageId = state.currentSendingMessageId;
      const message = session?.messages.find(m => m.id === messageId);
      
      // Reset if message is done, errored, or cancelled
      if (!message || message.status === 'done' || message.status === 'error' || message.status === 'cancelled') {
        state.isSending = false;
        state.currentSendingMessageId = null;
        updateSendButtonState();
      }
    }
  }
}

// Cancel the current message being sent
async function cancelMessage() {
  if (!state.isSending || !state.currentSendingMessageId) {
    console.log('[CANCEL] No message to cancel');
    return;
  }

  const messageId = state.currentSendingMessageId;
  const sessionId = state.currentSessionId;
  
  console.log('[CANCEL] Cancelling message:', messageId);
  setStatus('Cancelling...');

  // Close any active SSE streams for this message
  if (state.activeStreams.has(messageId)) {
    const stream = state.activeStreams.get(messageId);
    stream.close();
    state.activeStreams.delete(messageId);
    console.log('[CANCEL] Closed SSE stream for message:', messageId);
  }

  // Find and update the message status
  const session = state.sessions.find(s => s.id === sessionId);
  if (session) {
    const message = session.messages.find(m => m.id === messageId);
    if (message) {
      message.status = 'cancelled';
      message.reply = message.reply || '(Cancelled by user)';
      message.cancelled = true;
      
      // Show undo/redo buttons for the cancelled message
      message.showUndoRedo = true;
      
      console.log('[CANCEL] Message marked as cancelled:', messageId);
    }
    
    // Remove temp messages
    const tempMessages = session.messages.filter(m => m.id.startsWith('temp-') && m.status === 'queued');
    tempMessages.forEach(tempMsg => {
      const idx = session.messages.indexOf(tempMsg);
      if (idx > -1) {
        session.messages.splice(idx, 1);
      }
    });
    
    renderMessages(session);
  }

  // Reset sending state
  state.isSending = false;
  state.currentSendingMessageId = null;
  
  // Reset send button
  updateSendButtonState();
  
  hideLoadingIndicator();
  setStatus('Message cancelled');
  
  // Refresh session to sync with server
  try {
    await refreshCurrentSession();
  } catch (err) {
    console.warn('[CANCEL] Failed to refresh session:', err.message);
  }
}

// Update send button appearance based on state
function updateSendButtonState() {
  const btn = el.miniSendBtn;
  if (!btn) return;

  if (state.isSending) {
    // Show cancel button (square icon)
    btn.innerHTML = `
      <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <rect x="6" y="6" width="12" height="12" rx="2" ry="2"></rect>
      </svg>
    `;
    btn.title = 'Cancel message';
    btn.classList.add('cancel-mode');
    btn.disabled = false;
  } else {
    // Show send button (paper plane icon)
    btn.innerHTML = `
      <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <line x1="22" y1="2" x2="11" y2="13"></line>
        <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
      </svg>
    `;
    btn.title = 'Send message';
    btn.classList.remove('cancel-mode');
    btn.disabled = false;
  }
}

// Handle send button click - either send or cancel
function handleSendButtonClick(e) {
  if (state.isSending) {
    // Cancel the current message
    cancelMessage();
  } else {
    // Check if we're in plan mode and call the appropriate handler
    if (builderState.mode === 'plan') {
      // Use handleSend from builder.html which handles plan mode
      if (typeof handleSend === 'function') {
        handleSend(e);
      } else {
        console.warn('[BUILDER] handleSend not available');
      }
    } else {
      // Send a new message in build mode
      sendMessage();
    }
  }
}

// Expose for builder.html
window.sendMessage = sendMessage;
window.cancelMessage = cancelMessage;
window.handleSendButtonClick = handleSendButtonClick;

function hookEvents() {
  if (el.newChat) {
  el.newChat.addEventListener('click', async () => {
      // Get current session's appId to create new chat within same app
      const currentSession = state.sessions.find(s => s.id === state.currentSessionId);
      let appIdToUse = currentSession?.appId || null;
      if (!appIdToUse) {
        // Fall back to any available appId from existing sessions
        for (const session of state.sessions) {
          if (session.appId) {
            appIdToUse = session.appId;
            break;
          }
        }
      }
      
      // Create new chat within the same app by passing appId with reuseAppId=true
      if (appIdToUse) {
        await createSession({ appId: appIdToUse, reuseAppId: true });
      } else {
        await createSession({});
      }
    });
  }

  if (el.historyBtn) {
    el.historyBtn.addEventListener('click', openHistoryModal);
  }

  if (el.historyClose) {
    el.historyClose.addEventListener('click', closeHistoryModal);
  }

  if (el.historyModal) {
    el.historyModal.addEventListener('click', (e) => {
      if (e.target === el.historyModal) closeHistoryModal();
    });
  }

  // sendBtn removed - only miniSendBtn is used in builder
  window.showUpgradeModal = function () {
    if (state.accountPlan === 'enterprise') {
      alert('You are already on the Enterprise plan with full access.');
      return;
    }
    const modal = document.getElementById('upgrade-modal');
    if (modal) {
      modal.style.display = 'flex';
    }
  };



  // Upload media button functionality
  if (el.uploadMediaBtn && el.uploadMediaInput) {
    console.log('Upload media elements found, attaching event listeners');
    el.uploadMediaBtn.addEventListener('click', (e) => {
      console.log('Upload media button clicked, isPaidPlanClient:', isPaidPlanClient());
      
      e.preventDefault();
      e.stopPropagation();
      
      // Reset input to allow re-uploading the same file
      el.uploadMediaInput.value = '';
      
      // Check if user is on free plan
      if (!isPaidPlanClient()) {
        showUpgradeModal();
        return;
      }
      
      // Check if model supports media
      if (!currentModelSupportsMedia()) {
        setStatus('This model does not support image uploads. Please select a different model that supports media.');
        return;
      }
      
      // For paid users with media-supporting models, trigger file input click
      console.log('Triggering file input click');
      el.uploadMediaInput.click();
    });
    
    el.uploadMediaInput.addEventListener('change', async () => {
      console.log('File input changed, files:', el.uploadMediaInput.files);
      const files = el.uploadMediaInput.files ? Array.from(el.uploadMediaInput.files) : [];
      
      // Reset input immediately to allow same file selection again
      el.uploadMediaInput.value = '';
      
      if (!files.length) return;
      if (!isPaidPlanClient()) {
        showUpgradeModal();
        return;
      }
      if (!currentModelSupportsMedia()) {
        setStatus('This model does not support image uploads. Please select a different model that supports media.');
        return;
      }
      setStatus('Preparing images...');
      el.miniSendBtn.disabled = true;
      try {
        for (const file of files.slice(0, 6)) {
          if (!file || !(file.type || '').startsWith('image/')) continue;
          const att = await fileToCompressedWebpAttachment(file);
          pendingAttachments.push(att);
        }
        renderAttachmentPreview();
        
        // Show visual feedback
        const attachedCount = pendingAttachments.length;
        setStatus(`✓ ${attachedCount} image${attachedCount > 1 ? 's' : ''} attached successfully!`);
        
        // Briefly highlight the upload button to show feedback
        el.uploadMediaBtn.style.color = '#4ade80';
        el.uploadMediaBtn.style.fontWeight = 'bold';
        setTimeout(() => {
          el.uploadMediaBtn.style.color = '';
          el.uploadMediaBtn.style.fontWeight = '';
        }, 2000);
      } catch (err) {
        setStatus(err.message || 'Failed to attach image');
      } finally {
        el.miniSendBtn.disabled = false;
      }
    });
  } else {
    console.log('Upload media elements NOT found. el.uploadMediaBtn:', el.uploadMediaBtn, 'el.uploadMediaInput:', el.uploadMediaInput);
  }

  el.messageInput.addEventListener('input', () => {
    el.messageInput.style.height = 'auto';
    el.messageInput.style.height = (el.messageInput.scrollHeight) + 'px';
    // Cache the input content to localStorage for persistence across refreshes
    cacheMessageInput(el.messageInput.value);
  });

  // Support paste images
  el.messageInput.addEventListener('paste', async (e) => {
    try {
      const items = e.clipboardData && e.clipboardData.items ? Array.from(e.clipboardData.items) : [];
      const imageItem = items.find(it => it.type && it.type.startsWith('image/'));
      if (!imageItem) return;
      e.preventDefault();

      if (!isPaidPlanClient()) {
        showUpgradeModal();
        return;
      }
      if (!currentModelSupportsMedia()) {
        setStatus('This model does not support image uploads. Please select a different model that supports media.');
        return;
      }
      const blob = imageItem.getAsFile();
      if (!blob) return;

      const att = await fileToCompressedWebpAttachment(blob);
      pendingAttachments.push(att);
      renderAttachmentPreview();
      setStatus(`${pendingAttachments.length} image(s) attached`);
    } catch (err) { console.error('Paste handler error', err); }
  });

  if (el.cliSelect) {
    el.cliSelect.addEventListener('change', async () => {
      state.currentCli = el.cliSelect.value;
      await loadModels(state.currentCli);
      if (state.currentSessionId) {
        try {
          await api(`/api/sessions/${state.currentSessionId}`, { method: 'PATCH', body: JSON.stringify({ cli: state.currentCli }) });
          await refreshCurrentSession();
        } catch (err) { setStatus(`Failed to update CLI: ${err.message}`); }
      }
    });
  }

  if (el.confirmBuildProceed) {
    el.confirmBuildProceed.onclick = async () => {
      if (el.confirmBuildModal) el.confirmBuildModal.style.display = 'none';
      if (pendingPlanContent) {
        await executeBuild(pendingPlanContent);
        pendingPlanContent = null;
      }
    };
  }

  if (el.confirmBuildCancel) {
    el.confirmBuildCancel.onclick = () => {
      if (el.confirmBuildModal) el.confirmBuildModal.style.display = 'none';
      pendingPlanContent = null;
    };
  }

  if (el.confirmBuildClose) {
    el.confirmBuildClose.onclick = () => {
      if (el.confirmBuildModal) el.confirmBuildModal.style.display = 'none';
      pendingPlanContent = null;
    };
  }

  // Export modal functionality
  const exportZipBtn = document.getElementById('export-zip-btn');
  const exportModal = document.getElementById('export-modal');
  const exportCloseBtn = document.getElementById('export-close');
  const downloadZipBtn = document.getElementById('download-zip');
  const exportStatus = document.getElementById('export-status');
  const exportStatusText = document.getElementById('export-status-text');

  if (exportZipBtn) {
    exportZipBtn.addEventListener('click', async () => {
      if (!state.currentSessionId) {
        setStatus('No active session to export');
        return;
      }

      try {
        exportZipBtn.disabled = true;
        exportZipBtn.style.opacity = '0.7';

        const headers = state.userId ? { 'X-User-Id': state.userId } : {};
        const response = await fetch(`/api/export/zip?sessionId=${state.currentSessionId}`, {
          headers,
        });

        if (!response.ok) {
          throw new Error('Export failed');
        }

        const blob = await response.blob();
        const url = window.URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `wordpress-plugin-${state.currentSessionId}.zip`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        window.URL.revokeObjectURL(url);

      } catch (error) {
        console.error('Export failed:', error);
        setStatus('Export failed: ' + error.message);
      } finally {
        exportZipBtn.disabled = false;
        exportZipBtn.style.opacity = '1';
      }
    });
  }

  if (exportCloseBtn) {
    exportCloseBtn.addEventListener('click', () => {
      if (exportModal) exportModal.style.display = 'none';
    });
  }

  // Close modal when clicking outside
  if (exportModal) {
    exportModal.addEventListener('click', (e) => {
      if (e.target === exportModal) {
        exportModal.style.display = 'none';
      }
    });
  }

  if (downloadZipBtn) {
    downloadZipBtn.addEventListener('click', async () => {
      if (!state.currentSessionId) {
        setStatus('No active session to export');
        return;
      }

      try {
        // Show loading state
        downloadZipBtn.disabled = true;
        downloadZipBtn.style.opacity = '0.7';
        if (exportStatus) {
          exportStatus.style.display = 'block';
          exportStatusText.innerHTML = '<div style="width:16px; height:16px; border:2px solid var(--shopify-green); border-top:2px solid transparent; border-radius:50%; animation:spin 1s linear infinite;"></div> Preparing export...';
        }

        // Make the export request
        const headers = state.userId ? { 'X-User-Id': state.userId } : {};
        const response = await fetch(`/api/export/zip?sessionId=${state.currentSessionId}`, {
          headers,
        });

        if (!response.ok) {
          throw new Error('Export failed');
        }

        // Create blob and download
        const blob = await response.blob();
        const url = window.URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `wordpress-plugin-${state.currentSessionId}.zip`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        window.URL.revokeObjectURL(url);

        // Show success message
        if (exportStatus) {
          exportStatusText.innerHTML = '<div style="color:var(--shopify-green); font-weight:600; display:flex; align-items:center; gap:8px;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20,6 9,17 4,12"></polyline></svg> Download started!</div>';
        }

        // Close modal after short delay
        setTimeout(() => {
          if (exportModal) exportModal.style.display = 'none';
        }, 2000);

      } catch (error) {
        console.error('Export failed:', error);
        setStatus('Export failed: ' + error.message);

        // Show error state
        if (exportStatus) {
          exportStatusText.innerHTML = '<div style="color:#dc3545; font-weight:600; display:flex; align-items:center; gap:8px;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg> Export failed. Please try again.</div>';
        }
      } finally {
        // Reset button state
        if (downloadZipBtn) {
          downloadZipBtn.disabled = false;
          downloadZipBtn.style.opacity = '1';
        }
      }
    });
  }

  window.addEventListener('click', (e) => {
    if (e.target === el.confirmBuildModal) {
      el.confirmBuildModal.style.display = 'none';
      pendingPlanContent = null;
    }
  });

   el.messageInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
      if (state.isSending) {
        cancelMessage();
      } else {
        sendMessage();
      }
    }
  });

  el.quickButtons.forEach((btn) => {
    btn.addEventListener('click', () => {
      const tag = btn.dataset.quick;
      const map = {
        shorter: 'Please condense the last answer.',
        more: 'Tell me more about this topic.',
      };
      el.messageInput.value = map[tag] || '';
      el.messageInput.focus();
    });
  });

  if (el.modelSelect) {
    el.modelSelect.addEventListener('change', async () => {
      const selected = el.modelSelect.value;

      // Handle Hobby (free) plan lock - but only if NOT a programmatic change and user didn't just change it
      if (isFreePlan() && selected !== 'auto' && !programmaticModelChange && !userJustChangedModel) {
        // For free plans, only reset to auto if the selection is invalid or empty
        const validModels = state.models.map(m => m.id || m.name || m);
        if (!selected || !validModels.includes(selected)) {
          el.modelSelect.value = 'auto';
          state.selectedModelId = 'auto';
          updateModelSelectDisplay('auto');
          updateUsageProgressBar();
          syncUploadButtonState();
          showBlurredModelPreviewInline();
          setStatus('Model selection is automatic on the hobby plan');
          return;
        }
      }

      state.selectedModelId = selected;
      updateModelSelectDisplay(selected);
      updateUsageProgressBar();
      syncUploadButtonState();

      if (!selected) return;

      // Mark that user just changed model (prevent server from overwriting during refresh)
      if (!programmaticModelChange) {
        markUserModelChange();
      }

      // Sync selection to current session on server
      if (state.currentSessionId) {
        try {
          await api(`/api/sessions/${state.currentSessionId}`, {
            method: 'PATCH',
            body: JSON.stringify({ model: selected })
          });
          // Refresh session to confirm change was persisted
          await refreshCurrentSession();
        } catch (e) {
          console.error('Failed to update model on server:', e);
          setStatus(`Failed to update model: ${e.message}`);
        } finally {
          // Clear the flag after refresh completes - let timer handle it
          // Don't manually clear here to allow polling to complete
        }
      } else {
        // Clear flag if no session to sync to
        // Let the timer handle it
      }
    });
  }

  // Upgrade header button functionality (for builder page)
  const upgradeHeaderBtn = document.getElementById('upgrade-header-btn');
  if (upgradeHeaderBtn) {
    upgradeHeaderBtn.addEventListener('click', () => {
      if (typeof window.showUpgradeModal === 'function' && state.accountPlan !== 'enterprise') {
        window.showUpgradeModal();
      } else if (state.accountPlan === 'enterprise') {
        alert('You are already on the Enterprise plan with full access.');
      } else {
        window.location.href = '/upgrade';
      }
    });
  }

  const tokenLimitClose = document.getElementById('token-limit-close');
  const tokenLimitModal = document.getElementById('token-limit-modal');
  if (tokenLimitClose && tokenLimitModal) {
    tokenLimitClose.addEventListener('click', () => {
      tokenLimitModal.style.display = 'none';
    });

    tokenLimitModal.addEventListener('click', (e) => {
      if (e.target === tokenLimitModal) {
        tokenLimitModal.style.display = 'none';
      }
    });

    const upgradeLink = document.getElementById('token-limit-upgrade');
    if (upgradeLink) {
      if (state.accountPlan === 'enterprise' || state.accountPlan === 'professional') {
        upgradeLink.style.display = 'none';
      }
    }
  }
}

// Handle page visibility changes to maintain polling
document.addEventListener('visibilitychange', () => {
  const isVisible = document.visibilityState === 'visible';
  if (isVisible) {
    // User came back to the page, refresh immediately
    console.log('Page became visible, refreshing...');
    refreshCurrentSession().catch(err => console.error('Refresh failed', err));
    // Ensure polling interval is set
    if (!state.pollingTimer) {
      setPollingInterval(2500);
    }
  } else {
    // User left the page, but keep polling in the background at a slower rate
    console.log('Page became hidden, maintaining background polling...');
    if (state.pollingTimer) {
      setPollingInterval(5000); // Slower polling in background
    }
  }
});

// Handle page unload gracefully
window.addEventListener('beforeunload', (e) => {
  // Check if there are running processes
  const running = state.sessions.flatMap(s => s.messages || []).filter(m => m.status === 'running' || m.status === 'queued');
  if (running.length > 0) {
    console.log('Page unloading with running processes. They will continue on the server.');
    // Don't prevent unload, just log it
  }
});

// When user comes back to the page after a long time, ensure we reconnect to running processes
window.addEventListener('focus', () => {
  console.log('Window focused, checking for running processes to reconnect...');
  if (state.currentSessionId) {
    refreshCurrentSession().catch(err => console.error('Refresh on focus failed', err));
  }
});

// Builder-specific init - loads sessions but doesn't render session list
(async function initBuilder() {
  populateCliSelect();

  // Check opencode status on startup
  checkOpencodeStatus();

  // Warm account info early to avoid duplicate slow fetches when page loads quickly
  try { getAccountInfo(); } catch (_) { /* ignore warm-up errors */ }

  // Load sessions data immediately ONLY if not already loaded by builder.html
  // The builder.html DOMContentLoaded handler will call loadSessions() if needed
  if (!state.sessionsLoaded) {
    try {
      const data = await api('/api/sessions');
      state.sessions = data.sessions || [];
      
      // Ensure we have a valid current session after loading
      if (!state.currentSessionId && state.sessions.length > 0) {
        console.log('[BUILDER] Initializing current session from loaded sessions:', state.sessions[0].id);
        await selectSession(state.sessions[0].id);
      }
    } catch (error) {
      console.error('Failed to load sessions:', error);
    } finally {
      state.sessionsLoaded = true;
      try { window.dispatchEvent(new Event('sessionsLoaded')); } catch (_) { }
    }
  } else {
    // Sessions were already loaded, but we might still need to ensure current session
    if (!state.currentSessionId && state.sessions && state.sessions.length > 0) {
      console.log('[BUILDER] Sessions already loaded, ensuring current session:', state.sessions[0].id);
      await selectSession(state.sessions[0].id);
    }
  }

  // Load models and account plan AFTER sessions are loaded, so we can determine the correct CLI
  // Load plan FIRST before attaching event handlers to ensure plan checks work correctly
  try {
    await loadAccountPlan();
    await loadModels(state.currentCli);
    console.log('Models loaded successfully');
    syncUploadButtonState();
  } catch (err) {
    console.warn('Model load failed:', err);
  }

  // Attach event handlers AFTER account plan is loaded to avoid race conditions
  hookEvents();

  // Restore cached message input from localStorage (after event handlers are attached)
  restoreMessageInput();

  // Periodically check opencode status (reduced frequency to reduce CPU usage)
  setInterval(checkOpencodeStatus, 300000);

  // Keep polling going even in background (for running processes)
  // Start with reasonable interval that will be adjusted by refreshCurrentSession
  setPollingInterval(5000);

  // Load provider limits for fallback model selection
  loadProviderLimits();
  
  // Scroll to bottom after initialization completes - use multiple delays
  // to catch content that renders asynchronously
  setTimeout(() => scrollChatToBottom(), 500);
  setTimeout(() => scrollChatToBottom(), 1000);
  setTimeout(() => scrollChatToBottom(), 2000);
})();

async function loadProviderLimits() {
  try {
    const data = await api('/api/provider-limits');
    if (data?.opencodeBackupModel) {
      window.providerLimits = {
        opencodeBackupModel: data.opencodeBackupModel
      };
      console.log('[PROVIDER-LIMITS] Loaded backup model:', window.providerLimits.opencodeBackupModel);
    }
  } catch (err) {
    console.warn('[PROVIDER-LIMITS] Failed to load provider limits:', err);
    window.providerLimits = window.providerLimits || {};
  }
}
