/**
 * Workflow routes — /api/workflow/*, /api/generate, /api/workflows
 */
import fs from 'fs';
import path from 'path';
import { VL_CODE_HOME } from '../helpers.js';

// Checkpoint store: keyed by runID
const _checkpointStore = new Map();
const _workflowStore = new Map(); // runID → workflow JSON (for rerun)

function cloneJSON(value) {
  if (value == null) return value;
  if (typeof structuredClone === 'function') return structuredClone(value);
  return JSON.parse(JSON.stringify(value));
}

function getWorkflowDirs(server) {
  return [
    path.join(server.config.workDir, '.vl-code', 'workflows'),
    path.join(VL_CODE_HOME, 'workflows'),
  ];
}

function resolveWorkflow(server, workflowRef) {
  if (!workflowRef) return null;

  const dirs = getWorkflowDirs(server);
  const candidates = workflowRef.endsWith('.json') ? [workflowRef] : [workflowRef, `${workflowRef}.json`];

  for (const dir of dirs) {
    for (const file of candidates) {
      const fp = path.join(dir, file);
      if (fs.existsSync(fp)) {
        const workflowJson = JSON.parse(fs.readFileSync(fp, 'utf-8'));
        return {
          workflowJson,
          workflowKey: file.replace(/\.json$/i, ''),
          file,
          source: dir.includes('.vl-code') ? 'project' : 'global',
        };
      }
    }
  }

  // Fallback: resolve by workflow JSON name/title so editor can run workflows
  // whose file name differs from the embedded `name` field.
  for (const dir of dirs) {
    if (!fs.existsSync(dir)) continue;
    for (const file of fs.readdirSync(dir).filter((f) => f.endsWith('.json'))) {
      try {
        const workflowJson = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf-8'));
        if (workflowJson.name === workflowRef || workflowJson.title === workflowRef) {
          return {
            workflowJson,
            workflowKey: file.replace(/\.json$/i, ''),
            file,
            source: dir.includes('.vl-code') ? 'project' : 'global',
          };
        }
      } catch {}
    }
  }

  return null;
}

function resolveWorkflowContent(server, workflowRef, workDir = '') {
  if (!workflowRef) return null;

  const requestedWorkDir = String(workDir || '').trim();
  const baseDir = requestedWorkDir
    ? (path.isAbsolute(requestedWorkDir) ? requestedWorkDir : path.resolve(server.config.workDir, requestedWorkDir))
    : server.config.workDir;
  const candidates = [];
  const normalizedRef = String(workflowRef).trim();
  const withJson = normalizedRef.endsWith('.json') ? normalizedRef : `${normalizedRef}.json`;

  if (path.isAbsolute(normalizedRef)) {
    candidates.push(normalizedRef);
  } else {
    candidates.push(path.resolve(baseDir, normalizedRef));
    candidates.push(path.resolve(baseDir, withJson));
    candidates.push(path.resolve(baseDir, '.vl-code', 'workflows', normalizedRef));
    candidates.push(path.resolve(baseDir, '.vl-code', 'workflows', withJson));
  }

  for (const fullPath of candidates) {
    if (!fullPath || !fs.existsSync(fullPath)) continue;
    try {
      const workflowJson = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
      return {
        workflowJson,
        workflowKey: path.basename(fullPath).replace(/\.json$/i, ''),
        file: path.basename(fullPath),
        fullPath,
        source: fullPath.includes('.vl-code/workflows') ? 'project' : 'file',
      };
    } catch {}
  }

  return resolveWorkflow(server, normalizedRef);
}

function ensureActiveExecutors(server) {
  if (!server._activeExecutors) server._activeExecutors = new Map();
  return server._activeExecutors;
}

function getExecutorRunID(executor) {
  return executor?.getRunID?.() || executor?._serverRunID || executor?.getCheckpoint?.()?.workflowID || null;
}

function normalizeClientRunToken(value) {
  const token = String(value || '').trim();
  return token || null;
}

function setExecutorClientRunToken(executor, clientRunToken) {
  const token = normalizeClientRunToken(clientRunToken);
  if (executor && token) executor._clientRunToken = token;
  return token;
}

function getExecutorClientRunToken(executor) {
  return normalizeClientRunToken(executor?._clientRunToken);
}

function withRunContext(payload, runID, clientRunToken) {
  const data = payload && typeof payload === 'object' ? { ...payload } : {};
  if (runID) data.runID = runID;
  if (clientRunToken) data.clientRunToken = clientRunToken;
  return data;
}

function listStepTargets(step) {
  const targets = [];
  const push = (value) => {
    if (typeof value !== 'string' || !value || value === 'RETURN' || value === 'STOP' || value === 'BREAK') return;
    targets.push(value);
  };

  push(step?.next);
  push(step?.if_true);
  push(step?.if_false);
  push(step?.onError);

  for (const child of step?.children || []) push(child);
  for (const entry of step?.cases || []) {
    if (Array.isArray(entry)) push(entry[1]);
    else push(entry?.next || entry?.step);
  }
  if (step?.branches && typeof step.branches === 'object') {
    if (Array.isArray(step.branches)) {
      for (const entry of step.branches) {
        if (Array.isArray(entry)) push(entry[1]);
      }
    } else {
      for (const target of Object.values(step.branches)) push(target);
    }
  }

  return targets;
}

function collectReachableStepIDs(workflowJson, startStepID) {
  if (!workflowJson?.steps?.length || !startStepID) return new Set();
  const stepMap = new Map(workflowJson.steps.map((step) => [step.id, step]));
  const visited = new Set();
  const stack = [startStepID];

  while (stack.length) {
    const stepID = stack.pop();
    if (!stepID || visited.has(stepID)) continue;
    visited.add(stepID);
    const step = stepMap.get(stepID);
    if (!step) continue;
    for (const target of listStepTargets(step)) {
      if (!visited.has(target)) stack.push(target);
    }
  }

  return visited;
}

export function prepareCheckpointForRerun(workflowJson, checkpoint, stepID = null) {
  const cp = cloneJSON(checkpoint || {}) || {};
  if (stepID) cp.currentStepID = stepID;
  const startStepID = cp.currentStepID;
  const reachable = collectReachableStepIDs(workflowJson, startStepID);

  if (reachable.size && Array.isArray(cp.completedSteps)) {
    cp.completedSteps = cp.completedSteps.filter((id) => !reachable.has(id));
  }

  if (reachable.size && cp.completedBranches && typeof cp.completedBranches === 'object') {
    cp.completedBranches = Object.fromEntries(
      Object.entries(cp.completedBranches).filter(([parentID]) => !reachable.has(parentID))
    );
  }

  if (reachable.size && cp.loopProgress && typeof cp.loopProgress === 'object') {
    cp.loopProgress = Object.fromEntries(
      Object.entries(cp.loopProgress).filter(([loopID]) => !reachable.has(loopID))
    );
  }

  return cp;
}

function trackExecutor(server, executor, runID) {
  if (!runID) return;
  ensureActiveExecutors(server).set(runID, executor);
  executor._serverRunID = runID;
  server._activeExecutor = executor;
}

function untrackExecutor(server, executor, runID = getExecutorRunID(executor)) {
  const activeExecutors = ensureActiveExecutors(server);
  if (runID && activeExecutors.get(runID) === executor) {
    activeExecutors.delete(runID);
  }
  if (server._activeExecutor === executor) {
    server._activeExecutor = null;
  }
}

function resolveExecutor(server, runID) {
  const activeExecutors = ensureActiveExecutors(server);
  if (runID && activeExecutors.has(runID)) return activeExecutors.get(runID);
  if (runID) {
    for (const executor of activeExecutors.values()) {
      if (getExecutorRunID(executor) === runID) return executor;
    }
  }
  return server._activeExecutor || null;
}

function resetCurrentRunState(server, workflowName) {
  server._currentRunState.active = true;
  server._currentRunState.workflowName = workflowName || null;
  server._currentRunState.nodeStatuses = {};
  server._currentRunState.nodeDetails = {};
  server._currentRunState.timeline = [];
  server._currentRunState.startedAt = new Date().toISOString();
  server._currentRunState.finishedAt = null;
  server._currentRunState.lastError = null;
}

function updateCurrentRunNode(server, nodeId, status, details = {}) {
  if (!nodeId) return;
  server._currentRunState.nodeStatuses[nodeId] = status;
  const prev = server._currentRunState.nodeDetails[nodeId] || {};
  server._currentRunState.nodeDetails[nodeId] = {
    ...prev,
    ...details,
    status,
  };
}

function settleCurrentRunState(server, { error = null, markRunningAsDone = false } = {}) {
  if (markRunningAsDone) {
    for (const [nodeId, status] of Object.entries(server._currentRunState.nodeStatuses || {})) {
      if (status === 'running' || status === 'paused') {
        updateCurrentRunNode(server, nodeId, 'done');
      }
    }
  }
  server._currentRunState.active = false;
  server._currentRunState.workflowName = null;
  server._currentRunState.finishedAt = new Date().toISOString();
  if (error != null) server._currentRunState.lastError = String(error);
}

function setCheckpointStatus(runID, status) {
  if (!runID) return;
  const cp = _checkpointStore.get(runID);
  if (cp) cp.status = status;
}

export function setupWorkflowRoutes(app, server) {
  ensureActiveExecutors(server);
  // Current run state on server — sent to new SSE clients on connect for multi-window sync
  if (!server._currentRunState) {
    server._currentRunState = {
      active: false,
      workflowName: null,
      nodeStatuses: {},
      nodeDetails: {},
      timeline: [],
      startedAt: null,
      finishedAt: null,
      lastError: null,
    };
  } else {
    server._currentRunState.nodeStatuses = server._currentRunState.nodeStatuses || {};
    server._currentRunState.nodeDetails = server._currentRunState.nodeDetails || {};
    server._currentRunState.timeline = server._currentRunState.timeline || [];
  }
  // UI state — selected workflow in Flow panel, current tab
  if (!server._uiState) {
    server._uiState = { flowWorkflow: null, flowTab: 'generate' };
  }
  // --- Execute workflow (SSE) ---
  app.post('/api/workflow/execute', async (req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
    const send = (event, data) => res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);

    try {
      const { workflowName, params, projectDir, clientRunToken } = req.body;
      if (!workflowName) { send('error', { message: 'workflowName required' }); res.end(); return; }
      const normalizedClientRunToken = normalizeClientRunToken(clientRunToken);

      // Switch workspace if projectDir is specified (for generate workflows targeting a new project)
      if (projectDir && fs.existsSync(projectDir) && projectDir !== server.config.workDir) {
        if (server.config._switchWorkspace) {
          await server.config._switchWorkspace(projectDir, path.basename(projectDir));
          server.broadcast({ type: 'workspace_switched', workDir: projectDir });
        }
      }

      // Find workflow JSON
      const resolvedWorkflow = resolveWorkflow(server, workflowName);
      const workflowJson = resolvedWorkflow?.workflowJson || null;
      if (!workflowJson) { send('error', { message: `Workflow "${workflowName}" not found` }); res.end(); return; }

      // Store workflow for rerun
      _workflowStore.set(workflowName, workflowJson);
      if (resolvedWorkflow?.workflowKey) _workflowStore.set(resolvedWorkflow.workflowKey, workflowJson);
      if (workflowJson.name) _workflowStore.set(workflowJson.name, workflowJson);

      // Track current run state for new clients syncing on connect
      resetCurrentRunState(server, workflowName);

      send('workflow_start', withRunContext({ name: workflowName, stepCount: workflowJson.steps?.length || 0, model: server.config.model || 'unknown' }, null, normalizedClientRunToken));
      server.broadcast({ type: 'wf_start', workflowName, stepCount: workflowJson.steps?.length || 0, model: server.config.model || 'unknown', clientRunToken: normalizedClientRunToken });

      const { WorkflowExecutor } = await import('../../vl/workflow-executor.js');
      const executor = new WorkflowExecutor(server.config);
      if (server.symbolIndex) executor.symbolIndex = server.symbolIndex;
      setExecutorClientRunToken(executor, normalizedClientRunToken);
      server._activeExecutor = executor;

      req.socket?.once('close', () => {
        executor.abort();
        untrackExecutor(server, executor);
      });

      await executor.execute(workflowJson, params || {}, {
        onNodeStart: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          updateCurrentRunNode(server, info.nodeId, 'running', {
            title: info.title,
            type: info.type,
            input: info.input,
            resolvedInputs: info.resolvedInputs,
          });
          send('node_start', withRunContext(info, runID, token));
          // Broadcast FULL event data (not just nodeId+status) so any tab can render step cards
          server.broadcast({ type: 'workflow_node_update', nodeId: info.nodeId, status: 'running', runID, clientRunToken: token });
          server.broadcast({ type: 'wf_node_start', nodeId: info.nodeId, title: info.title, nodeType: info.type, input: info.input, resolvedInputs: info.resolvedInputs, runID, clientRunToken: token });
        },
        onNodeDone: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          updateCurrentRunNode(server, info.nodeId, 'done', {
            title: info.title,
            duration_ms: info.duration_ms,
            output: info.output,
            outputs: info.outputs,
            selected: info.selected,
          });
          send('node_done', withRunContext(info, runID, token));
          server.broadcast({ type: 'workflow_node_update', nodeId: info.nodeId, status: 'done', runID, clientRunToken: token });
          server.broadcast({ type: 'wf_node_done', nodeId: info.nodeId, title: info.title, duration_ms: info.duration_ms, output: info.output, outputs: info.outputs, selected: info.selected, runID, clientRunToken: token });
        },
        onNodeError: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          updateCurrentRunNode(server, info.nodeId, 'error', {
            title: info.title,
            error: info.error,
            detail: info.detail,
            duration_ms: info.duration_ms,
          });
          send('node_error', withRunContext(info, runID, token));
          server.broadcast({ type: 'workflow_node_update', nodeId: info.nodeId, status: 'error', runID, clientRunToken: token });
          server.broadcast({ type: 'wf_node_error', nodeId: info.nodeId, title: info.title, error: info.error, detail: info.detail, duration_ms: info.duration_ms, runID, clientRunToken: token });
        },
        onNodeSkipped: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          updateCurrentRunNode(server, info.nodeId, 'skipped', {
            reason: info.reason,
          });
          send('node_skipped', withRunContext(info, runID, token));
          server.broadcast({ type: 'workflow_node_update', nodeId: info.nodeId, status: 'skipped', runID, clientRunToken: token });
          server.broadcast({ type: 'wf_node_skipped', nodeId: info.nodeId, runID, clientRunToken: token });
        },
        onPause: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          updateCurrentRunNode(server, info.nodeId, 'paused', {
            title: info.title,
            reason: info.reason,
            resumeResultTarget: info.resumeResultTarget,
          });
          trackExecutor(server, executor, runID);
          send('pause', withRunContext(info, runID, token));
          server.broadcast({ type: 'workflow_node_update', nodeId: info.nodeId, status: 'paused', runID, clientRunToken: token });
        },
        onResumed: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          updateCurrentRunNode(server, info.nodeId, 'running');
          send('resumed', withRunContext(info, runID, token));
          server.broadcast({ type: 'workflow_node_update', nodeId: info.nodeId, status: 'running', runID, clientRunToken: token });
        },
        onVarsChanged: (nodeId, vars) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('workflow_vars', withRunContext({ nodeId, vars }, runID, token));
          server.broadcast({ type: 'wf_vars', nodeId, vars, runID, clientRunToken: token });
        },
        onLLMThinking: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('llm_thinking', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_llm_thinking', ...info, runID, clientRunToken: token });
        },
        onLLMToolUse: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('llm_tool_use', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_llm_tool_use', ...info, runID, clientRunToken: token });
        },
        onLLMToolResult: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('llm_tool_result', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_llm_tool_result', ...info, runID, clientRunToken: token });
        },
        onLLMDone: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('llm_done', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_llm_done', ...info, runID, clientRunToken: token });
        },
        onLLMError: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('llm_error', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_llm_error', ...info, runID, clientRunToken: token });
        },
        onToolStart: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('tool_start', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_tool_start', ...info, runID, clientRunToken: token });
        },
        onToolDone: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('tool_done', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_tool_done', ...info, runID, clientRunToken: token });
        },
        onToolError: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('tool_error', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_tool_error', ...info, runID, clientRunToken: token });
        },
        onToolMessage: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('tool_message', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_tool_message', ...info, runID, clientRunToken: token });
        },
        onFileStart: (fp) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('file_start', withRunContext({ path: fp }, runID, token));
          server.broadcast({ type: 'wf_file_start', path: fp, runID, clientRunToken: token });
        },
        onVarChanged: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('var_changed', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_var_changed', ...info, runID, clientRunToken: token });
        },
        onFileWritten: (fp) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('file_written', withRunContext({ path: fp }, runID, token));
          // Broadcast file_changed for real-time file tree update + file done for step cards
          server.broadcast({ type: 'file_changed', path: fp });
          server.broadcast({ type: 'wf_file_done', path: fp, runID, clientRunToken: token });
        },
        onFileChanged: (fp, sizeBytes) => {
          server.broadcast({ type: 'file_changed', path: fp, size_bytes: sizeBytes });
        },
        onCheckpoint: (cp) => {
          // Store checkpoint for resume/rerun API
          const runID = cp.workflowID || executor.getRunID();
          const token = getExecutorClientRunToken(executor);
          if (runID) {
            _checkpointStore.set(runID, cp);
            _workflowStore.set(runID, workflowJson);
            trackExecutor(server, executor, runID);
          }
          send('checkpoint', withRunContext({ stepID: cp.currentStepID, completedSteps: cp.completedSteps, checkpoint: cp }, runID, token));
          server.broadcast({ type: 'wf_checkpoint', runID, clientRunToken: token, stepID: cp.currentStepID, completedSteps: cp.completedSteps, checkpoint: cp });
        },
        onToken: (tokenText) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('token', withRunContext({ token: tokenText }, runID, token));
        },
        onText: (text) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('text', withRunContext({ text }, runID, token));
          server.broadcast({ type: 'wf_text', text, runID, clientRunToken: token });
        },
        onScreenshot: (data) => send('screenshot', { tool: 'workflow', screenshots: [data.screenshot] }),
        onDone: (summary) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          untrackExecutor(server, executor, runID);
          settleCurrentRunState(server, { markRunningAsDone: true });
          setCheckpointStatus(runID, 'completed');
          server.projectContext.scan().then(() => {
            server.broadcast({ type: 'project_reloaded' });
            server.autoExtractMeta();
          });
          send('done', withRunContext(summary, runID, token));
          server.broadcast({ type: 'wf_done', workflowName, ...summary, runID, clientRunToken: token });
        },
        onError: (err) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          untrackExecutor(server, executor, runID);
          settleCurrentRunState(server, { error: err });
          setCheckpointStatus(runID, 'failed');
          send('error', withRunContext({ message: err }, runID, token));
          server.broadcast({ type: 'wf_error', workflowName, error: err, runID, clientRunToken: token });
        },
      });
    } catch (e) {
      send('error', { message: e.message });
    }
    res.end();
  });

  // --- Resume paused workflow ---
  app.post('/api/workflow/resume', (req, res) => {
    const executor = resolveExecutor(server, req.body.runID);
    if (!executor) return res.json({ ok: false, error: 'No active workflow' });
    try {
      const result = executor.resume(req.body.nodeId, req.body.payload);
      if (!result) return res.json({ ok: false, error: 'Workflow is not paused' });
      res.json({ ok: true });
    } catch (e) {
      console.error('[Workflow] Resume error:', e.message);
      res.json({ ok: false, error: e.message });
    }
  });

  // --- Cancel paused workflow ---
  app.post('/api/workflow/cancel', (req, res) => {
    const executor = resolveExecutor(server, req.body.runID);
    if (!executor) return res.json({ ok: false, error: 'No active workflow' });
    try {
      const result = executor.cancelPause(req.body.nodeId);
      if (!result) return res.json({ ok: false, error: 'Workflow is not paused' });
      res.json({ ok: true });
    }
    catch (e) { res.json({ ok: false, error: e.message }); }
  });

  // --- Pause running workflow (workflow-editor v2.1) ---
  app.post('/api/workflow/:runID/pause', (req, res) => {
    const executor = resolveExecutor(server, req.params.runID);
    if (!executor) return res.json({ ok: false, error: 'No active workflow' });
    try { executor.abort(); untrackExecutor(server, executor, req.params.runID); res.json({ ok: true }); }
    catch (e) { res.json({ ok: false, error: e.message }); }
  });

  // --- Abort running workflow (workflow-editor v2.1) ---
  app.post('/api/workflow/:runID/abort', (req, res) => {
    const executor = resolveExecutor(server, req.params.runID);
    if (!executor) return res.json({ ok: false, error: 'No active workflow' });
    try { executor.abort(); untrackExecutor(server, executor, req.params.runID); res.json({ ok: true }); }
    catch (e) { res.json({ ok: false, error: e.message }); }
  });

  // --- Get latest checkpoint for a workflow run ---
  app.get('/api/workflow/:runID/checkpoint', (req, res) => {
    const cp = _checkpointStore.get(req.params.runID);
    if (!cp) {
      // Also check active executor
      const executor = resolveExecutor(server, req.params.runID);
      if (executor) {
        const live = executor.getCheckpoint();
        if (live) return res.json(live);
      }
      return res.status(404).json({ error: 'No checkpoint found' });
    }
    res.json(cp);
  });

  // --- List all available checkpoints ---
  app.get('/api/workflow/checkpoints', (req, res) => {
    const list = [];
    for (const [runID, cp] of _checkpointStore) {
      list.push({
        runID,
        status: cp.status,
        currentStepID: cp.currentStepID,
        completedSteps: cp.completedSteps,
        createdAt: cp.createdAt,
      });
    }
    res.json(list);
  });

  // --- Re-run workflow from a specific step ---
  app.post('/api/workflow/rerun', async (req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
    const send = (event, data) => res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);

    try {
      const { workflowName, checkpoint, stepID, overrides, clientRunToken } = req.body;
      if (!checkpoint) { send('error', { message: 'checkpoint required' }); res.end(); return; }
      const normalizedClientRunToken = normalizeClientRunToken(clientRunToken);

      // Find workflow JSON
      let workflowJson = _workflowStore.get(workflowName);
      if (!workflowJson) {
        workflowJson = resolveWorkflow(server, workflowName)?.workflowJson || null;
      }
      if (!workflowJson) { send('error', { message: `Workflow "${workflowName}" not found` }); res.end(); return; }

      // Override stepID if provided
      const cp = prepareCheckpointForRerun(workflowJson, checkpoint, stepID);

      resetCurrentRunState(server, workflowName || workflowJson.name || null);

      send('workflow_start', withRunContext({ name: workflowName, resumedFrom: cp.currentStepID, model: server.config.model || 'unknown' }, null, normalizedClientRunToken));
      server.broadcast({ type: 'wf_start', workflowName: workflowName || '', stepCount: workflowJson.steps?.length || 0, model: server.config.model || 'unknown', resumedFrom: cp.currentStepID, clientRunToken: normalizedClientRunToken });

      const { WorkflowExecutor } = await import('../../vl/workflow-executor.js');
      const executor = new WorkflowExecutor(server.config);
      if (server.symbolIndex) executor.symbolIndex = server.symbolIndex;
      setExecutorClientRunToken(executor, normalizedClientRunToken);
      server._activeExecutor = executor;

      req.socket?.once('close', () => {
        executor.abort();
        untrackExecutor(server, executor);
      });

      await executor.executeFrom(workflowJson, cp, overrides || {}, {
        onNodeStart: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          updateCurrentRunNode(server, info.nodeId, 'running', {
            title: info.title,
            type: info.type,
            input: info.input,
            resolvedInputs: info.resolvedInputs,
          });
          send('node_start', withRunContext(info, runID, token));
          server.broadcast({ type: 'workflow_node_update', nodeId: info.nodeId, status: 'running', runID, clientRunToken: token });
          server.broadcast({ type: 'wf_node_start', nodeId: info.nodeId, title: info.title, nodeType: info.type, input: info.input, resolvedInputs: info.resolvedInputs, runID, clientRunToken: token });
        },
        onNodeDone: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          updateCurrentRunNode(server, info.nodeId, 'done', {
            title: info.title,
            duration_ms: info.duration_ms,
            output: info.output,
            outputs: info.outputs,
            selected: info.selected,
          });
          send('node_done', withRunContext(info, runID, token));
          server.broadcast({ type: 'workflow_node_update', nodeId: info.nodeId, status: 'done', runID, clientRunToken: token });
          server.broadcast({ type: 'wf_node_done', nodeId: info.nodeId, title: info.title, duration_ms: info.duration_ms, output: info.output, outputs: info.outputs, selected: info.selected, runID, clientRunToken: token });
        },
        onNodeError: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          updateCurrentRunNode(server, info.nodeId, 'error', {
            title: info.title,
            error: info.error,
            detail: info.detail,
            duration_ms: info.duration_ms,
          });
          send('node_error', withRunContext(info, runID, token));
          server.broadcast({ type: 'workflow_node_update', nodeId: info.nodeId, status: 'error', runID, clientRunToken: token });
          server.broadcast({ type: 'wf_node_error', nodeId: info.nodeId, title: info.title, error: info.error, detail: info.detail, duration_ms: info.duration_ms, runID, clientRunToken: token });
        },
        onNodeSkipped: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          updateCurrentRunNode(server, info.nodeId, 'skipped', {
            reason: info.reason,
          });
          send('node_skipped', withRunContext(info, runID, token));
          server.broadcast({ type: 'workflow_node_update', nodeId: info.nodeId, status: 'skipped', runID, clientRunToken: token });
          server.broadcast({ type: 'wf_node_skipped', nodeId: info.nodeId, runID, clientRunToken: token });
        },
        onPause: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          updateCurrentRunNode(server, info.nodeId, 'paused', {
            title: info.title,
            reason: info.reason,
            resumeResultTarget: info.resumeResultTarget,
          });
          trackExecutor(server, executor, runID);
          send('pause', withRunContext(info, runID, token));
          server.broadcast({ type: 'workflow_node_update', nodeId: info.nodeId, status: 'paused', runID, clientRunToken: token });
        },
        onResumed: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          updateCurrentRunNode(server, info.nodeId, 'running');
          send('resumed', withRunContext(info, runID, token));
          server.broadcast({ type: 'workflow_node_update', nodeId: info.nodeId, status: 'running', runID, clientRunToken: token });
        },
        onVarsChanged: (nodeId, vars) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('workflow_vars', withRunContext({ nodeId, vars }, runID, token));
          server.broadcast({ type: 'wf_vars', nodeId, vars, runID, clientRunToken: token });
        },
        onLLMThinking: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('llm_thinking', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_llm_thinking', ...info, runID, clientRunToken: token });
        },
        onLLMToolUse: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('llm_tool_use', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_llm_tool_use', ...info, runID, clientRunToken: token });
        },
        onLLMToolResult: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('llm_tool_result', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_llm_tool_result', ...info, runID, clientRunToken: token });
        },
        onLLMDone: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('llm_done', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_llm_done', ...info, runID, clientRunToken: token });
        },
        onLLMError: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('llm_error', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_llm_error', ...info, runID, clientRunToken: token });
        },
        onToolStart: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('tool_start', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_tool_start', ...info, runID, clientRunToken: token });
        },
        onToolDone: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('tool_done', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_tool_done', ...info, runID, clientRunToken: token });
        },
        onToolError: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('tool_error', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_tool_error', ...info, runID, clientRunToken: token });
        },
        onToolMessage: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('tool_message', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_tool_message', ...info, runID, clientRunToken: token });
        },
        onFileStart: (fp) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('file_start', withRunContext({ path: fp }, runID, token));
          server.broadcast({ type: 'wf_file_start', path: fp, runID, clientRunToken: token });
        },
        onVarChanged: (info) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('var_changed', withRunContext(info, runID, token));
          server.broadcast({ type: 'wf_var_changed', ...info, runID, clientRunToken: token });
        },
        onFileWritten: (fp) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('file_written', withRunContext({ path: fp }, runID, token));
          server.broadcast({ type: 'file_changed', path: fp });
          server.broadcast({ type: 'wf_file_done', path: fp, runID, clientRunToken: token });
        },
        onFileChanged: (fp, sizeBytes) => {
          server.broadcast({ type: 'file_changed', path: fp, size_bytes: sizeBytes });
        },
        onCheckpoint: (cp) => {
          const runID = cp.workflowID || executor.getRunID();
          const token = getExecutorClientRunToken(executor);
          if (runID) {
            _checkpointStore.set(runID, cp);
            _workflowStore.set(runID, workflowJson);
            trackExecutor(server, executor, runID);
          }
          send('checkpoint', withRunContext({ stepID: cp.currentStepID, completedSteps: cp.completedSteps, checkpoint: cp }, runID, token));
          server.broadcast({ type: 'wf_checkpoint', runID, clientRunToken: token, stepID: cp.currentStepID, completedSteps: cp.completedSteps, checkpoint: cp });
        },
        onToken: (tokenText) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('token', withRunContext({ token: tokenText }, runID, token));
        },
        onText: (text) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          send('text', withRunContext({ text }, runID, token));
          server.broadcast({ type: 'wf_text', text, runID, clientRunToken: token });
        },
        onDone: (summary) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          untrackExecutor(server, executor, runID);
          settleCurrentRunState(server, { markRunningAsDone: true });
          setCheckpointStatus(runID, 'completed');
          server.projectContext.scan().then(() => {
            server.broadcast({ type: 'project_reloaded' });
            server.autoExtractMeta();
          });
          send('done', withRunContext(summary, runID, token));
          server.broadcast({ type: 'wf_done', workflowName: workflowName || '', ...summary, runID, clientRunToken: token });
        },
        onError: (err) => {
          const runID = getExecutorRunID(executor);
          const token = getExecutorClientRunToken(executor);
          untrackExecutor(server, executor, runID);
          settleCurrentRunState(server, { error: err });
          setCheckpointStatus(runID, 'failed');
          send('error', withRunContext({ message: err }, runID, token));
          server.broadcast({ type: 'wf_error', workflowName: workflowName || '', error: err, runID, clientRunToken: token });
        },
      });
    } catch (e) {
      send('error', { message: e.message });
    }
    res.end();
  });

  // --- Get current workflow variables ---
  app.get('/api/workflow/variables', (req, res) => {
    const executor = resolveExecutor(server, req.query.runID);
    if (!executor?._ctx) {
      return res.status(404).json({ error: 'No active workflow' });
    }
    const vars = executor._ctx.variables || {};
    res.json(sanitizeVarsForAPI(vars));
  });

  // --- Get written files list ---
  app.get('/api/workflow/artifacts', (req, res) => {
    const executor = resolveExecutor(server, req.query.runID);
    if (!executor) {
      return res.status(404).json({ error: 'No active workflow' });
    }
    res.json({ files: executor.filesWritten || [] });
  });

  // --- Current run state — for new clients syncing on connect ---
  app.get('/api/workflow/current-state', (req, res) => {
    const activeExecutors = ensureActiveExecutors(server);
    const executor = server._activeExecutor || activeExecutors.values().next().value || null;
    const runID = getExecutorRunID(executor);
    const clientRunToken = getExecutorClientRunToken(executor);
    const checkpoint = executor?.getCheckpoint?.() || (runID ? _checkpointStore.get(runID) || null : null);
    res.json({
      active: server._currentRunState.active && (activeExecutors.size > 0 || !!server._activeExecutor),
      workflowName: server._currentRunState.workflowName,
      nodeStatuses: server._currentRunState.nodeStatuses,
      nodeDetails: server._currentRunState.nodeDetails,
      runID,
      clientRunToken,
      currentStepID: checkpoint?.currentStepID || null,
      checkpoint,
    });
  });

  // --- Save ephemeral workflow ---
  app.post('/api/workflow/ephemeral', (req, res) => {
    try {
      const wfDir = path.join(server.config.workDir, '.vl-code', 'workflows');
      if (!fs.existsSync(wfDir)) fs.mkdirSync(wfDir, { recursive: true });
      const name = `_ephemeral-${Date.now()}`;
      fs.writeFileSync(path.join(wfDir, name + '.json'), JSON.stringify(req.body, null, 2));
      res.json({ ok: true, name });
    } catch (e) { res.json({ ok: false, error: e.message }); }
  });

  // --- List workflows ---
  app.get('/api/workflows', (req, res) => {
    const dirs = [
      path.join(server.config.workDir, '.vl-code', 'workflows'),
      path.join(VL_CODE_HOME, 'workflows'),
    ];
    const workflows = [];
    for (const dir of dirs) {
      if (!fs.existsSync(dir)) continue;
      try {
        for (const f of fs.readdirSync(dir).filter(f => f.endsWith('.json'))) {
          try {
            const content = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
            workflows.push({
              name: f.replace('.json', ''),
              file: f,
              title: content.name || content.title || f.replace('.json', ''),
              description: content.description || '',
              category: content.category || 'custom',
              stepCount: content.steps?.length || 0,
              source: dir.includes('.vl-code') ? 'project' : 'global',
            });
          } catch {}
        }
      } catch {}
    }
    res.json(workflows);
  });

  app.get('/api/workflow-content', (req, res) => {
    const workflowRef = String(req.query.ref || '').trim();
    const workDir = String(req.query.work_dir || req.query.workDir || '').trim();
    if (!workflowRef) return res.status(400).json({ error: 'ref required' });

    const resolved = resolveWorkflowContent(server, workflowRef, workDir);
    if (!resolved?.workflowJson) return res.status(404).json({ error: `Workflow "${workflowRef}" not found` });

    res.json({
      workflow: resolved.workflowJson,
      workflowRef: resolved.workflowKey || String(workflowRef).replace(/\.json$/i, ''),
      title: resolved.workflowJson.name || resolved.workflowJson.title || resolved.workflowKey || workflowRef,
      file: resolved.file || null,
      fullPath: resolved.fullPath || null,
      source: resolved.source || null,
      workDir: workDir || null,
    });
  });

  // --- Individual workflow CRUD ---
  app.get('/api/workflow/:name', (req, res) => {
    const wfFile = req.params.name.endsWith('.json') ? req.params.name : req.params.name + '.json';
    const dirs = [path.join(server.config.workDir, '.vl-code', 'workflows'), path.join(VL_CODE_HOME, 'workflows')];
    for (const dir of dirs) {
      const fp = path.join(dir, wfFile);
      if (fs.existsSync(fp)) return res.json(JSON.parse(fs.readFileSync(fp, 'utf-8')));
    }
    res.status(404).json({ error: 'Not found' });
  });

  app.post('/api/workflow/:name', (req, res) => {
    try {
      const wfDir = path.join(server.config.workDir, '.vl-code', 'workflows');
      if (!fs.existsSync(wfDir)) fs.mkdirSync(wfDir, { recursive: true });
      const wfFile = req.params.name.endsWith('.json') ? req.params.name : req.params.name + '.json';
      fs.writeFileSync(path.join(wfDir, wfFile), JSON.stringify(req.body, null, 2));
      res.json({ ok: true });
    } catch (e) { res.status(500).json({ error: e.message }); }
  });

  app.delete('/api/workflow/:name', (req, res) => {
    const wfFile = req.params.name.endsWith('.json') ? req.params.name : req.params.name + '.json';
    const fp = path.join(server.config.workDir, '.vl-code', 'workflows', wfFile);
    if (fs.existsSync(fp)) { fs.unlinkSync(fp); res.json({ ok: true }); }
    else res.status(404).json({ error: 'Not found' });
  });

  // --- Workflow selection preference ---
  app.get('/api/workflow-selection', (req, res) => {
    try {
      const profilePath = path.join(server.config.workDir, '.vl-code', 'project.json');
      if (fs.existsSync(profilePath)) {
        const profile = JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
        res.json({ defaultWorkflow: profile.defaultWorkflow || null });
      } else {
        res.json({ defaultWorkflow: null });
      }
    } catch { res.json({ defaultWorkflow: null }); }
  });

  app.post('/api/workflow-selection', (req, res) => {
    try {
      const workflow = req.body.workflow || req.body.defaultWorkflow || null;
      const profilePath = path.join(server.config.workDir, '.vl-code', 'project.json');
      const dir = path.dirname(profilePath);
      if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
      let profile = {};
      if (fs.existsSync(profilePath)) profile = JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
      profile.defaultWorkflow = workflow;
      fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
      // Broadcast to all windows so they update their selector
      server.broadcast({ type: 'wf_selection_changed', workflow });
      res.json({ ok: true });
    } catch (e) { res.status(500).json({ error: e.message }); }
  });

  // --- UI state (Flow tab selection, selected workflow) — multi-window sync ---
  app.get('/api/ui-state', (req, res) => {
    res.json(server._uiState || {});
  });

  app.post('/api/ui-state', (req, res) => {
    server._uiState = { ...(server._uiState || {}), ...req.body };
    server.broadcast({ type: 'ui_state_changed', ...server._uiState });
    res.json({ ok: true });
  });

  // --- Generate (legacy endpoint, delegates to workflow) ---
  app.post('/api/generate', async (req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
    const send = (event, data) => res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
    try {
      const session = server.getSession(req);
      const prompt = req.body.requirements || req.body.message || 'Generate a VL project';
      await session.orchestrator.processUserMessageStreaming(prompt, {
        onText: (text) => send('text', { text }),
        onToolCall: (name, input) => send('tool_call', { name }),
        onToolResult: (name, result) => send('tool_result', { name }),
        onDone: () => send('done', {}),
        onError: (err) => send('error', { message: err }),
      });
    } catch (e) { send('error', { message: e.message }); }
    res.end();
  });
}

/** Truncate large variable values for API response */
function sanitizeVarsForAPI(vars) {
  const out = {};
  for (const [k, v] of Object.entries(vars || {})) {
    if (typeof v === 'string' && v.length > 500) {
      out[k] = { _truncated: true, type: 'string', length: v.length, preview: v.substring(0, 200) + '...' };
    } else if (typeof v === 'object' && v !== null) {
      const s = JSON.stringify(v);
      if (s.length > 500) {
        out[k] = { _truncated: true, type: typeof v, length: s.length, preview: s.substring(0, 200) + '...' };
      } else {
        out[k] = v;
      }
    } else {
      out[k] = v;
    }
  }
  return out;
}
