/**
 * Character entity — value-object-driven.
 *
 * "I can't believe it's not Haskell": invariants at boundaries.
 * State is encapsulated in a CharacterState record type.
 */
import { Health } from '../value-objects/Health.ts';
import { Level } from '../value-objects/Level.ts';
import type { Status } from '../value-objects/Status.ts';
import { StatusAlive, StatusDead } from '../value-objects/Status.ts';
import type { CharacterState } from './CharacterState.ts';
import type { Faction } from '../factions/Faction.ts';
import type { DamageDealer } from '../magical-objects/magical-object-types.ts';
import type { Healer } from '../magical-objects/magical-object-types.ts';

export interface CharacterCtor {
  name: string;
  level: Level;
}

export interface CharacterCtorWithHealth {
  name: string;
  level: Level;
  health: number;
}

export interface CharacterCtorWithHealthAndStatus {
  name: string;
  level: Level;
  health: number;
  status: Status;
}

export class Character {
  #state: CharacterState;
  readonly #name: string;

  private constructor(state: CharacterState) {
    this.#state = state;
    this.#name = state.name;
  }

  /** Create a new character with default health (1000) and alive status. */
  static create({ name, level }: CharacterCtor): Character {
    const state: CharacterState = {
      name,
      health: Health.create(1000),
      status: StatusAlive,
      level,
      factions: new Set(),
    };
    return new Character(state);
  }

  /** Create a character with a specific health value (for testing). */
  static createWithHealth({ name, level, health }: CharacterCtorWithHealth): Character {
    const state: CharacterState = {
      name,
      health: Health.create(health),
      status: StatusAlive,
      level,
      factions: new Set(),
    };
    return new Character(state);
  }

  /** Create a character with specific health and status (for use by MagicalWeapon). */
  static createWithHealthAndStatus({
    name,
    level,
    health,
    status,
  }: CharacterCtorWithHealthAndStatus): Character {
    const state: CharacterState = {
      name,
      health: Health.create(health),
      status,
      level,
      factions: new Set(),
    };
    return new Character(state);
  }

  get name(): string {
    return this.#name;
  }

  get health(): Health {
    return this.#state.health;
  }

  get status(): Status {
    return this.#state.status;
  }

  get level(): Level {
    return this.#state.level;
  }

  get factions(): ReadonlySet<Faction> {
    return this.#state.factions;
  }

  /**
   * Check if this character is an ally of another.
   * Two characters are allies if they share at least one faction.
   * A character is never an ally of itself.
   */
  isAllyOf(other: Character): boolean {
    if (this === other) return false;
    for (const faction of this.#state.factions) {
      if (other.#state.factions.has(faction)) return true;
    }
    return false;
  }

  /**
   * Join a faction. Returns a new Character with the faction added.
   * Dead characters cannot join factions.
   */
  joinFaction(faction: Faction): Character {
    if (this.status.kind === 'dead') return this;
    const newFactions = new Set(this.#state.factions);
    newFactions.add(faction);
    return new Character({
      name: this.name,
      health: this.health,
      status: this.status,
      level: this.level,
      factions: newFactions,
    });
  }

  /**
   * Leave a faction. Returns a new Character with the faction removed.
   * Dead characters cannot leave factions.
   * If the character does not belong to the faction, returns the same reference.
   */
  leaveFaction(faction: Faction): Character {
    if (this.status.kind === 'dead') return this;
    if (!this.#state.factions.has(faction)) return this;
    const newFactions = new Set(this.#state.factions);
    newFactions.delete(faction);
    return new Character({
      name: this.name,
      health: this.health,
      status: this.status,
      level: this.level,
      factions: newFactions,
    });
  }

  /**
   * Heal an ally character. Returns a new Character with updated health.
   * Only allies can heal each other. Non-allies cannot heal.
   * Dead characters cannot be healed.
   */
  healAlly(ally: Character, amount: number): Character {
    // Dead characters cannot be healed
    if (ally.status.kind === 'dead') return ally;
    // Non-allies cannot heal
    if (!this.isAllyOf(ally)) return ally;
    // Increase health, capped at level-based maximum
    const maxHealth = Level.maxHealthForLevel(ally.level.value);
    const newHealth = ally.health.add(amount, maxHealth);
    return new Character({
      name: ally.name,
      health: newHealth,
      status: ally.status,
      level: ally.level,
      factions: ally.factions,
    });
  }

  /**
   * Deal damage to another character. Returns a new Character with updated state.
   * Does not mutate the attacker or the original target reference.
   */
  dealDamage(target: Character, damage: number): Character {
    // Self-damage is forbidden — use reference equality, not name
    if (this === target) return target;
    // Allies cannot deal damage to each other
    if (this.isAllyOf(target)) return target;
    // Dead characters cannot take damage
    if (target.status.kind === 'dead') return target;
    // Negative damage is invalid
    if (damage < 0) throw new Error(`Damage must be non-negative, got ${damage}`);
    // Level-based damage modifier
    const levelDiff = this.level.diff(target.level); // = this.level - target.level
    let actualDamage = damage;
    if (levelDiff <= -5) {
      // Target is ≥5 levels above → damage reduced by 50%
      actualDamage = Math.floor(damage * 0.5);
    } else if (levelDiff >= 5) {
      // Target is ≥5 levels below → damage increased by 50%
      actualDamage = Math.floor(damage * 1.5);
    }
    // Reduce health by the (possibly modified) damage amount
    const newHealth = target.health.sub(actualDamage);
    const newStatus = newHealth.value === 0 ? StatusDead : StatusAlive;
    return new Character({
      name: target.name,
      health: newHealth,
      status: newStatus,
      level: target.level,
      factions: target.factions,
    });
  }

  /**
   * Heal self by the given amount. Returns a new Character with updated state.
   * Dead characters cannot heal. Healing is capped at max health for level.
   */
  healSelf(amount: number): Character {
    // Dead characters cannot heal
    if (this.status.kind === 'dead') return this;
    // Negative heal is invalid
    if (amount < 0) throw new Error(`Heal amount must be non-negative, got ${amount}`);
    // Increase health, capped at level-based maximum
    const maxHealth = Level.maxHealthForLevel(this.level.value);
    const newHealth = this.health.add(amount, maxHealth);
    return new Character({
      name: this.name,
      health: newHealth,
      status: this.status,
      level: this.level,
      factions: this.factions,
    });
  }

  /**
   * Use a Magical Weapon to deal damage to a target.
   * Dead characters cannot use weapons. Only the owner can use a weapon.
   * Returns updated weapon and target.
   */
  useWeapon(weapon: DamageDealer, target: Character): { weapon: DamageDealer; target: Character } {
    // Dead characters cannot use weapons
    if (this.status.kind === 'dead') return { weapon, target };
    // Only the owner can use the weapon
    if (weapon.owner !== this) return { weapon, target };
    return weapon.use(target);
  }

  /**
   * Use a Healing Object to gain health.
   * Dead characters cannot use healing objects.
   * Returns updated object and character.
   */
  useHealingObject(object: Healer, amount: number): { object: Healer; character: Character } {
    // Dead characters cannot use healing objects
    if (this.status.kind === 'dead') return { object, character: this };
    return object.heal(this, amount);
  }
}
