import { getJobContext } from "../../job.js";
import { log } from "../../log.js";
import { DEFAULT_API_CONNECT_OPTIONS } from "../../types.js";
import { isDevMode, isHosted, resolveEnvVar } from "../../utils.js";
import { getDefaultInferenceUrl } from "../utils.js";
import {
  BaseStreamingTurnDetector,
  BaseStreamingTurnDetectorStream,
  DEFAULT_SAMPLE_RATE,
  SwapAbortError
} from "./base.js";
import { ThresholdOptions } from "./languages.js";
import { CloudTransport, LocalTransport } from "./transports.js";
class TurnDetector extends BaseStreamingTurnDetector {
  _model;
  _cloudOpts;
  _executor;
  constructor(opts = {}) {
    const auto = opts.version === void 0;
    const resolvedVersion = opts.version ?? (isHosted() || isDevMode() ? "v1" : "v1-mini");
    let resolvedModel = `turn-detector-${resolvedVersion}`;
    let cloudOpts;
    if (resolvedVersion === "v1") {
      const baseUrl = resolveEnvVar(
        opts.baseUrl,
        ["LIVEKIT_INFERENCE_URL"],
        getDefaultInferenceUrl()
      );
      const apiKey = resolveEnvVar(opts.apiKey, ["LIVEKIT_INFERENCE_API_KEY", "LIVEKIT_API_KEY"]);
      const apiSecret = resolveEnvVar(opts.apiSecret, [
        "LIVEKIT_INFERENCE_API_SECRET",
        "LIVEKIT_API_SECRET"
      ]);
      const missing = [];
      if (!baseUrl) missing.push("LIVEKIT_INFERENCE_URL");
      if (!apiKey) missing.push("LIVEKIT_API_KEY");
      if (!apiSecret) missing.push("LIVEKIT_API_SECRET");
      if (missing.length > 0) {
        if (auto) {
          log().warn(
            { missing },
            "LIVEKIT_INFERENCE_URL is set but creds are missing; falling back to 'v1-mini'"
          );
          resolvedModel = "turn-detector-v1-mini";
        } else {
          throw new Error(
            `TurnDetector(version='v1') requires ${missing.join(", ")} (env or constructor argument).`
          );
        }
      } else {
        cloudOpts = {
          baseUrl,
          apiKey,
          apiSecret,
          connOptions: opts.connOptions ?? DEFAULT_API_CONNECT_OPTIONS
        };
      }
    }
    const detectorOpts = {
      sampleRate: opts.sampleRate ?? DEFAULT_SAMPLE_RATE,
      thresholds: new ThresholdOptions(
        resolvedModel,
        opts.unlikelyThreshold,
        opts.backchannelThreshold
      )
    };
    super(detectorOpts);
    this._model = resolvedModel;
    this._cloudOpts = cloudOpts;
    this._warnThresholdOverride();
    if (opts.executor !== void 0) {
      this._executor = opts.executor;
    } else {
      try {
        this._executor = getJobContext().inferenceExecutor;
      } catch {
        this._executor = void 0;
      }
    }
  }
  /** Current model name. Starts at the construction-time selection and flips to
   * `'turn-detector-v1-mini'` after a cloud→local fallback: the detector and its
   * (single) active stream share one mutable `ThresholdOptions`, and the
   * stream writes the swap back here so EOU metrics and `audio_recognition`
   * see a consistent view. The fallback is one-way and sticky. */
  get model() {
    return this._model;
  }
  /** @internal Written by the active stream on cloud→local fallback. */
  _setModel(model) {
    this._model = model;
  }
  _warnThresholdOverride() {
    const overrides = this._opts.thresholds.overrides;
    if (overrides !== void 0) {
      log().warn(
        { unlikelyThreshold: overrides },
        "a non-default turn detection threshold was provided; the server provides calibrated defaults and overriding them may be suboptimal"
      );
    }
    const bcOverrides = this._opts.thresholds.backchannelOverrides;
    if (bcOverrides !== void 0) {
      log().warn(
        { backchannelThreshold: bcOverrides },
        "a non-default backchannel threshold was provided; the server provides calibrated defaults and overriding them may be suboptimal"
      );
    }
  }
  /** Replace the user threshold override at runtime. The shared
   * `ThresholdOptions` re-resolves against the current (server or shipped)
   * defaults, so an active stream picks it up immediately. */
  updateOptions(opts = {}) {
    if (opts.unlikelyThreshold !== void 0) {
      this._opts.thresholds.updateOverrides(opts.unlikelyThreshold);
    }
    if (opts.backchannelThreshold !== void 0) {
      this._opts.thresholds.updateBackchannelOverrides(opts.backchannelThreshold);
    }
    this._warnThresholdOverride();
  }
  stream(opts = {}) {
    const cloudOpts = this._cloudOpts !== void 0 ? { ...this._cloudOpts, connOptions: opts.connOptions ?? this._cloudOpts.connOptions } : void 0;
    const stream = new TurnDetectorStreamImpl({
      detector: this,
      opts: this._opts,
      cloudOpts,
      model: this._model,
      executor: this._executor
    });
    this._streams.add(stream);
    return stream;
  }
}
class TurnDetectorStreamImpl extends BaseStreamingTurnDetectorStream {
  _model;
  _cloudOpts;
  _executor;
  _isFallback = false;
  _warnedCloudFailure = false;
  _warnedLocalFailure = false;
  _detLogger = log();
  constructor(args) {
    const transport = args.transport ?? (args.model === "turn-detector-v1" ? new CloudTransport({
      detector: args.detector,
      opts: args.opts,
      cloudOpts: args.cloudOpts
    }) : new LocalTransport({ opts: args.opts, executor: args.executor }));
    super({ detector: args.detector, opts: args.opts, transport });
    this._model = args.model;
    this._cloudOpts = args.cloudOpts;
    this._executor = args.executor;
  }
  /** This stream's *current* model name (flips to `'turn-detector-v1-mini'`
   * after a cloud→local fallback). The swap is also written back to the owning
   * detector, which shares this stream's mutable `ThresholdOptions`. */
  get model() {
    return this._model;
  }
  get isFallback() {
    return this._isFallback;
  }
  /** @internal Test-visible. */
  get warnedCloudFailure() {
    return this._warnedCloudFailure;
  }
  /** @internal Test-visible. */
  get warnedLocalFailure() {
    return this._warnedLocalFailure;
  }
  /** @internal Test-visible. */
  get transport() {
    return this._transport;
  }
  /** @internal Test-visible: same logic as the path taken when `_run` catches
   * a cloud transport error. Tests call this directly to verify the warning
   * dedupe across multiple invocations on the same stream. */
  _fallBackToLocal(reason) {
    if (!this._warnedCloudFailure) {
      this._detLogger.warn(
        { reason: reason.message },
        "cloud turn detector failed; falling back to local mini model"
      );
      this._warnedCloudFailure = true;
    }
    this._emitDefaultForInflight();
    try {
      this._transport.detach();
    } catch {
    }
    this._opts.thresholds._toLocalFallback();
    if (this._detector instanceof TurnDetector) {
      this._detector._setModel("turn-detector-v1-mini");
    }
    this._transport = new LocalTransport({ opts: this._opts, executor: this._executor });
    this._transport.attach(this);
    this._model = "turn-detector-v1-mini";
    this._isFallback = true;
  }
  /** @internal Test-visible: same logic as the path taken when `_run` sees a
   * local transport error. */
  _onLocalFailure(reason) {
    if (!this._warnedLocalFailure) {
      this._detLogger.warn(
        { reason: reason.message },
        "local audio turn detector failed; defaulting to 1.0 and retrying on next turn"
      );
      this._warnedLocalFailure = true;
    }
    this._emitDefaultForInflight();
  }
  _emitDefaultForInflight() {
    const requestId = this._requestId;
    if (requestId !== void 0) {
      this._resolvePrediction(requestId, 1);
    }
  }
  async aclose() {
    try {
      this._transport.detach();
    } catch {
    }
    await super.aclose();
  }
  async _run() {
    while (true) {
      try {
        await this._raceWithSwap(this._transport.run());
        return;
      } catch (err) {
        if (err instanceof SwapAbortError) {
          if (this._closing) return;
          continue;
        }
        const e = err instanceof Error ? err : new Error(String(err));
        if (this._model === "turn-detector-v1") {
          this._fallBackToLocal(e);
          continue;
        }
        this._onLocalFailure(e);
        return;
      }
    }
  }
  _onPredictTimeout() {
    if (this._model === "turn-detector-v1") {
      this._signalSwap();
      this._fallBackToLocal(new Error("predict_end_of_turn"));
    }
  }
}
export {
  TurnDetector,
  TurnDetectorStreamImpl
};
//# sourceMappingURL=detector.js.map