import { existsSync } from "node:fs";
import koffi from "koffi";
import { dirname, join, resolve } from "node:path";
import { PassThrough, Writable } from "node:stream";
const SYMBOL_PREFIX = "copilot_runtime_";
const KEEP_ALIVE_INTERVAL_MS = 1 << 30;
let loadedLibraryPath;
let loadedLibrary;
function loadLibrary(libraryPath) {
  if (loadedLibrary) {
    if (loadedLibraryPath !== libraryPath) {
      throw new Error(
        `An in-process FFI runtime library is already loaded from '${loadedLibraryPath}'; loading a different library from '${libraryPath}' in the same process is not supported.`
      );
    }
    return loadedLibrary;
  }
  const lib = koffi.load(libraryPath);
  const outboundCallbackType = koffi.pointer(
    koffi.proto(
      `void ${SYMBOL_PREFIX}outbound(void *userData, uint8 *bytesPtr, size_t bytesLen)`
    )
  );
  loadedLibrary = {
    hostStart: lib.func(`${SYMBOL_PREFIX}host_start`, "uint32", [
      "uint8*",
      "size_t",
      "uint8*",
      "size_t"
    ]),
    hostShutdown: lib.func(`${SYMBOL_PREFIX}host_shutdown`, "bool", ["uint32"]),
    connectionOpen: lib.func(`${SYMBOL_PREFIX}connection_open`, "uint32", [
      "uint32",
      outboundCallbackType,
      "void*",
      "uint8*",
      "size_t",
      "uint8*",
      "size_t",
      "uint8*",
      "size_t"
    ]),
    connectionWrite: lib.func(`${SYMBOL_PREFIX}connection_write`, "bool", [
      "uint32",
      "uint8*",
      "size_t"
    ]),
    connectionClose: lib.func(`${SYMBOL_PREFIX}connection_close`, "bool", ["uint32"]),
    outboundCallbackType
  };
  loadedLibraryPath = libraryPath;
  return loadedLibrary;
}
function buildArgvJson(cliEntrypoint, args) {
  const argv = cliEntrypoint.toLowerCase().endsWith(".js") ? ["node", cliEntrypoint, "--embedded-host", "--no-auto-update"] : [cliEntrypoint, "--embedded-host", "--no-auto-update"];
  argv.push(...args);
  return Buffer.from(JSON.stringify(argv), "utf8");
}
function buildEnvJson(environment) {
  if (!environment) {
    return null;
  }
  const obj = {};
  for (const [key, value] of Object.entries(environment)) {
    if (value !== void 0) {
      obj[key] = value;
    }
  }
  if (Object.keys(obj).length === 0) {
    return null;
  }
  return Buffer.from(JSON.stringify(obj), "utf8");
}
class FfiRuntimeHost {
  constructor(libraryPath, cliEntrypoint, environment, args) {
    this.libraryPath = libraryPath;
    this.cliEntrypoint = cliEntrypoint;
    this.environment = environment;
    this.args = args;
    this.lib = loadLibrary(libraryPath);
    this.receiveStream = new PassThrough();
    this.sendStream = new Writable({
      // connection_write enqueues the frame into the runtime's inbound channel and
      // returns immediately, so a synchronous FFI call is sufficient here.
      write: (chunk, _encoding, callback) => {
        try {
          this.writeFrame(chunk);
          callback();
        } catch (error) {
          callback(error);
        }
      }
    });
  }
  libraryPath;
  cliEntrypoint;
  environment;
  args;
  lib;
  serverId = 0;
  connectionId = 0;
  disposed = false;
  outboundCallback;
  keepAliveTimer;
  /** The stream JSON-RPC reads server→client frames from. */
  receiveStream;
  /** The stream JSON-RPC writes client→server frames to. */
  sendStream;
  /**
   * Resolves the cdylib next to the given CLI entrypoint and prepares the FFI host.
   * The cdylib is resolved as `prebuilds/<prebuildsFolder>/runtime.node` relative to
   * the entrypoint directory (the napi-rs `<node-platform>-<arch>` layout, e.g.
   * `linux-x64`). Throws if it cannot be found.
   */
  static create(cliEntrypoint, prebuildsFolder, environment, args) {
    const fullEntrypoint = resolve(cliEntrypoint);
    const distDir = dirname(fullEntrypoint);
    const libraryPath = join(distDir, "prebuilds", prebuildsFolder, "runtime.node");
    if (!existsSync(libraryPath)) {
      throw new Error(`FFI runtime library not found. Looked for '${libraryPath}'.`);
    }
    return new FfiRuntimeHost(libraryPath, fullEntrypoint, environment, args);
  }
  /**
   * Starts the in-process runtime: spawns the CLI worker via the native host,
   * waits for readiness, and opens the FFI JSON-RPC connection.
   */
  async start() {
    const argvJson = buildArgvJson(this.cliEntrypoint, this.args);
    const envJson = buildEnvJson(this.environment);
    this.serverId = await new Promise((resolvePromise, rejectPromise) => {
      this.lib.hostStart.async(
        argvJson,
        argvJson.length,
        envJson,
        envJson ? envJson.length : 0,
        (error, result) => {
          if (error) {
            rejectPromise(error);
          } else {
            resolvePromise(result);
          }
        }
      );
    });
    if (!this.serverId) {
      throw new Error(
        `copilot_runtime_host_start failed (library '${this.libraryPath}', entrypoint '${this.cliEntrypoint}').`
      );
    }
    this.outboundCallback = koffi.register(
      (_userData, bytesPtr, bytesLen) => this.feedInbound(bytesPtr, bytesLen),
      this.lib.outboundCallbackType
    );
    this.connectionId = this.lib.connectionOpen(
      this.serverId,
      this.outboundCallback,
      null,
      null,
      0,
      null,
      0,
      null,
      0
    );
    if (!this.connectionId) {
      this.unregisterCallback();
      this.lib.hostShutdown(this.serverId);
      this.serverId = 0;
      throw new Error("copilot_runtime_connection_open failed.");
    }
    this.keepAliveTimer = setInterval(() => {
    }, KEEP_ALIVE_INTERVAL_MS);
  }
  writeFrame(frame) {
    if (this.disposed || !this.connectionId) {
      throw new Error("The in-process runtime connection is closed.");
    }
    const ok = this.lib.connectionWrite(this.connectionId, frame, frame.length);
    if (!ok) {
      throw new Error("Failed to write a frame to the in-process runtime connection.");
    }
  }
  /**
   * Native outbound (server→client) callback. koffi delivers it on the JS event loop
   * via a threadsafe function, so the frame is decoded and written straight to
   * {@link receiveStream}. The native pointer is only valid for this call, so the
   * bytes are copied out before returning.
   */
  feedInbound(bytesPtr, bytesLen) {
    try {
      if (this.disposed || this.receiveStream.writableEnded) {
        return;
      }
      const length = Number(bytesLen);
      if (!bytesPtr || length <= 0) {
        return;
      }
      const bytes = koffi.decode(
        bytesPtr,
        koffi.array("uint8", length, "Typed")
      );
      this.receiveStream.write(Buffer.from(bytes));
    } catch (error) {
      console.error(
        `In-process FFI inbound callback failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`
      );
    }
  }
  unregisterCallback() {
    if (this.outboundCallback === void 0) {
      return;
    }
    const callback = this.outboundCallback;
    this.outboundCallback = void 0;
    try {
      koffi.unregister(callback);
    } catch {
    }
  }
  /** Closes the FFI connection, shuts down the native host, and releases resources. */
  dispose() {
    if (this.disposed) {
      return;
    }
    this.disposed = true;
    if (this.keepAliveTimer !== void 0) {
      clearInterval(this.keepAliveTimer);
      this.keepAliveTimer = void 0;
    }
    try {
      if (this.connectionId) {
        this.lib.connectionClose(this.connectionId);
        this.connectionId = 0;
      }
    } catch {
    }
    try {
      if (this.serverId) {
        this.lib.hostShutdown(this.serverId);
        this.serverId = 0;
      }
    } catch {
    }
    this.receiveStream.end();
    this.unregisterCallback();
  }
}
export {
  FfiRuntimeHost
};
