/**
 * ManagedTimer: a named, lifecycle-managed wrapper around setTimeout.
 *
 * Eliminates repetitive clear→set→fire→clean patterns by providing:
 * - `schedule(delayMs, cb)` — cancel any pending timer, set a new one
 * - `scheduleAt(epochMs, cb)` — schedule by absolute time point
 * - `tryAdvanceTo(epochMs, cb)` — only reschedule if new time is *earlier*
 * - `cancel()` — cancel without triggering
 * - `flush()` — trigger immediately (for graceful shutdown)
 * - `pending` — whether a timer is waiting
 *
 * The optional `isDestroyed` guard prevents firing after the owner is torn down.
 */

/** @type {ReturnType<typeof setTimeout>} */
let TimerHandle;

export class ManagedTimer {
  /** @type {ReturnType<typeof setTimeout> | null} */
  handle = null;
  /** @type {(() => void) | null} */
  callback = null;
  /** Absolute epoch-ms when the current timer is scheduled to fire. */
  scheduledAt = 0;

  /**
   * @param {string} name - Human-readable name for logging.
   * @param {(() => boolean) | undefined} isDestroyed - If provided, checked before firing — skips callback when true.
   */
  constructor(name, isDestroyed) {
    this.name = name;
    this.isDestroyed = isDestroyed;
  }

  // ── Core operations ──────────────────────────────────

  /**
   * Cancel any pending timer and schedule a new one after `delayMs`.
   * The callback fires once; the timer auto-clears after firing.
   * @param {number} delayMs
   * @param {() => void} callback
   */
  schedule(delayMs, callback) {
    this.cancelInternal();
    this.callback = callback;
    this.scheduledAt = Date.now() + delayMs;
    this.handle = setTimeout(() => this.fire(), delayMs);
    // Don't let pipeline timers keep the process alive in CLI mode.
    // In gateway mode the server listener holds the event loop anyway.
    this.handle.unref();
  }

  /**
   * Cancel any pending timer and schedule to fire at an absolute epoch-ms.
   * If `epochMs` is in the past, fires on next tick (delay = 0).
   * @param {number} epochMs
   * @param {() => void} callback
   */
  scheduleAt(epochMs, callback) {
    this.cancelInternal();
    this.callback = callback;
    this.scheduledAt = epochMs;
    const delay = Math.max(0, epochMs - Date.now());
    this.handle = setTimeout(() => this.fire(), delay);
    this.handle.unref();
  }

  /**
   * Only reschedule if `epochMs` is *earlier* than the current scheduled time.
   * This implements the "downward-only" timer pattern (L2 scheduling).
   * If no timer is pending, behaves like `scheduleAt()`.
   *
   * @param {number} epochMs
   * @param {() => void} callback
   * @returns {boolean} true if the timer was actually advanced (or newly set).
   */
  tryAdvanceTo(epochMs, callback) {
    if (this.handle === null) {
      // No pending timer → set it
      this.scheduleAt(epochMs, callback);
      return true;
    }

    if (epochMs < this.scheduledAt) {
      // New time is earlier → reschedule
      this.scheduleAt(epochMs, callback);
      return true;
    }

    // Current timer is already earlier or equal → keep it
    return false;
  }

  /** Cancel the pending timer without triggering the callback. */
  cancel() {
    this.cancelInternal();
  }

  /**
   * Immediately trigger the callback (if pending) and clear the timer.
   * Used for graceful shutdown to flush pending work.
   *
   * Note: Unlike `fire()`, this method intentionally does NOT check `isDestroyed`.
   * This is by design — during shutdown, `destroy()` sets `destroyed = true` first,
   * then calls `flush()` to drain pending work. The `isDestroyed` guard only applies
   * to natural timer expiration via `fire()`, not to explicit shutdown flushes.
   */
  flush() {
    if (this.handle === null) return;
    const cb = this.callback;
    this.cancelInternal();
    if (cb) cb();
  }

  // ── Accessors ───────────────────────────────────────

  /** Whether a timer is currently pending. */
  get pending() {
    return this.handle !== null;
  }

  /** The epoch-ms when the current timer is scheduled to fire (0 if none). */
  get scheduledTime() {
    return this.handle !== null ? this.scheduledAt : 0;
  }

  // ── Internals ───────────────────────────────────────

  fire() {
    const cb = this.callback;
    this.handle = null;
    this.callback = null;
    this.scheduledAt = 0;

    if (this.isDestroyed?.()) return;
    if (cb) cb();
  }

  cancelInternal() {
    if (this.handle !== null) {
      clearTimeout(this.handle);
      this.handle = null;
    }
    this.callback = null;
    this.scheduledAt = 0;
  }
}
