import { Timestamp } from "@bufbuild/protobuf";
import { AgentInference } from "@livekit/protocol";
import { APIConnectionError, APIError, APIStatusError } from "../../_exceptions.js";
import { log } from "../../log.js";
import { createStreamChannel } from "../../stream/stream_channel.js";
import { intervalForRetry } from "../../types.js";
import { Task, delay } from "../../utils.js";
import { buildMetadataHeaders, connectWs, createAccessToken } from "../utils.js";
import {
  DEFAULT_SAMPLE_RATE
} from "./base.js";
import { EOT_INFERENCE_METHOD } from "./runner.js";
const AudioEncoding = AgentInference.AudioEncoding;
const ClientMessageCtor = AgentInference.ClientMessage;
const ServerMessageCtor = AgentInference.ServerMessage;
const InferenceStart = AgentInference.InferenceStart;
const InputAudio = AgentInference.InputAudio;
const SessionClose = AgentInference.SessionClose;
const SessionCreate = AgentInference.SessionCreate;
const SessionFlush = AgentInference.SessionFlush;
const SessionSettings = AgentInference.SessionSettings;
const WS_OPEN = 1;
function nowTimestamp() {
  const now = Date.now();
  return new Timestamp({
    seconds: BigInt(Math.floor(now / 1e3)),
    nanos: now % 1e3 * 1e6
  });
}
function timestampToMs(ts) {
  if (ts === void 0) return 0;
  return Number(ts.seconds) * 1e3 + Math.floor(ts.nanos / 1e6);
}
function durationToMs(d) {
  if (d === void 0) return 0;
  return Number(d.seconds) * 1e3 + Math.floor(d.nanos / 1e6);
}
const CLIENT_BUFFER_SECONDS = 1.2;
const CLIENT_BUFFER_SAMPLES = Math.floor(CLIENT_BUFFER_SECONDS * DEFAULT_SAMPLE_RATE);
class PcmRingBuffer {
  constructor(capacity) {
    this.capacity = capacity;
    this.buf = new Int16Array(capacity);
  }
  buf;
  writeIdx = 0;
  filled = 0;
  pushFrame(frame) {
    const src = frame.data;
    for (let i = 0; i < src.length; i++) {
      this.buf[this.writeIdx] = src[i];
      this.writeIdx = (this.writeIdx + 1) % this.capacity;
    }
    this.filled = Math.min(this.filled + src.length, this.capacity);
  }
  /** Returns a contiguous Int16Array snapshot of the last `filled` samples. */
  read() {
    const out = new Int16Array(this.filled);
    const start = (this.writeIdx - this.filled + this.capacity) % this.capacity;
    if (start + this.filled <= this.capacity) {
      out.set(this.buf.subarray(start, start + this.filled));
    } else {
      const tail = this.capacity - start;
      out.set(this.buf.subarray(start, this.capacity), 0);
      out.set(this.buf.subarray(0, this.filled - tail), tail);
    }
    return out;
  }
  /** Drop the oldest `n` samples. */
  shift(n) {
    this.filled = Math.max(0, this.filled - n);
  }
  get length() {
    return this.filled;
  }
}
class LocalTransport {
  _opts;
  _executor;
  _buf;
  _streamRef;
  _tasks = /* @__PURE__ */ new Set();
  _warnedNoExecutor = false;
  _logger = log();
  constructor(opts) {
    this._opts = opts.opts;
    this._executor = opts.executor;
    this._buf = new PcmRingBuffer(CLIENT_BUFFER_SAMPLES);
  }
  attach(stream) {
    this._streamRef = new WeakRef(stream);
  }
  runInference(requestId) {
    const snapshot = this._buf.read();
    const task = this._predict(requestId, snapshot);
    this._tasks.add(task);
    void task.finally(() => this._tasks.delete(task));
  }
  async _predict(requestId, pcmSnapshot) {
    var _a, _b;
    const stream = (_a = this._streamRef) == null ? void 0 : _a.deref();
    if (stream === void 0) return;
    if (this._executor === void 0) {
      if (!this._warnedNoExecutor) {
        this._warnedNoExecutor = true;
        this._logger.warn(
          "local audio EOT unavailable (no inference executor / native binding); defaulting predictions to 1.0 so turns still commit after minDelay"
        );
      }
      stream._resolvePrediction(requestId, 1);
      return;
    }
    const pcm = Buffer.from(
      pcmSnapshot.buffer,
      pcmSnapshot.byteOffset,
      pcmSnapshot.byteLength
    ).toString("base64");
    let prob = 0;
    let inferenceDurationMs = 0;
    try {
      const out = await this._executor.doInference(EOT_INFERENCE_METHOD, {
        pcm
      });
      prob = out.probability;
      inferenceDurationMs = out.inferenceDurationMs;
    } catch (err) {
      this._logger.error(
        { err: err instanceof Error ? err.message : String(err) },
        "local audio EOT inference (executor) failed"
      );
    }
    const freshStream = (_b = this._streamRef) == null ? void 0 : _b.deref();
    if (freshStream === void 0) return;
    freshStream._resolvePrediction(requestId, prob, { inferenceDuration: inferenceDurationMs });
  }
  async pushFrame(frame) {
    this._buf.pushFrame(frame);
  }
  async flush(_sentinel) {
    if (this._buf.length > 0) {
      this._buf.shift(this._buf.length);
    }
  }
  detach() {
    this._tasks.clear();
  }
  async run() {
    var _a;
    const stream = (_a = this._streamRef) == null ? void 0 : _a.deref();
    if (stream === void 0) return;
    await stream._drainAudioChannel();
  }
}
class CloudTransport {
  _detectorRef;
  _opts;
  _cloudOpts;
  _connOptions;
  _streamRef;
  _ws;
  _numRetries = 0;
  _connectCalls = 0;
  /** Outbound FIFO for the active connection; recreated per `_runOnce`. */
  _sendChannel;
  /** Set by `detach()`; stops the retry loop and suppresses the
   * connection-closed throw so a teardown can't trigger a reconnect. */
  _detached = false;
  /** Aborted by `detach()` to release the audio-drain reader lock so a
   * swapped-in transport can re-acquire the shared audio stream. */
  _runAbort;
  _logger = log();
  /** Optional connect override for tests; defaults to a real WS handshake. */
  _connectImpl;
  constructor(args) {
    this._detectorRef = new WeakRef(args.detector);
    this._opts = args.opts;
    this._cloudOpts = args.cloudOpts;
    this._connOptions = args.cloudOpts.connOptions;
    this._connectImpl = args.connect ? () => args.connect(this) : void 0;
  }
  /** @internal Test-visible: number of connect attempts. */
  get connectCalls() {
    return this._connectCalls;
  }
  /** @internal Test-visible: retry counter (resets to 0 after a connect). */
  get numRetries() {
    return this._numRetries;
  }
  attach(stream) {
    this._streamRef = new WeakRef(stream);
  }
  /** @internal Test-visible: true once the WS handshake is open. Not part of
   * the transport interface — the stream FSM does not gate on this. */
  transportReady() {
    return this._ws !== void 0 && this._ws.readyState === WS_OPEN;
  }
  runInference(requestId) {
    this._enqueue(
      new ClientMessageCtor({
        message: { case: "inferenceStart", value: new InferenceStart({ requestId }) }
      })
    );
  }
  async pushFrame(frame) {
    if (frame.data.byteLength === 0) return;
    this._enqueue(
      new ClientMessageCtor({
        message: {
          case: "inputAudio",
          value: new InputAudio({
            audio: new Uint8Array(frame.data.buffer, frame.data.byteOffset, frame.data.byteLength),
            numSamples: frame.samplesPerChannel,
            createdAt: nowTimestamp()
          })
        }
      })
    );
  }
  async flush(_sentinel) {
    this._enqueue(
      new ClientMessageCtor({ message: { case: "sessionFlush", value: new SessionFlush() } })
    );
  }
  detach() {
    var _a, _b;
    this._detached = true;
    (_a = this._runAbort) == null ? void 0 : _a.abort();
    void ((_b = this._sendChannel) == null ? void 0 : _b.close());
    const ws = this._ws;
    this._ws = void 0;
    try {
      ws == null ? void 0 : ws.close();
    } catch {
    }
  }
  _enqueue(msg) {
    if (this._ws === void 0 || this._ws.readyState !== WS_OPEN) return;
    const channel = this._sendChannel;
    if (channel === void 0 || channel.closed) return;
    void channel.write(msg).catch(() => {
    });
  }
  async _defaultConnect() {
    let baseUrl = this._cloudOpts.baseUrl;
    if (baseUrl.startsWith("http://")) baseUrl = baseUrl.replace("http://", "ws://");
    else if (baseUrl.startsWith("https://")) baseUrl = baseUrl.replace("https://", "wss://");
    const token = await createAccessToken(this._cloudOpts.apiKey, this._cloudOpts.apiSecret);
    const headers = { ...buildMetadataHeaders(), Authorization: `Bearer ${token}` };
    const ws = await connectWs(`${baseUrl}/eot`, headers, this._connOptions.timeoutMs);
    return ws;
  }
  _warnTransportLatency(msg) {
    const clientCreatedAtMs = timestampToMs(msg.clientCreatedAt);
    const transportLatency = Date.now() - clientCreatedAtMs;
    if (transportLatency > 500 && clientCreatedAtMs > 0) {
      this._logger.warn(
        { transportLatencyMs: transportLatency },
        "turn detection transport latency is too high"
      );
    }
  }
  _processServerMessage(msg) {
    var _a;
    const stream = (_a = this._streamRef) == null ? void 0 : _a.deref();
    if (stream === void 0) return;
    const kind = msg.message.case;
    if (kind === "eotPrediction") {
      const prediction = msg.message.value;
      const stats = prediction.inferenceStats;
      const requestSentAtMs = timestampToMs(stats == null ? void 0 : stats.latestClientCreatedAt);
      const detectionDelayMs = requestSentAtMs > 0 ? Date.now() - requestSentAtMs : 0;
      const inferenceDurationMs = durationToMs(stats == null ? void 0 : stats.serverE2eLatency);
      stream._resolvePrediction(msg.requestId ?? "", prediction.probability, {
        detectionDelay: detectionDelayMs,
        inferenceDuration: inferenceDurationMs,
        backchannelProbability: prediction.backchannelProbability
      });
      const detector = this._detectorRef.deref();
      if (detector !== void 0) {
        detector.emit("metrics_collected", {
          type: "eot_inference_metrics",
          timestamp: Date.now(),
          totalDuration: durationToMs(stats == null ? void 0 : stats.clientE2eLatency),
          predictionDuration: inferenceDurationMs,
          detectionDelay: detectionDelayMs,
          numRequests: 1,
          metadata: { modelName: detector.model, modelProvider: detector.provider }
        });
      }
    } else if (kind === "error") {
      const err = msg.message.value;
      throw new APIStatusError({
        message: err.message,
        options: { statusCode: err.code, requestId: msg.requestId }
      });
    } else if (kind === "sessionCreated") {
      this._warnTransportLatency(msg);
      const created = msg.message.value;
      stream.thresholdsOptions._updateDefaults(
        created.defaultThresholds,
        created.defaultThreshold,
        created.defaultBackchannelThresholds,
        created.defaultBackchannelThreshold
      );
      this._logger.debug(
        {
          model: stream.thresholdsOptions.model,
          thresholds: stream.thresholdsOptions.thresholds,
          defaultThreshold: stream.thresholdsOptions.defaultThreshold,
          overrides: stream.thresholdsOptions.overrides
        },
        "audio turn detector initialized"
      );
    } else if (kind === "sessionClosed" || kind === "inferenceStarted" || kind === "inferenceStopped") {
      this._warnTransportLatency(msg);
    } else {
      this._logger.warn({ kind }, "unexpected turn detector message");
    }
  }
  async run() {
    const maxRetries = this._connOptions.maxRetry;
    while (!this._detached && this._numRetries <= maxRetries) {
      try {
        await this._runOnce();
        return;
      } catch (err) {
        if (this._detached) return;
        if (!(err instanceof APIError) || maxRetries === 0 || !err.retryable) throw err;
        if (this._numRetries === maxRetries) {
          throw new APIConnectionError({
            message: `failed to connect livekit turn detector after ${this._numRetries} attempts`
          });
        }
        const retryIntervalMs = intervalForRetry(this._connOptions, this._numRetries);
        this._logger.warn(
          { err: err.message, attempt: this._numRetries, retryIntervalMs },
          "livekit turn detector connection failed; retrying"
        );
        await delay(retryIntervalMs);
        this._numRetries += 1;
      }
    }
  }
  async _runOnce() {
    var _a;
    const stream = (_a = this._streamRef) == null ? void 0 : _a.deref();
    if (stream === void 0) return;
    const runAbort = new AbortController();
    this._runAbort = runAbort;
    this._connectCalls += 1;
    const ws = await (this._connectImpl ?? this._defaultConnect.bind(this))();
    if (this._detached) {
      try {
        ws.close();
      } catch {
      }
      return;
    }
    this._numRetries = 0;
    this._ws = ws;
    const sendChannel = createStreamChannel();
    this._sendChannel = sendChannel;
    ws.send(
      new ClientMessageCtor({
        message: {
          case: "sessionCreate",
          value: new SessionCreate({
            settings: new SessionSettings({
              sampleRate: this._opts.sampleRate,
              encoding: AudioEncoding.PCM_S16LE
            })
          })
        },
        createdAt: nowTimestamp()
      }).toBinary()
    );
    let closingWs = false;
    let socketErr;
    const recvChannel = createStreamChannel();
    ws.on("message", (data) => {
      const chunk = data instanceof Buffer ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : Array.isArray(data) ? new Uint8Array(Buffer.concat(data)) : new Uint8Array(data);
      void recvChannel.write(chunk).catch(() => {
      });
    });
    ws.on("close", () => {
      void recvChannel.close();
      void sendChannel.close();
    });
    ws.on("error", (err) => {
      socketErr = err;
      void recvChannel.close();
      void sendChannel.close();
    });
    const drainAudioTask = Task.from(async () => {
      await stream._drainAudioChannel(runAbort.signal);
      if (runAbort.signal.aborted) return;
      closingWs = true;
      this._enqueue(
        new ClientMessageCtor({ message: { case: "sessionClose", value: new SessionClose() } })
      );
      await sendChannel.close();
    });
    const senderTask = Task.from(async () => {
      const reader = sendChannel.stream().getReader();
      try {
        while (true) {
          const { done, value: msg } = await reader.read();
          if (done) return;
          if (msg.createdAt === void 0) msg.createdAt = nowTimestamp();
          if (ws.readyState !== WS_OPEN) return;
          try {
            ws.send(msg.toBinary());
          } catch {
            return;
          }
        }
      } finally {
        reader.releaseLock();
      }
    });
    const recvTask = Task.from(async () => {
      const reader = recvChannel.stream().getReader();
      try {
        while (true) {
          const { done, value: chunk } = await reader.read();
          if (done) break;
          this._processServerMessage(ServerMessageCtor.fromBinary(chunk));
        }
      } finally {
        reader.releaseLock();
      }
      if (socketErr !== void 0 && !closingWs && !runAbort.signal.aborted) {
        throw new APIConnectionError({
          message: `turn detector connection error: ${socketErr.message}`,
          options: { retryable: false }
        });
      }
      if (!closingWs && !runAbort.signal.aborted) {
        throw new APIStatusError({
          message: "turn detector connection closed unexpectedly",
          options: { statusCode: -1, retryable: false }
        });
      }
    });
    try {
      await Promise.all([drainAudioTask.result, senderTask.result, recvTask.result]);
    } finally {
      drainAudioTask.cancel();
      senderTask.cancel();
      recvTask.cancel();
      void sendChannel.close();
      void recvChannel.close();
      this._ws = void 0;
      try {
        ws.close();
      } catch {
      }
    }
  }
}
export {
  CloudTransport,
  LocalTransport
};
//# sourceMappingURL=transports.js.map