import type { LaunchThrottleState } from './storage.js';

export const LAUNCH_GAP_MIN_MS = 9_000;
export const LAUNCH_GAP_MAX_MS = 12_000;
export const GENERIC_RATE_LIMIT_BASE_MS = 5 * 60_000;
export const GENERIC_RATE_LIMIT_MAX_MS = 5 * 60_000;

export class LaunchThrottleError extends Error {
  readonly code = 'LAUNCH_THROTTLED' as const;

  constructor(readonly retryAt: string, message = 'ChatGPT launch is throttled.') {
    super(`${message} Retry at ${retryAt}.`);
    this.name = 'LaunchThrottleError';
  }
}

export interface LaunchThrottleStore {
  load(): Promise<LaunchThrottleState>;
  save(state: LaunchThrottleState): Promise<void>;
}

export interface LaunchThrottleClock {
  now(): number;
  sleep(ms: number): Promise<void>;
}

export interface LaunchThrottleOptions {
  random?: () => number;
  clock?: LaunchThrottleClock;
}

function defaultClock(): LaunchThrottleClock {
  return {
    now: () => Date.now(),
    sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
  };
}

export function randomLaunchGapMs(random = Math.random): number {
  const bounded = Math.max(0, Math.min(0.999999999, random()));
  return Math.floor(LAUNCH_GAP_MIN_MS + bounded * (LAUNCH_GAP_MAX_MS - LAUNCH_GAP_MIN_MS + 1));
}

export function genericRateLimitBackoffMs(strike: number): number {
  const normalized = Math.max(1, Math.floor(strike));
  return Math.min(GENERIC_RATE_LIMIT_MAX_MS, GENERIC_RATE_LIMIT_BASE_MS * (2 ** (normalized - 1)));
}

export function computeRateLimitUntil(nowMs: number, strike: number, retryAt: string | null): number {
  const explicit = retryAt ? Date.parse(retryAt) : Number.NaN;
  if (Number.isFinite(explicit) && explicit > nowMs) return explicit;
  return nowMs + genericRateLimitBackoffMs(strike);
}

export class LaunchThrottle {
  private readonly random: () => number;
  private readonly clock: LaunchThrottleClock;

  constructor(private readonly store: LaunchThrottleStore, options: LaunchThrottleOptions = {}) {
    this.random = options.random ?? Math.random;
    this.clock = options.clock ?? defaultClock();
  }

  async reserve(deadlineIso: string): Promise<void> {
    const deadlineMs = Date.parse(deadlineIso);
    if (!Number.isFinite(deadlineMs)) throw new Error(`Invalid launch deadline: ${deadlineIso}`);

    const gapMs = randomLaunchGapMs(this.random);
    while (true) {
      const state = await this.store.load();
      const nowMs = this.clock.now();
      const gapUntil = state.lastLaunchStartedAt === null ? nowMs : state.lastLaunchStartedAt + gapMs;
      const rateLimitUntil = state.rateLimitUntil && state.rateLimitUntil > nowMs ? state.rateLimitUntil : nowMs;
      const allowedAt = Math.max(gapUntil, rateLimitUntil);

      if (allowedAt > deadlineMs) throw new LaunchThrottleError(new Date(allowedAt).toISOString());
      if (allowedAt > nowMs) {
        await this.clock.sleep(allowedAt - nowMs);
        continue;
      }

      await this.store.save({
        lastLaunchStartedAt: nowMs,
        rateLimitUntil: state.rateLimitUntil && state.rateLimitUntil > nowMs ? state.rateLimitUntil : null,
        rateLimitStrike: state.rateLimitUntil && state.rateLimitUntil > nowMs ? state.rateLimitStrike : 0,
      });
      return;
    }
  }

  async markRateLimited(retryAt: string | null): Promise<LaunchThrottleState> {
    const state = await this.store.load();
    const nowMs = this.clock.now();
    const strike = Math.max(1, state.rateLimitStrike + 1);
    const next: LaunchThrottleState = {
      ...state,
      rateLimitUntil: computeRateLimitUntil(nowMs, strike, retryAt),
      rateLimitStrike: strike,
    };
    await this.store.save(next);
    return next;
  }

  async markLaunchSucceeded(): Promise<void> {
    const state = await this.store.load();
    if (state.rateLimitStrike === 0 && state.rateLimitUntil === null) return;
    await this.store.save({ ...state, rateLimitUntil: null, rateLimitStrike: 0 });
  }
}
