import type { Person as IPerson, WorkStats } from "@nsdefs";
import type { PlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation";
import type { IReviverValue } from "../utils/JSONReviver";
import type { MoneySource } from "../utils/MoneySourceTracker";
import type { HP } from "./HP";
import type { Skills } from "./Skills";

import { CityName } from "@enums";
import { currentNodeMults } from "../BitNode/BitNodeMultipliers";
import { CONSTANTS } from "../Constants";
import { Player } from "../Player";
import { defaultMultipliers } from "./Multipliers";
import { calculateExp, calculateSkill } from "./formulas/skill";

// Base class representing a person-like object
export abstract class Person implements IPerson {
  hp: HP = { current: 10, max: 10 };
  skills: Skills = {
    hacking: 1,
    strength: 1,
    defense: 1,
    dexterity: 1,
    agility: 1,
    charisma: 1,
    intelligence: 0,
  };
  exp: Skills = {
    hacking: 0,
    strength: 0,
    defense: 0,
    dexterity: 0,
    agility: 0,
    charisma: 0,
    intelligence: 0,
  };

  persistentIntelligenceData = {
    exp: 0,
  };

  mults = defaultMultipliers();

  /** Augmentations */
  augmentations: PlayerOwnedAugmentation[] = [];
  queuedAugmentations: PlayerOwnedAugmentation[] = [];

  /** City that the person is in */
  city: CityName = CityName.Sector12;

  gainHackingExp(exp: number): void {
    if (isNaN(exp)) {
      console.error("ERR: NaN passed into Player.gainHackingExp()");
      return;
    }
    this.exp.hacking += exp;
    if (this.exp.hacking < 0) {
      this.exp.hacking = 0;
    }

    this.skills.hacking = calculateSkill(
      this.exp.hacking,
      this.mults.hacking * currentNodeMults.HackingLevelMultiplier,
    );
  }

  gainStrengthExp(exp: number): void {
    if (isNaN(exp)) {
      console.error("ERR: NaN passed into Player.gainStrengthExp()");
      return;
    }
    this.exp.strength += exp;
    if (this.exp.strength < 0) {
      this.exp.strength = 0;
    }

    this.skills.strength = calculateSkill(
      this.exp.strength,
      this.mults.strength * currentNodeMults.StrengthLevelMultiplier,
    );
  }

  gainDefenseExp(exp: number): void {
    if (isNaN(exp)) {
      console.error("ERR: NaN passed into player.gainDefenseExp()");
      return;
    }
    this.exp.defense += exp;
    if (this.exp.defense < 0) {
      this.exp.defense = 0;
    }

    this.skills.defense = calculateSkill(
      this.exp.defense,
      this.mults.defense * currentNodeMults.DefenseLevelMultiplier,
    );
    const ratio = this.hp.current / this.hp.max;
    this.hp.max = Math.floor(10 + this.skills.defense / 10);
    this.hp.current = Math.round(this.hp.max * ratio);
  }

  gainDexterityExp(exp: number): void {
    if (isNaN(exp)) {
      console.error("ERR: NaN passed into Player.gainDexterityExp()");
      return;
    }
    this.exp.dexterity += exp;
    if (this.exp.dexterity < 0) {
      this.exp.dexterity = 0;
    }

    this.skills.dexterity = calculateSkill(
      this.exp.dexterity,
      this.mults.dexterity * currentNodeMults.DexterityLevelMultiplier,
    );
  }

  gainAgilityExp(exp: number): void {
    if (isNaN(exp)) {
      console.error("ERR: NaN passed into Player.gainAgilityExp()");
      return;
    }
    this.exp.agility += exp;
    if (this.exp.agility < 0) {
      this.exp.agility = 0;
    }

    this.skills.agility = calculateSkill(
      this.exp.agility,
      this.mults.agility * currentNodeMults.AgilityLevelMultiplier,
    );
  }

  gainCharismaExp(exp: number): void {
    if (isNaN(exp)) {
      console.error("ERR: NaN passed into Player.gainCharismaExp()");
      return;
    }
    this.exp.charisma += exp;
    if (this.exp.charisma < 0) {
      this.exp.charisma = 0;
    }

    this.skills.charisma = calculateSkill(
      this.exp.charisma,
      this.mults.charisma * currentNodeMults.CharismaLevelMultiplier,
    );
  }

  overrideIntelligence(): void {
    // Reset intelligence data if the player has not unlocked Intelligence.
    // Note that this check cannot reset intelligence data in some edge cases (e.g., bitflume from non-BN5 to BN5). This
    // is an accepted limitation.
    // For more information, please check https://github.com/bitburner-official/bitburner-src/pull/2666
    if (Player.sourceFileLvl(5) === 0 && Player.bitNodeN !== 5) {
      this.skills.intelligence = 0;
      this.exp.intelligence = 0;
      this.persistentIntelligenceData.exp = 0;
      return;
    }
    const persistentIntelligenceSkill = this.calculateSkill(this.persistentIntelligenceData.exp, 1);
    // Reset exp and skill to the persistent values if there is no limit (intelligenceOverride) or the limit is greater
    // than or equal to the persistent skill.
    if (
      Player.bitNodeOptions.intelligenceOverride === undefined ||
      Player.bitNodeOptions.intelligenceOverride >= persistentIntelligenceSkill
    ) {
      this.exp.intelligence = this.persistentIntelligenceData.exp;
      this.skills.intelligence = persistentIntelligenceSkill;
      return;
    }
    // Limit exp and skill based on intelligenceOverride only if it's smaller than the persistent skill.
    this.exp.intelligence = calculateExp(Player.bitNodeOptions.intelligenceOverride, 1);
    this.skills.intelligence = Player.bitNodeOptions.intelligenceOverride;
  }

  gainIntelligenceExp(exp: number): void {
    if (isNaN(exp)) {
      console.error("ERROR: NaN passed into Player.gainIntelligenceExp()");
      return;
    }
    /**
     * Don't change sourceFileLvl to activeSourceFileLvl. When the player has int level, the ability to gain more int is
     * a permanent benefit.
     */
    if (Player.sourceFileLvl(5) > 0 || Player.bitNodeN === 5) {
      this.exp.intelligence += exp;
      this.skills.intelligence = Math.floor(this.calculateSkill(this.exp.intelligence, 1));
      this.persistentIntelligenceData.exp += exp;
    }
  }

  gainStats(retValue: WorkStats): void {
    this.gainHackingExp(retValue.hackExp * this.mults.hacking_exp);
    this.gainStrengthExp(retValue.strExp * this.mults.strength_exp);
    this.gainDefenseExp(retValue.defExp * this.mults.defense_exp);
    this.gainDexterityExp(retValue.dexExp * this.mults.dexterity_exp);
    this.gainAgilityExp(retValue.agiExp * this.mults.agility_exp);
    this.gainCharismaExp(retValue.chaExp * this.mults.charisma_exp);
    this.gainIntelligenceExp(retValue.intExp);
  }

  regenerateHp(amt: number): void {
    if (typeof amt !== "number") {
      console.warn(`Player.regenerateHp() called without a numeric argument: ${amt}`);
      return;
    }
    this.hp.current += amt;
    if (this.hp.current > this.hp.max) {
      this.hp.current = this.hp.max;
    }
  }

  updateSkillLevels(this: Person): void {
    for (const [skill, bnMult] of [
      ["hacking", "HackingLevelMultiplier"],
      ["strength", "StrengthLevelMultiplier"],
      ["defense", "DefenseLevelMultiplier"],
      ["dexterity", "DexterityLevelMultiplier"],
      ["agility", "AgilityLevelMultiplier"],
      ["charisma", "CharismaLevelMultiplier"],
    ] as const) {
      this.skills[skill] = Math.max(
        1,
        Math.floor(this.calculateSkill(this.exp[skill], this.mults[skill] * currentNodeMults[bnMult])),
      );
    }

    const ratio: number = Math.min(this.hp.current / this.hp.max, 1);
    this.hp.max = Math.floor(10 + this.skills.defense / 10);
    this.hp.current = Math.round(this.hp.max * ratio);
  }

  hasAugmentation(augName: string, ignoreQueued = false) {
    if (this.augmentations.some((a) => a.name === augName)) {
      return true;
    }
    if (!ignoreQueued && this.queuedAugmentations.some((a) => a.name === augName)) {
      return true;
    }
    return false;
  }

  travel(cityName: CityName): boolean {
    if (!Player.canAfford(CONSTANTS.TravelCost)) {
      return false;
    }

    Player.loseMoney(CONSTANTS.TravelCost, this.travelCostMoneySource());
    this.city = cityName;

    return true;
  }

  calculateSkill = calculateSkill; //Class version is equal to imported version

  /** Reset all multipliers to 1 */
  resetMultipliers() {
    this.mults = defaultMultipliers();
  }

  abstract travelCostMoneySource(): MoneySource;
  abstract takeDamage(amt: number): boolean;
  abstract whoAmI(): string;
  abstract toJSON(): IReviverValue;
}
