import { AbruptClose, CleanClose, UserDisconnect, ClientClosed, Hidden } from "./client-error-kind-enum.js";
import { isClientError, isServerError } from "./error.js";
import { TimeToConnectMs, LastConnectError, NotConnected, TimeToConnectMsV2, LastConnectErrorV2, TotalTimeToConnectMs } from "./metric-name-enum.js";
const DID_NOT_CONNECT_VALUE = 100 * 1e3;
const REPORT_INTERVAL_MS = 5e3;
function getLastConnectErrorValue(reason) {
  return `${isServerError(reason) ? "server_" : "client_"}${camelToSnake(reason.kind)}`;
}
function camelToSnake(kind) {
  return kind.split(/\.?(?=[A-Z])/).join("_").toLowerCase();
}
function shouldReportConnectError(reason) {
  if (!isClientError(reason)) {
    return true;
  }
  switch (reason.kind) {
    case Hidden:
    case ClientClosed:
    case UserDisconnect:
    case CleanClose:
    case AbruptClose:
      return false;
    default:
      return true;
  }
}
class MetricManager {
  #reportIntervalMs;
  #host;
  #reporter;
  #lc;
  #timerID;
  constructor(opts) {
    this.#reportIntervalMs = opts.reportIntervalMs;
    this.#host = opts.host;
    this.#reporter = opts.reporter;
    this.#lc = opts.lc;
    this.tags.push(`source:${opts.source}`);
    this.timeToConnectMs.set(DID_NOT_CONNECT_VALUE);
    this.#setNotConnectedReason("init");
    this.#timerID = setInterval(() => {
      void this.flush();
    }, this.#reportIntervalMs);
  }
  #metrics = [];
  // timeToConnectMs measures the time from the call to connect() to receiving
  // the 'connected' ws message. We record the DID_NOT_CONNECT_VALUE if the previous
  // connection attempt failed for any reason.
  //
  // We set the gauge using #connectStart as follows:
  // - #connectStart is undefined if we are disconnected or connected; it is
  //   defined only in the Connecting state, as a number representing the timestamp
  //   at which we started connecting.
  // - #connectStart is set to the current time when connect() is called.
  // - When we receive the 'connected' message we record the time to connect and
  //   set #connectStart to undefined.
  // - If disconnect() is called with a defined #connectStart then we record
  //   DID_NOT_CONNECT_VALUE and set #connectStart to undefined.
  //
  // TODO: this should be folded into the ConnectionManager.
  timeToConnectMs = this.#register(
    new Gauge(TimeToConnectMs)
  );
  // lastConnectError records the last error that occurred when connecting,
  // if any. It is cleared when connecting successfully or when reported, so this
  // state only gets reported if there was a failure during the reporting period and
  // we are still not connected.
  lastConnectError = this.#register(
    new State(
      LastConnectError,
      true
      // clearOnFlush
    )
  );
  // notConnected records the reason why the client is not currently connected.
  // It is cleared when the client successfully connects.
  #notConnected = this.#register(new State(NotConnected));
  // The time from the call to connect() to receiving the 'connected' ws message
  // for the current connection.  Cleared when the client is not connected.
  // TODO: Not actually currently cleared on disconnect untill there is a
  // connect error, or client reports disconnected and waiting for visible.
  // Should have a value iff _notConnected has no value.
  #timeToConnectMsV2 = this.#register(
    new Gauge(TimeToConnectMsV2)
  );
  // lastConnectErrorV2 records the last error that occurred when connecting,
  // if any. It is cleared when the client successfully connects or
  // stops trying to connect due to being hidden.
  // Should have a value iff notConnected state is NotConnectedReason.Error.
  #lastConnectErrorV2 = this.#register(
    new State(LastConnectErrorV2)
  );
  // The total time it took to connect across retries for the current
  // connection.  Cleared when the client is not connected.
  // TODO: Not actually currently cleared on disconnect until there is a
  // connect error, or client reports disconnected and waiting for visible.
  // See Zero.#totalToConnectStart for details of how this total is computed.
  // Should have a value iff _notConnected has no value.
  #totalTimeToConnectMs = this.#register(
    new Gauge(TotalTimeToConnectMs)
  );
  #setNotConnectedReason(reason) {
    this.#notConnected.set(reason);
  }
  setConnected(timeToConnectMs, totalTimeToConnectMs) {
    this.#notConnected.clear();
    this.#lastConnectErrorV2.clear();
    this.#timeToConnectMsV2.set(timeToConnectMs);
    this.#totalTimeToConnectMs.set(totalTimeToConnectMs);
  }
  setDisconnectedWaitingForVisible() {
    this.#timeToConnectMsV2.clear();
    this.#totalTimeToConnectMs.clear();
    this.#lastConnectErrorV2.clear();
    let notConnectedReason;
    switch (this.#notConnected.get()) {
      case "init":
        notConnectedReason = "hidden_was_init";
        break;
      case "error":
        notConnectedReason = "hidden_was_error";
        break;
      default:
        notConnectedReason = "hidden";
        break;
    }
    this.#setNotConnectedReason(notConnectedReason);
  }
  setConnectError(reason) {
    this.#timeToConnectMsV2.clear();
    this.#totalTimeToConnectMs.clear();
    this.#setNotConnectedReason("error");
    this.#lastConnectErrorV2.set(getLastConnectErrorValue(reason));
  }
  /**
   * Tags to include in all metrics.
   */
  tags = [];
  // Flushes all metrics to an array of time series (plural), one Series
  // per metric.
  async flush() {
    const lc = this.#lc;
    if (this.#timerID === null) {
      lc.error?.("MetricManager.flush() called but already stopped");
      return;
    }
    const allSeries = [];
    for (const metric of this.#metrics) {
      const series = metric.flush();
      if (series !== void 0) {
        allSeries.push({
          ...series,
          host: this.#host,
          tags: this.tags
        });
      }
    }
    if (allSeries.length === 0) {
      lc?.debug?.("No metrics to report");
      return;
    }
    try {
      await this.#reporter(allSeries);
    } catch (e) {
      lc?.error?.("Error reporting metrics", e);
    }
  }
  stop() {
    if (this.#timerID === null) {
      this.#lc.error?.("MetricManager.stop() called but already stopped");
      return;
    }
    clearInterval(this.#timerID);
    this.#timerID = null;
  }
  #register(metric) {
    this.#metrics.push(metric);
    return metric;
  }
}
function makePoint(ts, value) {
  return [ts, [value]];
}
class Gauge {
  #name;
  #value = void 0;
  constructor(name) {
    this.#name = name;
  }
  set(value) {
    this.#value = value;
  }
  get() {
    return this.#value;
  }
  clear() {
    this.#value = void 0;
  }
  flush() {
    if (this.#value === void 0) {
      return void 0;
    }
    const points = [makePoint(t(), this.#value)];
    return { metric: this.#name, points };
  }
}
function t() {
  return Math.round(Date.now() / 1e3);
}
class State {
  #prefix;
  #clearOnFlush;
  #current = void 0;
  constructor(prefix, clearOnFlush = false) {
    this.#prefix = prefix;
    this.#clearOnFlush = clearOnFlush;
  }
  set(state) {
    this.#current = state;
  }
  get() {
    return this.#current;
  }
  clear() {
    this.#current = void 0;
  }
  flush() {
    if (this.#current === void 0) {
      return void 0;
    }
    const gauge = new Gauge([this.#prefix, this.#current].join("_"));
    gauge.set(1);
    const series = gauge.flush();
    if (this.#clearOnFlush) {
      this.clear();
    }
    return series;
  }
}
export {
  DID_NOT_CONNECT_VALUE,
  Gauge,
  MetricManager,
  REPORT_INTERVAL_MS,
  State,
  getLastConnectErrorValue,
  shouldReportConnectError
};
//# sourceMappingURL=metrics.js.map
