const fs = require("fs");
const path = require("path");
const net = require("net");
const os = require("os");
const crypto = require("crypto");
const { spawn } = require("child_process");
const { resolveData } = require("./paths");
const { getRuntimeState, saveRuntimeState } = require("./config_manager");
const { normalizeExitCode, classifyLaunchError } = require("./error_codes");
const { persistDiagnostic, getLatestDiagnostic, tail } = require("./diagnostics");
const { detectHardware, estimateAllocation } = require("./hardware");

class RuntimeManager {
  constructor({ getConfig, getModel, runtimeManifest, onCrash, onDiagnostic }) {
    Object.assign(this, { getConfig, getModel, runtimeManifest, onCrash, onDiagnostic });
    this.child = null;
    this.port = null;
    this.startedAt = null;
    this.lastError = null;
    this.lastSelfTest = null;
    this.activeAcceleration = null;
    this.activeLogPath = null;
  }
  findBinary() {
    return findRecursive(resolveData("runtime"), process.platform === "win32" ? "llama-server.exe" : "llama-server");
  }
  modelPath() {
    const model = this.getModel(this.getConfig().selected_model_id);
    return model ? resolveData("models", model.filename) : null;
  }
  status() {
    const binary = this.findBinary();
    const model = this.modelPath();
    const acceleration = this.accelerationPlan();
    return {
      state: this.child && !this.child.killed ? "running" : this.lastError ? "error" : "stopped",
      runtime_installed: Boolean(binary),
      runtime_usable: this.lastSelfTest?.success ?? null,
      model_downloaded: Boolean(model && fs.existsSync(model)),
      port: this.port,
      pid: this.child?.pid || null,
      uptime_ms: this.startedAt ? Date.now() - this.startedAt : 0,
      last_error: this.lastError,
      last_self_test: this.lastSelfTest,
      executable_path: binary,
      working_directory: binary ? path.dirname(binary) : null,
      model_path: model,
      runtime_backend: acceleration.installed_backend,
      selected_backend: acceleration.backend,
      gpu_allocation_intent_percent: acceleration.gpu_allocation_intent_percent,
      gpu_allocation_actual_percent: acceleration.gpu_allocation_actual_percent,
      gpu_allocation_max_safe_percent: acceleration.gpu_allocation_max_safe_percent,
      gpu_allocation_clamped_reason: acceleration.gpu_allocation_clamped_reason,
      gpu_layers: acceleration.gpu_layers,
      estimated_gpu_memory_mb: acceleration.estimated_gpu_memory_mb,
      estimated_cpu_memory_mb: acceleration.estimated_cpu_memory_mb,
      total_vram_mb: acceleration.total_vram_mb,
      free_vram_mb: acceleration.free_vram_mb,
      managed_model_vram_mb: acceleration.managed_model_vram_mb,
      external_vram_estimate_mb: acceleration.external_vram_estimate_mb,
      intended_gpu_allocation: acceleration.gpu_allocation_intent_percent,
      actual_gpu_allocation: acceleration.gpu_allocation_actual_percent,
      context_size: this.getConfig().context_size,
      batch_size: this.getConfig().batch_size,
      ubatch_size: this.getConfig().ubatch_size,
      threads: this.getConfig().threads,
      max_output_tokens: this.getConfig().max_output_tokens,
      acceleration_warning: acceleration.warning,
      latest_diagnostic: getLatestDiagnostic()
    };
  }
  runtimeMetadata() {
    const file = resolveData("runtime", "lumi-runtime.json");
    if (!fs.existsSync(file)) return { backend: "cpu", version: null, legacy: true };
    try { return JSON.parse(fs.readFileSync(file, "utf8")); }
    catch { return { backend: "cpu", version: null, invalid: true }; }
  }
  accelerationPlan() {
    const cfg = this.getConfig();
    const model = this.getModel(cfg.selected_model_id);
    const hardware = detectHardware(model ? [model] : [], this.runtimeManifest);
    const installed = this.runtimeMetadata();
    const managedUsageMb = this.child && !this.child.killed
      ? this.activeAcceleration?.estimated_gpu_memory_mb || 0
      : 0;
    const allocation = estimateAllocation({
      model,
      contextSize: cfg.context_size,
      gpu: hardware.gpu,
      backend: installed.backend,
      intentPercent: cfg.gpu_allocation_intent_percent,
      managedUsageMb
    });
    if (this.child && !this.child.killed && this.activeAcceleration) {
      const active = this.activeAcceleration;
      const unsafe = active.gpu_allocation_actual_percent > allocation.gpu_allocation_max_safe_percent;
      const restartRequired =
        active.gpu_allocation_actual_percent !== allocation.gpu_allocation_actual_percent ||
        active.backend !== allocation.backend;
      return {
        ...allocation,
        gpu_allocation_actual_percent: active.gpu_allocation_actual_percent,
        workload_percent: active.gpu_allocation_actual_percent,
        gpu_layers: active.gpu_layers,
        estimated_gpu_memory_mb: active.estimated_gpu_memory_mb,
        estimated_cpu_memory_mb: active.estimated_cpu_memory_mb,
        gpu_allocation_clamped_reason: unsafe
          ? "external_vram_pressure_runtime_active"
          : restartRequired
            ? "restart_required"
            : allocation.gpu_allocation_clamped_reason,
        warning: unsafe
          ? "Other processes have reduced safe VRAM below the live allocation. Restart Lumi AI to apply a safer allocation."
          : restartRequired
            ? "Restart Lumi AI to apply the updated GPU allocation intent."
            : allocation.warning,
        installed_backend: installed.backend || "cpu",
        hardware_gpu: hardware.gpu
      };
    }
    return {
      ...allocation,
      installed_backend: installed.backend || "cpu",
      hardware_gpu: hardware.gpu
    };
  }
  async selfTest() {
    const binary = this.findBinary();
    if (!binary) return this.failDiagnostic("executable_missing", "RUNTIME_MISSING", "Runtime executable was not found.", { remediation_steps: ["Download or reinstall the managed runtime."] });
    const installation = this.verifyRuntimeInstallation();
    if (!installation.success) return this.failDiagnostic(installation.category, "INSTALL_VALIDATION_FAILED", installation.message, installation);
    const result = await runCaptured(binary, ["--help"], path.dirname(binary), 10000);
    fs.writeFileSync(resolveData("logs", "runtime-selftest.log"), `${result.stdout}\n${result.stderr}`.trim());
    if (result.error) {
      const decoded = classifyLaunchError(result.error);
      return this.failDiagnostic(decoded.category, decoded.code, result.error.message, { ...decoded, executable_path: binary, working_directory: path.dirname(binary), command_args: ["--help"], stdout_tail: tail(result.stdout), stderr_tail: tail(result.stderr) });
    }
    if (result.timedOut) return this.failDiagnostic("self_test_timeout", "SELF_TEST_TIMEOUT", "Runtime self-test exceeded 10 seconds.", { executable_path: binary, working_directory: path.dirname(binary), command_args: ["--help"], stdout_tail: tail(result.stdout), stderr_tail: tail(result.stderr) });
    if (result.code !== 0 || !/llama|usage|server|options/i.test(`${result.stdout}\n${result.stderr}`)) {
      const decoded = normalizeExitCode(result.code, result.signal);
      return this.failDiagnostic(decoded.category, decoded.code, "Runtime self-test failed.", { ...decoded, executable_path: binary, working_directory: path.dirname(binary), command_args: ["--help"], stdout_tail: tail(result.stdout), stderr_tail: tail(result.stderr) });
    }
    this.lastSelfTest = { success: true, timestamp: new Date().toISOString(), executable_path: binary, code: result.code };
    this.lastError = null;
    this.onDiagnostic?.({ kind: "runtime_self_test", status: "success" });
    return this.lastSelfTest;
  }
  verifyRuntimeInstallation() {
    const binary = this.findBinary();
    if (!binary) return { success: false, category: "executable_missing", message: "Runtime executable was not found." };
    const runtimeDir = resolveData("runtime");
    const size = folderSize(runtimeDir);
    if (size < 1024 * 1024) return { success: false, category: "incomplete_extraction", message: "Extracted runtime folder is unexpectedly small.", executable_path: binary, runtime_folder_size: size };
    if (process.platform !== "win32") {
      try { fs.accessSync(binary, fs.constants.X_OK); } catch { return { success: false, category: "permission_denied", message: "Runtime executable bit is not set.", executable_path: binary, runtime_folder_size: size }; }
    }
    if (process.platform === "win32") {
      const dlls = findFiles(runtimeDir, (name) => name.toLowerCase().endsWith(".dll"));
      if (!dlls.length) return { success: false, category: "missing_dependency", message: "No runtime DLL files were found after extraction.", executable_path: binary, runtime_folder_size: size };
      return { success: true, executable_path: binary, runtime_folder_size: size, dll_count: dlls.length };
    }
    return { success: true, executable_path: binary, runtime_folder_size: size };
  }
  async verifyModel(modelId = this.getConfig().selected_model_id) {
    const model = this.getModel(modelId);
    const file = model ? resolveData("models", model.filename) : null;
    if (!model || !file || !fs.existsSync(file)) return { success: false, category: "model_missing", message: "Selected model file is missing." };
    const stat = fs.statSync(file);
    if (stat.size !== model.size) return { success: false, category: "model_size_mismatch", message: `Expected ${model.size} bytes, found ${stat.size}.` };
    const header = Buffer.alloc(4);
    const descriptor = fs.openSync(file, "r");
    try { fs.readSync(descriptor, header, 0, 4, 0); } finally { fs.closeSync(descriptor); }
    if (header.toString("ascii") !== "GGUF") return { success: false, category: "model_invalid", message: "Selected file does not have a GGUF header." };
    const sha256 = await hashFile(file);
    if (sha256 !== model.sha256) return { success: false, category: "model_hash_mismatch", message: "Selected model SHA-256 does not match the manifest.", sha256 };
    return { success: true, file, size: stat.size, sha256 };
  }
  async start({ resume = false } = {}) {
    if (this.child && !this.child.killed) return this.status();
    this.onDiagnostic?.({ kind: "runtime_start", status: "attempt" });
    const selfTest = await this.selfTest();
    if (!selfTest.success) {
      this.onDiagnostic?.({ kind: "runtime_start", status: "failed", category: selfTest.category });
      throw new Error(selfTest.message || "Runtime self-test failed.");
    }
    const modelValidation = await this.verifyModel();
    if (!modelValidation.success) {
      const diagnostic = this.failDiagnostic(modelValidation.category, "MODEL_VALIDATION_FAILED", modelValidation.message, { model_path: this.modelPath() });
      saveRuntimeState({ ...getRuntimeState(), desired_state: "stopped", last_known_state: "error", last_stop_reason: modelValidation.category, last_manual_stop: false, last_crashed: false, last_diagnostic_category: modelValidation.category });
      throw new Error(diagnostic.message);
    }
    const binary = this.findBinary();
    const model = this.modelPath();
    this.port = await freePort();
    const cfg = this.getConfig();
    const threads = Number(cfg.threads) > 0 ? Number(cfg.threads) : os.cpus().length;
    const acceleration = this.accelerationPlan();
    const args = buildRuntimeArgs({
      port: this.port,
      modelPath: model,
      config: cfg,
      threads,
      acceleration
    });
    const logPath = resolveData("logs", `runtime-${Date.now()}.log`);
    const log = fs.openSync(logPath, "a");
    const child = spawn(binary, args, { cwd: path.dirname(binary), stdio: ["ignore", log, log], windowsHide: true, shell: false });
    fs.closeSync(log);
    this.child = child;
    this.activeAcceleration = acceleration;
    this.activeLogPath = logPath;
    this.startedAt = Date.now();
    this.lastError = null;
    child.once("error", (error) => {
      child.__spawnFailed = true;
      const decoded = classifyLaunchError(error);
      this.failDiagnostic(decoded.category, decoded.code, error.message, { ...decoded, executable_path: binary, working_directory: path.dirname(binary), command_args: args, model_path: model });
      if (this.child === child) this.child = null;
      if (this.child === null) {
        this.activeAcceleration = null;
        this.activeLogPath = null;
      }
      this.persistCrash(decoded.category, error.message, decoded.signed_exit_code);
    });
    child.once("exit", (code, signal) => {
      const expected = child.__manualStop || child.__spawnFailed;
      if (this.child === child) this.child = null;
      if (this.child === null) {
        this.activeAcceleration = null;
        this.activeLogPath = null;
      }
      if (!expected) {
        const decoded = normalizeExitCode(code, signal);
        const diagnostic = this.failDiagnostic(decoded.category, decoded.code, `Runtime exited before or after health readiness.`, { ...decoded, executable_path: binary, working_directory: path.dirname(binary), command_args: args, model_path: model });
        this.persistCrash(decoded.category, diagnostic.message, decoded.signed_exit_code);
      }
    });
    saveRuntimeState({ ...getRuntimeState(), desired_state: "running", last_known_state: "starting", last_crashed: false, last_manual_stop: false, last_stop_reason: resume ? "resuming" : "starting", selected_model_id: cfg.selected_model_id, runtime_backend: acceleration.backend, gpu_allocation_actual_percent: acceleration.gpu_allocation_actual_percent, gpu_allocation_max_safe_percent: acceleration.gpu_allocation_max_safe_percent, gpu_allocation_clamped_reason: acceleration.gpu_allocation_clamped_reason, gpu_layers: acceleration.gpu_layers });
    try {
      await waitHealth(this, 45000);
      saveRuntimeState({ ...getRuntimeState(), desired_state: "running", last_known_state: "running", last_crashed: false, last_manual_stop: false, last_stop_reason: resume ? "resumed" : "started", selected_model_id: cfg.selected_model_id });
      this.onDiagnostic?.({ kind: "runtime_start", status: "success", model_load_ms: Date.now() - this.startedAt });
      return this.status();
    } catch (error) {
      if (this.child) await this.stop({ manual: false, reason: "health_timeout" });
      const existing = getLatestDiagnostic();
      const preserveProcessExit = error.category === "process_exited_before_health" && existing?.raw_exit_code != null;
      if (!preserveProcessExit) {
        this.failDiagnostic(error.category || "health_timeout", "RUNTIME_HEALTH_FAILED", error.message, { executable_path: binary, working_directory: path.dirname(binary), command_args: args, model_path: model });
        saveRuntimeState({ ...getRuntimeState(), desired_state: "stopped", last_known_state: "error", last_stop_reason: error.category || "health_timeout", last_manual_stop: false, last_crashed: false, last_diagnostic_category: error.category || "health_timeout" });
      } else {
        error.message = `${existing.code}: ${existing.message}`;
      }
      this.onDiagnostic?.({ kind: "runtime_start", status: "failed", category: error.category || "health_timeout" });
      throw error;
    }
  }
  failDiagnostic(category, code, message, extra = {}) {
    this.lastError = message;
    this.lastSelfTest = category.startsWith("self_test") || code === "RUNTIME_MISSING" || extra.command_args?.[0] === "--help" ? { success: false, category, code, message } : this.lastSelfTest;
    const diagnostic = persistDiagnostic({ category, code, message, ...extra });
    if (extra.command_args?.[0] === "--help" || code === "RUNTIME_MISSING" || category === "self_test_timeout") {
      saveRuntimeState({ ...getRuntimeState(), desired_state: "stopped", last_known_state: "error", last_stop_reason: "self_test_failed", last_manual_stop: false, last_crashed: false, last_diagnostic_category: category, last_exit_code: extra.signed_exit_code ?? null });
      this.onDiagnostic?.({ kind: "runtime_self_test", status: "failed", category, code });
    }
    this.onDiagnostic?.({ kind: "runtime_diagnostic", status: "failed", category, code });
    return { success: false, ...diagnostic };
  }
  persistCrash(category, message, exitCode) {
    saveRuntimeState({ ...getRuntimeState(), desired_state: "stopped", last_known_state: "crashed", last_crashed: true, last_stop_reason: "runtime_crash", last_manual_stop: false, last_exit_code: exitCode ?? null, last_diagnostic_category: category });
    this.onCrash?.(message);
  }
  async stop({ manual = true, reason = "manual_stop" } = {}) {
    const wasRunning = Boolean(this.child && !this.child.killed);
    if (this.child) {
      const child = this.child;
      child.__manualStop = true;
      child.kill();
      await waitExit(child, 10000);
      if (this.child === child && !child.killed) child.kill("SIGKILL");
    }
    this.child = null;
    this.startedAt = null;
    this.activeAcceleration = null;
    this.activeLogPath = null;
    const resumeAfterShutdown = !manual && reason === "bot_shutdown" && wasRunning;
    saveRuntimeState({ ...getRuntimeState(), desired_state: resumeAfterShutdown ? "running" : "stopped", last_known_state: "stopped", last_stop_reason: reason, last_manual_stop: manual, last_crashed: false });
    return this.status();
  }
  async restart() { await this.stop({ manual: false, reason: "restart" }); return this.start(); }
  async health() {
    const status = this.status();
    if (status.state !== "running") return { ...status, healthy: false };
    try {
      const response = await fetch(`http://127.0.0.1:${this.port}/health`, { signal: AbortSignal.timeout(2000) });
      if (!response.ok) return { ...status, healthy: false, health_status: "http_error", health_http_status: response.status };
      try {
        const body = await response.json();
        return { ...status, healthy: true, health_status: "ready", health_response: body };
      } catch {
        return { ...status, healthy: false, health_status: "invalid_json" };
      }
    } catch (error) {
      return { ...status, healthy: false, health_status: error.name === "TimeoutError" ? "connection_timeout" : "connection_refused" };
    }
  }
  async infer(messages, maxTokens = 300, { signal = null, timeoutMs = null } = {}) {
    if (!this.port) throw new Error("Runtime is offline.");
    const configuredTimeout = Number(timeoutMs ?? this.getConfig().hard_generation_timeout_ms);
    const hardTimeoutMs = Math.max(30000, Math.min(3600000, Number.isFinite(configuredTimeout) ? configuredTimeout : 600000));
    const controller = new AbortController();
    let hardTimedOut = false;
    const abortFromCaller = () => controller.abort();
    if (signal?.aborted) throw requestCancelledError();
    signal?.addEventListener("abort", abortFromCaller, { once: true });
    const timer = setTimeout(() => {
      hardTimedOut = true;
      controller.abort();
    }, hardTimeoutMs);
    timer.unref?.();
    try {
      const response = await fetch(`http://127.0.0.1:${this.port}/v1/chat/completions`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ model: "local", messages, max_tokens: maxTokens, temperature: 0.2 }),
        signal: controller.signal
      });
      if (!response.ok) throw new Error(`Inference failed (${response.status})`);
      return response.json();
    } catch (error) {
      if (hardTimedOut) {
        throw Object.assign(
          new Error(`Main model generation exceeded the ${Math.ceil(hardTimeoutMs / 1000)}s hard timeout.`),
          { name: "TimeoutError", code: "HARD_GENERATION_TIMEOUT", timeout_ms: hardTimeoutMs }
        );
      }
      if (signal?.aborted || error.name === "AbortError") throw requestCancelledError();
      throw error;
    } finally {
      clearTimeout(timer);
      signal?.removeEventListener("abort", abortFromCaller);
    }
  }
}

class GateRuntimeManager {
  constructor({ getConfig, getModel, onDiagnostic }) {
    Object.assign(this, { getConfig, getModel, onDiagnostic });
    this.child = null;
    this.port = null;
    this.startedAt = null;
    this.lastError = null;
    this.activeLogPath = null;
  }

  model() {
    return this.getModel(this.getConfig().gate?.model_id);
  }

  modelPath() {
    const model = this.model();
    return model ? resolveData("models", model.filename) : null;
  }

  status() {
    const model = this.model();
    const modelPath = this.modelPath();
    const contextSize = Math.max(512, Math.min(4096, Number(this.getConfig().gate?.context_size) || 1024));
    const modelMb = model ? Math.ceil(model.size / 1048576) : 0;
    const contextMb = Math.ceil((contextSize / 4096) * 192);
    return {
      state: this.child && !this.child.killed ? "running" : this.lastError ? "error" : "stopped",
      healthy: false,
      model_id: model?.id || null,
      model_downloaded: Boolean(modelPath && fs.existsSync(modelPath)),
      port: this.port,
      pid: this.child?.pid || null,
      uptime_ms: this.startedAt ? Date.now() - this.startedAt : 0,
      last_error: this.lastError,
      estimated_cpu_memory_mb: modelMb + contextMb,
      estimated_gpu_memory_mb: 0,
      backend: "cpu",
      log_path: this.activeLogPath
    };
  }

  async verifyModel() {
    const model = this.model();
    const file = this.modelPath();
    if (!model || !file || !fs.existsSync(file)) {
      return { success: false, message: "Gate model file is missing." };
    }
    const stat = fs.statSync(file);
    if (stat.size !== model.size) {
      return { success: false, message: `Gate model expected ${model.size} bytes, found ${stat.size}.` };
    }
    const header = Buffer.alloc(4);
    const descriptor = fs.openSync(file, "r");
    try { fs.readSync(descriptor, header, 0, 4, 0); } finally { fs.closeSync(descriptor); }
    if (header.toString("ascii") !== "GGUF") {
      return { success: false, message: "Gate model does not have a GGUF header." };
    }
    const sha256 = await hashFile(file);
    if (sha256 !== model.sha256) {
      return { success: false, message: "Gate model SHA-256 does not match the manifest.", sha256 };
    }
    return { success: true, file, sha256 };
  }

  async start() {
    if (this.child && !this.child.killed) return this.health();
    const binary = findRecursive(
      resolveData("runtime"),
      process.platform === "win32" ? "llama-server.exe" : "llama-server"
    );
    if (!binary) throw new Error("Runtime executable was not found for the gate.");
    const validation = await this.verifyModel();
    if (!validation.success) throw new Error(validation.message);
    const cfg = this.getConfig();
    const gate = cfg.gate || {};
    this.port = await freePort();
    const threads = Math.max(1, Math.min(
      os.cpus().length,
      Number(gate.threads) || Math.min(2, os.cpus().length)
    ));
    const args = buildRuntimeArgs({
      port: this.port,
      modelPath: validation.file,
      config: { context_size: Math.max(512, Math.min(4096, Number(gate.context_size) || 1024)) },
      threads,
      acceleration: { gpu_layers: 0 }
    });
    const logPath = resolveData("logs", `gate-runtime-${Date.now()}.log`);
    const log = fs.openSync(logPath, "a");
    const child = spawn(binary, args, {
      cwd: path.dirname(binary),
      stdio: ["ignore", log, log],
      windowsHide: true,
      shell: false
    });
    fs.closeSync(log);
    this.child = child;
    this.activeLogPath = logPath;
    this.startedAt = Date.now();
    this.lastError = null;
    child.once("error", (error) => {
      child.__spawnFailed = true;
      this.lastError = error.message;
      if (this.child === child) this.child = null;
      this.onDiagnostic?.({ kind: "gate_runtime", status: "failed", message: error.message });
    });
    child.once("exit", (code, signal) => {
      const expected = child.__manualStop || child.__spawnFailed;
      if (this.child === child) this.child = null;
      if (!expected) {
        this.lastError = `Gate runtime exited (${code ?? signal ?? "unknown"}).`;
        this.onDiagnostic?.({ kind: "gate_runtime", status: "failed", code, signal });
      }
    });
    try {
      await waitHealth(this, 45000);
      this.onDiagnostic?.({
        kind: "gate_runtime",
        status: "success",
        model: this.model()?.id,
        model_load_ms: Date.now() - this.startedAt
      });
      return this.health();
    } catch (error) {
      this.lastError = error.message;
      await this.stop();
      throw error;
    }
  }

  async stop() {
    if (this.child) {
      const child = this.child;
      child.__manualStop = true;
      child.kill();
      await waitExit(child, 10000);
      if (this.child === child && !child.killed) child.kill("SIGKILL");
    }
    this.child = null;
    this.port = null;
    this.startedAt = null;
    this.activeLogPath = null;
    return this.status();
  }

  async restart() {
    await this.stop();
    return this.start();
  }

  async health() {
    const status = this.status();
    if (status.state !== "running") return status;
    try {
      const response = await fetch(`http://127.0.0.1:${this.port}/health`, {
        signal: AbortSignal.timeout(2000)
      });
      return { ...status, healthy: response.ok };
    } catch {
      return { ...status, healthy: false };
    }
  }

  async infer(messages, maxTokens = 64, timeoutMs = null) {
    if (!this.port) throw new Error("Gate runtime is offline.");
    const configured = Number(timeoutMs ?? this.getConfig().gate?.timeout_ms);
    const timeout = Math.max(1000, Math.min(5000, Number.isFinite(configured) ? configured : 3000));
    const response = await fetch(`http://127.0.0.1:${this.port}/v1/chat/completions`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        model: "local-gate",
        messages,
        max_tokens: maxTokens,
        temperature: 0,
        chat_template_kwargs: { enable_thinking: false }
      }),
      signal: AbortSignal.timeout(timeout)
    });
    if (!response.ok) throw new Error(`Gate inference failed (${response.status})`);
    return response.json();
  }
}

function combinedResourceEstimate({ main, gate, hardware }) {
  const mainCpu = Math.max(0, Number(main?.estimated_cpu_memory_mb) || 0);
  const gateCpu = Math.max(0, Number(gate?.estimated_cpu_memory_mb) || 0);
  const mainGpu = Math.max(0, Number(main?.estimated_gpu_memory_mb) || 0);
  const gateGpu = Math.max(0, Number(gate?.estimated_gpu_memory_mb) || 0);
  const totalRam = mainCpu + gateCpu;
  const totalVram = mainGpu + gateGpu;
  const hostRam = Math.max(0, Number(hardware?.total_ram_mb) || 0);
  const hostVram = Math.max(0, Number(hardware?.gpu?.vram_mb) || 0);
  const warnings = [];
  if (hostRam && totalRam > hostRam * 0.85) {
    warnings.push("Gate and main model estimates exceed 85% of host RAM.");
  }
  if (hostVram && totalVram > hostVram * 0.9) {
    warnings.push("Gate and main model estimates exceed 90% of host VRAM.");
  }
  return {
    main_cpu_memory_mb: mainCpu,
    gate_cpu_memory_mb: gateCpu,
    total_cpu_memory_mb: totalRam,
    main_gpu_memory_mb: mainGpu,
    gate_gpu_memory_mb: gateGpu,
    total_gpu_memory_mb: totalVram,
    exceeds_host_capacity: warnings.length > 0,
    warnings
  };
}

function findRecursive(dir, name) {
  if (!fs.existsSync(dir)) return null;
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    const target = path.join(dir, entry.name);
    if (entry.isFile() && entry.name === name) return target;
    if (entry.isDirectory()) { const found = findRecursive(target, name); if (found) return found; }
  }
  return null;
}
function freePort() {
  return new Promise((resolve, reject) => {
    const server = net.createServer();
    server.listen(0, "127.0.0.1", () => { const port = server.address().port; server.close(() => resolve(port)); });
    server.on("error", reject);
  });
}
function runCaptured(executable, args, cwd, timeoutMs) {
  return new Promise((resolve) => {
    const child = spawn(executable, args, { cwd, windowsHide: true, shell: false });
    let stdout = "", stderr = "", settled = false, timedOut = false, timer;
    const finish = (result) => { if (settled) return; settled = true; clearTimeout(timer); resolve({ stdout, stderr, timedOut, ...result }); };
    child.stdout.on("data", (chunk) => { stdout = tail(stdout + chunk, 12000); });
    child.stderr.on("data", (chunk) => { stderr = tail(stderr + chunk, 12000); });
    child.once("error", (error) => finish({ error }));
    child.once("exit", (code, signal) => finish({ code, signal }));
    timer = setTimeout(() => { timedOut = true; child.kill(); }, timeoutMs);
  });
}
async function waitHealth(manager, timeout) {
  const end = Date.now() + timeout;
  let lastCategory = "connection_refused";
  while (Date.now() < end) {
    if (!manager.child) throw Object.assign(new Error("Runtime process exited before health became ready."), { category: "process_exited_before_health" });
    try {
      const response = await fetch(`http://127.0.0.1:${manager.port}/health`, { signal: AbortSignal.timeout(2000) });
      if (!response.ok) lastCategory = "http_error";
      else {
        try { await response.json(); return; }
        catch { lastCategory = "invalid_json"; }
      }
    } catch (error) {
      lastCategory = error.name === "TimeoutError" ? "connection_timeout" : "connection_refused";
    }
    await new Promise((resolve) => setTimeout(resolve, 500));
  }
  throw Object.assign(new Error(`Runtime process remained alive but health did not become ready within 45 seconds (${lastCategory}).`), { category: lastCategory === "connection_refused" ? "model_load_timeout" : lastCategory });
}
function waitExit(child, timeout) {
  return new Promise((resolve) => {
    if (child.exitCode != null) return resolve();
    const timer = setTimeout(resolve, timeout);
    child.once("exit", () => { clearTimeout(timer); resolve(); });
  });
}
async function hashFile(file) {
  const hash = crypto.createHash("sha256");
  for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
  return hash.digest("hex");
}
function folderSize(dir) {
  if (!fs.existsSync(dir)) return 0;
  return fs.readdirSync(dir, { withFileTypes: true }).reduce((total, entry) => {
    const target = path.join(dir, entry.name);
    return total + (entry.isDirectory() ? folderSize(target) : entry.isFile() ? fs.statSync(target).size : 0);
  }, 0);
}
function findFiles(dir, predicate) {
  if (!fs.existsSync(dir)) return [];
  return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
    const target = path.join(dir, entry.name);
    return entry.isDirectory() ? findFiles(target, predicate) : entry.isFile() && predicate(entry.name) ? [target] : [];
  });
}

function buildRuntimeArgs({ port, modelPath, config, threads, acceleration }) {
  const args = [
    "--host", "127.0.0.1",
    "--port", String(port),
    "-m", modelPath,
    "-c", String(config.context_size || 4096),
    "-t", String(threads),
    "-b", String(Math.max(32, Number(config.batch_size) || 512)),
    "-ub", String(Math.max(16, Math.min(Number(config.batch_size) || 512, Number(config.ubatch_size) || 128)))
  ];
  if (acceleration?.gpu_layers > 0) {
    args.push("-ngl", String(acceleration.gpu_layers));
  }
  return args;
}

function requestCancelledError() {
  return Object.assign(
    new Error("Assistant request was cancelled."),
    { name: "AbortError", code: "REQUEST_CANCELLED" }
  );
}

module.exports = {
  RuntimeManager,
  GateRuntimeManager,
  combinedResourceEstimate,
  runCaptured,
  buildRuntimeArgs
};
