/**
 * WebServer — VLCode Lite main server class
 * Express + SSE, no VLClaw dependency
 */
import express from 'express';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { getDefaultCoreDocIds, getDefaultDocIdOverrides } from '../data/doc-paths.js';
import { APP_VERSION } from '../utils/config.js';
import { resolveDataPath, resolveNodeModulePath, resolvePublicPath, resolveSeedWorkflowDir } from '../utils/bundle-paths.js';
import { CloudAPI } from '../cloud/cloud-api.js';
import { SessionPool } from '../core/session-pool.js';
import { registerAllTools, registerIntelligenceTools, loadUserTools } from '../tools/index.js';
import { VL_CODE_HOME, ensureVLCodeHome, getCookie, ensureVLBible, ensureProjectProfile, loadWorkspaces, saveWorkspaces, registerInstance, applyProjectProfileToConfig } from './helpers.js';
import { setupSSE } from './sse.js';

// Route modules
import { setupChatRoutes } from './routes/chat.js';
import { setupFileRoutes } from './routes/files.js';
import { setupProjectRoutes } from './routes/project.js';
import { setupWorkspaceRoutes } from './routes/workspace.js';
import { setupCompileRoutes } from './routes/compile.js';
import { setupWorkflowRoutes } from './routes/workflow.js';
import { setupCloudRoutes } from './routes/cloud.js';
import { setupIntelligenceRoutes } from './routes/intelligence.js';
import { setupToolRoutes } from './routes/tools.js';
import { setupConversationRoutes } from './routes/conversation.js';
import { setupMiscRoutes } from './routes/misc.js';

const MAX_WORKFLOW_TIMELINE = 200;
const MAX_SERVER_PORT = 4099;

function ensureWorkflowStateShape(state, workDir = '') {
  if (!state || typeof state !== 'object') {
    return {
      active: false,
      workflowName: null,
      nodeStatuses: {},
      nodeDetails: {},
      timeline: [],
      startedAt: null,
      finishedAt: null,
      lastError: null,
      workDir,
    };
  }
  state.nodeStatuses = state.nodeStatuses || {};
  state.nodeDetails = state.nodeDetails || {};
  state.timeline = state.timeline || [];
  state.startedAt = state.startedAt || null;
  state.finishedAt = state.finishedAt || null;
  state.lastError = state.lastError || null;
  if (!state.workDir) state.workDir = workDir;
  return state;
}

function summarizeWorkflowValue(value, maxLen = 240) {
  if (value == null) return value;
  if (typeof value === 'string') {
    return value.length > maxLen ? `${value.slice(0, maxLen)}... (${value.length} chars)` : value;
  }
  if (Array.isArray(value)) {
    return value.slice(0, 10).map((item) => summarizeWorkflowValue(item, 120));
  }
  if (typeof value === 'object') {
    const out = {};
    for (const [key, inner] of Object.entries(value).slice(0, 20)) {
      out[key] = summarizeWorkflowValue(inner, 120);
    }
    return out;
  }
  return value;
}

function buildWorkflowEventRecord(event = {}) {
  const record = {
    type: event.type || 'unknown',
    ts: event.ts || new Date().toISOString(),
  };
  for (const key of [
    'workflowName', 'nodeId', 'status', 'title', 'nodeType', 'reason',
    'error', 'detail', 'path', 'stepID', 'runID', 'model', 'name',
    'tool_use_id', 'duration_ms', 'completedSteps', 'selected',
  ]) {
    if (event[key] !== undefined) record[key] = summarizeWorkflowValue(event[key]);
  }
  if (event.input !== undefined) record.input = summarizeWorkflowValue(event.input);
  if (event.resolvedInputs !== undefined) record.resolvedInputs = summarizeWorkflowValue(event.resolvedInputs);
  if (event.output !== undefined) record.output = summarizeWorkflowValue(event.output);
  if (event.outputs !== undefined) record.outputs = summarizeWorkflowValue(event.outputs);
  if (event.text !== undefined) record.text = summarizeWorkflowValue(event.text, 400);
  if (event.usage !== undefined) record.usage = summarizeWorkflowValue(event.usage);
  return record;
}

export class WebServer {
  constructor({ config, orchestrator, projectContext, contextManager, toolRegistry, symbolIndex, impactAnalyzer, autoFix, localWorkspace, blueprintContext }) {
    this.config = config;
    this.orchestrator = orchestrator;
    this.projectContext = projectContext;
    this.contextManager = contextManager;
    this.toolRegistry = toolRegistry;
    this.symbolIndex = symbolIndex;
    this.impactAnalyzer = impactAnalyzer;
    this.autoFix = autoFix;
    this.localWorkspace = localWorkspace;
    this.blueprintContext = blueprintContext;
    this.app = express();
    this.sseClients = new Set();
    this._windowCounter = 0;
    this._activeExecutor = null;
    this._startTime = Date.now();
    this._fileLocks = new Map();
    this._lastCompileResult = null;
    this.httpServer = null;

    // Undo stack
    if (this.config) this.config._undoStack = this.config._undoStack || [];

    // SSE heartbeat
    this._sseHeartbeat = setInterval(() => {
      for (const client of this.sseClients) {
        try { client.write(': heartbeat\n\n'); } catch {}
      }
    }, 25000);

    // SessionPool: per-chatId isolation
    this.sessionPool = new SessionPool({
      config: this.config,
      toolRegistry: this.toolRegistry,
      projectContext: this.projectContext,
      cli: orchestrator?.cli,
      symbolIndex: this.symbolIndex,
      impactAnalyzer: this.impactAnalyzer,
      blueprintContext: this.blueprintContext,
      generationPipeline: orchestrator?.generationPipeline,
      sessionManager: orchestrator?.sessionManager,
      smartContext: orchestrator?.smartContext,
      hooks: orchestrator?.hooks,
      getBroadcast: () => (msg) => this.broadcast(msg),
    });
    // Wire autosave loader so sessions restore from disk on creation
    this.sessionPool._loadAutosave = (chatId) => this.loadAutosave(chatId);
    // Seed session '0' with pre-initialized orchestrator + restore from disk
    if (orchestrator && contextManager) {
      const entry = {
        chatId: '0',
        contextManager: this.contextManager,
        orchestrator: this.orchestrator,
        abortController: null,
        lastActivity: Date.now(),
        convName: null,
        convStatus: 'active',
        createdAt: Date.now(),
      };
      // Restore session '0' from autosave
      const saved = this.loadAutosave('0');
      if (saved?.messages?.length) {
        contextManager.messages = saved.messages;
        contextManager.turnCount = saved.turnCount || Math.ceil(saved.messages.length / 2);
        contextManager.estimateTokens();
        if (saved.todos?.length) orchestrator.todos = saved.todos;
        if (saved.planMode) orchestrator.planMode = true;
        orchestrator.isFirstMessage = saved.isFirstMessage ?? false;
        if (saved.convName) entry.convName = saved.convName;
      }
      this.sessionPool._sessions.set('0', entry);
    }

    // ConversationRegistry
    this._convRegistry = {
      conversations: [{ id: 0, name: 'Chat 1', dom: '' }],
      activeConvId: 0,
      convIdCounter: 1,
    };
    try {
      const wsPath = path.join(this.config.workDir, '.vl-code', 'workspace.json');
      if (fs.existsSync(wsPath)) {
        const ws = JSON.parse(fs.readFileSync(wsPath, 'utf-8'));
        if (ws.chatState?.conversations?.length) {
          this._convRegistry = ws.chatState;
        }
      }
    } catch {}

    // Workspace tabs
    this._workspaceStates = new Map();
    this._openTabs = this._loadOpenTabs();

    if (this.config.workDir && !this.config.noWorkspace && !this._openTabs.find(t => t.path === this.config.workDir)) {
      this._openTabs.push({ path: this.config.workDir, name: path.basename(this.config.workDir), openedAt: new Date().toISOString() });
      this._saveOpenTabs();
    }

    // Wire broadcast
    if (this.orchestrator) this.orchestrator._broadcast = (msg) => this.broadcast(msg);
    if (this.config) this.config._broadcast = (msg) => this.broadcast(msg);

    // Wire active executor management (used by VLGenerate tool to register its executor for pause/resume)
    if (this.config) {
      this.config._setActiveExecutor = (executor) => { this._activeExecutor = executor; };
      this.config._getActiveExecutor = () => this._activeExecutor;
      this.config._getWorkflowState = () => {
        this._currentRunState = ensureWorkflowStateShape(this._currentRunState, this.getVisibleWorkDir());
        return {
          active: !!this._currentRunState.active,
          workflowName: this._currentRunState.workflowName || null,
          nodeStatuses: { ...(this._currentRunState.nodeStatuses || {}) },
          nodeDetails: { ...(this._currentRunState.nodeDetails || {}) },
          timeline: [...(this._currentRunState.timeline || [])],
          startedAt: this._currentRunState.startedAt || null,
          finishedAt: this._currentRunState.finishedAt || null,
          lastError: this._currentRunState.lastError || null,
          workDir: this.getVisibleWorkDir(),
        };
      };
      this.config._recordWorkflowEvent = (event) => {
        this._currentRunState = ensureWorkflowStateShape(this._currentRunState, this.getVisibleWorkDir());
        const record = buildWorkflowEventRecord(event);
        this._currentRunState.timeline.push(record);
        if (this._currentRunState.timeline.length > MAX_WORKFLOW_TIMELINE) {
          this._currentRunState.timeline.splice(0, this._currentRunState.timeline.length - MAX_WORKFLOW_TIMELINE);
        }
        if (record.nodeId) {
          const prev = this._currentRunState.nodeDetails[record.nodeId] || {};
          this._currentRunState.nodeDetails[record.nodeId] = {
            ...prev,
            nodeId: record.nodeId,
            title: record.title || prev.title || record.nodeId,
            type: record.nodeType || prev.type || null,
            status: record.status || prev.status || null,
            input: record.input !== undefined ? record.input : prev.input,
            resolvedInputs: record.resolvedInputs !== undefined ? record.resolvedInputs : prev.resolvedInputs,
            output: record.output !== undefined ? record.output : prev.output,
            outputs: record.outputs !== undefined ? record.outputs : prev.outputs,
            selected: record.selected !== undefined ? record.selected : prev.selected,
            error: record.error !== undefined ? record.error : prev.error,
            detail: record.detail !== undefined ? record.detail : prev.detail,
            duration_ms: record.duration_ms !== undefined ? record.duration_ms : prev.duration_ms,
            updatedAt: record.ts,
          };
        }
        if (record.error) this._currentRunState.lastError = String(record.error);
        if (record.type === 'wf_done' || record.type === 'workflow_done') {
          this._currentRunState.finishedAt = record.ts;
        }
      };
      this.config._onWorkflowStart = (workflowName, meta = {}) => {
        this._currentRunState = ensureWorkflowStateShape(this._currentRunState, this.getVisibleWorkDir());
        this._currentRunState.active = true;
        this._currentRunState.workflowName = workflowName || null;
        this._currentRunState.nodeStatuses = {};
        this._currentRunState.nodeDetails = {};
        this._currentRunState.timeline = [];
        this._currentRunState.startedAt = meta.startedAt || new Date().toISOString();
        this._currentRunState.finishedAt = null;
        this._currentRunState.lastError = null;
      };
      this.config._onWorkflowNodeStatus = (nodeId, status, detail = null) => {
        if (!nodeId) return;
        this._currentRunState = ensureWorkflowStateShape(this._currentRunState, this.getVisibleWorkDir());
        this._currentRunState.nodeStatuses[nodeId] = status;
        const prev = this._currentRunState.nodeDetails[nodeId] || {};
        this._currentRunState.nodeDetails[nodeId] = {
          ...prev,
          ...(detail && typeof detail === 'object' ? summarizeWorkflowValue(detail) : {}),
          nodeId,
          status,
          updatedAt: new Date().toISOString(),
        };
      };
      this.config._onWorkflowError = (workflowName, error = null) => {
        this._activeExecutor = null;
        this._currentRunState = ensureWorkflowStateShape(this._currentRunState, this.getVisibleWorkDir());
        this._currentRunState.active = false;
        this._currentRunState.workflowName = null;
        this._currentRunState.finishedAt = new Date().toISOString();
        this._currentRunState.lastError = error ? String(error) : this._currentRunState.lastError;
      };
      this.config._onWorkflowDone = () => {
        this._activeExecutor = null;
        this._currentRunState = ensureWorkflowStateShape(this._currentRunState, this.getVisibleWorkDir());
        this._currentRunState.active = false;
        this._currentRunState.workflowName = null;
        this._currentRunState.finishedAt = new Date().toISOString();
        this.projectContext.scan().then(() => {
          this.broadcast({ type: 'project_reloaded' });
          this.autoExtractMeta();
        });
      };
    }

    // Wire workspace switch callback (used by VLGenerate to create+switch before running workflow)
    if (this.config) this.config._switchWorkspace = async (dirPath, name) => {
      await this.switchWorkspace(dirPath, { name, noWorkspace: false, saveAsRecent: true });
    };

    this.config.coreDocIds = {
      ...getDefaultCoreDocIds(),
      ...(this.config.coreDocIds || {}),
    };
    this.config.docIdOverrides = {
      ...getDefaultDocIdOverrides(),
      ...(this.config.docIdOverrides || {}),
    };
    if (!this.config.noWorkspace) {
      try { applyProjectProfileToConfig(this.config); } catch {}
    }

    // Restore cookie
    if (!this.config.cookie) {
      try {
        const profilePath = path.join(this.config.workDir, '.vl-code', 'project.json');
        if (fs.existsSync(profilePath)) {
          const profile = JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
          if (profile.cookie) this.config.cookie = profile.cookie;
        }
        if (!this.config.cookie) {
          const globalAuth = path.join(VL_CODE_HOME, 'auth.json');
          if (fs.existsSync(globalAuth)) {
            const auth = JSON.parse(fs.readFileSync(globalAuth, 'utf-8'));
            if (auth.cookie) this.config.cookie = auth.cookie;
          }
        }
      } catch {}
    }

    this.cloud = new CloudAPI(this.config);
    this._setupMiddleware();
    this._setupRoutes();
  }

  _setupMiddleware() {
    this.app.use(express.json({ limit: '50mb' }));
    this.app.use('/lib/codemirror', express.static(resolveNodeModulePath('codemirror')));
    this.app.use(express.static(resolvePublicPath(), { etag: false, lastModified: false, setHeaders: (res) => res.setHeader('Cache-Control', 'no-cache, no-store') }));
  }

  _setupRoutes() {
    setupSSE(this.app, this);
    setupChatRoutes(this.app, this);
    setupFileRoutes(this.app, this);
    setupProjectRoutes(this.app, this);
    setupWorkspaceRoutes(this.app, this);
    setupCompileRoutes(this.app, this);
    setupWorkflowRoutes(this.app, this);
    setupCloudRoutes(this.app, this);
    setupIntelligenceRoutes(this.app, this);
    setupToolRoutes(this.app, this);
    setupConversationRoutes(this.app, this);
    setupMiscRoutes(this.app, this);
  }

  // --- Core methods ---

  broadcast(data) {
    const msg = `data: ${JSON.stringify(data)}\n\n`;
    for (const client of this.sseClients) {
      try { client.write(msg); } catch {}
    }
  }

  getSession(req) {
    const chatId = req.body?.chatId || req.query?.chatId || '0';
    return this.sessionPool.getOrCreate(chatId);
  }

  isBusy() {
    for (const [, s] of this.sessionPool._sessions) {
      if (s.abortController && !s.abortController.signal.aborted) return true;
    }
    return false;
  }

  reregisterVLTools() {
    if (!this.toolRegistry) return;
    this.toolRegistry.clear();
    registerAllTools(this.toolRegistry, this.config, this.projectContext, { localWorkspace: this.localWorkspace });
    if (this.symbolIndex && this.impactAnalyzer && this.autoFix) {
      registerIntelligenceTools(this.toolRegistry, this.config, {
        symbolIndex: this.symbolIndex, impactAnalyzer: this.impactAnalyzer,
        autoFix: this.autoFix, projectContext: this.projectContext,
      });
    }
    // Load user-defined tools from ~/.vl-code/tools/ (async, non-blocking)
    loadUserTools(this.toolRegistry, this.config).catch(() => {});
  }

  hasSelectedWorkspace() {
    return !this.config.noWorkspace;
  }

  getVisibleWorkDir() {
    return this.hasSelectedWorkspace() ? this.config.workDir : '';
  }

  _buildEmptySummary() {
    return {
      projectName: null,
      totalFiles: 0,
      breakdown: '',
      version: null,
    };
  }

  async switchWorkspace(dirPath, { name, noWorkspace = false, saveAsRecent = true } = {}) {
    const previousVisibleDir = this.getVisibleWorkDir();

    if (previousVisibleDir) {
      for (const [chatId, s] of this.sessionPool._sessions) {
        this.autosaveSession(chatId, s).catch(() => {});
      }
      this._workspaceStates.set(previousVisibleDir, {
        convRegistry: JSON.parse(JSON.stringify(this._convRegistry)),
      });
      this.persistChatState();
    }

    for (const [chatId] of this.sessionPool._sessions) {
      this.sessionPool.delete(chatId);
    }

    if (!noWorkspace) {
      const seedDir = resolveSeedWorkflowDir();
      ensureProjectProfile(dirPath, seedDir);
      const list = loadWorkspaces();
      if (!list.find(w => w.path === dirPath)) {
        list.push({ path: dirPath, name: name || path.basename(dirPath), addedAt: new Date().toISOString() });
        saveWorkspaces(list);
      }
      if (!this._openTabs.find(t => t.path === dirPath)) {
        this._openTabs.push({ path: dirPath, name: name || path.basename(dirPath), openedAt: new Date().toISOString() });
        this._saveOpenTabs();
      }
      if (saveAsRecent) this._saveLastWorkspace(dirPath);
    }

    this.config.noWorkspace = !!noWorkspace;
    this.config.workDir = dirPath;
    if (noWorkspace) {
      this.config.coreDocIds = getDefaultCoreDocIds();
      this.config.docIdOverrides = getDefaultDocIdOverrides();
    } else {
      applyProjectProfileToConfig(this.config);
    }

    if (this.projectContext) {
      this.projectContext.workDir = dirPath;
      await this.projectContext.load();
    }

    this.reregisterVLTools?.();
    if (this.symbolIndex) await this.symbolIndex.build().catch(() => {});
    if (this.blueprintContext) await this.blueprintContext.load().catch(() => {});

    if (!noWorkspace && this._workspaceStates.has(dirPath)) {
      const saved = this._workspaceStates.get(dirPath);
      this._convRegistry = saved.convRegistry;
    } else if (!noWorkspace) {
      this._convRegistry = {
        conversations: [{ id: 0, name: 'Chat 1', dom: '' }],
        activeConvId: 0,
        convIdCounter: 1,
      };
      try {
        const wsPath = path.join(dirPath, '.vl-code', 'workspace.json');
        if (fs.existsSync(wsPath)) {
          const ws = JSON.parse(fs.readFileSync(wsPath, 'utf-8'));
          if (ws.chatState?.conversations?.length) this._convRegistry = ws.chatState;
        }
      } catch {}
    } else {
      this._convRegistry = {
        conversations: [{ id: 0, name: 'Chat 1', dom: '' }],
        activeConvId: 0,
        convIdCounter: 1,
      };
    }

    await this.config._onWorkspaceSwitched?.(dirPath, { noWorkspace });

    if (this._serverPort) {
      registerInstance(this._serverPort, noWorkspace ? '' : dirPath, process.pid);
    }

    const visibleDir = this.getVisibleWorkDir();
    this.broadcast({ type: 'workspace_switched', workDir: visibleDir });
    this.broadcast({ type: 'ws_tabs_changed', tabs: this._openTabs, activeDir: visibleDir });

    return {
      ok: true,
      workDir: visibleDir,
      isVL: noWorkspace ? false : this.projectContext.isVLProject(),
      summary: noWorkspace ? this._buildEmptySummary() : this.projectContext.getSummary(),
      chatState: this._convRegistry,
    };
  }

  async autoExtractMeta() {
    try {
      const { extractFromFileTree } = await import('../vl/metadata-extractor.js');
      const fileTree = {};
      for (const file of this.projectContext.getAllFiles()) {
        try {
          fileTree[file.path] = fs.readFileSync(file.fullPath, 'utf-8');
        } catch {}
      }
      if (Object.keys(fileTree).length === 0) return;
      const meta = extractFromFileTree(fileTree, this.config.workDir);
      if (meta) {
        const metaPath = path.join(this.config.workDir, '.vl-code', 'ProjectMeta.json');
        const dir = path.dirname(metaPath);
        if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
        fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
        this.projectContext.projectMeta = meta;
        this.broadcast({ type: 'metadata_ready', meta });
      }
    } catch (e) {
      this.broadcast({ type: 'metadata_skipped', reason: e.message });
    }
  }

  async autosaveSession(chatId, session) {
    try {
      const savePath = path.join(this.config.workDir, '.vl-code', 'autosave');
      if (!fs.existsSync(savePath)) fs.mkdirSync(savePath, { recursive: true });
      const cm = session.contextManager;
      const orch = session.orchestrator;
      const data = {
        chatId,
        savedAt: new Date().toISOString(),
        messageCount: cm.messages.length,
        messages: cm.messages,
        // Orchestrator state
        todos: orch?.todos || [],
        planMode: !!orch?.planMode,
        planContent: orch?.planContent || null,
        isFirstMessage: !!orch?.isFirstMessage,
        // Context manager state
        turnCount: cm.turnCount,
        compressionCount: cm.compressionCount,
        totalTokensSaved: cm.totalTokensSaved,
        totalInputTokens: cm.totalInputTokens,
        totalOutputTokens: cm.totalOutputTokens,
        // Session metadata
        convName: session.convName || null,
        convStatus: session.convStatus || 'active',
      };
      fs.writeFileSync(path.join(savePath, `session-${chatId}.json`), JSON.stringify(data));
    } catch {}
  }

  /** Load autosaved session data for a chatId (returns null if none) */
  loadAutosave(chatId) {
    try {
      const fp = path.join(this.config.workDir, '.vl-code', 'autosave', `session-${chatId}.json`);
      if (fs.existsSync(fp)) return JSON.parse(fs.readFileSync(fp, 'utf-8'));
    } catch {}
    return null;
  }

  persistChatState() {
    try {
      const wsDir = path.join(this.config.workDir, '.vl-code');
      if (!fs.existsSync(wsDir)) fs.mkdirSync(wsDir, { recursive: true });
      const wsPath = path.join(wsDir, 'workspace.json');
      let ws = {};
      try { ws = JSON.parse(fs.readFileSync(wsPath, 'utf-8')); } catch {}
      ws.chatState = this._convRegistry;
      ws.updatedAt = new Date().toISOString();
      fs.writeFileSync(wsPath, JSON.stringify(ws, null, 2));
    } catch {}
  }

  // --- Workspace tabs persistence ---

  _loadOpenTabs() {
    try {
      const tabsFile = path.join(VL_CODE_HOME, 'open-tabs.json');
      if (fs.existsSync(tabsFile)) return JSON.parse(fs.readFileSync(tabsFile, 'utf-8'));
    } catch {}
    return [];
  }

  _saveOpenTabs() {
    try {
      ensureVLCodeHome();
      fs.writeFileSync(path.join(VL_CODE_HOME, 'open-tabs.json'), JSON.stringify(this._openTabs, null, 2));
    } catch {}
  }

  _loadLastWorkspace() {
    try {
      const fp = path.join(VL_CODE_HOME, 'last-workspace.json');
      if (fs.existsSync(fp)) {
        const data = JSON.parse(fs.readFileSync(fp, 'utf-8'));
        return data.workDir || null;
      }
    } catch {}
    return null;
  }

  _saveLastWorkspace(dirPath) {
    try {
      ensureVLCodeHome();
      fs.writeFileSync(path.join(VL_CODE_HOME, 'last-workspace.json'), JSON.stringify({ workDir: dirPath, savedAt: new Date().toISOString() }, null, 2));
    } catch {}
  }

  async start(port) {
    // Ensure VL Bible + project profile
    ensureVLBible(resolveDataPath());
    const seedDir = resolveSeedWorkflowDir();
    if (!this.config.noWorkspace) {
      ensureProjectProfile(this.config.workDir, seedDir);
      applyProjectProfileToConfig(this.config);
    }

    // Re-load project context from the currently selected workspace (or empty-window dir)
    if (this.projectContext) {
      this.projectContext.workDir = this.config.workDir;
      await this.projectContext.load();
    }
    // Rebuild symbol index for restored workspace
    if (this.symbolIndex) {
      await this.symbolIndex.build().catch(() => {});
    }

    return new Promise((resolve, reject) => {
      const tryListen = (candidatePort) => {
        const server = this.app.listen(candidatePort, () => {
          this.httpServer = server;
          this._serverPort = candidatePort;
          if (this.config) this.config.port = candidatePort;
          if (this.orchestrator?.client) this.orchestrator.client.serverPort = candidatePort;
          resolve(candidatePort);
        });

        server.on('error', (err) => {
          if (err.code === 'EADDRINUSE' && candidatePort < MAX_SERVER_PORT) {
            tryListen(candidatePort + 1);
            return;
          }
          if (err.code === 'EADDRINUSE') {
            console.error(`Port ${candidatePort} is already in use. No free port found up to ${MAX_SERVER_PORT}.`);
          } else {
            console.error(`Server error: ${err.message}`);
          }
          reject(err);
        });
      };

      tryListen(port);
    });
  }

  async stop() {
    if (this._sseHeartbeat) {
      clearInterval(this._sseHeartbeat);
      this._sseHeartbeat = null;
    }
    if (!this.httpServer) return;
    await new Promise((resolve) => this.httpServer.close(() => resolve()));
    this.httpServer = null;
    this._serverPort = null;
  }
}
