// ══════════════════════════════════════════════════════════════
// nterm — Session lifecycle + tab management
// ══════════════════════════════════════════════════════════════
// Depends on: state, diag, perf, blockCtx, escapeHtml,
//             scheduleGutterRender, updateStatus, updateStatusRight
//
// Changes from v0.5:
// - Terminal fontSize reads from zoom.js via _getTerminalFontSize()
//   instead of hardcoded 13px, enabling Ctrl+Alt+=/-/0 resizing

// ── Tab management ───────────────────────────────────────────
function createTab(tabId, displayName) {
  const tabBar = document.getElementById('tab-bar');
  const btn = document.createElement('button');
  btn.className = 'tab-btn';
  btn.dataset.tabId = tabId;
  btn.innerHTML = `
    <span class="status-dot connecting"></span>
    <span class="tab-label">${escapeHtml(displayName)}</span>
    <span class="close-tab" title="Close">&times;</span>
  `;
  btn.addEventListener('click', (e) => {
    if (e.target.classList.contains('close-tab')) closeSession(tabId);
    else switchTab(tabId);
  });
  btn.addEventListener('auxclick', (e) => {
    if (e.button === 1) closeSession(tabId);
  });
  tabBar.appendChild(btn);
}

function createPane(tabId) {
  const panels = document.getElementById('terminal-panels');
  const pane = document.createElement('div');
  pane.className = 'terminal-pane';
  pane.id = `pane-${tabId}`;
  pane.innerHTML = `<div class="terminal-pane-inner">
    <div class="block-gutter" id="gutter-${tabId}"></div>
    <div class="xterm-container" style="flex:1;height:100%;min-width:0"></div>
  </div>`;
  panels.appendChild(pane);
}

function switchTab(tabId) {
  document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
  document.querySelectorAll('.terminal-pane').forEach(p => p.classList.remove('active'));

  const tab = document.querySelector(`.tab-btn[data-tab-id="${tabId}"]`);
  const pane = document.getElementById(`pane-${tabId}`);
  if (tab) tab.classList.add('active');
  if (pane) pane.classList.add('active');

  state.activeTab = tabId;

  const session = state.sessions[tabId];
  if (session) {
    setTimeout(() => {
      session.fitAddon.fit();
      session.term.focus();
    }, 10);
    updateStatusRight(tabId);
    if (session.status === 'connected') {
      updateStatus(`Connected to ${session.config.hostname}`);
    } else if (session.status === 'error') {
      updateStatus(`Error on ${session.config.hostname}`, 'error');
    } else if (session.status === 'connecting') {
      updateStatus(`Connecting to ${session.config.hostname}...`);
    } else {
      updateStatus(`Disconnected from ${session.config.hostname}`);
    }
  }

  // Refresh diag views on tab switch
  if (state.diagOpen && state.diagView === 'sniffer') diag.renderSniffer();
  if (state.diagOpen && state.diagView === 'prompts') diag.renderPrompts();
  if (state.diagOpen && state.diagView === 'blocks') diag.renderBlocks();
  diag.updateBlockCount(tabId);
  if (typeof updateSnifferIndicator === 'function') updateSnifferIndicator(tabId);
  scheduleGutterRender(tabId);
}

function closeSession(tabId) {
  const session = state.sessions[tabId];
  if (session) {
    // ── Clean up keepalive ──
    if (session._keepalive) clearInterval(session._keepalive);
    if (session._onVisible) document.removeEventListener('visibilitychange', session._onVisible);

    if (session.socket) session.socket.close();
    session.term.dispose();
    delete state.sessions[tabId];
    delete diag.snifferAccumulators[tabId];
    delete diag.snifferDetectors[tabId];
    delete diag.snifferPrompts[tabId];
    delete diag.snifferBlocks[tabId];
    delete diag.snifferLines[tabId];
  }

  const tab = document.querySelector(`.tab-btn[data-tab-id="${tabId}"]`);
  const pane = document.getElementById(`pane-${tabId}`);
  if (tab) tab.remove();
  if (pane) pane.remove();

  const remaining = Object.keys(state.sessions);
  if (remaining.length > 0) {
    switchTab(remaining[remaining.length - 1]);
  } else {
    state.activeTab = null;
    document.getElementById('empty-state').style.display = 'flex';
    updateStatus('No active sessions');
    document.getElementById('status-right').textContent = 'nterm 0.6.0';
  }

  diag.log('event', tabId, 'Session closed');
}

function updateTabStatus(tabId, status) {
  const tab = document.querySelector(`.tab-btn[data-tab-id="${tabId}"]`);
  if (!tab) return;
  const dot = tab.querySelector('.status-dot');
  dot.className = 'status-dot';
  if (status === 'connected') dot.classList.add('connected');
  else if (status === 'error') dot.classList.add('error');
  else if (status === 'connecting') dot.classList.add('connecting');
  else if (status === 'stale') dot.classList.add('stale');
}

// ── Session creation ─────────────────────────────────────────
function createSession(tabId, config, onReady) {
  const term = new Terminal({
    fontFamily: "'JetBrains Mono', 'Consolas', 'Courier New', monospace",
    fontSize: (typeof window._getTerminalFontSize === 'function') ? window._getTerminalFontSize() : 14,
    theme: {
      background: '#0a0e14',
      foreground: '#d4dce8',
      cursor: '#00d4aa',
      cursorAccent: '#0a0e14',
      selectionBackground: 'rgba(0,212,170,0.25)',
      black: '#0a0e14',
      red: '#f85149',
      green: '#3fb950',
      yellow: '#e3b341',
      blue: '#58a6ff',
      magenta: '#e872b0',
      cyan: '#00d4aa',
      white: '#d4dce8',
      brightBlack: '#3d4f65',
      brightRed: '#f85149',
      brightGreen: '#3fb950',
      brightYellow: '#e3b341',
      brightBlue: '#58a6ff',
      brightMagenta: '#e872b0',
      brightCyan: '#00d4aa',
      brightWhite: '#ffffff',
    },
    cursorBlink: true,
    allowProposedApi: true,
  });

  const fitAddon = new FitAddon.FitAddon();
  term.loadAddon(fitAddon);

  state.sessions[tabId] = {
    term,
    fitAddon,
    socket: null,
    config,
    status: 'connecting',
    stats: {},
    displayName: config.hostname,
    // ── Keepalive state ──
    _keepalive: null,
    _onVisible: null,
    _lastPing: 0,
    _lastPong: 0,
    _pingLatency: null,
    _staleWarned: false,
  };

  createTab(tabId, config.hostname);
  createPane(tabId);
  switchTab(tabId);

  const container = document.querySelector(`#pane-${tabId} .xterm-container`);
  term.open(container);
  fitAddon.fit();

  // Input → WebSocket
  term.onData((data) => {
    const session = state.sessions[tabId];
    if (session?.socket?.readyState === WebSocket.OPEN) {
      session.socket.send(JSON.stringify({ type: 'input', data }));
    }
  });

  // Resize → WebSocket (debounced)
  const debouncedResize = debounce((cols, rows) => {
    const session = state.sessions[tabId];
    if (session?.socket?.readyState === WebSocket.OPEN) {
      session.socket.send(JSON.stringify({ type: 'resize', cols, rows }));
    }
  }, 300);
  term.onResize(({ cols, rows }) => debouncedResize(cols, rows));

  // Container resize → fit
  const resizeObserver = new ResizeObserver(() => {
    if (container.offsetWidth > 0 && container.offsetHeight > 0) {
      fitAddon.fit();
    }
  });
  resizeObserver.observe(container);

  // Gutter bar: re-render on viewport changes
  term.onRender(() => scheduleGutterRender(tabId));
  term.onScroll(() => scheduleGutterRender(tabId));

  // Belt-and-suspenders: viewport scroll listener
  setTimeout(() => {
    const vpEl = container.querySelector('.xterm-viewport');
    if (vpEl) {
      vpEl.addEventListener('scroll', () => scheduleGutterRender(tabId), { passive: true });
    }
  }, 100);

  // Gutter bar: click handler for context menu
  const gutterEl = document.getElementById('gutter-' + tabId);
  if (gutterEl) {
    gutterEl.addEventListener('click', (e) => {
      const mark = e.target.closest('.gutter-mark');
      if (!mark) return;
      e.stopPropagation();

      if (blockCtx._activeGutterMark) {
        blockCtx._activeGutterMark.classList.remove('gutter-active');
      }
      mark.classList.add('gutter-active');
      blockCtx._activeGutterMark = mark;

      const blockId = mark.dataset.blockId;
      blockCtx.show(e.clientX + 4, e.clientY - 8, tabId, blockId);
    });

    gutterEl.addEventListener('contextmenu', (e) => {
      const mark = e.target.closest('.gutter-mark');
      if (!mark) return;
      e.preventDefault();
      e.stopPropagation();

      if (blockCtx._activeGutterMark) {
        blockCtx._activeGutterMark.classList.remove('gutter-active');
      }
      mark.classList.add('gutter-active');
      blockCtx._activeGutterMark = mark;

      const blockId = mark.dataset.blockId;
      blockCtx.show(e.clientX, e.clientY, tabId, blockId);
    });
  }

  connectWebSocket(tabId, config, onReady);

  document.getElementById('empty-state').style.display = 'none';
  updateStatus(`Connecting to ${config.hostname}...`);
  diag.log('event', tabId, `Connecting ${config.username}@${config.hostname}:${config.port} via ${config.use_vault ? 'vault' : config.key_content ? 'key(paste)' : config.key_file ? 'key(file)' : 'password'}${config.legacy_mode ? ' [legacy]' : ''}`);
}

// ── WebSocket connection ─────────────────────────────────────
function connectWebSocket(tabId, config, onReady) {
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  const wsUrl = `${protocol}//${window.location.host}/ws/terminal/${tabId}`;

  let socket;
  try {
    socket = new WebSocket(wsUrl);
  } catch (e) {
    updateStatus(`Cannot reach server`, 'error');
    diag.log('event', tabId, `WebSocket construction failed: ${e.message}`);
    if (onReady) onReady();
    return;
  }

  state.sessions[tabId].socket = socket;

  socket.onopen = () => {
    const session = state.sessions[tabId];

    socket.send(JSON.stringify({ type: 'connect', ...config }));
    diag.log('send', tabId, `connect message sent${config.use_vault ? ' [vault]' : ''}${config.credential_name ? ` [cred:${config.credential_name}]` : ''}`);

    // ── Keepalive ping ──────────────────────────────────────
    // 25s interval: browsers throttle backgrounded tabs to 1/min,
    // so 25s guarantees at least one ping per throttle window.
    const keepalive = setInterval(() => {
      if (socket.readyState === WebSocket.OPEN) {
        session._lastPing = Date.now();
        socket.send(JSON.stringify({ type: 'ping' }));
      } else {
        clearInterval(keepalive);
      }
    }, 25000);
    session._keepalive = keepalive;

    // ── Wake from background — immediate ping ───────────────
    const onVisible = () => {
      if (document.visibilityState === 'visible' && socket.readyState === WebSocket.OPEN) {
        session._lastPing = Date.now();
        socket.send(JSON.stringify({ type: 'ping' }));
        diag.log('send', tabId, 'keepalive ping (visibility wake)');
      }
    };
    document.addEventListener('visibilitychange', onVisible);
    session._onVisible = onVisible;
  };

  socket.onmessage = (event) => {
    try {
      perf.start('ws_message');
      const msg = JSON.parse(event.data);
      const session = state.sessions[tabId];
      if (!session) { perf.end('ws_message'); return; }

      switch (msg.type) {
        case 'ssh_output':
          // Write to terminal
          perf.start('xterm_write');
          session.term.write(Uint8Array.from(atob(msg.data), c => c.charCodeAt(0)));
          perf.end('xterm_write');

          // Feed sniffer pipeline
          diag.feedSniffer(tabId, msg.data);

          // Throttled recv log
          session.stats._recvCount = (session.stats._recvCount || 0) + 1;
          if (session.stats._recvCount % 50 === 1) {
            diag.log('recv', tabId, `${atob(msg.data).length}B (msg #${session.stats._recvCount})`);
          }
          break;

        // ── Keepalive pong ────────────────────────────────────
        case 'pong':
          session._lastPong = Date.now();
          if (session._lastPing) {
            session._pingLatency = session._lastPong - session._lastPing;
          }
          // Recovery: clear stale warning if it was set
          if (session._staleWarned) {
            session._staleWarned = false;
            session.status = 'connected';
            updateTabStatus(tabId, 'connected');
            diag.log('event', tabId, `Heartbeat recovered (${session._pingLatency}ms)`);
            if (state.activeTab === tabId) {
              updateStatus(`Connected to ${session.config.hostname}`);
            }
          }
          break;

        case 'connected':
          session.status = 'connected';
          session._lastPong = Date.now();  // treat connect ack as first heartbeat
          updateTabStatus(tabId, 'connected');
          if (state.activeTab === tabId) {
            updateStatus(`Connected to ${msg.hostname}`);
            updateStatusRight(tabId);
          }
          diag.log('event', tabId, `Connected via ${msg.auth_method}${msg.vault_credential ? ' [vault:' + msg.vault_credential + ']' : ''}`);

          // ── Post-connect refit ────────────────────────────────
          // The initial fit() fires before DOM layout settles, so
          // full-screen apps (htop, vim) get the wrong geometry.
          // Refit twice: once after a paint frame, once after the
          // PTY has had time to process the resize.
          requestAnimationFrame(() => {
            if (state.sessions[tabId]) {
              session.fitAddon.fit();
              setTimeout(() => {
                if (state.sessions[tabId]) session.fitAddon.fit();
              }, 150);
            }
          });

          if (onReady) { onReady(); onReady = null; }
          break;

        case 'disconnected':
          session.status = 'disconnected';
          session.stats = msg.stats || session.stats;
          updateTabStatus(tabId, 'disconnected');
          if (state.activeTab === tabId) {
            updateStatus(`Disconnected from ${session.config.hostname}`);
          }
          diag.log('event', tabId, `Disconnected — ${JSON.stringify(msg.stats || {})}`);
          break;

        case 'error':
          session.status = 'error';
          updateTabStatus(tabId, 'error');
          if (state.activeTab === tabId) {
            updateStatus(`Error: ${msg.message}`, 'error');
          }
          diag.log('event', tabId, `Error: ${msg.message}`);
          if (onReady) { onReady(); onReady = null; }
          break;

        case 'auth_required':
          // Vault resolution failed — prompt for manual credentials
          // and re-send the connect on the same WebSocket.
          session.status = 'connecting';
          session.term.write(
            '\r\n\x1b[33m[Credential Required]\x1b[0m ' +
            (msg.message || '') + '\r\n'
          );
          diag.log('event', tabId, `Auth required: ${msg.message}`);
          if (state.activeTab === tabId) {
            updateStatus(`Credentials needed for ${msg.hostname}`, 'error');
          }

          _showAuthPrompt(msg, (creds) => {
            if (!creds) {
              // User cancelled
              session.term.write('\x1b[31m[Cancelled]\x1b[0m\r\n');
              session.status = 'disconnected';
              updateTabStatus(tabId, 'disconnected');
              return;
            }
            // Re-send connect with manual credentials
            session.term.write('\x1b[36mConnecting with manual credentials...\x1b[0m\r\n');
            socket.send(JSON.stringify({
              type: 'connect',
              hostname: msg.hostname,
              port: msg.port || 22,
              username: creds.username,
              password: creds.password || null,
              key_file: creds.key_file || null,
              key_content: creds.key_content || null,
              key_passphrase: creds.key_passphrase || null,
              legacy_mode: msg.legacy_mode || false,
              vendor_hint: msg.vendor || '',
            }));
          });
          break;

        case 'auth_error':
          // Legacy fallback — same as error
          session.status = 'error';
          updateTabStatus(tabId, 'error');
          session.term.write(
            '\r\n\x1b[31m[Authentication Error]\x1b[0m\r\n' +
            '\x1b[33m' + (msg.message || '').replace(/\n/g, '\r\n') + '\x1b[0m\r\n'
          );
          if (state.activeTab === tabId) {
            updateStatus(`Auth error: ${msg.hostname || 'unknown'}`, 'error');
          }
          diag.log('event', tabId, `Auth error: ${msg.message}`);
          if (onReady) { onReady(); onReady = null; }
          break;
      }
      perf.end('ws_message');
    } catch (e) {
      console.error('WebSocket message error:', e);
      diag.log('event', tabId, `Parse error: ${e.message}`);
    }
  };

  socket.onclose = () => {
    const session = state.sessions[tabId];
    if (session && session.status !== 'disconnected') {
      session.status = 'disconnected';
      updateTabStatus(tabId, 'disconnected');
      session.term.write('\r\n\x1b[33m[Connection closed]\x1b[0m\r\n');
      diag.log('event', tabId, 'WebSocket closed');
    }
    if (onReady) { onReady(); onReady = null; }
  };

  socket.onerror = (err) => {
    console.error('WebSocket error:', err);
    diag.log('event', tabId, 'WebSocket error');
    if (onReady) { onReady(); onReady = null; }
  };
}

// ── Auth prompt modal ─────────────────────────────────────────
// Shown when vault resolution fails. Collects manual credentials
// and calls back so the WebSocket can re-send the connect message.

function _showAuthPrompt(msg, callback) {
  // Remove any existing prompt
  const existing = document.getElementById('auth-prompt-overlay');
  if (existing) existing.remove();

  const hostname = msg.hostname || '';
  const username = msg.username || '';
  const reason = msg.message || 'Credentials required';

  const overlay = document.createElement('div');
  overlay.id = 'auth-prompt-overlay';
  overlay.style.cssText = `
    position: fixed; inset: 0; z-index: 10000;
    background: rgba(0,0,0,0.6);
    display: flex; align-items: center; justify-content: center;
  `;

  overlay.innerHTML = `
    <div style="
      background: var(--bg-panel, #0d1117);
      border: 1px solid var(--border, #1e2a36);
      border-radius: 8px;
      padding: 24px;
      width: 380px;
      box-shadow: 0 12px 40px rgba(0,0,0,0.5);
      font-family: 'JetBrains Mono', 'Consolas', monospace;
    ">
      <div style="color: var(--cyan, #2dd4bf); font-size: 14px; font-weight: 600; margin-bottom: 8px;">
        Credentials Required
      </div>
      <div style="color: var(--text-dim, #6a7585); font-size: 11px; margin-bottom: 16px;">
        ${escapeHtml(reason)}
      </div>
      <div style="color: var(--text-dim, #6a7585); font-size: 11px; margin-bottom: 4px;">Host</div>
      <div style="color: var(--text, #c8d6e5); font-size: 12px; margin-bottom: 12px;">
        ${escapeHtml(hostname)}
      </div>
      <div style="color: var(--text-dim, #6a7585); font-size: 11px; margin-bottom: 4px;">Username</div>
      <input id="auth-prompt-user" type="text" value="${escapeHtml(username)}" style="
        width: 100%; box-sizing: border-box;
        background: var(--bg-surface, #1a1a2e);
        border: 1px solid var(--border, #333);
        color: var(--text, #ccc);
        font-size: 12px; font-family: inherit;
        padding: 6px 8px; border-radius: 4px;
        margin-bottom: 12px; outline: none;
      " />
      <div style="color: var(--text-dim, #6a7585); font-size: 11px; margin-bottom: 4px;">Password</div>
      <input id="auth-prompt-pass" type="password" style="
        width: 100%; box-sizing: border-box;
        background: var(--bg-surface, #1a1a2e);
        border: 1px solid var(--border, #333);
        color: var(--text, #ccc);
        font-size: 12px; font-family: inherit;
        padding: 6px 8px; border-radius: 4px;
        margin-bottom: 16px; outline: none;
      " />
      <div style="display: flex; gap: 8px; justify-content: flex-end;">
        <button id="auth-prompt-cancel" style="
          background: var(--bg-surface, #1a1a2e);
          border: 1px solid var(--border, #333);
          color: var(--text-dim, #6a7585);
          padding: 6px 16px; border-radius: 4px;
          cursor: pointer; font: 12px inherit;
        ">Cancel</button>
        <button id="auth-prompt-ok" style="
          background: var(--cyan, #2dd4bf);
          border: 1px solid var(--cyan, #2dd4bf);
          color: var(--bg, #0a0e14);
          padding: 6px 16px; border-radius: 4px;
          cursor: pointer; font: 12px inherit; font-weight: 600;
        ">Connect</button>
      </div>
    </div>
  `;

  document.body.appendChild(overlay);

  const userInput = document.getElementById('auth-prompt-user');
  const passInput = document.getElementById('auth-prompt-pass');
  const okBtn = document.getElementById('auth-prompt-ok');
  const cancelBtn = document.getElementById('auth-prompt-cancel');

  // Focus password if username is pre-filled, otherwise username
  if (username) {
    passInput.focus();
  } else {
    userInput.focus();
  }

  const submit = () => {
    const creds = {
      username: userInput.value.trim(),
      password: passInput.value || null,
    };
    overlay.remove();
    if (creds.username && creds.password) {
      callback(creds);
    } else {
      callback(null);
    }
  };

  const cancel = () => {
    overlay.remove();
    callback(null);
  };

  okBtn.addEventListener('click', submit);
  cancelBtn.addEventListener('click', cancel);

  // Enter submits, Escape cancels
  overlay.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') submit();
    if (e.key === 'Escape') cancel();
  });

  // Click outside cancels
  overlay.addEventListener('click', (e) => {
    if (e.target === overlay) cancel();
  });
}

// ── Vault-aware connect (for session tree) ───────────────────

async function vaultConnect(hostname, port, options) {
  /**
   * Connect to a device using vault credentials if available.
   *
   * Called by the session tree on double-click when vault is unlocked.
   * Falls back to populating Quick Connect if no vault match.
   *
   * options: {
   *   credential_name: string,  // explicit credential (from session extras)
   *   username: string,         // override username
   *   vendor: string,           // vendor hint
   *   legacy_mode: boolean,
   * }
   */
  options = options || {};
  port = port || 22;

  // Try vault resolution
  const match = await vaultMatchHost(hostname, port);

  if (match) {
    const tabId = crypto.randomUUID();
    const config = {
      hostname: hostname,
      port: port,
      username: match.username || options.username || '',
      use_vault: true,
      credential_name: options.credential_name || match.credential_name,
      legacy_mode: options.legacy_mode || false,
    };

    const btn = document.getElementById('btn-connect');
    btn.disabled = true;
    btn.textContent = 'Connecting...';
    btn.classList.add('connecting');

    createSession(tabId, config, () => {
      btn.disabled = false;
      btn.textContent = 'Connect';
      btn.classList.remove('connecting');
    });

    // Set vendor hint
    if (options.vendor && state.sessions[tabId]) {
      state.sessions[tabId].vendorOverride = options.vendor;
    }
    if (state.sessions[tabId]) {
      state.sessions[tabId].snifferEnabled = state.snifferDefault;
      updateSnifferIndicator(tabId);
    }

    return true;  // connected via vault
  }

  return false;  // no vault match, caller should populate Quick Connect
}