import fs from 'fs/promises';
import pLimit from 'p-limit';
import ValueCache from '../common/ValueCache.mjs';
import prefixedConsole from '../common/prefixedConsole.mjs';
import { calculateCacheExpiry } from '../common/util.mjs';

export const SPLATNET3_WEB_SERVICE_ID = '4834290508791808';

// Concurrent request limit
const limit = pLimit(5);

export default class SplatNet3Client
{
  baseUrl = 'https://api.lp1.av5ja.srv.nintendo.net';
  bulletToken = null;
  queryHashes = null;

  constructor(nsoClient, acceptLanguage = 'en-US') {
    this.console = prefixedConsole('SplatNet', nsoClient.region);
    this.nsoClient = nsoClient;
    this.acceptLanguage = acceptLanguage;
  }

  get hasSession() {
    return !!this.bulletToken;
  }

  // Query hashes
  async _loadQueryHashes() {
    if (!this.queryHashes) {
      // Data from: https://github.com/imink-app/SplatNet3/blob/master/Data/splatnet3_webview_data.json
      let data = await fs.readFile(new URL('./queryHashes.json', import.meta.url));
      this.queryHashes = JSON.parse(data);
    }
  }

  async _webViewVersion() {
    await this._loadQueryHashes();

    return this.queryHashes?.version;
  }

  async _queryHash(name) {
    await this._loadQueryHashes();

    return this.queryHashes?.graphql?.hash_map?.[name];
  }

  // Bullet token

  async _startSession() {
    this.bulletToken = await this.getBulletToken();
  }

  async _maybeStartSession() {
    if (!this.hasSession) {
      await this._startSession();
    }
  }

  async getBulletToken(useCache = true) {
    let bulletTokenCache = new ValueCache(`${this.nsoClient.cachePrefix}.bulletToken`);

    let bulletToken = useCache
      ? await bulletTokenCache.getData()
      : null;

    if (!bulletToken) {
      let webServiceToken = await this.nsoClient.getWebServiceToken(SPLATNET3_WEB_SERVICE_ID);
      bulletToken = await this._createBulletToken(webServiceToken, bulletTokenCache);
    }

    return bulletToken;
  }

  async _createBulletToken(webServiceToken, bulletTokenCache) {
    this.console.info('Creating bullet token...');

    let response = await fetch(this.baseUrl + '/api/bullet_tokens', {
      method: 'POST',
      headers: {
        'X-Web-View-Ver': await this._webViewVersion(),
        'X-NACOUNTRY': 'US', // TODO
        'X-GameWebToken': webServiceToken,
        'Accept-Language': this.acceptLanguage,
        'User-Agent': process.env.USER_AGENT,
      },
    });

    if (!response.ok) {
      throw new Error(`Invalid bullet token response code: ${response.status}`);
    }

    let bulletToken = await response.json();

    // We can assume the token expires after 7200 seconds
    let expiry = calculateCacheExpiry(7200);
    await bulletTokenCache.setData(bulletToken, expiry);

    this.console.debug(`Caching bullet token until: ${expiry}`);

    return bulletToken;
  }

  // GraphQL

  async getGraphQL(body = {}) {
    await this._maybeStartSession();
    let webViewVersion = await this._webViewVersion();

    let response = await limit(() => fetch(this.baseUrl + '/api/graphql', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.bulletToken.bulletToken}`,
        'X-Web-View-Ver': webViewVersion,
        'Content-Type': 'application/json',
        'Accept-Language': this.acceptLanguage,
      },
      body: JSON.stringify(body),
    }));

    if (!response.ok) {
      throw new Error(`Invalid GraphQL response code: ${response.status}`);
    }

    return await response.json();
  }

  async getGraphQLPersistedQuery(version, query, variables = {}) {
    let sha256Hash = await this._queryHash(query) ?? query;

    let body = {
      extensions: { persistedQuery: { version, sha256Hash } },
      variables,
    };

    return await this.getGraphQL(body);
  }

  // Specific queries

  getStageScheduleData() {
    return this.getGraphQLPersistedQuery(1, 'StageScheduleQuery');
  }

  getStageRecordData() {
    return this.getGraphQLPersistedQuery(1, 'StageRecordQuery');
  }

  getGesotownData() {
    return this.getGraphQLPersistedQuery(1, 'GesotownQuery');
  }

  getCoopHistoryData() {
    return this.getGraphQLPersistedQuery(1, 'CoopHistoryQuery');
  }

  getFestRecordDataPage(cursor = null) {
    return this.getGraphQLPersistedQuery(1, 'FestRecordPaginationQuery', { cursor, first: 100 });
  }

  getFestDetailData(festId) {
    return this.getGraphQLPersistedQuery(1, 'DetailFestRecordDetailQuery', { festId });
  }

  getFestRankingData(festId) {
    return this.getGraphQLPersistedQuery(1, 'DetailRankingQuery', { festId });
  }

  getFestRankingPage(teamId, cursor) {
    return this.getGraphQLPersistedQuery(1, 'RankingHoldersFestTeamRankingHoldersPaginationQuery', { cursor, first: 25, id: teamId });
  }

  getCurrentFestData() {
    return this.getGraphQLPersistedQuery(1, 'useCurrentFestQuery');
  }

  getXRankingData(region) {
    return this.getGraphQLPersistedQuery(1, 'XRankingQuery', { region });
  }

  getXRankingDetailQueryTypes() {
    return [
      {
        name: 'Splat Zones Leaderboard',
        key: 'splatzones',
        id: 'DetailTabViewXRankingArRefetchQuery',
        dataKey: 'xRankingAr',
      },
      {
        name: 'Splat Zones Top Weapons',
        key: 'splatzones.weapons',
        id: 'DetailTabViewWeaponTopsArRefetchQuery',
        dataKey: 'weaponTopsAr',
      },
      {
        name: 'Clam Blitz Leaderboard',
        key: 'clamblitz',
        id: 'DetailTabViewXRankingClRefetchQuery',
        dataKey: 'xRankingCl',
      },
      {
        name: 'Clam Blitz Top Weapons',
        key: 'clamblitz.weapons',
        id: 'DetailTabViewWeaponTopsClRefetchQuery',
        dataKey: 'weaponTopsCl',
      },
      {
        name: 'Rainmaker Leaderboard',
        key: 'rainmaker',
        id: 'DetailTabViewXRankingGlRefetchQuery',
        dataKey: 'xRankingGl',
      },
      {
        name: 'Rainmaker Top Weapons',
        key: 'rainmaker.weapons',
        id: 'DetailTabViewWeaponTopsGlRefetchQuery',
        dataKey: 'weaponTopsGl',
      },
      {
        name: 'Tower Control Leaderboard',
        key: 'towercontrol',
        id: 'DetailTabViewXRankingLfRefetchQuery',
        dataKey: 'xRankingLf',
      },
      {
        name: 'Tower Control Top Weapons',
        key: 'towercontrol.weapons',
        id: 'DetailTabViewWeaponTopsLfRefetchQuery',
        dataKey: 'weaponTopsLf',
      },
    ];
  }

  getXRankingDetail(type, id, page = 1) {
    // The API splits this data into 5 pages of 100 results (param: "page").
    // Each page is segmented into smaller 25-record chunks (by default) as well (param: "first").
    // We can just pass a larger number (up to 100) into the "first" param
    // to retrieve all results for that page.
    let first = 100;

    return this.getGraphQLPersistedQuery(1, type.id, { id, first, page });
  }

  async *getXRankingDetailPages(type, id) {
    for (let page = 1; page <= 5; page++) {
      yield await this.getXRankingDetail(type, id, page);
    }
  }
}
