// ──────────────────────────────────────────────────────────────────────────────
// IGDB (Internet Game Database) API service
// Uses Twitch OAuth for authentication.  All requests go through a simple
// throttle so we never exceed the 4 req/s rate-limit.
// ──────────────────────────────────────────────────────────────────────────────

const TWITCH_CLIENT_ID = 'n6u8unhmwvhzsrvw2d2nb2a3qxapsl';
const TWITCH_CLIENT_SECRET = 'h6f6g2r6yyxkfg2xsob0jt8p994s7v';
const TWITCH_AUTH_URL = 'https://id.twitch.tv/oauth2/token';
const IGDB_API_URL = 'https://api.igdb.com/v4';

// ── Types ────────────────────────────────────────────────────────────────────

export interface IgdbGameInfo {
  igdbId: number;
  name: string;
  coverUrl: string | null;
  genres: string[];
  platforms: string[];
  rating: number | null;
  firstReleaseDate: string | null;
  summary: string | null;
  igdbUrl: string | null;
}

// ── Token cache ──────────────────────────────────────────────────────────────

interface TokenCache {
  accessToken: string;
  expiresAt: number; // unix-ms
}

let tokenCache: TokenCache | null = null;

/**
 * Obtain (and cache) a Twitch OAuth bearer token for the IGDB API.
 * Tokens typically last ~60 days.  We refresh automatically when the cached
 * token is expired (or about to expire within 60 seconds).
 */
export async function getAccessToken(): Promise<string> {
  if (tokenCache && Date.now() < tokenCache.expiresAt - 60_000) {
    return tokenCache.accessToken;
  }

  console.log('[IGDB] Fetching new Twitch OAuth token...');

  const params = new URLSearchParams({
    client_id: TWITCH_CLIENT_ID,
    client_secret: TWITCH_CLIENT_SECRET,
    grant_type: 'client_credentials',
  });

  const res = await fetch(TWITCH_AUTH_URL, {
    method: 'POST',
    body: params,
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`[IGDB] Token request failed (${res.status}): ${text}`);
  }

  const data = (await res.json()) as {
    access_token: string;
    expires_in: number;
    token_type: string;
  };

  tokenCache = {
    accessToken: data.access_token,
    expiresAt: Date.now() + data.expires_in * 1000,
  };

  console.log(
    `[IGDB] Token acquired, expires in ${Math.round(data.expires_in / 86400)} days`,
  );

  return tokenCache.accessToken;
}

// ── Rate-limit throttle (max 4 req/s → min 250 ms between requests) ─────────

const MIN_REQUEST_INTERVAL_MS = 250;
let lastRequestTime = 0;

async function throttle(): Promise<void> {
  const now = Date.now();
  const elapsed = now - lastRequestTime;
  if (elapsed < MIN_REQUEST_INTERVAL_MS) {
    await new Promise((resolve) =>
      setTimeout(resolve, MIN_REQUEST_INTERVAL_MS - elapsed),
    );
  }
  lastRequestTime = Date.now();
}

// ── Generic IGDB query runner ────────────────────────────────────────────────

/**
 * Send an IGDB Apicalypse query to the given endpoint.
 *
 * @param endpoint  e.g. "games", "covers"
 * @param body      IGDB query-language string
 * @returns         parsed JSON array
 */
export async function igdbQuery(
  endpoint: string,
  body: string,
): Promise<any[]> {
  const token = await getAccessToken();
  await throttle();

  const url = `${IGDB_API_URL}/${endpoint}`;
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Client-ID': TWITCH_CLIENT_ID,
      Authorization: `Bearer ${token}`,
      'Content-Type': 'text/plain',
    },
    body,
  });

  if (!res.ok) {
    const text = await res.text();
    console.error(`[IGDB] Query to ${endpoint} failed (${res.status}): ${text}`);
    throw new Error(`[IGDB] Query failed (${res.status}): ${text}`);
  }

  return (await res.json()) as any[];
}

// ── Cover URL helper ─────────────────────────────────────────────────────────

/**
 * Convert an IGDB cover URL (or hash) into a full HTTPS image URL.
 *
 * IGDB returns cover.url like `//images.igdb.com/igdb/image/upload/t_thumb/co1234.jpg`
 * This helper swaps in the requested size template and ensures HTTPS.
 *
 * @param hash  The cover hash or raw URL returned by the API
 * @param size  Image size template – defaults to `t_cover_big` (264x374)
 */
export function igdbCoverUrl(
  hash: string,
  size: string = 't_cover_big',
): string {
  // If the caller passed a full IGDB url, extract the hash portion
  // e.g. "//images.igdb.com/igdb/image/upload/t_thumb/co1234.jpg" → "co1234"
  let imageHash = hash;

  const match = hash.match(/\/t_\w+\/(.+?)(?:\.\w+)?$/);
  if (match) {
    imageHash = match[1];
  }

  return `https://images.igdb.com/igdb/image/upload/${size}/${imageHash}.jpg`;
}

// ── Raw response → IgdbGameInfo mapper ───────────────────────────────────────

function mapToGameInfo(raw: any): IgdbGameInfo {
  let coverUrl: string | null = null;
  if (raw.cover?.url) {
    coverUrl = igdbCoverUrl(raw.cover.url);
  }

  let firstReleaseDate: string | null = null;
  if (typeof raw.first_release_date === 'number') {
    firstReleaseDate = new Date(raw.first_release_date * 1000)
      .toISOString()
      .slice(0, 10);
  }

  return {
    igdbId: raw.id,
    name: raw.name ?? 'Unknown',
    coverUrl,
    genres: Array.isArray(raw.genres)
      ? raw.genres.map((g: any) => g.name).filter(Boolean)
      : [],
    platforms: Array.isArray(raw.platforms)
      ? raw.platforms.map((p: any) => p.name).filter(Boolean)
      : [],
    rating: typeof raw.rating === 'number' ? Math.round(raw.rating) : null,
    firstReleaseDate,
    summary: raw.summary ?? null,
    igdbUrl: raw.url ?? null,
  };
}

// ── Game lookup functions ────────────────────────────────────────────────────

/**
 * Find a game by its Steam app-id using IGDB external-game references.
 * Category 1 = Steam in IGDB's external-game categories.
 */
export async function lookupByAppId(
  steamAppId: number,
): Promise<IgdbGameInfo | null> {
  const body = [
    'fields name,cover.url,genres.name,platforms.name,rating,',
    'first_release_date,summary,url,external_games.uid,external_games.category;',
    `where external_games.category = 1 & external_games.uid = "${steamAppId}";`,
    'limit 1;',
  ].join(' ');

  const results = await igdbQuery('games', body);
  if (results.length === 0) return null;

  return mapToGameInfo(results[0]);
}

/**
 * Fallback: search for a game by name.
 */
export async function lookupByName(
  name: string,
): Promise<IgdbGameInfo | null> {
  const safeName = name.replace(/"/g, '\\"');
  const body = [
    `search "${safeName}";`,
    'fields name,cover.url,genres.name,platforms.name,rating,',
    'first_release_date,summary,url;',
    'limit 1;',
  ].join(' ');

  const results = await igdbQuery('games', body);
  if (results.length === 0) return null;

  return mapToGameInfo(results[0]);
}

/**
 * Try to look up a game by Steam app-id first; fall back to name search.
 */
export async function lookupGame(
  steamAppId: number,
  gameName: string,
): Promise<IgdbGameInfo | null> {
  const byId = await lookupByAppId(steamAppId);
  if (byId) return byId;

  console.log(
    `[IGDB] No result for Steam appid ${steamAppId}, falling back to name search: "${gameName}"`,
  );
  return lookupByName(gameName);
}

// ── Batch enrichment ─────────────────────────────────────────────────────────

/** In-memory cache keyed by Steam appid.  Persists for the server lifetime. */
const enrichmentCache = new Map<number, IgdbGameInfo>();

/**
 * Enrich a list of Steam games with IGDB metadata.
 *
 * - Processes in batches of 4 (respecting the 4 req/s rate limit).
 * - 300 ms pause between batches.
 * - Uses an in-memory cache so repeated calls for the same appid are free.
 *
 * @returns Map keyed by Steam appid → IgdbGameInfo (only matched games)
 */
export async function enrichGames(
  games: Array<{ appid: number; name: string }>,
): Promise<Map<number, IgdbGameInfo>> {
  const result = new Map<number, IgdbGameInfo>();
  const toFetch: Array<{ appid: number; name: string }> = [];

  // Check cache first
  for (const game of games) {
    const cached = enrichmentCache.get(game.appid);
    if (cached) {
      result.set(game.appid, cached);
    } else {
      toFetch.push(game);
    }
  }

  if (toFetch.length === 0) {
    console.log(`[IGDB] All ${games.length} games found in cache`);
    return result;
  }

  console.log(`[IGDB] ${result.size} cache hits, ${toFetch.length} to fetch`);

  const BATCH_SIZE = 4;
  for (let i = 0; i < toFetch.length; i += BATCH_SIZE) {
    if (i > 0) {
      // Pause between batches
      await new Promise((resolve) => setTimeout(resolve, 300));
    }

    const batch = toFetch.slice(i, i + BATCH_SIZE);
    const promises = batch.map(async (game) => {
      try {
        const info = await lookupGame(game.appid, game.name);
        if (info) {
          enrichmentCache.set(game.appid, info);
          result.set(game.appid, info);
        }
      } catch (err) {
        console.error(
          `[IGDB] Error enriching appid ${game.appid} ("${game.name}"):`,
          err,
        );
      }
    });

    await Promise.all(promises);
  }

  console.log(
    `[IGDB] Enrichment complete: ${result.size}/${games.length} games matched`,
  );

  return result;
}
