/**
 * WasmToolHost: Loads and executes WASM plugins via Extism.
 *
 * Wraps the Extism plugin lifecycle: load from file/Buffer, call exported
 * functions with string I/O, register host functions (used by CredentialBoundary
 * for secret injection), and clean up resources.
 */

import { readFile } from 'node:fs/promises';
import createPlugin from '@extism/extism';

/** WASM magic bytes: \0asm */
const WASM_MAGIC = new Uint8Array([0x00, 0x61, 0x73, 0x6d]);

/**
 * Validate that a buffer starts with the WASM magic bytes.
 * @param {Uint8Array|Buffer} buf
 * @returns {boolean}
 */
function isWasmBinary(buf) {
  if (!buf || buf.length < 4) return false;
  for (let i = 0; i < 4; i++) {
    if (buf[i] !== WASM_MAGIC[i]) return false;
  }
  return true;
}

export class WasmToolHost {
  /** @type {import('@extism/extism').Plugin|null} */
  #plugin = null;
  /** @type {boolean} */
  #closed = false;
  /** @type {string|null} */
  #source = null;

  /**
   * Load an Extism plugin from a file path or Buffer.
   *
   * @param {string|Uint8Array|Buffer} wasmSource - Path to .wasm file, or raw WASM bytes
   * @param {object} [options]
   * @param {Record<string, Record<string, Function>>} [options.functions] - Host functions keyed by namespace
   * @param {Record<string, string>} [options.config] - Static plugin configuration
   * @param {boolean} [options.useWasi=true] - Enable WASI support
   * @param {number} [options.timeoutMs=30000] - Plugin load/call timeout in ms
   * @returns {Promise<WasmToolHost>}
   */
  async load(wasmSource, options = {}) {
    if (this.#plugin) {
      throw new Error('WasmToolHost: plugin already loaded. Call close() first.');
    }

    const { functions, config, useWasi = true, timeoutMs = 30000 } = options;
    let wasmData;
    let sourceLabel;

    let manifest;

    if (typeof wasmSource === 'string') {
      // File path: read to validate magic bytes, then use path-based manifest
      sourceLabel = wasmSource;
      try {
        wasmData = await readFile(wasmSource);
      } catch (err) {
        throw new Error(
          `WasmToolHost: failed to read WASM file "${wasmSource}": ${err.message}`
        );
      }
      if (!isWasmBinary(wasmData)) {
        throw new Error(
          `WasmToolHost: invalid WASM binary from ${sourceLabel} (missing magic bytes \\0asm)`
        );
      }
      manifest = { wasm: [{ path: wasmSource }] };
    } else if (wasmSource instanceof Uint8Array || Buffer.isBuffer(wasmSource)) {
      wasmData = wasmSource;
      sourceLabel = `<Buffer ${wasmSource.length} bytes>`;
      if (!isWasmBinary(wasmData)) {
        throw new Error(
          `WasmToolHost: invalid WASM binary from ${sourceLabel} (missing magic bytes \\0asm)`
        );
      }
      manifest = { wasm: [{ data: wasmData }] };
    } else {
      throw new Error(
        'WasmToolHost: wasmSource must be a file path (string) or Uint8Array/Buffer'
      );
    }

    this.#source = sourceLabel;

    const pluginOptions = {
      useWasi,
      runInWorker: false,
    };
    if (functions) pluginOptions.functions = functions;
    if (config) pluginOptions.config = config;

    const loadPromise = createPlugin(manifest, pluginOptions);
    const timeoutPromise = new Promise((_, reject) =>
      setTimeout(
        () => reject(new Error(`WasmToolHost: plugin load timed out after ${timeoutMs}ms`)),
        timeoutMs,
      )
    );

    try {
      this.#plugin = await Promise.race([loadPromise, timeoutPromise]);
    } catch (err) {
      throw new Error(
        `WasmToolHost: failed to load plugin from ${sourceLabel}: ${err.message}`
      );
    }

    return this;
  }

  /**
   * Call a named export function on the loaded plugin.
   *
   * @param {string} functionName - Name of the WASM export to call
   * @param {string} [input=''] - String input to pass to the function
   * @returns {Promise<string>} String output from the function
   */
  async call(functionName, input = '') {
    if (this.#closed) {
      throw new Error('WasmToolHost: plugin is closed');
    }
    if (!this.#plugin) {
      throw new Error('WasmToolHost: no plugin loaded. Call load() first.');
    }
    if (typeof functionName !== 'string' || !functionName) {
      throw new Error('WasmToolHost: functionName must be a non-empty string');
    }

    try {
      const result = await this.#plugin.call(functionName, input);
      if (result === null || result === undefined) {
        return '';
      }
      return result.text();
    } catch (err) {
      // Distinguish "function not found" from other errors
      const msg = err.message || String(err);
      if (msg.includes('not found') || msg.includes('does not exist') || msg.includes('unknown export')) {
        throw new Error(
          `WasmToolHost: function "${functionName}" not found in plugin (source: ${this.#source})`
        );
      }
      throw new Error(
        `WasmToolHost: call to "${functionName}" failed: ${msg}`
      );
    }
  }

  /**
   * Close the plugin and release resources.
   */
  async close() {
    if (this.#plugin) {
      try {
        await this.#plugin.close();
      } catch {
        // Best-effort cleanup
      }
      this.#plugin = null;
    }
    this.#closed = true;
  }

  /**
   * Whether the plugin has been closed.
   * @returns {boolean}
   */
  get closed() {
    return this.#closed;
  }

  /**
   * Whether a plugin is currently loaded.
   * @returns {boolean}
   */
  get loaded() {
    return this.#plugin !== null && !this.#closed;
  }

  /**
   * Source label for the loaded plugin.
   * @returns {string|null}
   */
  get source() {
    return this.#source;
  }
}
