import parsePrometheusTextFormat from "parse-prometheus-text-format";
import { promiseVoid } from "../../../../shared/src/resolved-promises.js";
import { Subscription } from "../../types/subscription.js";
import { RunningState } from "../running-state.js";
const CHECK_INTERVAL_MS = 6e4;
const MIN_CLEANUP_DELAY_MS = 3e4;
class BackupMonitor {
  id = "backup-monitor";
  #lc;
  #backupURL;
  #metricsEndpoint;
  #changeStreamer;
  #state = new RunningState(this.id);
  #reservations = /* @__PURE__ */ new Map();
  #watermarks = /* @__PURE__ */ new Map();
  #lastWatermark = "";
  #cleanupDelayMs;
  #checkMetricsTimer;
  constructor(lc, backupURL, metricsEndpoint, changeStreamer, initialCleanupDelayMs) {
    this.#lc = lc.withContext("component", this.id);
    this.#backupURL = backupURL;
    this.#metricsEndpoint = metricsEndpoint;
    this.#changeStreamer = changeStreamer;
    this.#cleanupDelayMs = Math.max(
      initialCleanupDelayMs,
      MIN_CLEANUP_DELAY_MS
      // purely for peace of mind
    );
    this.#lc.info?.(
      `backup monitor started ${initialCleanupDelayMs} ms after snapshot restore`
    );
  }
  run() {
    this.#lc.info?.(
      `monitoring backups at ${this.#metricsEndpoint} with ${this.#cleanupDelayMs} ms cleanup delay`
    );
    this.#checkMetricsTimer = setInterval(
      this.checkWatermarksAndScheduleCleanup,
      CHECK_INTERVAL_MS
    );
    return this.#state.stopped();
  }
  startSnapshotReservation(taskID) {
    this.#lc.info?.(`pausing change-log cleanup while ${taskID} snapshots`);
    this.#reservations.get(taskID)?.sub.cancel();
    const sub = Subscription.create({
      // If the reservation still exists when the connection closes
      // (e.g. subscriber crashed), clean it up without updating the
      // cleanup delay.
      cleanup: () => this.endReservation(taskID, false)
    });
    this.#reservations.set(taskID, { start: /* @__PURE__ */ new Date(), sub });
    void this.#changeStreamer.getChangeLogState().then((changeLogState) => {
      sub.push([
        "status",
        { tag: "status", backupURL: this.#backupURL, ...changeLogState }
      ]);
    }).catch((e) => {
      this.#lc.warn?.(`failing snapshot reservation`, e);
      sub.fail(e);
    });
    return sub;
  }
  endReservation(taskID, updateCleanupDelay = true) {
    const res = this.#reservations.get(taskID);
    if (res === void 0) {
      return;
    }
    this.#reservations.delete(taskID);
    const { start, sub } = res;
    sub.cancel();
    if (updateCleanupDelay) {
      const duration = Date.now() - start.getTime();
      this.#lc.info?.(`snapshot initialized by ${taskID} in ${duration} ms`);
      if (duration > this.#cleanupDelayMs) {
        this.#cleanupDelayMs = duration;
        this.#lc.info?.(`increased cleanup delay to ${duration} ms`);
      }
    }
  }
  // Exported for testing
  checkWatermarksAndScheduleCleanup = async () => {
    try {
      await this.#checkWatermarks();
    } catch (e) {
      this.#lc.warn?.(`unable to fetch metrics at ${this.#metricsEndpoint}`, e);
    }
    try {
      this.#scheduleCleanup();
    } catch (e) {
      this.#lc.warn?.(`error scheduling cleanup`, e);
    }
  };
  async *#fetchWatermarks() {
    const metricsEndpoint = this.#metricsEndpoint;
    const signal = this.#state.signal;
    let resp;
    try {
      resp = await fetch(metricsEndpoint, { signal });
    } catch (e) {
      if (signal.aborted) {
        return;
      }
      this.#lc.warn?.(`unable to fetch metrics at ${this.#metricsEndpoint}`, e);
      return;
    }
    if (!resp.ok) {
      this.#lc.warn?.(
        `unable to fetch metrics at ${this.#metricsEndpoint}: ${await resp.text()}`
      );
      return;
    }
    const families = parsePrometheusTextFormat(await resp.text());
    for (const family of families) {
      if (family.type === "GAUGE" && family.name === "litestream_replica_progress") {
        for (const metric of family.metrics) {
          const watermark = metric.labels?.watermark;
          const name = metric.labels?.name;
          const time = new Date(parseFloat(metric.value) * 1e3);
          if (watermark) {
            yield { watermark, time, name };
          }
        }
      }
    }
  }
  async #checkWatermarks() {
    for await (const { watermark, name, time } of this.#fetchWatermarks()) {
      if (watermark > this.#lastWatermark && !this.#watermarks.has(watermark)) {
        this.#lc.info?.(
          `replicated watermark=${watermark} to ${name} at ${time.toISOString()}.`
        );
        this.#watermarks.set(watermark, time);
      }
    }
  }
  #scheduleCleanup() {
    if (this.#reservations.size > 0) {
      this.#lc.info?.(
        `watermark cleanup paused for snapshot(s): ${[...this.#reservations.keys()]}`
      );
      return;
    }
    const latestCleanupTime = Date.now() - this.#cleanupDelayMs;
    let maxWatermark = "";
    for (const [watermark, backupTime] of this.#watermarks.entries()) {
      if (backupTime.getTime() <= latestCleanupTime && watermark > maxWatermark) {
        maxWatermark = watermark;
      }
    }
    if (maxWatermark.length) {
      this.#changeStreamer.scheduleCleanup(maxWatermark);
      for (const watermark of this.#watermarks.keys()) {
        if (watermark <= maxWatermark) {
          this.#watermarks.delete(watermark);
        }
      }
      this.#lastWatermark = maxWatermark;
    }
  }
  stop() {
    clearInterval(this.#checkMetricsTimer);
    for (const { sub } of this.#reservations.values()) {
      sub.cancel();
    }
    this.#state.stop(this.#lc);
    return promiseVoid;
  }
}
export {
  BackupMonitor,
  CHECK_INTERVAL_MS
};
//# sourceMappingURL=backup-monitor.js.map
