// Main application orchestrator
(async function () {
  // Check if auth is required
  const authStatus = await fetch('/api/auth/status').then((r) => r.json()).catch(() => ({ authRequired: true }));
  if (authStatus.authRequired) {
    const token = localStorage.getItem('cmux-remote-token');
    if (!token) {
      window.location.href = '/login.html';
      return;
    }
  }

  // State
  let currentWorkspace = null;
  let currentSurface = null;
  let lastWorkspaces = [];

  // URL hash helpers — format: #wsRef/surfaceRef
  function getHashSurface() {
    const hash = location.hash.slice(1);
    if (!hash) return null;
    const slash = hash.indexOf('/');
    if (slash === -1) return null;
    return {
      ws: decodeURIComponent(hash.slice(0, slash)),
      surface: decodeURIComponent(hash.slice(slash + 1)),
    };
  }

  function setHashSurface(wsRef, surfaceRef) {
    history.replaceState(null, '', '#' + encodeURIComponent(wsRef) + '/' + encodeURIComponent(surfaceRef));
  }

  function findAndSwitch(workspaces, wsRef, surfaceRef) {
    for (const workspace of workspaces) {
      if (workspace.ref === wsRef) {
        for (const pane of workspace.panes) {
          for (const surface of pane.surfaces) {
            if (surface.ref === surfaceRef) {
              switchSurface(wsRef, surfaceRef);
              return true;
            }
          }
        }
      }
    }
    return false;
  }

  // Components
  const ws = new WebSocketClient();
  const terminal = new TerminalView(document.getElementById('terminal-output'));
  const statusDot = document.getElementById('connection-status');
  const terminalContainer = document.getElementById('terminal-container');
  const browserView = document.getElementById('browser-view');
  const terminalOutput = document.getElementById('terminal-output');

  // ── Image zoom viewer ──────────────────────────────────────────
  class ImageZoomViewer {
    constructor(img, container) {
      this._img = img;
      this._con = container;
      this._scale = 1;
      this._x = 0; this._y = 0;
      this._lastDist = 0;
      this._dragging = false;
      this._lastX = 0; this._lastY = 0;

      container.addEventListener('touchstart',  this._ts.bind(this), { passive: false });
      container.addEventListener('touchmove',   this._tm.bind(this), { passive: false });
      container.addEventListener('touchend',    () => { this._dragging = false; }, { passive: true });
      container.addEventListener('wheel',       this._wheel.bind(this), { passive: false });
      container.addEventListener('mousedown',   this._md.bind(this));
      container.addEventListener('mousemove',   this._mm.bind(this));
      container.addEventListener('mouseup',     this._mu.bind(this));
      container.addEventListener('mouseleave',  this._mu.bind(this));
      container.addEventListener('dblclick',    () => this.reset());
    }

    reset() { this._scale = 1; this._x = 0; this._y = 0; this._apply(); }

    _apply() {
      this._img.style.transform = `translate(${this._x}px, ${this._y}px) scale(${this._scale})`;
    }

    _clampScale(s) { return Math.min(10, Math.max(0.5, s)); }

    _ts(e) {
      if (e.touches.length === 2) {
        e.preventDefault();
        this._lastDist = this._dist(e.touches[0], e.touches[1]);
      } else if (e.touches.length === 1) {
        this._dragging = true;
        this._lastX = e.touches[0].clientX;
        this._lastY = e.touches[0].clientY;
      }
    }

    _tm(e) {
      e.preventDefault();
      if (e.touches.length === 2) {
        const d = this._dist(e.touches[0], e.touches[1]);
        this._scale = this._clampScale(this._scale * (d / this._lastDist));
        this._lastDist = d;
        this._apply();
      } else if (e.touches.length === 1 && this._dragging) {
        this._x += e.touches[0].clientX - this._lastX;
        this._y += e.touches[0].clientY - this._lastY;
        this._lastX = e.touches[0].clientX;
        this._lastY = e.touches[0].clientY;
        this._apply();
      }
    }

    _wheel(e) {
      e.preventDefault();
      this._scale = this._clampScale(this._scale * (e.deltaY < 0 ? 1.12 : 0.9));
      this._apply();
    }

    _md(e) { this._dragging = true; this._lastX = e.clientX; this._lastY = e.clientY; this._con.classList.add('dragging'); }
    _mm(e) {
      if (!this._dragging) return;
      this._x += e.clientX - this._lastX;
      this._y += e.clientY - this._lastY;
      this._lastX = e.clientX; this._lastY = e.clientY;
      this._apply();
    }
    _mu() { this._dragging = false; this._con.classList.remove('dragging'); }

    _dist(t1, t2) {
      return Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);
    }
  }

  const browserLoading = document.getElementById('browser-loading');
  const zoomBtn      = document.getElementById('browser-zoom-btn');
  const zoomOverlay  = document.getElementById('img-zoom-overlay');
  const zoomContainer = document.getElementById('img-zoom-container');
  const zoomImg      = document.getElementById('img-zoom-img');
  const zoomClose    = document.getElementById('img-zoom-close');
  let zoomViewer = null;

  zoomBtn.addEventListener('click', () => {
    zoomImg.src = browserView.src;
    zoomOverlay.hidden = false;
    if (!zoomViewer) {
      zoomViewer = new ImageZoomViewer(zoomImg, zoomContainer);
    } else {
      zoomViewer.reset();
    }
  });

  zoomClose.addEventListener('click', () => { zoomOverlay.hidden = true; });
  const sidebar = new Sidebar((wsRef, surfaceRef) => {
    switchSurface(wsRef, surfaceRef);
  });

  const gestures = new GestureHandler(sidebar);
  const theme = new ThemeSwitcher();

  // Scrollback state
  const PAGE_LINES = 500;
  let scrollbackLines = 0; // 0 = live view
  let inScrollMode = false;
  let scrollbackPending = false;
  // Set once capture-pane stops returning new lines (history exhausted). Without
  // this, a surface with little/no scrollback flashes the loader forever: each
  // wheel-up at the top re-requests, the response adds nothing, the view stays put
  // at scrollTop 0, so the next wheel-up fires again. Reset on surface switch /
  // when live updates resume (more history may exist later).
  let noMoreHistory = false;
  const scrollbackLoading = document.getElementById('scrollback-loading');

  function requestScrollback(lines) {
    if (!currentWorkspace || !currentSurface) return;
    scrollbackPending = true;
    if (scrollbackLoading) scrollbackLoading.hidden = false;
    ws.requestScroll(currentWorkspace, currentSurface, lines);
  }

  function exitScrollMode() {
    inScrollMode = false;
    scrollbackLines = 0;
    scrollbackPending = false;
    noMoreHistory = false;
    if (scrollbackLoading) scrollbackLoading.hidden = true;
    terminal.autoScroll = true;
    // Re-subscribe to get fresh live content
    if (currentWorkspace && currentSurface) {
      ws.subscribe(currentWorkspace, currentSurface);
    }
  }

  // Scroll-driven history: reaching the top (with scroll room) loads older
  // history; reaching the bottom resumes live updates. Entry is explicit (only
  // on an actual scroll-to-top), never automatic — that avoids the freeze the
  // old auto-load-when-fits caused.
  terminalContainer.addEventListener('scroll', () => {
    const c = terminalContainer;
    const atBottom = c.scrollHeight - c.scrollTop - c.clientHeight < 30;

    if (atBottom) {
      if (inScrollMode) exitScrollMode();
      return;
    }

    if (c.scrollTop < 40 && !scrollbackPending && terminal.lines.length &&
        currentWorkspace && currentSurface) {
      loadOlderHistory();
    }
  });

  function loadOlderHistory() {
    if (scrollbackPending || noMoreHistory || !terminal.lines.length ||
        !currentWorkspace || !currentSurface) return;
    inScrollMode = true;
    terminal.autoScroll = false; // pin position; render-fix won't snap to bottom
    scrollbackLines += PAGE_LINES;
    requestScrollback(scrollbackLines);
  }

  // Desktop has no touch scroll. When the live screen fits the viewport exactly
  // there is no scroll room, so the 'scroll' event never fires and history can't
  // be reached. Catch the wheel-up intent directly to load older history.
  terminalContainer.addEventListener('wheel', (e) => {
    if (e.deltaY < 0 && terminalContainer.scrollTop < 40) {
      loadOlderHistory();
    }
  }, { passive: true });

  const keyboard = new VirtualKeyboard(
    (text) => {
      if (currentWorkspace && currentSurface) {
        if (inScrollMode) exitScrollMode();
        ws.sendText(currentWorkspace, currentSurface, text);
      }
    },
    (key) => {
      if (key === 'PageUp') {
        if (noMoreHistory) return;
        inScrollMode = true;
        scrollbackLines += PAGE_LINES;
        requestScrollback(scrollbackLines);
        return;
      }
      if (key === 'PageDown') {
        if (inScrollMode) {
          scrollbackLines = Math.max(0, scrollbackLines - PAGE_LINES);
          if (scrollbackLines === 0) {
            exitScrollMode();
          } else {
            requestScrollback(scrollbackLines);
          }
        }
        return;
      }
      if (inScrollMode) exitScrollMode();
      if (currentWorkspace && currentSurface) {
        ws.sendKey(currentWorkspace, currentSurface, key);
      }
    },
    (text) => {
      // onSubmitText: send text + server-side Enter (awaited ordering)
      if (currentWorkspace && currentSurface) {
        if (inScrollMode) exitScrollMode();
        ws.sendTextSubmit(currentWorkspace, currentSurface, text);
      }
    }
  );

  const claudeKeyboard = new ClaudeKeyboard(
    (text) => {
      if (currentWorkspace && currentSurface) {
        ws.sendText(currentWorkspace, currentSurface, text);
      }
    },
    (key) => {
      if (currentWorkspace && currentSurface) {
        ws.sendKey(currentWorkspace, currentSurface, key);
      }
    }
  );

  function handleClaudeAction(action, payload) {
    if (!currentWorkspace || !currentSurface) return;
    if (action === 'select') {
      ws.sendText(currentWorkspace, currentSurface, payload + '\n');
    } else if (action === 'mode-toggle') {
      ws.sendKey(currentWorkspace, currentSurface, 'Shift-Tab');
    }
  }

  // Reload button
  document.getElementById('reload-btn').addEventListener('click', () => location.reload());

  // Horizontal scroll toggle
  const scrollXCb = document.getElementById('scroll-x-cb');
  scrollXCb.addEventListener('change', () => {
    terminalContainer.classList.toggle('scroll-x', scrollXCb.checked);
  });

  // Keyboard toggle — restore saved state
  const keyboardToggle = document.getElementById('keyboard-toggle');
  const virtualKeyboardEl = document.getElementById('virtual-keyboard');

  const savedKbCollapsed = localStorage.getItem('cmux-kb-collapsed');
  if (savedKbCollapsed !== null) {
    virtualKeyboardEl.classList.toggle('collapsed', savedKbCollapsed === 'true');
  } else if (window.innerWidth >= 1024) {
    virtualKeyboardEl.classList.add('collapsed');
  }

  keyboardToggle.addEventListener('click', () => {
    virtualKeyboardEl.classList.toggle('collapsed');
    localStorage.setItem('cmux-kb-collapsed', virtualKeyboardEl.classList.contains('collapsed'));
  });

  window.addEventListener('resize', () => {
    if (window.innerWidth < 1024) {
      virtualKeyboardEl.classList.remove('collapsed');
      localStorage.removeItem('cmux-kb-collapsed');
    }
  });

  // Settings panel
  const settingsBtn = document.getElementById('settings-btn');
  const settingsPanel = document.getElementById('settings-panel');
  const settingsOverlay = document.getElementById('settings-overlay');
  const settingsClose = document.getElementById('settings-close');
  settingsBtn.addEventListener('click', () => {
    settingsPanel.classList.add('open');
    settingsOverlay.classList.add('visible');
  });
  function closeSettings() {
    settingsPanel.classList.remove('open');
    settingsOverlay.classList.remove('visible');
  }
  settingsClose.addEventListener('click', closeSettings);
  settingsOverlay.addEventListener('click', closeSettings);

  function updateTitle(wsRef, surfaceRef) {
    const el = document.getElementById('app-title');
    if (!el) return;
    let rawTitle = surfaceRef || '';
    for (const workspace of lastWorkspaces) {
      if (workspace.ref === wsRef) {
        for (const pane of workspace.panes) {
          for (const surface of pane.surfaces) {
            if (surface.ref === surfaceRef) {
              rawTitle = surface.title || surface.ref;
              break;
            }
          }
        }
      }
    }
    const status = Sidebar.parseStatus(rawTitle);
    el.innerHTML = '';
    if (status.state) {
      const dot = document.createElement('span');
      dot.className = 'title-status ' + status.state;
      el.appendChild(dot);
    }
    el.appendChild(document.createTextNode(status.title));
  }


  // Connection events
  // The `.stale` veil dims the WHOLE terminal (a 40%-opacity overlay). Showing
  // it on every brief disconnect makes the screen visibly "fade out then back"
  // whenever the socket flaps/reconnects (common on remote/tunnel connections).
  // Debounce it: only dim once a disconnect has lasted long enough that the view
  // is genuinely stale; a fast reconnect clears the pending dim with no flash.
  let staleTimer = null;
  ws.on('connected', () => {
    statusDot.classList.add('connected');
    statusDot.classList.remove('reconnecting');
    statusDot.title = 'Connected';
    if (staleTimer) { clearTimeout(staleTimer); staleTimer = null; }
    terminalContainer.classList.remove('stale');
  });

  ws.on('disconnected', () => {
    statusDot.classList.remove('connected');
    statusDot.classList.add('reconnecting');
    statusDot.title = 'Reconnecting...';
    if (!staleTimer && !terminalContainer.classList.contains('stale')) {
      staleTimer = setTimeout(() => {
        terminalContainer.classList.add('stale');
        staleTimer = null;
      }, 2500);
    }
  });

  ws.on('reconnecting', ({ attempt }) => {
    statusDot.title = `Reconnecting (${attempt})...`;
  });

  ws.on('auth-ok', () => {
    ws.listWorkspaces();
    // Re-subscribe to current surface after reconnect
    if (currentWorkspace && currentSurface) {
      ws.subscribe(currentWorkspace, currentSurface);
    }
  });

  ws.on('workspaces', (msg) => {
    sidebar.setWorkspaces(msg.workspaces);
    lastWorkspaces = msg.workspaces;

    // Auto-subscribe only on first load (no surface selected yet)
    if (!currentSurface && msg.workspaces.length > 0) {
      // 1. Try to restore from URL hash
      const saved = getHashSurface();
      if (saved && findAndSwitch(msg.workspaces, saved.ws, saved.surface)) return;

      // 2. Try to restore from localStorage
      try {
        const ls = JSON.parse(localStorage.getItem('cmux-last-surface') || 'null');
        if (ls && findAndSwitch(msg.workspaces, ls.ws, ls.surface)) return;
      } catch {}

      // 3. Fall back to first terminal surface
      const firstWs = msg.workspaces[0];
      for (const pane of firstWs.panes) {
        for (const surface of pane.surfaces) {
          if (surface.type === 'terminal') {
            switchSurface(firstWs.ref, surface.ref);
            return;
          }
        }
      }
      // 3. Fall back to first surface of any type
      if (firstWs.panes.length > 0 && firstWs.panes[0].surfaces.length > 0) {
        const s = firstWs.panes[0].surfaces[0];
        switchSurface(firstWs.ref, s.ref);
      }
    }
  });

  ws.on('screen', (msg) => {
    if (msg.surface !== currentSurface) return;
    // While viewing loaded history, pause live screen replacement.
    if (inScrollMode && !msg.scrollback) return;

    if (msg.scrollback) {
      // User-triggered history load (scroll-to-top or PageUp): show it, pause
      // live updates, and preserve the user's reading position. Older lines are
      // prepended, so shift scrollTop by the height added above.
      const prevScrollHeight = terminalContainer.scrollHeight;
      const prevScrollTop = terminalContainer.scrollTop;
      const prevLineCount = terminal.lines.length;

      terminal.setContent(msg.lines);
      terminal.autoScroll = false;
      scrollbackPending = false;
      if (scrollbackLoading) scrollbackLoading.hidden = true;

      // capture-pane returned no additional lines → history is exhausted. Stop
      // further requests so the loader doesn't flash on every wheel-up at the top.
      if (msg.lines.length <= prevLineCount) {
        noMoreHistory = true;
      }

      requestAnimationFrame(() => {
        const added = terminalContainer.scrollHeight - prevScrollHeight;
        // Preserve the reading position: keep whatever is on screen exactly where it
        // is and let the freshly-loaded older lines sit ABOVE it, so the view doesn't
        // jump — scrolling up further then reveals the history smoothly. (Older lines
        // are prepended, so shift scrollTop down by the height added above.)
        terminalContainer.scrollTop = prevScrollTop + Math.max(added, 0);
      });
    } else {
      terminal.setContent(msg.lines);
    }
  });

  ws.on('screen-diff', (msg) => {
    if (msg.surface === currentSurface && !inScrollMode) {
      terminal.applyDiff(msg.patches);
    }
  });

  ws.on('browser-screenshot', (msg) => {
    if (msg.surface !== currentSurface) return;
    const mime = msg.mime || 'image/jpeg';
    const src = `data:${mime};base64,${msg.imageData}`;
    terminalOutput.hidden = true;
    terminalContainer.classList.add('browser-mode');

    // Hide loading bar, show image
    browserLoading.hidden = true;
    browserLoading.classList.remove('flash');
    browserView.hidden = false;
    zoomBtn.hidden = false;
    browserView.src = src;
    if (!zoomOverlay.hidden) zoomImg.src = src;
  });

  async function updateCodexMode(surfaceRef) {
    // Skip if claude mode is already active (mutually exclusive)
    if (document.body.classList.contains('claude-mode')) return;
    try {
      const data = await fetch('/api/codex-surfaces').then(r => r.json());
      if (surfaceRef !== currentSurface) return;
      const isCodex = data.surfaces.includes(surfaceRef);
      document.body.classList.toggle('codex-mode', isCodex);
      if (terminal.setCodexMode) terminal.setCodexMode(isCodex);
    } catch {
      if (surfaceRef !== currentSurface) return;
      document.body.classList.remove('codex-mode');
      if (terminal.setCodexMode) terminal.setCodexMode(false);
    }
  }

  async function updateClaudeMode(surfaceRef) {
    try {
      const data = await fetch('/api/claude-surfaces').then(r => r.json());
      if (surfaceRef !== currentSurface) return;
      const isClaudeCode = data.surfaces.includes(surfaceRef);
      document.body.classList.toggle('claude-mode', isClaudeCode);
      terminal.setClaudeMode(isClaudeCode, handleClaudeAction);
      if (isClaudeCode) {
        claudeKeyboard.activate();
      } else {
        claudeKeyboard.deactivate();
      }
    } catch {
      if (surfaceRef !== currentSurface) return;
      document.body.classList.remove('claude-mode');
      terminal.setClaudeMode(false, null);
      claudeKeyboard.deactivate();
    }
  }

  function switchSurface(wsRef, surfaceRef) {
    if (currentSurface) ws.unsubscribe(currentSurface);
    inScrollMode = false;
    scrollbackLines = 0;
    scrollbackPending = false;
    noMoreHistory = false;
    if (scrollbackLoading) scrollbackLoading.hidden = true;

    currentWorkspace = wsRef;
    currentSurface = surfaceRef;
    updateTitle(wsRef, surfaceRef);
    localStorage.setItem('cmux-last-surface', JSON.stringify({ ws: wsRef, surface: surfaceRef }));

    // Immediately reset claude/codex mode; update functions will re-enable if needed
    document.body.classList.remove('claude-mode');
    document.body.classList.remove('codex-mode');
    terminal.setClaudeMode(false, null);
    if (terminal.setCodexMode) terminal.setCodexMode(false);
    claudeKeyboard.deactivate();

    terminal.clear();
    terminal.autoScroll = true;
    browserView.hidden = true;
    browserLoading.hidden = true;
    browserLoading.classList.remove('flash');
    zoomBtn.hidden = true;
    zoomOverlay.hidden = true;
    terminalOutput.hidden = false;
    terminalContainer.classList.remove('browser-mode');
    ws.subscribe(wsRef, surfaceRef);
    sidebar.setActiveSurface(surfaceRef);
    setHashSurface(wsRef, surfaceRef);
    updateClaudeMode(surfaceRef);
    updateCodexMode(surfaceRef);

    // If this is a browser surface, show loading indicator immediately
    const surfaceInfo = lastWorkspaces.flatMap(w => w.panes.flatMap(p => p.surfaces))
      .find(s => s.ref === surfaceRef);
    if (surfaceInfo?.type === 'browser') {
      terminalOutput.hidden = true;
      terminalContainer.classList.add('browser-mode');
      browserLoading.hidden = false;
      browserLoading.classList.add('flash');
    }
  }

  // Browser back/forward navigation
  window.addEventListener('hashchange', () => {
    const saved = getHashSurface();
    if (saved && ws.isConnected()) {
      findAndSwitch(lastWorkspaces, saved.ws, saved.surface);
    }
  });

  // Visibility API: pause polling when hidden, reconnect when visible
  let hiddenSince = 0;
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
      hiddenSince = Date.now();
      if (currentSurface) ws.unsubscribe(currentSurface);
    } else {
      const elapsed = hiddenSince ? Date.now() - hiddenSince : 0;
      hiddenSince = 0;
      if (!currentWorkspace || !currentSurface) return;

      // iOS kills WebSocket connections in background.
      // If hidden >5s or connection is dead, force fresh reconnect.
      if (!ws.isConnected() || elapsed > 5000) {
        ws.reconnectNow();
      } else {
        ws.subscribe(currentWorkspace, currentSurface);
      }
    }
  });

  // Periodic workspace refresh
  setInterval(() => {
    if (ws.isConnected()) {
      ws.listWorkspaces();
    }
  }, 10000);

  // Start
  ws.connect(authStatus.authRequired);
})();
