/**
 * Schedules `work` for an idle moment on the render thread. Prefers
 * `requestIdleCallback` when available (Chromium, Electron). Falls back to
 * `setTimeout(fn, 0)` so callers can rely on always-eventually scheduling.
 *
 * Returns a `cancel` function so callers can abort a pending callback.
 */
export function runWhenIdle(work: () => void, timeoutMs = 1500): () => void {
  type IdleCallbackHandle = number;
  interface IdleDeadline {
    didTimeout: boolean;
    timeRemaining(): number;
  }
  type IdleRequest = (handler: (deadline: IdleDeadline) => void, opts?: { timeout: number }) => IdleCallbackHandle;
  type IdleCancel = (handle: IdleCallbackHandle) => void;

  interface MaybeIdleGlobal {
    requestIdleCallback?: IdleRequest;
    cancelIdleCallback?: IdleCancel;
  }

  const idleGlobal = (typeof globalThis === 'undefined' ? {} : globalThis) as MaybeIdleGlobal;
  const idleRequest = idleGlobal.requestIdleCallback;
  const idleCancel = idleGlobal.cancelIdleCallback;

  if (typeof idleRequest === 'function') {
    const handle = idleRequest(() => work(), { timeout: timeoutMs });

    return () => {
      if (typeof idleCancel === 'function') {
        idleCancel(handle);
      }
    };
  }

  const fallback = setTimeout(work, 0);

  return () => clearTimeout(fallback);
}

/**
 * Promise wrapper around `runWhenIdle`. Resolves once `work` finishes
 * (or its returned promise resolves). Errors propagate.
 */
export function whenIdle<T>(work: () => T | Promise<T>, timeoutMs = 1500): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    runWhenIdle(() => {
      try {
        Promise.resolve(work()).then(resolve, reject);
      } catch (err) {
        reject(err);
      }
    }, timeoutMs);
  });
}
