import { ParticipantKind, RoomEvent, TrackKind } from "@livekit/rtc-node";
import { ThrowsPromise } from "@livekit/throws-transformer/throws";
import { RoomServiceClient } from "livekit-server-sdk";
import { AsyncLocalStorage } from "node:async_hooks";
import { mkdir, writeFile } from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { log } from "./log.js";
import { flushOtelLogs, setupCloudTracer, uploadSessionReport } from "./telemetry/index.js";
import { isCloud } from "./utils.js";
import { AgentsConsole } from "./voice/console_io.js";
import { createSessionReport, sessionReportToJSON } from "./voice/report.js";
const jobContextStorage = new AsyncLocalStorage();
function getJobContext(required = true) {
  const ctx = jobContextStorage.getStore();
  if (!ctx && required) {
    throw new Error("no job context found, are you running this code inside a job entrypoint?");
  }
  return ctx;
}
function runWithJobContext(context, fn) {
  return jobContextStorage.run(context, fn);
}
function runWithJobContextAsync(context, fn) {
  return jobContextStorage.run(context, fn);
}
var AutoSubscribe = /* @__PURE__ */ ((AutoSubscribe2) => {
  AutoSubscribe2[AutoSubscribe2["SUBSCRIBE_ALL"] = 0] = "SUBSCRIBE_ALL";
  AutoSubscribe2[AutoSubscribe2["SUBSCRIBE_NONE"] = 1] = "SUBSCRIBE_NONE";
  AutoSubscribe2[AutoSubscribe2["VIDEO_ONLY"] = 2] = "VIDEO_ONLY";
  AutoSubscribe2[AutoSubscribe2["AUDIO_ONLY"] = 3] = "AUDIO_ONLY";
  return AutoSubscribe2;
})(AutoSubscribe || {});
class FunctionExistsError extends Error {
  constructor(msg) {
    super(msg);
    Object.setPrototypeOf(this, new.target.prototype);
  }
}
class JobContext {
  #proc;
  #info;
  #room;
  #onConnect;
  #onShutdown;
  /** @internal */
  shutdownCallbacks = [];
  #participantEntrypoints = [];
  #participantTasks = {};
  #logger;
  #inferenceExecutor;
  /** @internal */
  _primaryAgentSession;
  /** @internal */
  _sessionDirectory;
  connected = false;
  constructor(proc, info, room, onConnect, onShutdown, inferenceExecutor) {
    var _a;
    this.#proc = proc;
    this.#info = info;
    this.#room = room;
    this.#onConnect = onConnect;
    this.#onShutdown = onShutdown;
    this.onParticipantConnected = this.onParticipantConnected.bind(this);
    this.#room.on(RoomEvent.ParticipantConnected, this.onParticipantConnected);
    this.#logger = log().child({
      jobId: this.#info.job.id,
      roomName: (_a = this.#info.job.room) == null ? void 0 : _a.name
    });
    this.#inferenceExecutor = inferenceExecutor;
    const agentsConsole = AgentsConsole.getInstance();
    this._sessionDirectory = agentsConsole.enabled ? agentsConsole.sessionDirectory : path.join(os.tmpdir(), "livekit-agents", `job-${this.#info.job.id}`);
  }
  get proc() {
    return this.#proc;
  }
  get job() {
    return this.#info.job;
  }
  get workerId() {
    return this.#info.workerId;
  }
  /** @returns The room the agent was called into */
  get room() {
    return this.#room;
  }
  get info() {
    return this.#info;
  }
  /** @returns Whether this job is a locally-synthesized fake job (e.g. console mode). */
  get isFakeJob() {
    return this.#info.fakeJob ?? false;
  }
  /** @returns The agent's participant if connected to the room, otherwise `undefined` */
  get agent() {
    return this.#room.localParticipant;
  }
  /** @returns The global inference executor */
  get inferenceExecutor() {
    return this.#inferenceExecutor;
  }
  /**
   * @returns The session directory for storing recordings and session data.
   */
  get sessionDirectory() {
    return this._sessionDirectory;
  }
  /** Adds a promise to be awaited when {@link JobContext.shutdown | shutdown} is called. */
  addShutdownCallback(callback) {
    this.shutdownCallbacks.push(callback);
  }
  async waitForParticipant(identity) {
    if (!this.#room.isConnected) {
      throw new Error("room is not connected");
    }
    for (const p of this.#room.remoteParticipants.values()) {
      if ((!identity || p.identity === identity) && p.info.kind != ParticipantKind.AGENT) {
        return p;
      }
    }
    return new ThrowsPromise((resolve, reject) => {
      const onParticipantConnected = (participant) => {
        if ((!identity || participant.identity === identity) && participant.info.kind != ParticipantKind.AGENT) {
          clearHandlers();
          resolve(participant);
        }
      };
      const onDisconnected = () => {
        clearHandlers();
        reject(new Error("Room disconnected while waiting for participant"));
      };
      const clearHandlers = () => {
        this.#room.off(RoomEvent.ParticipantConnected, onParticipantConnected);
        this.#room.off(RoomEvent.Disconnected, onDisconnected);
      };
      this.#room.on(RoomEvent.ParticipantConnected, onParticipantConnected);
      this.#room.on(RoomEvent.Disconnected, onDisconnected);
    });
  }
  /**
   * Connects the agent to the room.
   *
   * @remarks
   * It is recommended to run this command as early in the function as possible, as executing it
   * later may cause noticeable delay between user and agent joins.
   *
   * @see {@link https://github.com/livekit/node-sdks/tree/main/packages/livekit-rtc#readme |
   * @livekit/rtc-node} for more information about the parameters.
   */
  async connect(e2ee, autoSubscribe = 0 /* SUBSCRIBE_ALL */, rtcConfig) {
    if (this.connected) {
      return;
    }
    if (this.isFakeJob) {
      this.#logger.debug("job_ctx.connect() is a no-op in console mode");
      this.#onConnect();
      this.connected = true;
      return;
    }
    const opts = {
      e2ee,
      autoSubscribe: autoSubscribe == 0 /* SUBSCRIBE_ALL */,
      rtcConfig,
      dynacast: false
    };
    await this.#room.connect(this.#info.url, this.#info.token, opts);
    this.#onConnect();
    this.#room.remoteParticipants.forEach(this.onParticipantConnected);
    if ([3 /* AUDIO_ONLY */, 2 /* VIDEO_ONLY */].includes(autoSubscribe)) {
      const desiredKind = autoSubscribe === 3 /* AUDIO_ONLY */ ? TrackKind.KIND_AUDIO : TrackKind.KIND_VIDEO;
      const subscribeIfMatch = (pub) => {
        if (pub.kind === desiredKind) {
          pub.setSubscribed(true);
        }
      };
      this.#room.remoteParticipants.forEach((p) => {
        p.trackPublications.forEach(subscribeIfMatch);
      });
      this.#room.on(RoomEvent.TrackPublished, subscribeIfMatch);
    }
    this.connected = true;
  }
  /** Deletes the room and disconnects all participants. */
  async deleteRoom(roomName) {
    if (this.isFakeJob) {
      this.#logger.warn("job_ctx.deleteRoom() is not executed in console mode");
      return;
    }
    const targetRoomName = roomName ?? this.#room.name;
    if (!targetRoomName) {
      this.#logger.warn("cannot delete room because room name is missing");
      return;
    }
    try {
      const client = new RoomServiceClient(this.#info.url, this.#info.apiKey, this.#info.apiSecret);
      await client.deleteRoom(targetRoomName);
      this.#logger.info({ roomName: targetRoomName }, "room deleted");
    } catch (error) {
      this.#logger.warn({ error, roomName: targetRoomName }, "error while deleting room");
    }
  }
  makeSessionReport(session) {
    var _a, _b;
    const targetSession = session || this._primaryAgentSession;
    if (!targetSession) {
      throw new Error("Cannot prepare report, no AgentSession was found");
    }
    const recorderIO = targetSession._recorderIO;
    if (recorderIO && recorderIO.recording) {
      throw new Error("Cannot create the AgentSession report, the RecorderIO is still recording");
    }
    return createSessionReport({
      jobId: this.job.id,
      roomId: ((_a = this.job.room) == null ? void 0 : _a.sid) || "",
      room: ((_b = this.job.room) == null ? void 0 : _b.name) || "",
      options: targetSession.sessionOptions,
      events: targetSession._recordedEvents,
      enableRecording: targetSession._enableRecording,
      recordingOptions: targetSession._recordingOptions,
      chatHistory: targetSession.history.copy(),
      startedAt: targetSession._startedAt,
      audioRecordingPath: recorderIO == null ? void 0 : recorderIO.outputPath,
      audioRecordingStartedAt: recorderIO == null ? void 0 : recorderIO.recordingStartedAt,
      modelUsage: targetSession._usageCollector.flatten()
    });
  }
  async _onSessionEnd() {
    const session = this._primaryAgentSession;
    if (!session) {
      return;
    }
    const recorderIO = session._recorderIO;
    if (recorderIO == null ? void 0 : recorderIO.recording) {
      this.#logger.warn("recorder_io is still recording at session end, closing it");
      await recorderIO.close();
    }
    const report = this.makeSessionReport(session);
    const agentsConsole = AgentsConsole.getInstance();
    if (agentsConsole.enabled && agentsConsole.record) {
      try {
        await mkdir(this._sessionDirectory, { recursive: true });
        await writeFile(
          path.join(this._sessionDirectory, "session_report.json"),
          JSON.stringify(sessionReportToJSON(report), null, 2)
        );
      } catch (error) {
        this.#logger.error({ error }, "failed to save the session report");
      }
    }
    if (!this.isFakeJob) {
      const url = new URL(this.#info.url);
      if (report.enableRecording && isCloud(url)) {
        try {
          await uploadSessionReport({
            agentName: this.job.agentName,
            cloudHostname: url.hostname,
            report
          });
          this.#logger.info(
            {
              jobId: report.jobId,
              roomId: report.roomId
            },
            "Session report uploaded to LiveKit Cloud"
          );
        } catch (error) {
          this.#logger.error({ error }, "Failed to upload session report");
        }
      }
    }
    this.#logger.debug(
      {
        jobId: report.jobId,
        roomId: report.roomId,
        eventsCount: report.events.length
      },
      "Session ended, report generated"
    );
    session._recordedEvents = [];
    try {
      await flushOtelLogs();
    } catch (error) {
      this.#logger.error({ error }, "Failed to flush OTEL logs");
    }
  }
  /**
   * Gracefully shuts down the job, and runs all shutdown promises.
   *
   * @param reason - Optional reason for shutdown
   */
  shutdown(reason = "") {
    this.#onShutdown(reason);
  }
  /** @internal */
  onParticipantConnected(p) {
    var _a;
    for (const callback of this.#participantEntrypoints) {
      if (((_a = this.#participantTasks[p.identity]) == null ? void 0 : _a.callback) == callback) {
        this.#logger.warn(
          "a participant has joined before a prior prticipant task matching the same identity has finished:",
          p.identity
        );
      }
      const result = callback(this, p);
      result.finally(() => {
        var _a2;
        if (((_a2 = this.#participantTasks[p.identity]) == null ? void 0 : _a2.result) === result) {
          delete this.#participantTasks[p.identity];
        }
      });
      this.#participantTasks[p.identity] = { callback, result };
    }
  }
  /**
   * Adds a promise to be awaited whenever a new participant joins the room.
   *
   * @throws {@link FunctionExistsError} if an entrypoint already exists
   */
  addParticipantEntrypoint(callback) {
    if (this.#participantEntrypoints.includes(callback)) {
      throw new FunctionExistsError("entrypoints cannot be added more than once");
    }
    this.#participantEntrypoints.push(callback);
  }
  async initRecording(options) {
    if (this.isFakeJob) {
      return;
    }
    const url = new URL(this.#info.url);
    if (!isCloud(url)) {
      return;
    }
    if (!options.traces && !options.logs) {
      return;
    }
    this.#logger.debug({ hostname: url.hostname }, "Configuring session recording (cloud tracer)");
    await setupCloudTracer({
      roomId: this.job.room.sid,
      jobId: this.job.id,
      cloudHostname: url.hostname,
      enableTraces: options.traces,
      enableLogs: options.logs
    });
  }
}
class JobProcess {
  #pid = process.pid;
  userData = {};
  get pid() {
    return this.#pid;
  }
}
class JobRequest {
  #job;
  #onReject;
  #onAccept;
  /** @internal */
  constructor(job, onReject, onAccept) {
    this.#job = job;
    this.#onReject = onReject;
    this.#onAccept = onAccept;
  }
  /** @returns The ID of the job, set by the LiveKit server */
  get id() {
    return this.#job.id;
  }
  /** @see {@link https://www.npmjs.com/package/@livekit/protocol | @livekit/protocol} */
  get job() {
    return this.#job;
  }
  /** @see {@link https://www.npmjs.com/package/@livekit/protocol | @livekit/protocol} */
  get room() {
    return this.#job.room;
  }
  /** @see {@link https://www.npmjs.com/package/@livekit/protocol | @livekit/protocol} */
  get publisher() {
    return this.#job.participant;
  }
  /** @returns The agent's name, as set in {@link WorkerOptions} */
  get agentName() {
    return this.#job.agentName;
  }
  /** Rejects the job. */
  async reject() {
    await this.#onReject();
  }
  /** Accepts the job, launching it on an idle child process. */
  async accept(name = "", identity = "", metadata = "", attributes) {
    if (identity === "") identity = "agent-" + this.id;
    this.#onAccept({ name, identity, metadata, attributes });
  }
}
export {
  AutoSubscribe,
  FunctionExistsError,
  JobContext,
  JobProcess,
  JobRequest,
  getJobContext,
  runWithJobContext,
  runWithJobContextAsync
};
//# sourceMappingURL=job.js.map