const BASE_URL = 'https://api.beatleader.com';

// Simple in-memory cache for GET requests to BeatLeader
// Caches JSON responses by URL for a short TTL to reduce backend load
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
const MAX_RETRIES = 5;
const INITIAL_BACKOFF_MS = 1000;
const MAX_BACKOFF_MS = 60_000;
const BACKOFF_FACTOR = 2;
type CacheEntry = { expiresAt: number; data: unknown };
const responseCache: Map<string, CacheEntry> = new Map();
const WEBSITE_COOKIE_HEADER = 'Cookie';

export class RateLimitError extends Error {
  readonly status: number = 429;
  readonly retryAfterMs?: number;
  constructor(message: string, retryAfterMs?: number) {
    super(message);
    this.name = 'RateLimitError';
    this.retryAfterMs = retryAfterMs;
  }
}

async function requestWith429Retry(
  fetchFn: typeof fetch,
  url: string,
  init?: RequestInit
): Promise<Response> {
  let attempt = 0;
  let backoffMs = INITIAL_BACKOFF_MS;
  // eslint-disable-next-line no-constant-condition
  while (true) {
    try {
      const res = await fetchFn(url, init);
      if (res.status === 429) {
        attempt += 1;
        const retryAfterHeader = res.headers.get('Retry-After');
        const retryAfterSec = retryAfterHeader ? Number(retryAfterHeader) : NaN;
        const waitMs = Number.isFinite(retryAfterSec) ? retryAfterSec * 1000 : backoffMs;
        if (attempt > MAX_RETRIES) {
          throw new RateLimitError('BeatLeader rate limit exceeded', waitMs);
        }
        await new Promise((r) => setTimeout(r, waitMs));
        backoffMs = Math.min(backoffMs * BACKOFF_FACTOR, MAX_BACKOFF_MS);
        continue;
      }
      return res;
    } catch (err) {
      attempt += 1;
      if (attempt > MAX_RETRIES) throw err;
      await new Promise((r) => setTimeout(r, backoffMs));
      backoffMs = Math.min(backoffMs * BACKOFF_FACTOR, MAX_BACKOFF_MS);
    }
  }
}

async function fetchJsonCached(
  fetchFn: typeof fetch,
  url: string,
  options: { headers?: Record<string, string> } = {},
  ttlMs = CACHE_TTL_MS
): Promise<unknown> {
  const now = Date.now();
  const hasAuth = Boolean(options.headers && (options.headers['Authorization'] || options.headers[WEBSITE_COOKIE_HEADER]));
  const cacheKey = hasAuth ? null : url;
  if (cacheKey) {
    const cached = responseCache.get(cacheKey);
    if (cached && cached.expiresAt > now) {
      return cached.data;
    }
  }

  const res = await requestWith429Retry(fetchFn, url, { headers: options.headers });
  if (!res.ok) throw new Error(`BeatLeader request failed: ${res.status}`);
  const data = await res.json();
  if (cacheKey) {
    responseCache.set(cacheKey, { expiresAt: now + ttlMs, data });
  }
  return data;
}

type QueryParams = Record<string, string | number | boolean | undefined | null>;

function buildQuery(params: QueryParams): string {
  const searchParams = new URLSearchParams();
  Object.entries(params).forEach(([key, value]) => {
    if (value === undefined || value === null || value === '') return;
    searchParams.set(key, String(value));
  });
  const qs = searchParams.toString();
  return qs ? `?${qs}` : '';
}

export class BeatLeaderAPI {
  private readonly fetchFn: typeof fetch;
  private readonly accessToken?: string;
  private readonly websiteCookieHeader?: string;

  constructor(fetchFn: typeof fetch, accessToken?: string, websiteCookieHeader?: string) {
    this.fetchFn = fetchFn;
    this.accessToken = accessToken;
    this.websiteCookieHeader = websiteCookieHeader;
  }

  private buildHeaders(): Record<string, string> | undefined {
    if (this.accessToken) return { Authorization: `Bearer ${this.accessToken}` };
    if (this.websiteCookieHeader) return { [WEBSITE_COOKIE_HEADER]: this.websiteCookieHeader };
    return undefined;
  }

  async getPlayer(playerId: string): Promise<unknown> {
    const url = `${BASE_URL}/player/${encodeURIComponent(playerId)}`;
    return fetchJsonCached(this.fetchFn, url, { headers: this.buildHeaders() });
  }

  async getPlayerScores(
    playerId: string,
    params: {
      page?: number;
      count?: number;
      leaderboardContext?: string;
      sortBy?: string | number;
      order?: 'asc' | 'desc' | string;
      search?: string;
      diff?: string;
      mode?: string;
      requirements?: string;
      type?: string;
      hmd?: string;
      modifiers?: string;
      stars_from?: string | number;
      stars_to?: string | number;
      eventId?: string | number;
      includeIO?: boolean;
    } = {}
  ): Promise<unknown> {
    const query = buildQuery({
      page: params.page,
      count: params.count,
      leaderboardContext: params.leaderboardContext,
      sortBy: params.sortBy,
      order: params.order,
      search: params.search,
      diff: params.diff,
      mode: params.mode,
      requirements: params.requirements,
      type: params.type,
      hmd: params.hmd,
      modifiers: params.modifiers,
      stars_from: params.stars_from,
      stars_to: params.stars_to,
      eventId: params.eventId,
      includeIO: params.includeIO
    });

    const url = `${BASE_URL}/player/${encodeURIComponent(playerId)}/scores${query}`;
    return fetchJsonCached(this.fetchFn, url, { headers: this.buildHeaders() });
  }

  async getLeaderboard(
    hash: string,
    options: { diff?: string; mode?: string; page?: number; count?: number } = {}
  ): Promise<unknown> {
    const diff = options.diff ?? 'ExpertPlus';
    const mode = options.mode ?? 'Standard';
    const query = buildQuery({ page: options.page, count: options.count });
    const url = `${BASE_URL}/v5/scores/${encodeURIComponent(hash)}/${encodeURIComponent(
      diff
    )}/${encodeURIComponent(mode)}${query}`;
    return fetchJsonCached(this.fetchFn, url, { headers: this.buildHeaders() });
  }

  async getRankedLeaderboards(params: { stars_from?: number; stars_to?: number; page?: number; count?: number } = {}): Promise<unknown> {
    const query = buildQuery({
      page: params.page,
      count: params.count,
      type: 'ranked',
      stars_from: params.stars_from,
      stars_to: params.stars_to
    });
    const url = `${BASE_URL}/leaderboards${query}`;
    return fetchJsonCached(this.fetchFn, url, { headers: this.buildHeaders() });
  }

  async getUser(): Promise<unknown> {
    const url = `${BASE_URL}/user`;
    return fetchJsonCached(this.fetchFn, url, { headers: this.buildHeaders() }, 30_000);
  }

  async getLeaderboardsByHash(hash: string): Promise<unknown> {
    const url = `${BASE_URL}/leaderboards/hash/${encodeURIComponent(hash)}`;
    return fetchJsonCached(this.fetchFn, url, { headers: this.buildHeaders() });
  }
}

export function createBeatLeaderAPI(fetchFn: typeof fetch, accessToken?: string, websiteCookieHeader?: string): BeatLeaderAPI {
  return new BeatLeaderAPI(fetchFn, accessToken, websiteCookieHeader);
}


