import 'server-only';
import type { IgdbGame, IgdbTokenResponse } from './types';

// IGDB (api.igdb.com/v4) is the primary, license-appropriate metadata
// source for this project: it's built specifically for third-party game
// database apps and is free for non-commercial use (see .env.example —
// commercial partnership must be arranged separately since this site
// carries ads: https://api-docs.igdb.com, partner@igdb.com).
//
// Auth is Twitch's OAuth2 client-credentials flow. Docs:
// https://api-docs.igdb.com/#authentication

const TOKEN_URL = 'https://id.twitch.tv/oauth2/token';
const BASE_URL = 'https://api.igdb.com/v4';

let cachedToken: { token: string; expiresAt: number } | null = null;

function requireEnv(name: string): string {
  const value = process.env[name];
  if (!value) {
    throw new Error(
      `Missing required env var ${name}. Copy .env.example to .env and fill in IGDB credentials from https://dev.twitch.tv/console/apps`,
    );
  }
  return value;
}

async function getAccessToken(): Promise<string> {
  const now = Date.now();
  if (cachedToken && cachedToken.expiresAt > now + 60_000) {
    return cachedToken.token;
  }

  const clientId = requireEnv('IGDB_CLIENT_ID');
  const clientSecret = requireEnv('IGDB_CLIENT_SECRET');

  const url = new URL(TOKEN_URL);
  url.searchParams.set('client_id', clientId);
  url.searchParams.set('client_secret', clientSecret);
  url.searchParams.set('grant_type', 'client_credentials');

  const res = await fetch(url, { method: 'POST' });
  if (!res.ok) {
    throw new Error(`IGDB token request failed: ${res.status} ${await res.text()}`);
  }

  const data = (await res.json()) as IgdbTokenResponse;
  cachedToken = {
    token: data.access_token,
    expiresAt: now + data.expires_in * 1000,
  };
  return cachedToken.token;
}

/**
 * Low-level IGDB request. `query` is an Apicalypse string, e.g.
 * `"fields name,slug; where rating > 0; sort rating desc; limit 50;"`.
 */
async function igdbRequest<T>(endpoint: string, query: string): Promise<T> {
  const clientId = requireEnv('IGDB_CLIENT_ID');
  const token = await getAccessToken();

  const res = await fetch(`${BASE_URL}/${endpoint}`, {
    method: 'POST',
    headers: {
      'Client-ID': clientId,
      Authorization: `Bearer ${token}`,
      Accept: 'application/json',
      'Content-Type': 'text/plain',
    },
    body: query,
    // Discovery/generation happen in background jobs — never cache the
    // fetch, we want the current catalog state every time.
    cache: 'no-store',
  });

  if (!res.ok) {
    throw new Error(`IGDB ${endpoint} request failed: ${res.status} ${await res.text()}`);
  }

  return res.json() as Promise<T>;
}

const GAME_FIELDS = [
  'name',
  'slug',
  'summary',
  'storyline',
  'first_release_date',
  'rating',
  'rating_count',
  'url',
  'genres.name',
  'platforms.name',
  'game_modes.name',
  'cover.image_id',
  'screenshots.image_id',
  'videos.video_id',
  'age_ratings.rating',
  'age_ratings.category',
  'involved_companies.developer',
  'involved_companies.publisher',
  'involved_companies.company.name',
  'websites.url',
  'websites.category',
  'updated_at',
].join(',');

/**
 * Page through IGDB's catalog for discovery, ordered by popularity so the
 * most-searched-for games get pages first. This is the "continuously
 * discover new games" engine, not a one-shot dump of the whole database.
 */
export async function discoverGames(offset: number, limit = 50): Promise<IgdbGame[]> {
  // category filters out DLC/bundles/ports so discovery finds base games
  // first. IGDB's category enum (0=main_game, 4=standalone_expansion,
  // 8=remake, 9=remaster at time of writing) occasionally gets new values —
  // confirm against https://api-docs.igdb.com/#game-enums before relying
  // on it, and adjust here if it's drifted.
  const query = `
    fields ${GAME_FIELDS};
    where rating_count > 5 & category = (0,4,8,9);
    sort rating_count desc;
    limit ${limit};
    offset ${offset};
  `;
  return igdbRequest<IgdbGame[]>('games', query);
}

export async function getGameByIgdbId(id: number): Promise<IgdbGame | null> {
  const query = `fields ${GAME_FIELDS}; where id = ${id};`;
  const results = await igdbRequest<IgdbGame[]>('games', query);
  return results[0] ?? null;
}

export async function searchGames(term: string, limit = 20): Promise<IgdbGame[]> {
  // IGDB's dedicated /search endpoint understands relevance ranking better
  // than a `where name ~ *term*` filter on /games.
  const escaped = term.replace(/"/g, '\\"');
  const query = `
    fields ${GAME_FIELDS};
    search "${escaped}";
    limit ${limit};
  `;
  return igdbRequest<IgdbGame[]>('games', query);
}

/** Fetch the current state of many games at once, for the staleness check. */
export async function getGamesByIgdbIds(ids: number[]): Promise<IgdbGame[]> {
  if (ids.length === 0) return [];
  const query = `
    fields ${GAME_FIELDS};
    where id = (${ids.join(',')});
    limit ${ids.length};
  `;
  return igdbRequest<IgdbGame[]>('games', query);
}
