import {
  AugmentationName,
  CityName,
  CompanyName,
  CompletedProgramName,
  FactionName,
  FactionDiscovery,
  JobName,
  LocationName,
  ToastVariant,
} from "@enums";

import type { PlayerObject } from "./PlayerObject";
import type { ProgramFilePath } from "../../Paths/ProgramFilePath";

import { applyAugmentation } from "../../Augmentation/AugmentationHelpers";
import { PlayerOwnedAugmentation } from "../../Augmentation/PlayerOwnedAugmentation";
import { currentNodeMults } from "../../BitNode/BitNodeMultipliers";
import { CodingContractRewardType, ICodingContractReward } from "../../CodingContract/Contract";
import { Company } from "../../Company/Company";
import { Companies } from "../../Company/Companies";
import { getNextCompanyPositionHelper } from "../../Company/GetNextCompanyPosition";
import { getJobRequirements, getJobRequirementText } from "../../Company/GetJobRequirements";
import { CompanyPosition } from "../../Company/CompanyPosition";
import { CONSTANTS } from "../../Constants";
import { Exploit } from "../../Exploits/Exploit";
import { Faction } from "../../Faction/Faction";
import { Factions } from "../../Faction/Factions";
import { FactionInvitationEvents } from "../../Faction/ui/FactionInvitationManager";
import { resetGangs } from "../../Gang/AllGangs";
import { SleeveWorkType } from "../Sleeve/Work/Work";
import { calculateSkillProgress as calculateSkillProgressF, ISkillProgress } from "../formulas/skill";
import { AddToAllServers, createUniqueRandomIp } from "../../Server/AllServers";
import { safelyCreateUniqueServer } from "../../Server/ServerHelpers";
import { Location } from "../../Locations/Location";

import { SpecialServers } from "../../Server/data/SpecialServers";
import { applySourceFile } from "../../SourceFile/applySourceFile";
import { applyExploit } from "../../Exploits/applyExploits";
import { SourceFiles } from "../../SourceFile/SourceFiles";
import { getHospitalizationCost } from "../../Hospital/Hospital";

import { formatMoney } from "../../ui/formatNumber";
import { MoneySource, MoneySourceTracker } from "../../utils/MoneySourceTracker";
import { dialogBoxCreate } from "../../ui/React/DialogBox";

import { SnackbarEvents } from "../../ui/React/Snackbar";
import { achievements } from "../../Achievements/Achievements";

import { isCompanyWork } from "../../Work/CompanyWork";
import { canAccessBitNodeFeature } from "../../BitNode/BitNodeUtils";
import { AlertEvents } from "../../ui/React/AlertManager";
import { Augmentations } from "../../Augmentation/Augmentations";
import { PlayerEventType, PlayerEvents } from "./PlayerEvents";
import type { Result } from "@nsdefs";
import type { AchievementId } from "../../Achievements/Types";
import { Infiltration } from "../../Infiltration/Infiltration";
import { recalculateNumberOfOwnedSleeves } from "../Sleeve/SleeveCovenantPurchases";
import { Player } from "@player";
import { getRandomIntInclusive } from "../../utils/helpers/getRandomIntInclusive";
import { getRecordKeys } from "../../Types/Record";

export function init(this: PlayerObject): void {
  /* Initialize Player's home computer */
  const t_homeComp = safelyCreateUniqueServer({
    adminRights: true,
    hostname: "home",
    ip: createUniqueRandomIp(),
    isConnectedTo: true,
    maxRam: 8,
    organizationName: "Home PC",
    purchasedByPlayer: true,
  });
  this.currentServer = SpecialServers.Home;
  AddToAllServers(t_homeComp);

  this.getHomeComputer().pushProgram(CompletedProgramName.nuke);
}

export function prestigeAugmentation(this: PlayerObject): void {
  this.currentServer = SpecialServers.Home;

  this.numPeopleKilled = 0;

  //Reset stats
  this.skills.hacking = 1;

  this.skills.strength = 1;
  this.skills.defense = 1;
  this.skills.dexterity = 1;
  this.skills.agility = 1;

  this.skills.charisma = 1;

  this.exp.hacking = 0;
  this.exp.strength = 0;
  this.exp.defense = 0;
  this.exp.dexterity = 0;
  this.exp.agility = 0;
  this.exp.charisma = 0;

  this.money = 1000 + CONSTANTS.Donations;

  this.city = CityName.Sector12;
  this.location = LocationName.TravelAgency;

  this.jobs = {};

  this.purchasedServers = [];

  this.factions = [];
  this.factionInvitations = [];
  // Clear any pending invitation modals
  FactionInvitationEvents.emit({ type: "ClearAll" });

  this.queuedAugmentations = [];

  recalculateNumberOfOwnedSleeves();

  this.sleeves.forEach((sleeve) => (sleeve.shock <= 0 ? sleeve.synchronize() : sleeve.shockRecovery()));

  this.lastUpdate = new Date().getTime();

  // Statistics Trackers
  this.playtimeSinceLastAug = 0;
  this.lastAugReset = this.lastUpdate;
  this.scriptProdSinceLastAug = 0;
  this.moneySourceA.reset();

  this.hacknetNodes.length = 0;
  this.hashManager.prestige();

  // Reapply augs, re-calculate skills and reset HP
  this.reapplyAllAugmentations(true);
  this.hp.current = this.hp.max;

  this.finishWork(true, true);
  // We need to call overrideIntelligence here instead of prestigeSourceFile to reset intelligence data when installing
  // augmentations.
  this.overrideIntelligence();
}

export function prestigeSourceFile(this: PlayerObject): void {
  this.entropy = 0;
  this.prestigeAugmentation();
  this.karma = 0;
  // Duplicate sleeves are reset to level 1 every Bit Node (but the number of sleeves you have persists)
  this.sleeves.forEach((sleeve) => sleeve.prestige());

  if (this.bitNodeN === 10) {
    for (let i = 0; i < this.sleeves.length; i++) {
      this.sleeves[i].shock = Math.min(25, this.sleeves[i].shock);
      this.sleeves[i].sync = Math.max(25, this.sleeves[i].sync);
    }
  }

  this.gang = null;
  resetGangs();
  this.corporation = null;
  this.bladeburner = null;

  // Reset Stock market
  this.hasWseAccount = false;
  this.hasTixApiAccess = false;
  this.has4SData = false;
  this.has4SDataTixApi = false;

  // BitNode 3: Corporatocracy
  this.corporation = null;

  this.moneySourceB.reset();
  this.playtimeSinceLastBitnode = 0;
  this.lastNodeReset = this.lastUpdate;
  this.augmentations = [];
}

export function receiveInvite(this: PlayerObject, factionName: FactionName): void {
  const faction = Factions[factionName];
  if (this.factionInvitations.includes(factionName) || faction.alreadyInvited || faction.isMember || faction.isBanned) {
    return;
  }
  this.factionInvitations.push(factionName);
  faction.discovery = FactionDiscovery.known;
}

export function receiveRumor(this: PlayerObject, factionName: FactionName): void {
  const faction = Factions[factionName];
  if (faction.discovery === FactionDiscovery.unknown) {
    faction.discovery = FactionDiscovery.rumored;
  }
  if (this.factionRumors.has(factionName) || faction.isMember || faction.alreadyInvited) {
    return;
  }

  this.factionRumors.add(factionName);
}

//Calculates skill level progress based on experience. The same formula will be used for every skill
export function calculateSkillProgress(this: PlayerObject, exp: number, mult = 1): ISkillProgress {
  return calculateSkillProgressF(exp, mult);
}

export function hasProgram(this: PlayerObject, programName: CompletedProgramName | ProgramFilePath): boolean {
  const home = this.getHomeComputer();
  return home.programs.includes(programName);
}

export function setMoney(this: PlayerObject, money: number): void {
  if (isNaN(money)) {
    console.error("NaN passed into Player.setMoney()");
    return;
  }
  this.money = money;
}

export function gainMoney(this: PlayerObject, money: number, source: MoneySource): void {
  if (isNaN(money)) {
    console.error("NaN passed into Player.gainMoney()");
    return;
  }

  this.money = this.money + money;
  this.recordMoneySource(money, source);
}

export function loseMoney(this: PlayerObject, money: number, source: MoneySource): void {
  if (isNaN(money)) {
    console.error("NaN passed into Player.loseMoney()");
    return;
  }
  if (this.money === Infinity && money === Infinity) return;
  this.money = this.money - money;
  this.recordMoneySource(-1 * money, source);
}

export function canAfford(this: PlayerObject, cost: number): boolean {
  if (isNaN(cost)) {
    console.error(`NaN passed into Player.canAfford()`);
    return false;
  }
  return this.money >= cost;
}

export function recordMoneySource(this: PlayerObject, amt: number, source: MoneySource): void {
  if (!(this.moneySourceA instanceof MoneySourceTracker)) {
    console.warn(`Player.moneySourceA was not properly initialized. Resetting`);
    this.moneySourceA = new MoneySourceTracker();
  }
  if (!(this.moneySourceB instanceof MoneySourceTracker)) {
    console.warn(`Player.moneySourceB was not properly initialized. Resetting`);
    this.moneySourceB = new MoneySourceTracker();
  }
  this.moneySourceA.record(amt, source);
  this.moneySourceB.record(amt, source);
}

export function startFocusing(this: PlayerObject): void {
  this.focus = true;
}

export function stopFocusing(this: PlayerObject): void {
  this.focus = false;
}

// Returns true if hospitalized, false otherwise
export function takeDamage(this: PlayerObject, amt: number): boolean {
  if (typeof amt !== "number") {
    console.warn(`Player.takeDamage() called without a numeric argument: ${amt}`);
    return false;
  }

  this.hp.current -= amt;
  if (this.hp.current <= 0) {
    this.hospitalize(false);
    return true;
  } else {
    return false;
  }
}

export function hospitalize(this: PlayerObject, suppressNotification: boolean): number {
  const cost = getHospitalizationCost();
  this.loseMoney(cost, "hospitalization");
  this.hp.current = this.hp.max;
  if (!suppressNotification) {
    SnackbarEvents.emit(`You've been hospitalized for ${formatMoney(cost)}`, ToastVariant.SUCCESS, 2000);
  }
  PlayerEvents.emit(PlayerEventType.Hospitalized);
  return cost;
}

/**
 * Company job application. Determines the job that the Player should get (if any) at the given company.
 * @param this The player instance
 * @param company The company being applied to
 * @param position A specific position
 * @param sing Whether this is being called from the applyToCompany() Netscript Singularity function
 * @returns The name of the Job received (if any). May be higher or lower than the job applied to.
 */
export function applyForJob(
  this: PlayerObject,
  company: Company,
  position: CompanyPosition,
): Result<{ jobName: JobName }> {
  if (!company) {
    return { success: false, message: `Invalid company: ${company}.` };
  }

  // Start searching the job track from the provided point (which may not be the entry position)
  let pos = position;
  if (!this.isQualified(company, pos)) {
    return {
      success: false,
      message: `Unfortunately, you do not qualify for this position.\n${getJobRequirementText(company, pos)}`,
    };
  }

  if (!company.hasPosition(pos)) {
    return {
      success: false,
      message: `Company ${company.name} does not have position ${pos.name}.`,
    };
  }

  let nextPos = getNextCompanyPositionHelper(pos);
  while (nextPos && company.hasPosition(nextPos) && this.isQualified(company, nextPos)) {
    pos = nextPos;
    nextPos = getNextCompanyPositionHelper(pos);
  }

  // Check if player already has the assigned job
  if (this.jobs[company.name] === pos.name) {
    let errorMessage;
    const nextPos = getNextCompanyPositionHelper(pos);
    if (nextPos === null) {
      errorMessage = `You are already ${pos.name}! No promotion available.`;
    } else if (!company.hasPosition(nextPos)) {
      errorMessage = `You already have the highest ${pos.field} position available at ${company.name}! No promotion available.`;
    } else {
      errorMessage = `Unfortunately, you do not qualify for a promotion.\n${getJobRequirementText(company, nextPos)}`;
    }
    return { success: false, message: errorMessage };
  }

  this.jobs[company.name] = pos.name;

  return {
    success: true,
    message: `${pos.hiredText} at ${company.name}!`,
    jobName: pos.name,
  };
}

/**
 * Get a job position that the player can apply for.
 * @param this The player instance
 * @param company The Company being applied to
 * @param entryPosType Job field (Software, Business, etc)
 * @returns The highest job the player can apply for at this company, if any
 */
export function getNextCompanyPosition(
  this: PlayerObject,
  company: Company,
  entryPosType: CompanyPosition,
): CompanyPosition | null {
  let pos: CompanyPosition | null = entryPosType;
  let nextPos = getNextCompanyPositionHelper(pos);
  // Find the highest-level job in this category that the player is currently able to apply for.
  while (nextPos && company.hasPosition(nextPos) && this.isQualified(company, nextPos)) {
    pos = nextPos;
    nextPos = getNextCompanyPositionHelper(pos);
  }
  // If the player already has this position, return the one after that (if any).
  if (this.jobs[company.name] == pos.name) {
    pos = nextPos;
  }
  return pos;
}

export function quitJob(this: PlayerObject, company: CompanyName, suppressDialog?: boolean): void {
  if (isCompanyWork(this.currentWork) && this.currentWork.companyName === company) {
    this.finishWork(true);
  }
  for (const sleeve of this.sleeves) {
    if (sleeve.currentWork?.type === SleeveWorkType.COMPANY && sleeve.currentWork.companyName === company) {
      sleeve.stopWork();
      if (!suppressDialog) {
        dialogBoxCreate(`You quit ${company} while one of your sleeves was working there. The sleeve is now idle.`);
      }
    }
  }
  // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
  delete this.jobs[company];
}

/**
 * Method to see if the player has at least one job assigned to them
 * @param this The player instance
 * @returns Whether the user has at least one job
 */
export function hasJob(this: PlayerObject): boolean {
  return Boolean(Object.keys(this.jobs).length);
}

//Checks if the Player is qualified for a certain position
export function isQualified(this: PlayerObject, company: Company, position: CompanyPosition): boolean {
  const reqs = getJobRequirements(company, position);
  return reqs.every((req) => req.isSatisfied(this));
}

/********** Reapplying Augmentations and Source File ***********/
export function reapplyAllAugmentations(this: PlayerObject, resetMultipliers = true): void {
  if (resetMultipliers) {
    this.resetMultipliers();
  }

  for (const playerAug of this.augmentations) {
    const augName = playerAug.name;

    if (augName == AugmentationName.NeuroFluxGovernor) {
      for (let i = 0; i < playerAug.level; ++i) {
        applyAugmentation(playerAug, true);
      }
      continue;
    }
    applyAugmentation(playerAug, true);
  }

  this.updateSkillLevels();
}

export function reapplyAllSourceFiles(this: PlayerObject): void {
  //Will always be called after reapplyAllAugmentations() so multipliers do not have to be reset
  //this.resetMultipliers();

  for (const [bn, lvl] of this.activeSourceFiles) {
    const srcFileKey = "SourceFile" + bn;
    const sourceFileObject = SourceFiles[srcFileKey];
    if (!sourceFileObject) {
      console.error(`Invalid source file number: ${bn}`);
      continue;
    }
    applySourceFile(bn, lvl);
  }
  applyExploit();
  this.updateSkillLevels();
}

/**
 * Checks whether a player meets the requirements for joining each faction, and returns an array of all invitations the player should receive.
 * Also handles receiving rumors for factions if the rumor requirements are met.
 */
export function checkForFactionInvitations(this: PlayerObject): Faction[] {
  const invitedFactions = [];
  for (const faction of Object.values(Factions)) {
    if (faction.isMember) continue;
    if (faction.alreadyInvited) continue;
    // Handle invites
    const { inviteReqs, rumorReqs } = faction.getInfo();
    if (!faction.isBanned && inviteReqs.isSatisfied(this)) invitedFactions.push(faction);
    // Handle rumors
    if (this.factionRumors.has(faction.name)) continue;
    if (rumorReqs.isSatisfied(this)) this.receiveRumor(faction.name);
  }
  return invitedFactions;
}

/************* BitNodes **************/
export function setBitNodeNumber(this: PlayerObject, n: number): void {
  this.bitNodeN = n;
}

export function queueAugmentation(this: PlayerObject, name: AugmentationName): void {
  if (name !== AugmentationName.NeuroFluxGovernor) {
    for (const aug of this.queuedAugmentations) {
      if (name === aug.name) {
        AlertEvents.emit(`Tried to queue ${name} twice. This is a bug. Please contact developers.`);
        return;
      }
    }

    for (const aug of this.augmentations) {
      if (aug.name === name) {
        AlertEvents.emit(
          `Tried to queue ${name}, but this augmentation was installed. This is a bug. Please contact developers.`,
        );
        return;
      }
    }
  }

  const queuedAugmentation = new PlayerOwnedAugmentation(name);
  if (name === AugmentationName.NeuroFluxGovernor) {
    const augmentation = Augmentations[name];
    queuedAugmentation.level = augmentation.getNextLevel();
  }
  this.queuedAugmentations.push(queuedAugmentation);
}

/************* Coding Contracts **************/
export function gainCodingContractReward(
  this: PlayerObject,
  reward: ICodingContractReward | null,
  difficulty: number,
  rewardScaling: number,
): string {
  if (!reward) {
    return `No reward for this contract`;
  }
  // The new standard is smaller, more frequent rewards - a third of the reward size of the previous
  const adjustedScaling = rewardScaling / 3;

  switch (reward.type) {
    case CodingContractRewardType.FactionReputation: {
      const factionsThatAllowHacking = Player.factions.filter((fac) => Factions[fac].getInfo().offerHackingWork);
      if (factionsThatAllowHacking.length === 0) {
        return this.gainCodingContractReward({ type: CodingContractRewardType.Money }, difficulty, adjustedScaling);
      }
      const randomFaction = factionsThatAllowHacking[getRandomIntInclusive(0, factionsThatAllowHacking.length - 1)];
      const repGain = CONSTANTS.CodingContractBaseFactionRepGain * difficulty * adjustedScaling;
      Factions[randomFaction].playerReputation += repGain;
      return `Gained ${repGain} faction reputation for ${randomFaction}`;
    }
    case CodingContractRewardType.FactionReputationAll: {
      const factionsThatAllowHacking = Player.factions.filter((fac) => Factions[fac].getInfo().offerHackingWork);
      if (factionsThatAllowHacking.length === 0) {
        return this.gainCodingContractReward({ type: CodingContractRewardType.Money }, difficulty, adjustedScaling);
      }

      const totalGain = CONSTANTS.CodingContractBaseFactionRepGain * difficulty * adjustedScaling;
      const gainPerFaction = Math.floor(totalGain / factionsThatAllowHacking.length);
      for (const facName of factionsThatAllowHacking) {
        Factions[facName].playerReputation += gainPerFaction;
      }
      return `Gained ${gainPerFaction} reputation for each of the following factions: ${factionsThatAllowHacking.join(
        ", ",
      )}`;
    }
    case CodingContractRewardType.CompanyReputation: {
      const companies = getRecordKeys(Player.jobs);
      if (companies.length === 0) {
        return this.gainCodingContractReward(
          {
            type:
              Math.random() < 0.5
                ? CodingContractRewardType.FactionReputation
                : CodingContractRewardType.FactionReputationAll,
          },
          difficulty,
          adjustedScaling,
        );
      }
      const randomCompany = companies[getRandomIntInclusive(0, companies.length - 1)];
      const repGain = CONSTANTS.CodingContractBaseCompanyRepGain * difficulty * adjustedScaling;
      Companies[randomCompany].playerReputation += repGain;
      return `Gained ${repGain} company reputation for ${randomCompany}`;
    }
    case CodingContractRewardType.Money: {
      const moneyGain =
        CONSTANTS.CodingContractBaseMoneyGain * difficulty * currentNodeMults.CodingContractMoney * adjustedScaling;
      this.gainMoney(moneyGain, "codingcontract");
      return `Gained ${formatMoney(moneyGain)}`;
    }
    default: {
      // Verify type switch statement is exhaustive
      const __a: never = reward;
    }
  }
  throw new Error("Invalid coding contract reward type");
}

export function gotoLocation(this: PlayerObject, to: LocationName): boolean {
  this.location = to;
  return true;
}

export function canAccessGrafting(this: PlayerObject): boolean {
  return canAccessBitNodeFeature(10);
}

export function giveExploit(this: PlayerObject, exploit: Exploit): void {
  if (!this.exploits.includes(exploit)) {
    this.exploits.push(exploit);
    SnackbarEvents.emit("SF -1 acquired!", ToastVariant.SUCCESS, 2000);
  }
}

export function giveAchievement(this: PlayerObject, achievementId: AchievementId): void {
  const achievement = achievements[achievementId];
  if (!achievement) {
    return;
  }
  if (!this.achievements.map((a) => a.ID).includes(achievementId)) {
    this.achievements.push({ ID: achievementId, unlockedOn: new Date().getTime() });
    SnackbarEvents.emit(`Unlocked Achievement: "${achievement.Name}"`, ToastVariant.SUCCESS, 2000);
  }
}

export function getCasinoWinnings(this: PlayerObject): number {
  return this.moneySourceA.casino;
}

export function canAccessCotMG(this: PlayerObject): boolean {
  return canAccessBitNodeFeature(13);
}

/**
 * To ensure the "SF override" option work properly, this function should only be used in special cases. In most cases,
 * activeSourceFileLvl should be used instead.
 */
export function sourceFileLvl(this: PlayerObject, n: number): number {
  return this.sourceFiles.get(n) ?? 0;
}

export function activeSourceFileLvl(this: PlayerObject, n: number): number {
  if (this.bitNodeOptions.sourceFileOverrides.has(n)) {
    return this.bitNodeOptions.sourceFileOverrides.get(n) ?? 0;
  }
  return this.sourceFiles.get(n) ?? 0;
}

export function focusPenalty(this: PlayerObject): number {
  let focus = 1;
  if (!this.hasAugmentation(AugmentationName.NeuroreceptorManager, true)) {
    focus = this.focus ? 1 : CONSTANTS.BaseFocusBonus;
  }
  return focus;
}

/** This doesn't change the current page; that is up to the caller. */
export function initInfiltration(this: PlayerObject, location: Location): void {
  if (!location.infiltrationData)
    throw new Error(`trying to start infiltration at ${location.name} but the infiltrationData is null`);
  this.infiltration = new Infiltration(location);
}
