"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var ladders_exports = {};
__export(ladders_exports, {
  Ladders: () => Ladders
});
module.exports = __toCommonJS(ladders_exports);
var import_ladders_challenges = require("./ladders-challenges");
/**
 * Matchmaker
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * This keeps track of challenges to battle made between users, setting up
 * matches between users looking for a battle, and starting new battles.
 *
 * @license MIT
 */
const LadderStore = (typeof Config === "object" && Config.remoteladder ? require("./ladders-remote") : require("./ladders-local")).LadderStore;
const SECONDS = 1e3;
const PERIODIC_MATCH_INTERVAL = 60 * SECONDS;
const searches = /* @__PURE__ */ new Map();
class Ladder extends LadderStore {
  constructor(formatid) {
    super(formatid);
  }
  async prepBattle(connection, challengeType, team = null, isRated = false) {
    const user = connection.user;
    const userid = user.id;
    if (team === null)
      team = user.battleSettings.team;
    if (Rooms.global.lockdown && Rooms.global.lockdown !== "pre") {
      let message = `The server is restarting. Battles will be available again in a few minutes.`;
      if (Rooms.global.lockdown === "ddos") {
        message = `The server is under attack. Battles cannot be started at this time.`;
      }
      connection.popup(message);
      return null;
    }
    if (Punishments.isBattleBanned(user)) {
      connection.popup(`You are barred from starting any new games until your battle ban expires.`);
      return null;
    }
    const gameCount = user.games.size;
    if (Monitor.countConcurrentBattle(gameCount, connection)) {
      return null;
    }
    if (Monitor.countPrepBattle(connection.ip, connection)) {
      return null;
    }
    try {
      this.formatid = Dex.formats.validate(this.formatid);
    } catch (e) {
      connection.popup(`Your selected format is invalid:

- ${e.message}`);
      return null;
    }
    let rating = 0;
    let valResult;
    let removeNicknames = !!(user.locked || user.namelocked);
    const regex = /(?:^|])([^|]*)\|([^|]*)\|/g;
    let match = regex.exec(team);
    let unownWord = "";
    while (match) {
      const nickname = match[1];
      const speciesid = toID(match[2] || match[1]);
      if (speciesid.length <= 6 && speciesid.startsWith("unown")) {
        unownWord += speciesid.charAt(5) || "a";
      }
      if (nickname) {
        const filtered = Chat.nicknamefilter(nickname, user);
        if (typeof filtered === "string" && (!filtered || filtered !== match[1])) {
          connection.popup(
            `Your team was rejected for the following reason:

- Your Pok\xE9mon has a banned nickname: ${match[1]}`
          );
          return null;
        } else if (filtered === false) {
          removeNicknames = true;
        }
      }
      match = regex.exec(team);
    }
    if (unownWord) {
      const filtered = Chat.nicknamefilter(unownWord, user);
      if (!filtered || filtered !== unownWord) {
        connection.popup(
          `Your team was rejected for the following reason:

- Your Unowns spell out a banned word: ${unownWord.toUpperCase()}`
        );
        return null;
      }
    }
    if (isRated && !Ladders.disabled) {
      const uid = user.id;
      [valResult, rating] = await Promise.all([
        TeamValidatorAsync.get(this.formatid).validateTeam(team, { removeNicknames, user: uid }),
        this.getRating(uid)
      ]);
      if (uid !== user.id) {
        return null;
      }
      if (!rating)
        rating = 1;
    } else {
      if (Ladders.disabled) {
        connection.popup(`The ladder is temporarily disabled due to technical difficulties - you will not receive ladder rating for this game.`);
        rating = 1;
      }
      const validator = TeamValidatorAsync.get(this.formatid);
      valResult = await validator.validateTeam(team, { removeNicknames, user: user.id });
    }
    if (!valResult.startsWith("1")) {
      connection.popup(
        `Your team was rejected for the following reasons:

- ` + valResult.slice(1).replace(/\n/g, `
- `)
      );
      return null;
    }
    const settings = { ...user.battleSettings, team: valResult.slice(1) };
    user.battleSettings.inviteOnly = false;
    user.battleSettings.hidden = false;
    return new import_ladders_challenges.BattleReady(userid, this.formatid, settings, rating, challengeType);
  }
  static getChallenging(userid) {
    const userChalls = Ladders.challenges.get(userid);
    if (userChalls) {
      for (const chall of userChalls) {
        if (chall.from === userid)
          return chall;
      }
    }
    return null;
  }
  async makeChallenge(connection, targetUser) {
    const user = connection.user;
    if (targetUser === user) {
      connection.popup(`You can't battle yourself. The best you can do is open PS in Private Browsing (or another browser) and log into a different username, and battle that username.`);
      return false;
    }
    if (Ladder.getChallenging(user.id)) {
      connection.popup(`You are already challenging someone. Cancel that challenge before challenging someone else.`);
      return false;
    }
    let blockChallenge;
    if (typeof targetUser.settings.blockChallenges === "boolean") {
      blockChallenge = targetUser.settings.blockChallenges;
    } else if (targetUser.settings.blockChallenges === "friends") {
      blockChallenge = !targetUser.friends?.has(user.id);
    } else {
      blockChallenge = !Users.globalAuth.atLeast(user, targetUser.settings.blockChallenges);
    }
    if (blockChallenge && !user.can("bypassblocks", targetUser)) {
      connection.popup(`The user '${targetUser.name}' is not accepting challenges right now.`);
      Chat.maybeNotifyBlocked("challenge", targetUser, user);
      return false;
    }
    if (Date.now() < user.lastChallenge + 10 * SECONDS && !Config.nothrottle) {
      connection.popup(`You challenged less than 10 seconds after your last challenge! It's cancelled in case it's a misclick.`);
      return false;
    }
    const currentChallenges = Ladders.challenges.get(targetUser.id);
    if (currentChallenges && currentChallenges.length >= 3 && !user.autoconfirmed) {
      connection.popup(
        `This user already has 3 pending challenges.
You must be autoconfirmed to challenge them.`
      );
      return false;
    }
    const ready = await this.prepBattle(connection, "challenge");
    if (!ready)
      return false;
    const existingChall = Ladders.challenges.search(user.id, targetUser.id);
    if (existingChall) {
      if (existingChall.from === targetUser.id && existingChall.to === user.id && existingChall.format === this.formatid && existingChall.ready) {
        if (Ladders.challenges.remove(existingChall)) {
          Ladders.match([existingChall.ready, ready]);
          return true;
        }
      } else {
        connection.popup(`There's already a challenge (${existingChall.format}) between you and ${targetUser.name}!`);
        Ladders.challenges.update(user.id, targetUser.id);
        return false;
      }
    }
    Ladders.challenges.add(new import_ladders_challenges.BattleChallenge(user.id, targetUser.id, ready));
    Ladders.challenges.send(user.id, targetUser.id, `/log ${user.name} wants to battle!`);
    user.lastChallenge = Date.now();
    Chat.runHandlers("onChallenge", user, targetUser, ready.formatid);
    return true;
  }
  static async acceptChallenge(connection, chall) {
    const ladder = Ladders(chall.format);
    const ready = await ladder.prepBattle(connection, "challenge");
    if (!ready)
      return;
    if (Ladders.challenges.remove(chall)) {
      return Ladders.match([chall.ready, ready]);
    }
    return;
  }
  cancelSearch(user) {
    const formatid = toID(this.formatid);
    const formatTable = Ladders.searches.get(formatid);
    if (!formatTable)
      return false;
    if (!formatTable.searches.has(user.id))
      return false;
    formatTable.searches.delete(user.id);
    Ladder.updateSearch(user);
    return true;
  }
  static cancelSearches(user) {
    let cancelCount = 0;
    for (const formatTable of Ladders.searches.values()) {
      const search = formatTable.searches.get(user.id);
      if (!search)
        continue;
      formatTable.searches.delete(user.id);
      cancelCount++;
    }
    Ladder.updateSearch(user);
    return cancelCount;
  }
  getSearcher(search) {
    const formatid = toID(this.formatid);
    const user = Users.get(search.userid);
    if (!user?.connected || user.id !== search.userid) {
      const formatTable = Ladders.searches.get(formatid);
      if (formatTable)
        formatTable.searches.delete(search.userid);
      if (user?.connected) {
        user.popup(`You changed your name and are no longer looking for a battle in ${formatid}`);
        Ladder.updateSearch(user);
      }
      return null;
    }
    return user;
  }
  static getSearches(user) {
    const userSearches = [];
    for (const [formatid, formatTable] of Ladders.searches) {
      if (formatTable.searches.has(user.id))
        userSearches.push(formatid);
    }
    return userSearches;
  }
  static updateSearch(user, connection = null) {
    let games = {};
    let atLeastOne = false;
    for (const roomid of user.games) {
      const room = Rooms.get(roomid);
      if (!room) {
        Monitor.warn(`while searching, room ${roomid} expired for user ${user.id} in rooms ${[...user.inRooms]} and games ${[...user.games]}`);
        user.games.delete(roomid);
        continue;
      }
      const game = room.game;
      if (!game) {
        Monitor.warn(`while searching, room ${roomid} has no game for user ${user.id} in rooms ${[...user.inRooms]} and games ${[...user.games]}`);
        user.games.delete(roomid);
        continue;
      }
      games[roomid] = game.title + (game.allowRenames ? "" : "*");
      atLeastOne = true;
    }
    if (!atLeastOne)
      games = null;
    const searching = Ladders.getSearches(user);
    (connection || user).send(`|updatesearch|` + JSON.stringify({
      searching,
      games
    }));
  }
  hasSearch(user) {
    const formatid = toID(this.formatid);
    const formatTable = Ladders.searches.get(formatid);
    if (!formatTable)
      return false;
    return formatTable.searches.has(user.id);
  }
  /**
   * Validates a user's team and fetches their rating for a given format
   * before creating a search for a battle.
   */
  async searchBattle(user, connection) {
    if (!user.connected)
      return;
    const format = Dex.formats.get(this.formatid);
    if (!format.searchShow) {
      connection.popup(`Error: Your format ${format.id} is not ladderable.`);
      return;
    }
    const oldUserid = user.id;
    const search = await this.prepBattle(connection, format.rated ? "rated" : "unrated", null, format.rated !== false);
    if (oldUserid !== user.id)
      return;
    if (!search)
      return;
    this.addSearch(search, user);
  }
  /**
   * Verifies whether or not a match made between two users is valid. Returns
   */
  matchmakingOK(matches) {
    const formatid = toID(this.formatid);
    const users = matches.map(([ready, user]) => user);
    const userids = users.map((user) => user.id);
    if (new Set(users).size !== users.length)
      return false;
    if (Config.noipchecks) {
      users[0].lastMatch = users[1].id;
      users[1].lastMatch = users[0].id;
      return true;
    }
    if (new Set(users.map((user) => user.latestIp)).size !== users.length)
      return false;
    for (const user of users) {
      if (userids.includes(user.lastMatch))
        return false;
    }
    let searchRange = 100;
    const times = matches.map(([search]) => search.time);
    const elapsed = Date.now() - Math.min(...times);
    if (formatid === "gen8ou" || formatid === "gen8oucurrent" || formatid === "gen8oususpecttest" || formatid === "gen8randombattle") {
      searchRange = 50;
    }
    searchRange += elapsed / 300;
    if (searchRange > 300)
      searchRange = 300 + (searchRange - 300) / 10;
    if (searchRange > 600)
      searchRange = 600;
    const ratings = matches.map(([search]) => search.rating);
    if (Math.max(...ratings) - Math.min(...ratings) > searchRange)
      return false;
    matches[0][1].lastMatch = matches[1][1].id;
    matches[1][1].lastMatch = matches[0][1].id;
    return true;
  }
  /**
   * Starts a search for a battle for a user under the given format.
   */
  addSearch(newSearch, user) {
    const formatid = newSearch.formatid;
    let formatTable = Ladders.searches.get(formatid);
    if (!formatTable) {
      formatTable = {
        playerCount: Dex.formats.get(formatid).playerCount,
        searches: /* @__PURE__ */ new Map()
      };
      Ladders.searches.set(formatid, formatTable);
    }
    if (formatTable.searches.has(user.id)) {
      user.popup(`Couldn't search: You are already searching for a ${formatid} battle.`);
      return;
    }
    const matches = [newSearch];
    for (const search of formatTable.searches.values()) {
      const searcher = this.getSearcher(search);
      if (!searcher)
        continue;
      const matched = this.matchmakingOK([[search, searcher], [newSearch, user]]);
      if (matched) {
        matches.push(search);
      }
      if (matches.length >= formatTable.playerCount) {
        for (const matchedSearch of matches)
          formatTable.searches.delete(matchedSearch.userid);
        Ladder.match(matches);
        return;
      }
    }
    formatTable.searches.set(newSearch.userid, newSearch);
    Ladder.updateSearch(user);
  }
  /**
   * Creates a match for a new battle for each format in this.searches if a
   * valid match can be made. This is run periodically depending on
   * PERIODIC_MATCH_INTERVAL.
   */
  static periodicMatch() {
    for (const [formatid, formatTable] of Ladders.searches) {
      if (formatTable.playerCount > 2)
        continue;
      const matchmaker = Ladders(formatid);
      let longest = null;
      for (const search of formatTable.searches.values()) {
        if (!longest) {
          const longestSearcher2 = matchmaker.getSearcher(search);
          if (!longestSearcher2)
            continue;
          longest = [search, longestSearcher2];
          continue;
        }
        const searcher = matchmaker.getSearcher(search);
        if (!searcher)
          continue;
        const [longestSearch, longestSearcher] = longest;
        const matched = matchmaker.matchmakingOK([[search, searcher], [longestSearch, longestSearcher]]);
        if (matched) {
          formatTable.searches.delete(search.userid);
          formatTable.searches.delete(longestSearch.userid);
          Ladder.match([longestSearch, search]);
          return;
        }
      }
    }
  }
  static match(readies) {
    const formatid = readies[0].formatid;
    if (readies.some((ready) => ready.formatid !== formatid))
      throw new Error(`Format IDs don't match`);
    const players = [];
    let missingUser = null;
    let minRating = Infinity;
    for (const ready of readies) {
      const user = Users.get(ready.userid);
      if (!user) {
        missingUser = ready.userid;
        break;
      }
      players.push({
        user,
        team: ready.settings.team,
        rating: ready.rating,
        hidden: ready.settings.hidden,
        inviteOnly: ready.settings.inviteOnly
      });
      if (ready.rating < minRating)
        minRating = ready.rating;
    }
    if (missingUser) {
      for (const ready of readies) {
        Users.get(ready.userid)?.popup(`Sorry, your opponent ${missingUser} went offline before your battle could start.`);
      }
      return void 0;
    }
    const format = Dex.formats.get(formatid);
    const delayedStart = format.playerCount > players.length ? "multi" : false;
    return Rooms.createBattle({
      format: formatid,
      players,
      rated: minRating,
      challengeType: readies[0].challengeType,
      delayedStart
    });
  }
}
function getLadder(formatid) {
  return new Ladder(formatid);
}
const periodicMatchInterval = setInterval(
  () => Ladder.periodicMatch(),
  PERIODIC_MATCH_INTERVAL
);
const Ladders = Object.assign(getLadder, {
  BattleReady: import_ladders_challenges.BattleReady,
  LadderStore,
  Ladder,
  BattleChallenge: import_ladders_challenges.BattleChallenge,
  GameChallenge: import_ladders_challenges.GameChallenge,
  BattleInvite: import_ladders_challenges.BattleInvite,
  cancelSearches: Ladder.cancelSearches,
  updateSearch: Ladder.updateSearch,
  acceptChallenge: Ladder.acceptChallenge,
  visualizeAll: Ladder.visualizeAll,
  getSearches: Ladder.getSearches,
  match: Ladder.match,
  searches,
  challenges: import_ladders_challenges.challenges,
  periodicMatchInterval,
  // tells the client to ask the server for format information
  formatsListPrefix: LadderStore.formatsListPrefix,
  disabled: false
});
//# sourceMappingURL=ladders.js.map
