import {
  JobType,
  ParticipantPermission,
  ServerMessage,
  WorkerMessage,
  WorkerStatus
} from "@livekit/protocol";
import { ThrowsPromise } from "@livekit/throws-transformer/throws";
import { AccessToken, RoomServiceClient } from "livekit-server-sdk";
import { EventEmitter } from "node:events";
import { availableParallelism } from "node:os";
import { extname } from "node:path";
import { WebSocket } from "ws";
import { APIStatusError } from "./_exceptions.js";
import { getCpuMonitor } from "./cpu.js";
import { HTTPServer } from "./http_server.js";
import { _getLocalInferenceModule } from "./inference/_warmup.js";
import { EOT_INFERENCE_METHOD } from "./inference/eot/runner.js";
import { InferenceRunner } from "./inference_runner.js";
import { InferenceProcExecutor } from "./ipc/inference_proc_executor.js";
import { ProcPool } from "./ipc/proc_pool.js";
import { JobRequest } from "./job.js";
import { log } from "./log.js";
import { Future, rejectOnAbort } from "./utils.js";
import { version } from "./version.js";
const MAX_RECONNECT_ATTEMPTS = 10;
const ASSIGNMENT_TIMEOUT = 7.5 * 1e3;
const UPDATE_LOAD_INTERVAL = 2.5 * 1e3;
const PROJECT_TYPE = "nodejs";
let localEotRunnerRegistered = false;
function maybeRegisterLocalEotRunner() {
  if (localEotRunnerRegistered) return;
  localEotRunnerRegistered = true;
  if (InferenceRunner.registeredRunners[EOT_INFERENCE_METHOD]) return;
  if (_getLocalInferenceModule() === void 0) {
    log().warn(
      "@livekit/local-inference native binding unavailable; local audio EOT disabled (predictions will degrade to a positive default). cloud EOT and other turn detection modes are unaffected."
    );
    return;
  }
  const ext = extname(import.meta.url);
  InferenceRunner.registerRunner(
    EOT_INFERENCE_METHOD,
    new URL(`./inference/eot/runner${ext}`, import.meta.url).toString()
  );
}
class Default {
  static loadThreshold(production) {
    if (production) {
      return 0.7;
    } else {
      return Infinity;
    }
  }
  static numIdleProcesses(production) {
    if (production) {
      return Math.min(availableParallelism(), 4);
    } else {
      return 0;
    }
  }
  static port(production) {
    if (production) {
      return 8081;
    } else {
      return 0;
    }
  }
}
class MissingCredentialsError extends Error {
  constructor(msg) {
    super(msg);
    Object.setPrototypeOf(this, new.target.prototype);
  }
}
class WorkerError extends Error {
  constructor(msg) {
    super(msg);
    Object.setPrototypeOf(this, new.target.prototype);
  }
}
const defaultInitializeProcessFunc = (_) => _;
const defaultRequestFunc = async (ctx) => {
  await ctx.accept();
};
const cpuMonitor = getCpuMonitor();
const defaultCpuLoad = async (_worker) => {
  return cpuMonitor.cpuPercent(UPDATE_LOAD_INTERVAL);
};
class WorkerPermissions {
  canPublish;
  canSubscribe;
  canPublishData;
  canUpdateMetadata;
  canPublishSources;
  hidden;
  constructor(canPublish = true, canSubscribe = true, canPublishData = true, canUpdateMetadata = true, canPublishSources = [], hidden = false) {
    this.canPublish = canPublish;
    this.canSubscribe = canSubscribe;
    this.canPublishData = canPublishData;
    this.canUpdateMetadata = canUpdateMetadata;
    this.canPublishSources = canPublishSources;
    this.hidden = hidden;
  }
}
class ServerOptions {
  agent;
  requestFunc;
  loadFunc;
  loadThreshold;
  numIdleProcesses;
  shutdownProcessTimeout;
  initializeProcessTimeout;
  permissions;
  agentName;
  agentNameIsEnv;
  serverType;
  maxRetry;
  wsURL;
  apiKey;
  apiSecret;
  workerToken;
  host;
  port;
  logLevel;
  production;
  jobMemoryWarnMB;
  jobMemoryLimitMB;
  /** @param options - Worker options */
  constructor({
    agent,
    requestFunc = defaultRequestFunc,
    loadFunc = defaultCpuLoad,
    loadThreshold = void 0,
    numIdleProcesses = void 0,
    shutdownProcessTimeout = 60 * 1e3,
    initializeProcessTimeout = 10 * 1e3,
    permissions = new WorkerPermissions(),
    agentName = "",
    agentNameIsEnv = void 0,
    serverType = JobType.JT_ROOM,
    maxRetry = MAX_RECONNECT_ATTEMPTS,
    wsURL = "ws://localhost:7880",
    apiKey = void 0,
    apiSecret = void 0,
    workerToken = void 0,
    host = "0.0.0.0",
    port = void 0,
    logLevel = "info",
    production = false,
    jobMemoryWarnMB = 1e3,
    jobMemoryLimitMB = 0
  }) {
    this.agent = agent;
    if (!this.agent) {
      throw new Error("No Agent file was passed to the worker");
    }
    this.requestFunc = requestFunc;
    this.loadFunc = loadFunc;
    this.loadThreshold = loadThreshold || Default.loadThreshold(production);
    this.numIdleProcesses = numIdleProcesses || Default.numIdleProcesses(production);
    this.shutdownProcessTimeout = shutdownProcessTimeout;
    this.initializeProcessTimeout = initializeProcessTimeout;
    this.permissions = permissions;
    if (agentName) {
      this.agentName = agentName;
      this.agentNameIsEnv = agentNameIsEnv ?? false;
    } else if (process.env.LIVEKIT_AGENT_NAME) {
      this.agentName = process.env.LIVEKIT_AGENT_NAME;
      this.agentNameIsEnv = agentNameIsEnv ?? true;
    } else {
      this.agentName = "";
      this.agentNameIsEnv = agentNameIsEnv ?? false;
    }
    this.serverType = serverType;
    this.maxRetry = maxRetry;
    this.wsURL = wsURL;
    this.apiKey = apiKey;
    this.apiSecret = apiSecret;
    this.workerToken = workerToken;
    this.host = host;
    this.port = port || Default.port(production);
    this.logLevel = logLevel;
    this.production = production;
    this.jobMemoryWarnMB = jobMemoryWarnMB;
    this.jobMemoryLimitMB = jobMemoryLimitMB;
  }
}
class PendingAssignment {
  promise = new ThrowsPromise((resolve) => {
    this.resolve = resolve;
  });
  resolve(arg) {
    arg;
  }
}
class AgentServer {
  #opts;
  #procPool;
  #id = "unregistered";
  #closed = true;
  #draining = false;
  #connecting = false;
  #tasks = [];
  #pending = {};
  #close = new Future();
  event = new EventEmitter();
  #session = void 0;
  #httpServer;
  #logger = log().child({ version });
  #inferenceExecutor;
  /* @throws {@link MissingCredentialsError} if URL, API key or API secret are missing */
  constructor(opts) {
    opts.wsURL = opts.wsURL || process.env.LIVEKIT_URL || "";
    opts.apiKey = opts.apiKey || process.env.LIVEKIT_API_KEY || "";
    opts.apiSecret = opts.apiSecret || process.env.LIVEKIT_API_SECRET || "";
    if (opts.wsURL === "")
      throw new MissingCredentialsError(
        "URL is required: Set LIVEKIT_URL, run with --url, or pass wsURL in ServerOptions"
      );
    if (opts.apiKey === "")
      throw new MissingCredentialsError(
        "API Key is required: Set LIVEKIT_API_KEY, run with --api-key, or pass apiKey in ServerOptions"
      );
    if (opts.apiSecret === "")
      throw new MissingCredentialsError(
        "API Secret is required: Set LIVEKIT_API_SECRET, run with --api-secret, or pass apiSecret in ServerOptions"
      );
    if (opts.workerToken) {
      process.env.LIVEKIT_WORKER_TOKEN = opts.workerToken;
      if (opts.loadFunc !== defaultCpuLoad) {
        this.#logger.warn(
          "custom loadFunc is not supported when deploying to Cloud, using defaults"
        );
        opts.loadFunc = defaultCpuLoad;
      }
      const loadThreshold = Default.loadThreshold(opts.production);
      if (opts.loadThreshold !== loadThreshold) {
        this.#logger.warn(
          "custom loadThreshold is not supported when deploying to Cloud, using defaults"
        );
        opts.loadThreshold = loadThreshold;
      }
    }
    maybeRegisterLocalEotRunner();
    if (Object.entries(InferenceRunner.registeredRunners).length) {
      this.#inferenceExecutor = new InferenceProcExecutor({
        runners: InferenceRunner.registeredRunners,
        initializeTimeout: 3e4,
        closeTimeout: 5e3,
        memoryWarnMB: 2e3,
        memoryLimitMB: 0,
        pingInterval: 5e3,
        pingTimeout: 6e4,
        highPingThreshold: 2500
      });
    }
    this.#procPool = new ProcPool(
      opts.agent,
      opts.numIdleProcesses,
      opts.initializeProcessTimeout,
      opts.shutdownProcessTimeout,
      this.#inferenceExecutor,
      opts.jobMemoryWarnMB,
      opts.jobMemoryLimitMB
    );
    this.#opts = opts;
    const healthCheck = () => {
      if (this.#inferenceExecutor && !this.#inferenceExecutor.isAlive) {
        return { healthy: false, message: "inference process not running" };
      }
      if (this.#closed || this.#connecting || !this.#session || this.#session.readyState !== WebSocket.OPEN) {
        return { healthy: false, message: "not connected to livekit" };
      }
      return { healthy: true, message: "OK" };
    };
    const getWorkerInfo = () => ({
      agent_name: opts.agentName,
      agent_name_is_env: opts.agentNameIsEnv,
      worker_type: JobType[opts.serverType],
      active_jobs: this.activeJobs.length,
      sdk_version: version,
      project_type: PROJECT_TYPE
    });
    this.#httpServer = new HTTPServer(opts.host, opts.port, healthCheck, getWorkerInfo);
  }
  /** @throws {@link WorkerError} if worker failed to connect or already running */
  async run() {
    if (!this.#closed) {
      throw new WorkerError("worker is already running");
    }
    if (this.#inferenceExecutor) {
      await this.#inferenceExecutor.start();
      await this.#inferenceExecutor.initialize();
    }
    this.#logger.info("starting worker");
    this.#closed = false;
    this.#procPool.start();
    const workerWS = async () => {
      let retries = 0;
      this.#connecting = true;
      while (!this.#closed) {
        const url = new URL(this.#opts.wsURL);
        url.protocol = url.protocol.replace("http", "ws");
        const token = new AccessToken(this.#opts.apiKey, this.#opts.apiSecret);
        token.addGrant({ agent: true });
        const jwt = await token.toJwt();
        const wsUrl = new URL(url + "agent");
        if (this.#opts.workerToken) {
          wsUrl.searchParams.append("worker_token", this.#opts.workerToken);
        }
        this.#session = new WebSocket(wsUrl, {
          headers: { authorization: "Bearer " + jwt }
        });
        try {
          await new ThrowsPromise((resolve, reject) => {
            this.#session.on("open", resolve);
            this.#session.on("error", (error) => reject(error));
            this.#session.on("close", (code) => reject(new Error(`WebSocket returned ${code}`)));
          });
          retries = 0;
          this.#logger.debug("connected to LiveKit server");
          await this.#runWS(this.#session);
        } catch (e) {
          if (this.#closed) return;
          if (retries >= this.#opts.maxRetry) {
            throw new WorkerError(
              `failed to connect to LiveKit server (${this.#opts.wsURL}) after ${retries} attempts: ${e}`
            );
          }
          retries++;
          const delay = Math.min(retries * 2, 10);
          this.#logger.warn(
            { error: e, retry_count: retries, max_retry: this.#opts.maxRetry },
            `failed to connect to LiveKit server (${this.#opts.wsURL}), retrying in ${delay} seconds: (${retries}/${this.#opts.maxRetry})`
          );
          await new ThrowsPromise((resolve) => setTimeout(resolve, delay * 1e3));
        }
      }
    };
    await ThrowsPromise.all([workerWS(), this.#httpServer.run()]);
    this.#close.resolve();
  }
  get id() {
    return this.#id;
  }
  get activeJobs() {
    return this.#procPool.processes.filter((proc) => proc.runningJob).map((proc) => proc.runningJob);
  }
  /** @throws {@link WorkerError} if worker did not drain in time */
  async drain(timeout) {
    if (this.#draining) {
      return;
    }
    this.#logger.debug("draining worker");
    this.#draining = true;
    this.event.emit(
      "worker_msg",
      new WorkerMessage({
        message: {
          case: "updateWorker",
          value: {
            status: WorkerStatus.WS_FULL
          }
        }
      })
    );
    const joinJobs = async () => {
      return ThrowsPromise.all(
        this.#procPool.processes.map((proc) => {
          if (!proc.runningJob) {
            proc.close();
          }
          return proc.join();
        })
      );
    };
    const promises = [joinJobs()];
    if (timeout) {
      promises.push(
        rejectOnAbort(AbortSignal.timeout(timeout)).catch(() => {
          throw new WorkerError("timed out draining");
        })
      );
    }
    await ThrowsPromise.race(promises);
  }
  async simulateJob(roomName, participantIdentity) {
    const client = new RoomServiceClient(this.#opts.wsURL, this.#opts.apiKey, this.#opts.apiSecret);
    const room = await client.createRoom({ name: roomName });
    let participant = void 0;
    if (participantIdentity) {
      try {
        participant = await client.getParticipant(
          roomName,
          participantIdentity
        );
      } catch (e) {
        this.#logger.fatal(
          `participant with identity ${participantIdentity} not found in room ${roomName}`
        );
        throw e;
      }
    }
    this.event.emit(
      "worker_msg",
      new WorkerMessage({
        message: {
          case: "simulateJob",
          value: {
            type: JobType.JT_PUBLISHER,
            room,
            participant
          }
        }
      })
    );
  }
  async #runWS(ws) {
    let closingWS = false;
    const send = (msg) => {
      if (closingWS) {
        this.event.off("worker_msg", send);
        return;
      }
      ws.send(msg.toBinary());
    };
    this.event.on("worker_msg", send);
    const close = new ThrowsPromise((resolve, reject) => {
      ws.addEventListener("close", (event) => {
        closingWS = true;
        if (!this.#closed) {
          reject(
            new APIStatusError({
              message: "worker connection closed unexpectedly",
              options: {
                statusCode: event.code || -1,
                body: {
                  code: event.code,
                  reason: event.reason,
                  wasClean: event.wasClean
                }
              }
            })
          );
          return;
        }
        resolve();
      });
    });
    ws.addEventListener("error", (event) => {
      this.#logger.error("worker error:", event.message);
    });
    ws.addEventListener("message", (event) => {
      let data;
      if (event.data instanceof Uint8Array) {
        data = event.data;
      } else if (event.data instanceof ArrayBuffer) {
        data = new Uint8Array(event.data);
      } else if (Array.isArray(event.data)) {
        data = Buffer.concat(event.data);
      } else {
        let wsData = String(event.data);
        if (wsData.length > 128) {
          wsData = `${wsData.slice(0, 128)}...(+${wsData.length - 128} more)`;
        }
        const type = typeof event.data;
        this.#logger.warn({ type, ws_data: wsData }, `unexpected message type: ${type}`);
        return;
      }
      const msg = new ServerMessage();
      msg.fromBinary(data);
      if (this.#connecting && msg.message.case !== "register") {
        throw new WorkerError("expected register response as first message");
      }
      switch (msg.message.case) {
        case "register": {
          this.#id = msg.message.value.workerId;
          this.#logger.child({ id: this.id, server_info: msg.message.value.serverInfo }).info("registered worker");
          this.event.emit(
            "worker_registered",
            msg.message.value.workerId,
            msg.message.value.serverInfo
          );
          this.#connecting = false;
          break;
        }
        case "availability": {
          if (!msg.message.value.job) return;
          const task = this.#availability(msg.message.value);
          this.#tasks.push(task);
          task.finally(() => {
            const taskIndex = this.#tasks.indexOf(task);
            if (taskIndex !== -1) {
              this.#tasks.splice(taskIndex, 1);
            } else {
              throw new Error(`task ${task} not found in tasks`);
            }
          });
          break;
        }
        case "assignment": {
          if (!msg.message.value.job) return;
          const job = msg.message.value.job;
          if (job.id in this.#pending) {
            const task = this.#pending[job.id];
            delete this.#pending[job.id];
            task == null ? void 0 : task.resolve(msg.message.value);
          } else {
            this.#logger.child({ job }).warn("received assignment for unknown job " + job.id);
          }
          break;
        }
        case "termination": {
          const task = this.#termination(msg.message.value);
          this.#tasks.push(task);
          task.finally(() => {
            const taskIndex = this.#tasks.indexOf(task);
            if (taskIndex !== -1) {
              this.#tasks.splice(taskIndex, 1);
            } else {
              throw new Error(`task ${task} not found in tasks`);
            }
          });
          break;
        }
      }
    });
    this.event.emit(
      "worker_msg",
      new WorkerMessage({
        message: {
          case: "register",
          value: {
            type: this.#opts.serverType,
            agentName: this.#opts.agentName,
            allowedPermissions: new ParticipantPermission({
              canPublish: this.#opts.permissions.canPublish,
              canSubscribe: this.#opts.permissions.canSubscribe,
              canPublishData: this.#opts.permissions.canPublishData,
              canUpdateMetadata: this.#opts.permissions.canUpdateMetadata,
              hidden: this.#opts.permissions.hidden,
              agent: true
            }),
            version
          }
        }
      })
    );
    let currentStatus = WorkerStatus.WS_AVAILABLE;
    const loadMonitor = setInterval(() => {
      if (closingWS) clearInterval(loadMonitor);
      if (this.#draining) {
        if (currentStatus !== WorkerStatus.WS_FULL) {
          currentStatus = WorkerStatus.WS_FULL;
          this.event.emit(
            "worker_msg",
            new WorkerMessage({
              message: {
                case: "updateWorker",
                value: {
                  load: 1,
                  status: WorkerStatus.WS_FULL
                }
              }
            })
          );
        }
        return;
      }
      const oldStatus = currentStatus;
      this.#opts.loadFunc(this).then((currentLoad) => {
        const isFull = currentLoad >= this.#opts.loadThreshold;
        const currentlyAvailable = !isFull;
        currentStatus = currentlyAvailable ? WorkerStatus.WS_AVAILABLE : WorkerStatus.WS_FULL;
        if (oldStatus != currentStatus) {
          const extra = { load: currentLoad, loadThreshold: this.#opts.loadThreshold };
          if (isFull) {
            this.#logger.child(extra).info("worker is at full capacity, marking as unavailable");
          } else {
            this.#logger.child(extra).info("worker is below capacity, marking as available");
          }
        }
        this.event.emit(
          "worker_msg",
          new WorkerMessage({
            message: {
              case: "updateWorker",
              value: {
                load: currentLoad,
                status: currentStatus
              }
            }
          })
        );
      }).catch((e) => {
        this.#logger.warn({ error: e }, "failed to measure CPU load");
      });
    }, UPDATE_LOAD_INTERVAL);
    try {
      await close;
    } finally {
      ws.removeAllListeners();
    }
  }
  async #availability(msg) {
    var _a, _b;
    let answered = false;
    const onReject = async () => {
      answered = true;
      this.event.emit(
        "worker_msg",
        new WorkerMessage({
          message: {
            case: "availability",
            value: {
              jobId: msg.job.id,
              available: false
            }
          }
        })
      );
    };
    const onAccept = async (args) => {
      var _a2;
      answered = true;
      this.event.emit(
        "worker_msg",
        new WorkerMessage({
          message: {
            case: "availability",
            value: {
              jobId: msg.job.id,
              available: true,
              participantIdentity: args.identity,
              participantName: args.name,
              participantMetadata: args.metadata,
              participantAttributes: args.attributes
            }
          }
        })
      );
      this.#pending[req.id] = new PendingAssignment();
      const timer = setTimeout(() => {
        this.#logger.child({ req }).warn(`assignment for job ${req.id} timed out`);
        return;
      }, ASSIGNMENT_TIMEOUT);
      const asgn = await ((_a2 = this.#pending[req.id]) == null ? void 0 : _a2.promise.then(async (asgn2) => {
        clearTimeout(timer);
        return asgn2;
      }));
      if (asgn) {
        try {
          await this.#procPool.launchJob({
            acceptArguments: args,
            job: msg.job,
            url: asgn.url || this.#opts.wsURL,
            token: asgn.token,
            workerId: this.id,
            apiKey: this.#opts.apiKey,
            apiSecret: this.#opts.apiSecret
          });
        } catch (e) {
          this.#logger.child({ requestId: req.id }).error(e, "error launching job");
        }
      } else {
        this.#logger.child({ requestId: req.id }).warn("pending assignment not found");
      }
    };
    const req = new JobRequest(msg.job, onReject, onAccept);
    this.#logger.child({ jobId: (_a = msg.job) == null ? void 0 : _a.id, resuming: msg.resuming, agentName: this.#opts.agentName }).info("received job request");
    if (this.#draining) {
      this.#logger.child({ jobId: (_b = msg.job) == null ? void 0 : _b.id, resuming: msg.resuming, agentName: this.#opts.agentName }).info("Worker is draining and no longer available, rejecting job");
      await req.reject();
      return;
    }
    const jobRequestTask = async () => {
      try {
        await this.#opts.requestFunc(req);
      } catch (e) {
        this.#logger.child({ job: msg.job, resuming: msg.resuming, agentName: this.#opts.agentName }).info("jobRequestFunc failed");
        await onReject();
      }
      if (!answered) {
        this.#logger.child({ job: msg.job, resuming: msg.resuming, agentName: this.#opts.agentName }).info("no answer was given inside the jobRequestFunc, automatically rejecting the job");
      }
    };
    const task = jobRequestTask();
    this.#tasks.push(task);
    task.finally(() => {
      const taskIndex = this.#tasks.indexOf(task);
      if (taskIndex !== -1) {
        this.#tasks.splice(taskIndex, 1);
      } else {
        throw new Error(`task ${task} not found in tasks`);
      }
    });
  }
  async #termination(msg) {
    const proc = this.#procPool.getByJobId(msg.jobId);
    if (proc === null) {
      return;
    }
    await proc.close().catch((e) => this.#logger.error(e, "Error terminating job"));
  }
  async close() {
    var _a, _b;
    if (this.#closed) {
      await this.#close.await;
      return;
    }
    this.#logger.debug("shutting down worker");
    this.#closed = true;
    await ((_a = this.#inferenceExecutor) == null ? void 0 : _a.close());
    await this.#procPool.close();
    await this.#httpServer.close();
    await ThrowsPromise.allSettled(this.#tasks);
    (_b = this.#session) == null ? void 0 : _b.close();
    await this.#close.await;
  }
}
const Worker = AgentServer;
const WorkerOptions = ServerOptions;
export {
  AgentServer,
  MissingCredentialsError,
  ServerOptions,
  Worker,
  WorkerError,
  WorkerOptions,
  WorkerPermissions,
  defaultInitializeProcessFunc
};
//# sourceMappingURL=worker.js.map