export const DEFAULT_READ_ONLY_LONG_POLL_TIMEOUT_MS = 120_000;
export const READ_ONLY_LONG_POLL_TRANSPORT_MARGIN_MS = 15_000;

export interface ReadOnlyLongPollClock {
  now(): number;
}

export interface ReadOnlyLongPollResult<T> {
  value: T;
  completed: boolean;
  timedOut: boolean;
  cancelled: boolean;
  elapsedMs: number;
  observations: number;
  cursor: string;
}

export function parseStrictDuration(value: string, label = "duration"): number {
  const match = /^(\d+)(ms|s|m)$/u.exec(value);
  if (match === null) throw new Error(`${label} must match <integer>ms|s|m`);
  const amount = Number.parseInt(match[1]!, 10);
  if (!Number.isSafeInteger(amount) || amount <= 0) throw new Error(`${label} must be greater than zero`);
  const multiplier = match[2] === "ms" ? 1 : match[2] === "s" ? 1_000 : 60_000;
  const durationMs = amount * multiplier;
  if (!Number.isSafeInteger(durationMs) || durationMs > 3_600_000) throw new Error(`${label} must not exceed 60m`);
  return durationMs;
}

export function readOnlyLongPollTransportTimeoutMs(timeoutMs: number): number {
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 3_600_000) throw new Error("long-poll timeoutMs must be between 1ms and 60m");
  return timeoutMs + READ_ONLY_LONG_POLL_TRANSPORT_MARGIN_MS;
}

export async function runReadOnlyLongPoll<T>(options: {
  observe: () => Promise<T>;
  completed: (value: T) => boolean;
  waitForChange: (remainingMs: number, signal?: AbortSignal) => Promise<void>;
  cursor: (value: T) => string;
  timeoutMs?: number;
  clock?: ReadOnlyLongPollClock;
  signal?: AbortSignal;
}): Promise<ReadOnlyLongPollResult<T>> {
  const timeoutMs = options.timeoutMs ?? DEFAULT_READ_ONLY_LONG_POLL_TIMEOUT_MS;
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error("timeoutMs must be a positive integer");
  const clock = options.clock ?? { now: () => Date.now() };
  const startedAt = clock.now();
  let observations = 0;
  let value = await options.observe();
  observations += 1;
  while (!options.completed(value)) {
    if (options.signal?.aborted === true) {
      return { value, completed: false, timedOut: false, cancelled: true, elapsedMs: clock.now() - startedAt, observations, cursor: options.cursor(value) };
    }
    const remainingMs = timeoutMs - (clock.now() - startedAt);
    if (remainingMs <= 0) {
      return { value, completed: false, timedOut: true, cancelled: false, elapsedMs: clock.now() - startedAt, observations, cursor: options.cursor(value) };
    }
    await options.waitForChange(remainingMs, options.signal);
    value = await options.observe();
    observations += 1;
  }
  return { value, completed: true, timedOut: false, cancelled: false, elapsedMs: clock.now() - startedAt, observations, cursor: options.cursor(value) };
}
