/**
 * VL AutoTest - Workflow Runner
 * Submits Spec 3.16 workflow JSONs to the VL Workflow Engine (POST /ih5/play/stream)
 * and consumes the SSE event stream, returning per-step results.
 *
 * The engine uses the platform's Claude model — no API key needed.
 */

function log(tag, msg) {
  const ts = new Date().toISOString().slice(11, 23);
  console.log(`[${ts}] [WfRunner] [${tag}] ${msg}`);
}

export class WorkflowRunner {
  /**
   * @param {string} engineUrl - e.g. 'https://editor.visuallogic.ai/ih5/play/stream'
   * @param {string} cookie - VL auth cookie (optional, may be needed for some engines)
   */
  constructor(engineUrl, cookie = '') {
    this.engineUrl = engineUrl;
    this.cookie = cookie;
    this.timeout = 180_000; // 3 minutes
  }

  /**
   * Submit a workflow and consume the SSE stream.
   *
   * @param {Object} wfJson  - Spec 3.16 workflow JSON
   * @param {Object} params  - runParams.params key-value pairs
   * @param {Function} onEvent - callback for each SSE event { type, step_id, payload, seq, ts }
   * @returns {Promise<Object>} - { success, results: { [stepId]: { _result, _meta, duration_ms } }, error?, duration_ms? }
   */
  async run(wfJson, params, onEvent) {
    const body = JSON.stringify({
      wfJson,
      runParams: { params },
    });

    log('Run', `Submitting workflow "${wfJson.name}" (${wfJson.steps?.length || 0} steps, ${Object.keys(params).length} params)`);

    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), this.timeout);

    let response;
    try {
      response = await fetch(this.engineUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...(this.cookie ? { 'Cookie': this.cookie } : {}),
        },
        body,
        signal: controller.signal,
      });
    } catch (fetchErr) {
      clearTimeout(timer);
      if (fetchErr.name === 'AbortError') {
        throw new Error(`Workflow engine timeout after ${this.timeout / 1000}s`);
      }
      throw new Error(`Workflow engine unreachable: ${fetchErr.message}`);
    }

    if (!response.ok) {
      clearTimeout(timer);
      const errText = await response.text().catch(() => '');
      throw new Error(`Workflow engine HTTP ${response.status}: ${errText.slice(0, 300)}`);
    }

    // State for accumulating results
    const tokenBuffers = {};   // step_id → accumulated text
    const stepResults = {};    // step_id → { _result, _meta, duration_ms }
    let workflowResult = null;

    try {
      await this._consumeSSE(response, (event) => {
        const { type, step_id, payload } = event;

        // Forward raw event to caller for UI updates
        onEvent?.(event);

        switch (type) {
          case 'workflow_start':
            log('Run', `Workflow started: run_id=${event.run_id}`);
            break;

          case 'step_start':
            log('Run', `Step started: ${step_id} (${payload?.step_type || ''})`);
            tokenBuffers[step_id] = '';
            break;

          case 'llm_token':
            if (step_id) {
              if (!tokenBuffers[step_id]) tokenBuffers[step_id] = '';
              tokenBuffers[step_id] += payload?.delta || '';
            }
            break;

          case 'llm_done':
            if (step_id) {
              if (!stepResults[step_id]) stepResults[step_id] = {};
              stepResults[step_id]._result = tokenBuffers[step_id] || '';
              stepResults[step_id]._meta = payload; // { finish_reason, usage, model, latency_ms }
              log('Run', `LLM done: ${step_id} (${stepResults[step_id]._result.length} chars, ${payload?.latency_ms || '?'}ms)`);
            }
            break;

          case 'step_done':
            if (step_id) {
              if (!stepResults[step_id]) stepResults[step_id] = {};
              stepResults[step_id].duration_ms = payload?.duration_ms;
              log('Run', `Step done: ${step_id} (${payload?.duration_ms || '?'}ms)`);
            }
            break;

          case 'step_print':
            if (step_id) {
              log('Print', `[${step_id}] ${payload?.message || ''}`);
            }
            break;

          case 'step_error':
            log('Error', `Step error: ${step_id} — ${JSON.stringify(payload?.error || payload)}`);
            break;

          case 'step_skipped':
            log('Run', `Step skipped: ${step_id} (if=false)`);
            break;

          case 'step_paused':
            log('Run', `Step paused: ${step_id} — ${payload?.message || 'awaiting resume'}`);
            if (step_id) {
              if (!stepResults[step_id]) stepResults[step_id] = {};
              stepResults[step_id].paused = true;
              stepResults[step_id].pauseMessage = payload?.message;
            }
            break;

          case 'step_resumed':
            log('Run', `Step resumed: ${step_id}`);
            break;

          case 'fork_start':
            log('Run', `Fork started: ${step_id} (${payload?.branches || '?'} branches)`);
            break;

          case 'fork_branch_done':
            log('Run', `Fork branch done: ${step_id}/${payload?.branch_index}`);
            break;

          case 'fork_done':
            log('Run', `Fork complete: ${step_id} (${payload?.duration_ms || '?'}ms)`);
            if (step_id) {
              if (!stepResults[step_id]) stepResults[step_id] = {};
              stepResults[step_id].duration_ms = payload?.duration_ms;
            }
            break;

          case 'workflow_done':
            workflowResult = {
              success: true,
              stop_id: payload?.stop_id,
              duration_ms: payload?.duration_ms,
            };
            log('Run', `Workflow done in ${payload?.duration_ms || '?'}ms (stop: ${payload?.stop_id})`);
            break;

          case 'workflow_failed':
            workflowResult = {
              success: false,
              error: payload?.error || payload,
              failed_step: payload?.failed_step_id,
            };
            log('Error', `Workflow failed at ${payload?.failed_step_id}: ${JSON.stringify(payload?.error || payload)}`);
            break;

          case 'workflow_cancelled':
            workflowResult = { success: false, error: { message: 'Workflow cancelled' } };
            log('Run', 'Workflow cancelled');
            break;
        }
      });
    } finally {
      clearTimeout(timer);
    }

    // If no workflow_done/failed received (stream ended abruptly)
    if (!workflowResult) {
      workflowResult = { success: false, error: { message: 'SSE stream ended without workflow completion event' } };
      log('Error', 'Stream ended without workflow_done or workflow_failed');
    }

    return {
      ...workflowResult,
      results: stepResults,
    };
  }

  /**
   * Consume SSE stream from a fetch Response.
   * Handles chunked data, partial lines, and the SSE protocol.
   */
  async _consumeSSE(response, onEvent) {
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });

        // Split on newlines; keep last incomplete line in buffer
        const lines = buffer.split('\n');
        buffer = lines.pop() || '';

        for (const line of lines) {
          const trimmed = line.trim();
          if (!trimmed || trimmed.startsWith(':')) continue; // comment or empty
          if (!trimmed.startsWith('data:')) continue;

          const jsonStr = trimmed.slice(5).trim();
          if (!jsonStr || jsonStr === '[DONE]') continue;

          try {
            const event = JSON.parse(jsonStr);
            onEvent(event);
          } catch {
            // Skip unparseable SSE data lines
          }
        }
      }

      // Process any remaining buffer
      if (buffer.trim().startsWith('data:')) {
        const jsonStr = buffer.trim().slice(5).trim();
        if (jsonStr && jsonStr !== '[DONE]') {
          try {
            onEvent(JSON.parse(jsonStr));
          } catch {}
        }
      }
    } finally {
      reader.releaseLock();
    }
  }
}
