import { AgentSession as pb } from "@livekit/protocol";
import { AudioFrame, AudioResampler } from "@livekit/rtc-node";
import * as path from "node:path";
import { log } from "../log.js";
import { createStreamChannel } from "../stream/stream_channel.js";
import { Future, Task } from "../utils.js";
import { AudioInput, AudioOutput } from "./io.js";
const WIRE_SAMPLE_RATE = 48e3;
const AGENT_SAMPLE_RATE = 24e3;
function consoleFrameToRtc(frame) {
  const aligned = new ArrayBuffer(frame.data.byteLength);
  new Uint8Array(aligned).set(frame.data);
  return new AudioFrame(
    new Int16Array(aligned),
    frame.sampleRate,
    frame.numChannels,
    frame.samplesPerChannel
  );
}
function rtcFrameToConsole(frame) {
  return new pb.AgentSessionMessage_ConsoleIO_AudioFrame({
    data: new Uint8Array(frame.data.buffer, frame.data.byteOffset, frame.data.byteLength),
    sampleRate: frame.sampleRate,
    numChannels: frame.channels,
    samplesPerChannel: frame.samplesPerChannel
  });
}
class TcpAudioInput extends AudioInput {
  channel = createStreamChannel();
  resampler = new AudioResampler(WIRE_SAMPLE_RATE, AGENT_SAMPLE_RATE, 1);
  closed = false;
  constructor() {
    super();
    this.multiStream.addInputStream(this.channel.stream());
  }
  pushFrame(frame) {
    if (this.closed) return;
    for (const resampled of this.resampler.push(consoleFrameToRtc(frame))) {
      void this.channel.write(resampled);
    }
  }
  async close() {
    this.closed = true;
    await this.channel.close();
    await super.close();
  }
}
class TcpAudioOutput extends AudioOutput {
  transport;
  resampler = new AudioResampler(AGENT_SAMPLE_RATE, WIRE_SAMPLE_RATE, 1);
  pushedDurationMs = 0;
  captureStartedAt = 0;
  flushTask;
  playoutDone = new Future();
  interrupted = new Future();
  constructor(transport) {
    super(AGENT_SAMPLE_RATE, void 0, { pause: true });
    this.transport = transport;
  }
  async captureFrame(frame) {
    await super.captureFrame(frame);
    if (this.flushTask && !this.flushTask.done) {
      log().error("captureFrame called while previous flush is in progress");
      await this.flushTask.result;
    }
    if (this.pushedDurationMs === 0) {
      this.captureStartedAt = Date.now();
      this.onPlaybackStarted(Date.now());
    }
    this.pushedDurationMs += frame.samplesPerChannel / frame.sampleRate * 1e3;
    for (const resampled of this.resampler.push(frame)) {
      await this.transport.sendMessage(
        new pb.AgentSessionMessage({
          message: { case: "audioOutput", value: rtcFrameToConsole(resampled) }
        })
      );
    }
  }
  flush() {
    super.flush();
    void this.transport.sendMessage(
      new pb.AgentSessionMessage({
        message: {
          case: "audioPlaybackFlush",
          value: new pb.AgentSessionMessage_ConsoleIO_AudioPlaybackFlush()
        }
      })
    );
    if (this.pushedDurationMs > 0) {
      if (this.flushTask && !this.flushTask.done) {
        log().error("flush called while previous flush is in progress");
        this.flushTask.cancel();
      }
      this.playoutDone = new Future();
      this.interrupted = new Future();
      this.flushTask = Task.from(() => this.runPlayoutHandshake());
    }
  }
  clearBuffer() {
    void this.transport.sendMessage(
      new pb.AgentSessionMessage({
        message: {
          case: "audioPlaybackClear",
          value: new pb.AgentSessionMessage_ConsoleIO_AudioPlaybackClear()
        }
      })
    );
    if (this.pushedDurationMs > 0) {
      this.interrupted.resolve();
    }
  }
  /** Called by {@link SessionHost} when the broker reports playout finished. */
  notifyPlayoutFinished() {
    if (!this.playoutDone.done) {
      this.playoutDone.resolve();
    }
  }
  async runPlayoutHandshake() {
    try {
      await Promise.race([this.playoutDone.await, this.interrupted.await]);
    } catch {
      return;
    }
    const interrupted = this.interrupted.done && !this.playoutDone.done;
    let playedMs;
    if (interrupted) {
      const elapsed = Date.now() - this.captureStartedAt;
      playedMs = Math.min(Math.max(0, elapsed), this.pushedDurationMs);
    } else {
      playedMs = this.pushedDurationMs;
    }
    this.onPlaybackFinished({ playbackPosition: playedMs / 1e3, interrupted });
    this.pushedDurationMs = 0;
    this.interrupted = new Future();
  }
}
class AgentsConsole {
  static _instance;
  static getInstance() {
    if (!AgentsConsole._instance) {
      AgentsConsole._instance = new AgentsConsole();
    }
    return AgentsConsole._instance;
  }
  /** @internal — reset the process-wide singleton (test only). */
  static _reset() {
    AgentsConsole._instance = void 0;
  }
  enabled = false;
  record = false;
  transport;
  audioInput;
  audioOutput;
  /**
   * Directory where console recordings (audio + session report) are written,
   * relative to the working directory. Mirrors python's `AgentsConsole`.
   */
  sessionDirectory;
  acquired = false;
  constructor() {
    const now = /* @__PURE__ */ new Date();
    const pad = (n) => String(n).padStart(2, "0");
    this.sessionDirectory = path.join(
      "console-recordings",
      `session-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
    );
  }
  get ioAcquired() {
    return this.acquired;
  }
  /** Wire the session's IO for console mode. */
  acquireIo(session) {
    if (this.acquired) {
      throw new Error("the console IO was already acquired by another session");
    }
    this.acquired = true;
    session.input.audio = this.audioInput ?? null;
    session.output.audio = this.audioOutput ?? null;
    session.output.transcription = null;
  }
}
export {
  AgentsConsole,
  TcpAudioInput,
  TcpAudioOutput
};
//# sourceMappingURL=console_io.js.map