/**
 * WorkflowEngineManager — manages the lifecycle of the local Go workflow engine
 *
 * Responsibilities:
 *   - Locate the Go binary (configurable path, project-local, bundled, system PATH)
 *   - Start in server mode (./workflow server --port <PORT>)
 *   - Monitor health via GET /health
 *   - Execute workflows via POST /run (SSE stream)
 *   - CLI mode fallback for one-shot execution
 *   - Graceful shutdown and auto-restart on crash
 *   - Bridge LLM configuration (ANTHROPIC_API_KEY → LITELLM_*)
 */
import { spawn, execSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { streamSSEEvents } from './sse-parser.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

export class WorkflowEngineManager {
  /**
   * @param {Object} config
   * @param {string} [config.workflowBinaryPath] - Explicit path to Go binary
   * @param {number} [config.workflowEnginePort=8090] - Server port
   * @param {string} [config.workDir] - Project working directory
   * @param {string} [config.workspaceRoot] - Workspace root for file I/O
   * @param {string} [config.apiKey] - Anthropic API key
   * @param {string} [config.model] - LLM model name
   */
  constructor(config) {
    this.config = config;
    this.port = config.workflowEnginePort || 8090;
    this.workspaceRoot = config.workspaceRoot || config.workDir;
    this.baseUrl = `http://localhost:${this.port}`;

    this._process = null;
    this._healthy = false;
    this._restartCount = 0;
    this._maxRestarts = 5;
    this._binaryPath = null; // Resolved lazily
    this._stopping = false;
    this._stderrLog = [];
  }

  /**
   * Find the Go workflow binary in known locations.
   * Search order:
   *   1. config.workflowBinaryPath (explicit)
   *   2. <workDir>/../workflow-dist/workflow (sibling to project)
   *   3. <module>/../../workflow-dist/workflow (bundled with vl-code)
   *   4. System PATH via `which workflow`
   * @returns {string|null} Absolute path to the binary, or null
   */
  _findBinary() {
    if (this.config.workflowBinaryPath) {
      const p = path.resolve(this.config.workflowBinaryPath);
      if (fs.existsSync(p)) return p;
    }

    // Sibling to project working directory
    const candidates = [
      path.resolve(this.config.workDir, '..', 'workflow-dist', 'workflow'),
      path.resolve(this.config.workDir, 'workflow-dist', 'workflow'),
      path.resolve(__dirname, '..', '..', 'workflow-dist', 'workflow'),
      path.resolve(__dirname, '..', '..', '..', 'workflow-dist', 'workflow'),
    ];

    for (const p of candidates) {
      if (fs.existsSync(p)) return p;
    }

    // System PATH
    try {
      const result = execSync('which workflow', { encoding: 'utf-8', timeout: 3000 }).trim();
      if (result && fs.existsSync(result)) return result;
    } catch {
      // Not in PATH
    }

    return null;
  }

  /**
   * Remove macOS quarantine attribute if present.
   * @param {string} binaryPath
   */
  _removeQuarantine(binaryPath) {
    if (process.platform !== 'darwin') return;
    try {
      execSync(`xattr -d com.apple.quarantine "${binaryPath}" 2>/dev/null`, { timeout: 3000 });
    } catch {
      // Already clean or no xattr
    }
  }

  /**
   * Build environment variables for the Go engine process.
   * Maps VL-Code's Anthropic config to the Go engine's LITELLM_* format.
   * @returns {Object} Environment variables
   */
  _buildEngineEnv() {
    const env = { ...process.env };

    // Bridge LLM configuration
    if (this.config.apiKey) {
      env.LITELLM_KEY = this.config.apiKey;
      // Anthropic keys start with sk-ant- → direct Anthropic mode
      if (this.config.apiKey.startsWith('sk-ant-')) {
        env.LITELLM_URL = 'https://api.anthropic.com';
      }
    }

    // Model mapping (Go engine uses LITELLM_MODEL)
    if (this.config.model) {
      env.LITELLM_MODEL = this.config.model;
    }

    // Workspace and server config
    env.WORKSPACE_ROOT = this.workspaceRoot;
    env.PORT = String(this.port);

    return env;
  }

  /**
   * Start the Go workflow engine in server mode.
   * Non-blocking — spawns the process and returns immediately.
   * Use waitForReady() to wait for the health check to pass.
   * @throws {Error} If the binary is not found
   */
  async start() {
    if (this._process) return; // Already running

    this._binaryPath = this._findBinary();
    if (!this._binaryPath) {
      throw new Error(
        'Workflow engine binary not found. Set VL_WORKFLOW_BINARY or place it in workflow-dist/'
      );
    }

    this._removeQuarantine(this._binaryPath);

    // Ensure the binary is executable
    try {
      fs.chmodSync(this._binaryPath, 0o755);
    } catch {
      // May fail on some systems, that's okay
    }

    const env = this._buildEngineEnv();
    this._stderrLog = [];
    this._stopping = false;

    this._process = spawn(this._binaryPath, ['server'], {
      env,
      cwd: path.dirname(this._binaryPath),
      stdio: ['ignore', 'pipe', 'pipe'],
    });

    // Capture stderr for diagnostics
    this._process.stderr.on('data', (data) => {
      const line = data.toString().trim();
      if (line) {
        this._stderrLog.push(line);
        // Keep only last 50 lines
        if (this._stderrLog.length > 50) this._stderrLog.shift();
      }
    });

    // Handle unexpected exit
    this._process.on('exit', (code, signal) => {
      this._healthy = false;
      this._process = null;

      if (!this._stopping && this._restartCount < this._maxRestarts) {
        this._restartCount++;
        const delay = Math.min(1000 * Math.pow(2, this._restartCount - 1), 16000);
        setTimeout(() => {
          if (!this._stopping) {
            this.start().then(() => this.waitForReady(8000)).catch(() => {});
          }
        }, delay);
      }
    });

    // Handle errors
    this._process.on('error', (err) => {
      this._healthy = false;
      this._process = null;
    });
  }

  /**
   * Wait for the engine to become healthy (health check passes).
   * @param {number} [timeoutMs=10000] - Maximum wait time in milliseconds
   * @throws {Error} If the engine doesn't become healthy within the timeout
   */
  async waitForReady(timeoutMs = 10000) {
    const start = Date.now();
    const interval = 300;

    while (Date.now() - start < timeoutMs) {
      try {
        const response = await fetch(`${this.baseUrl}/health`, {
          signal: AbortSignal.timeout(2000),
        });
        if (response.ok) {
          this._healthy = true;
          this._restartCount = 0;
          return;
        }
      } catch {
        // Not ready yet
      }
      await new Promise((r) => setTimeout(r, interval));
    }

    throw new Error(
      `Workflow engine did not become healthy within ${timeoutMs}ms. ` +
      `Stderr: ${this._stderrLog.slice(-5).join(' | ')}`
    );
  }

  /**
   * Stop the engine gracefully.
   */
  async stop() {
    this._stopping = true;
    this._healthy = false;

    if (this._process) {
      this._process.kill('SIGTERM');

      // Wait up to 3 seconds for graceful shutdown
      await new Promise((resolve) => {
        const timeout = setTimeout(() => {
          if (this._process) {
            this._process.kill('SIGKILL');
          }
          resolve();
        }, 3000);

        if (this._process) {
          this._process.on('exit', () => {
            clearTimeout(timeout);
            resolve();
          });
        } else {
          clearTimeout(timeout);
          resolve();
        }
      });

      this._process = null;
    }
  }

  /**
   * Check if the engine is running and healthy.
   * @returns {boolean}
   */
  isReady() {
    return this._healthy && this._process !== null;
  }

  /**
   * Get engine status info.
   * @returns {Object}
   */
  getStatus() {
    return {
      available: this._binaryPath !== null,
      ready: this.isReady(),
      port: this.port,
      pid: this._process?.pid || null,
      restartCount: this._restartCount,
      binaryPath: this._binaryPath,
      recentLogs: this._stderrLog.slice(-10),
    };
  }

  /**
   * Execute a workflow via the HTTP server, returning an async generator of SSE events.
   *
   * @param {Object} options
   * @param {string} [options.workflowFile] - Absolute path to workflow JSON file
   * @param {Object} [options.workflowDef] - Inline workflow definition (alternative to workflowFile)
   * @param {Object} [options.params] - Runtime parameters
   * @param {string} [options.workspaceId] - Workspace ID for file scoping
   * @param {string} [options.mode='create'] - Execution mode (create, patch, regenerate, validate)
   * @yields {Object} Parsed SSE event objects from Go engine
   */
  async *streamWorkflow({ workflowFile, workflowDef, params, workspaceId, mode = 'create' }) {
    if (!this.isReady()) {
      throw new Error('Workflow engine is not ready. Call start() and waitForReady() first.');
    }

    const body = {
      runParams: {
        params: params || {},
        workspaceId: workspaceId || path.basename(this.config.workDir),
        mode,
      },
    };

    if (workflowFile) {
      body.workflowFile = workflowFile;
    } else if (workflowDef) {
      body.workflowDef = workflowDef;
    } else {
      throw new Error('Either workflowFile or workflowDef is required');
    }

    const response = await fetch(`${this.baseUrl}/run`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });

    if (!response.ok) {
      const text = await response.text();
      throw new Error(`Engine returned ${response.status}: ${text}`);
    }

    // Stream SSE events from the response body
    yield* streamSSEEvents(response.body);
  }

  /**
   * Execute a workflow via the HTTP server, collecting all events.
   * Convenience wrapper around streamWorkflow().
   *
   * @param {Object} options - Same as streamWorkflow()
   * @returns {Promise<{ events: Object[], status: string }>}
   */
  async runWorkflow(options) {
    const events = [];
    let status = 'completed';

    for await (const event of this.streamWorkflow(options)) {
      events.push(event);
      if (event.type === 'workflow_failed' || event.type === 'workflow_error') {
        status = 'failed';
      }
    }

    return { events, status };
  }

  /**
   * Execute a workflow via CLI mode (fallback when server is not available).
   * Spawns a one-shot `./workflow run` process.
   *
   * Note: CLI mode outputs human-readable text, not SSE events.
   * This method parses the output to synthesize workflow_start and workflow_done events.
   *
   * @param {Object} options
   * @param {string} options.workflowFile - Path to workflow JSON file
   * @param {Object} [options.params] - Runtime parameters
   * @param {string} [options.workspaceId] - Workspace ID
   * @param {string} [options.mode='create'] - Execution mode
   * @yields {Object} Synthetic events based on CLI output
   */
  async *runWorkflowCLI({ workflowFile, params, workspaceId, mode = 'create' }) {
    const binaryPath = this._binaryPath || this._findBinary();
    if (!binaryPath) {
      throw new Error('Workflow engine binary not found');
    }

    this._removeQuarantine(binaryPath);

    const args = [
      'run',
      '-workflow', workflowFile,
      '-workspace-id', workspaceId || path.basename(this.config.workDir),
      '-mode', mode,
      '-v', // Verbose output
    ];

    if (params && Object.keys(params).length > 0) {
      args.push('-params', JSON.stringify(params));
    }

    const env = this._buildEngineEnv();
    const proc = spawn(binaryPath, args, {
      env,
      cwd: path.dirname(binaryPath),
      stdio: ['ignore', 'pipe', 'pipe'],
    });

    // Emit synthetic workflow_start
    yield { type: 'workflow_start', run_id: 'cli_run', seq: 0, ts: new Date().toISOString(), payload: {} };

    // Parse stdout for step info (human-readable format: "  → StepName")
    let stdout = '';
    let stderr = '';
    let seq = 1;

    proc.stdout.on('data', (data) => { stdout += data.toString(); });
    proc.stderr.on('data', (data) => { stderr += data.toString(); });

    // Wait for process to exit
    const exitCode = await new Promise((resolve) => {
      proc.on('exit', (code) => resolve(code));
    });

    // Parse stdout to extract step events
    for (const line of stdout.split('\n')) {
      const stepMatch = line.match(/^\s*→\s+(\S+)/);
      if (stepMatch) {
        yield { type: 'step_start', run_id: 'cli_run', seq: seq++, ts: new Date().toISOString(), step_id: stepMatch[1], payload: {} };
        yield { type: 'step_done', run_id: 'cli_run', seq: seq++, ts: new Date().toISOString(), step_id: stepMatch[1], payload: {} };
      }

      const skipMatch = line.match(/^\s*↷\s+(\S+)/);
      if (skipMatch) {
        yield { type: 'step_skipped', run_id: 'cli_run', seq: seq++, ts: new Date().toISOString(), step_id: skipMatch[1], payload: {} };
      }

      const errorMatch = line.match(/^\s*✗\s+(\S+)\s+ERROR:\s*(.*)/);
      if (errorMatch) {
        yield { type: 'step_error', run_id: 'cli_run', seq: seq++, ts: new Date().toISOString(), step_id: errorMatch[1], payload: { error: errorMatch[2] } };
      }
    }

    // Emit workflow_done or workflow_failed
    if (exitCode === 0) {
      yield { type: 'workflow_done', run_id: 'cli_run', seq: seq++, ts: new Date().toISOString(), step_id: null, payload: {} };
    } else {
      yield {
        type: 'workflow_failed', run_id: 'cli_run', seq: seq++, ts: new Date().toISOString(), step_id: null,
        payload: { error: stderr.trim().slice(-500) || `Process exited with code ${exitCode}` },
      };
    }
  }
}
