extends Node

@onready var role_system: Node = get_node("../RoleSystem")
@onready var cooldown_system: Node = get_node("../CooldownSystem")

func _ready() -> void:
  EventBus.ability_use_requested.connect(_on_ability_request)

func _on_ability_request(player: Node, index: int) -> void:
  if not is_instance_valid(player) or not Stats.has(player):
    return
  var target_str: String = ""
  var tv: Variant = (player as Node).get("current_target")
  if tv != null and is_instance_valid(tv) and tv is Node:
    target_str = String((tv as Node).get_path())
  if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
    _request_use.rpc_id(1, String(player.get_path()), index, target_str)
    return
  _execute(player, index, target_str)

@rpc("any_peer", "reliable")
func _request_use(path_str: String, index: int, target_str: String) -> void:
  var p: Node = get_node_or_null(NodePath(path_str))
  if p == null:
    return
  if target_str != "":
    var t := get_node_or_null(NodePath(target_str))
    if t and Stats.has(t):
      p.set("current_target", t)
  _execute(p, index, target_str)

func _execute(player: Node, index: int, _target_str: String = "") -> void:
  if index < 0 or index > 3:
    return
  var role: int = int(Stats.get_stat(player, "role", GameState.ROLE_DAMAGE))
  var set: AbilitySet = role_system.get_set(role)
  if set == null or index >= set.abilities.size():
    return
  var ability: Ability = set.abilities[index]
  if ability == null:
    return
  if not cooldown_system.is_ready(player, index):
    return
  if ability.uses_gcd and not cooldown_system.is_gcd_ready(player):
    return
  cooldown_system.set_cooldown(player, index, ability.cooldown, float(Stats.get_stat(player, "gcd_time", 0.5)) if ability.uses_gcd else 0.0)
  EventBus.ability_used.emit(player, index, ability)
  _apply_ability(player, ability)

func _apply_ability(player: Node, ability: Ability) -> void:
  match ability.type:
    Ability.Type.SINGLE:
      _apply_single(player, ability)
    Ability.Type.AOE:
      _apply_aoe(player, ability)
    Ability.Type.UTILITY:
      _apply_utility(player, ability)
    Ability.Type.ULT:
      _apply_ult(player, ability)
    _:
      pass

func _apply_single(player: Node, ability: Ability) -> void:
  var dmg_mult: float = float(Stats.get_stat(player, "buff_damage", 1.0))
  var heal_mult: float = float(Stats.get_stat(player, "buff_heal", 1.0))
  var target: Node = _resolve_target(player, ability)
  if target == null:
    return
  if (player as Node3D).global_position.distance_to((target as Node3D).global_position) > ability.ability_range:
    return
  if ability.is_heal:
    EventBus.heal_requested.emit(player, target, ability.damage * heal_mult)
  else:
    EventBus.damage_requested.emit(player, target, ability.damage * dmg_mult, ability.element)
  if ability.shield_value > 0.0:
    _apply_shield(player, ability.shield_value)

func _apply_aoe(player: Node, ability: Ability) -> void:
  var dmg_mult: float = float(Stats.get_stat(player, "buff_damage", 1.0))
  var heal_mult: float = float(Stats.get_stat(player, "buff_heal", 1.0))
  var origin: Vector3 = (player as Node3D).global_position
  var center: Vector3 = origin
  var target: Node = _resolve_target(player, ability)
  if target and (target as Node3D).global_position.distance_to(origin) <= ability.ability_range:
    center = (target as Node3D).global_position
  if ability.is_heal:
    for ally in get_tree().get_nodes_in_group("player"):
      if (ally as Node3D).global_position.distance_to(center) <= ability.aoe_radius:
        EventBus.heal_requested.emit(player, ally, ability.damage * heal_mult)
  else:
    var hits: int = 0
    for foe in Stats.entities_in_group(&"enemies"):
      if (foe as Node3D).global_position.distance_to(center) <= ability.aoe_radius:
        EventBus.damage_requested.emit(player, foe, ability.damage * dmg_mult, ability.element)
        hits += 1
    for foe in Stats.entities_in_group(&"portals"):
      if (foe as Node3D).global_position.distance_to(center) <= ability.aoe_radius:
        EventBus.damage_requested.emit(player, foe, ability.damage * dmg_mult, ability.element)
        hits += 1
    for foe in Stats.entities_in_group(&"gates"):
      if (foe as Node3D).global_position.distance_to(center) <= ability.aoe_radius:
        EventBus.damage_requested.emit(player, foe, ability.damage * dmg_mult, ability.element)
        hits += 1
    if ability.shield_value > 0.0:
      _apply_shield(player, ability.shield_value * hits)

func _apply_utility(player: Node, ability: Ability) -> void:
  if ability.shield_multiplier > 0.0:
    var max_shield: float = float(Stats.get_stat(player, "max_shield", 0.0))
    var add: float = max_shield * ability.shield_multiplier
    Stats.set_stat(player, "shield", min(max_shield, float(Stats.get_stat(player, "shield", 0.0)) + add))
    EventBus.shield_changed.emit(player, Stats.get_stat(player, "shield"), max_shield)

func _apply_ult(player: Node, ability: Ability) -> void:
  if ability.is_heal:
    _apply_aoe(player, ability)
  elif ability.shield_multiplier > 0.0:
    var max_shield: float = float(Stats.get_stat(player, "max_shield", 0.0))
    Stats.set_stat(player, "shield", min(max_shield * (1.0 + ability.shield_multiplier), float(Stats.get_stat(player, "shield", 0.0)) + max_shield * ability.shield_multiplier))
    EventBus.shield_changed.emit(player, Stats.get_stat(player, "shield"), max_shield)
  else:
    var target: Node = _resolve_target(player, ability)
    if target == null:
      return
    var dmg_mult: float = float(Stats.get_stat(player, "buff_damage", 1.0))
    EventBus.damage_requested.emit(player, target, ability.damage * dmg_mult, ability.element)
    if ability.aoe_radius > 0.0:
      var center: Vector3 = (target as Node3D).global_position
      for foe in Stats.entities_in_group(&"enemies"):
        if foe == target:
          continue
        if (foe as Node3D).global_position.distance_to(center) <= ability.aoe_radius:
          EventBus.damage_requested.emit(player, foe, ability.damage * 0.5 * dmg_mult, ability.element)

func _apply_shield(player: Node, amount: float) -> void:
  var max_shield: float = float(Stats.get_stat(player, "max_shield", 0.0))
  Stats.set_stat(player, "shield", min(max_shield, float(Stats.get_stat(player, "shield", 0.0)) + amount))
  EventBus.shield_changed.emit(player, Stats.get_stat(player, "shield"), max_shield)

func _resolve_target(player: Node, ability: Ability) -> Node:
  if ability.is_heal:
    var lowest: Node = null
    var lowest_pct: float = 2.0
    for ally in get_tree().get_nodes_in_group("player"):
      if not Stats.has(ally):
        continue
      var hp: float = float(Stats.get_stat(ally, "health", 0.0))
      var max_hp: float = float(Stats.get_stat(ally, "max_health", 1.0))
      if max_hp <= 0.0 or hp <= 0.0:
        continue
      var pct: float = hp / max_hp
      if pct < lowest_pct:
        lowest = ally
        lowest_pct = pct
    return lowest
  var tv: Variant = (player as Node).get("current_target")
  if tv != null and is_instance_valid(tv) and tv is Node and Stats.has(tv):
    return tv as Node
  var nearest: Node = null
  var nearest_dist: float = INF
  for foe in Stats.entities_in_group(&"enemies") + Stats.entities_in_group(&"portals") + Stats.entities_in_group(&"gates"):
    var d: float = (foe as Node3D).global_position.distance_to((player as Node3D).global_position)
    if d < nearest_dist and d <= ability.ability_range:
      nearest = foe
      nearest_dist = d
  return nearest
