/**
 * HMR-safe singleton timers for module-scope background workers.
 *
 * Next.js Fast-Refresh re-imports server modules on every save, which means
 * any `setInterval` registered at module scope will silently stack: the old
 * timer keeps ticking inside the previous module instance while the new
 * import starts another one. Over a long dev session this stacks dozens of
 * duplicate workers all racing for the same DB rows.
 *
 * `registerHmrTimer` stashes the timer pair on `globalThis` keyed by a
 * stable string. To keep `start*Worker()` calls from request handlers
 * idempotent (the original contract) it ALSO accepts a per-module-instance
 * `moduleToken`: when a caller re-registers using the same token, the
 * helper no-ops. Only when the token differs (i.e. a fresh module
 * evaluation after Fast Refresh) does it clear the previous pair and
 * re-arm. In production (no HMR) the token never changes, so behaviour is
 * identical to the pre-existing module-scoped guards.
 */

import { runWithoutRequestContext } from '@server/logger/context';

type TimerEntry = {
  initial: NodeJS.Timeout | null;
  recurring: NodeJS.Timeout | null;
  moduleToken: object;
};

const G = globalThis as unknown as { __hmrTimers?: Map<string, TimerEntry> };
G.__hmrTimers ||= new Map<string, TimerEntry>();
const timers = G.__hmrTimers;

export interface HmrTimerOptions {
  /** Globally-unique key for this worker (e.g. 'marketing-dispatcher'). */
  key: string;
  /**
   * Per-module-instance token. Declare at module scope (e.g.
   * `const MODULE_TOKEN = {};`) so repeated runtime calls from the same
   * module instance share the same identity (and no-op), while a Fast
   * Refresh re-import yields a fresh identity (and replaces the timer).
   */
  moduleToken: object;
  /** Delay before the first tick fires. */
  initialDelayMs: number;
  /** Cadence for subsequent ticks. */
  intervalMs: number;
  /** The tick function. Errors must be handled by the caller. */
  tick: () => void | Promise<void>;
}

/**
 * Register the HMR-guarded timer for `key`. Same `moduleToken` → no-op.
 * Different `moduleToken` (or no prior registration) → clear any prior
 * pair and arm a fresh setTimeout + setInterval.
 *
 * Returns `true` if a fresh registration happened, `false` if the existing
 * registration was kept (useful for "log only on first start" patterns).
 */
export function registerHmrTimer(opts: HmrTimerOptions): boolean {
  const existing = timers.get(opts.key);
  if (existing && existing.moduleToken === opts.moduleToken) {
    return false;
  }
  if (existing) {
    if (existing.initial) clearTimeout(existing.initial);
    if (existing.recurring) clearInterval(existing.recurring);
  }
  const entry: TimerEntry = {
    initial: null,
    recurring: null,
    moduleToken: opts.moduleToken,
  };
  entry.initial = setTimeout(() => {
    runWithoutRequestContext(() => {
      void opts.tick();
    });
  }, opts.initialDelayMs);
  entry.recurring = setInterval(() => {
    runWithoutRequestContext(() => {
      void opts.tick();
    });
  }, opts.intervalMs);
  timers.set(opts.key, entry);
  return true;
}

/** Stop and forget the HMR-guarded timer for `key`. */
export function clearHmrTimer(key: string): void {
  const existing = timers.get(key);
  if (!existing) return;
  if (existing.initial) clearTimeout(existing.initial);
  if (existing.recurring) clearInterval(existing.recurring);
  timers.delete(key);
}

/** Whether a timer is currently registered for `key`. */
export function isHmrTimerRegistered(key: string): boolean {
  return timers.has(key);
}
