extends Node2D

# Sword Projectile - Travels away from player and deals damage

@export var damage: float = 20.0
@export var initial_speed: float = 200.0 # Very fast initial speed
@export var deceleration: float = 750.0 # How quickly it slows down
@export var lifetime: float = 0.5 # How long the projectile lasts
@export var max_distance: float = 10.0 # Maximum distance to travel

var current_speed: float = 0.0

var travel_direction: Vector2 = Vector2.RIGHT
var elapsed_time: float = 0.0
var distance_traveled: float = 0.0
var player_owner: Node = null
var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup)

@onready var sprite = $Sprite2D
@onready var hit_area = $Area2D

func _ready():
	$SfxSwosh.play()
	# Connect area signals (only if not already connected)
	if hit_area and not hit_area.body_entered.is_connected(_on_body_entered):
		hit_area.body_entered.connect(_on_body_entered)

func setup(direction: Vector2, owner_player: Node, damage_value: float = 20.0):
	travel_direction = direction.normalized()
	player_owner = owner_player
	damage = damage_value # Set damage from player
	current_speed = initial_speed
	
	# Rotate sprite to face travel direction
	rotation = direction.angle()

func _physics_process(delta):
	elapsed_time += delta
	
	# Check lifetime
	if elapsed_time >= lifetime or distance_traveled >= max_distance:
		$Area2D.set_deferred("monitoring", false)
		self.visible = false
		if $SfxImpactWall.playing:
			await $SfxImpactWall.finished
		if $SfxImpact.playing:
			await $SfxImpact.finished
		queue_free()
		return
	
	# Decelerate quickly
	current_speed -= deceleration * delta
	current_speed = max(0.0, current_speed) # Don't go negative
	
	# Move in travel direction
	var movement = travel_direction * current_speed * delta
	global_position += movement
	distance_traveled += movement.length()
	
	# Fade out quickly (based on speed)
	var alpha = current_speed / initial_speed # 1.0 at start, 0.0 when stopped
	if sprite:
		sprite.modulate.a = alpha

func _on_body_entered(body):
	# Don't hit the owner
	if body == player_owner:
		return
	
	# Don't hit the same target twice - use Dictionary for O(1) lookup to prevent race conditions
	if body in hit_targets:
		return
	
	# CRITICAL: Only the projectile owner (authority) should deal damage
	# This prevents duplicate damage when the projectile exists on both server and clients
	# Server creates the projectile first, then clients create it via _sync_attack
	# Without this check, both server and client projectiles would deal damage
	if player_owner and not player_owner.is_multiplayer_authority():
		return # Only the authority (creator) of the projectile can deal damage
	
	# Add to hit_targets IMMEDIATELY to prevent multiple hits (mark as hit before processing)
	hit_targets[body] = true
	
	# Friendly fire: only skip when owner is also a player. Enemies can hit players.
	if body.is_in_group("player") and body.has_method("rpc_take_damage"):
		if player_owner and player_owner.is_in_group("player"):
			return # Owner is player → pass through, no damage
		# Owner is enemy → deal damage to player (same pattern as enemy_base)
		var attacker_pos = player_owner.global_position if player_owner else global_position
		var player_peer_id = body.get_multiplayer_authority()
		if player_peer_id != 0:
			if multiplayer.get_unique_id() == player_peer_id:
				body.rpc_take_damage(damage, attacker_pos, false, false)
			else:
				body.rpc_take_damage.rpc_id(player_peer_id, damage, attacker_pos, false, false)
		else:
			body.rpc_take_damage.rpc(damage, attacker_pos, false, false)
		if has_node("SfxImpact"):
			$SfxImpact.play()
		return

	# Deal damage to enemies - only authority (creator) deals damage
	if body.is_in_group("enemy") and body.has_method("rpc_take_damage"):
		var attacker_pos = player_owner.global_position if player_owner else global_position
		var is_crit = get_meta("is_crit") if has_meta("is_crit") else false
		
		# Check hit chance (based on player's DEX stat)
		var hit_roll = randf()
		var hit_chance = 0.95 # Base hit chance
		if player_owner and player_owner.character_stats:
			hit_chance = player_owner.character_stats.hit_chance
		var is_miss = hit_roll >= hit_chance
		
		if is_miss:
			# Attack missed - show MISS text, don't deal damage, don't play impact sound
			print("Player MISSED enemy: ", body.name, "! (hit chance: ", hit_chance * 100.0, "%)")
			# Show MISS text on the enemy
			if body.has_method("_show_damage_number"):
				body._show_damage_number(0.0, attacker_pos, false, true, false) # is_miss = true
			return # Don't deal damage, don't play impact sound, don't cause knockback
		
		# Hit successful - play impact sound and deal damage
		$SfxImpact.play()
		
		# Use game_world to route damage request instead of direct RPC to avoid node path issues
		var game_world = get_tree().get_first_node_in_group("game_world")
		var enemy_name = body.name
		var enemy_index = body.get_meta("enemy_index") if body.has_meta("enemy_index") else -1
		
		if game_world and game_world.has_method("_request_enemy_damage"):
			if multiplayer.is_server():
				# Server can call directly
				game_world._request_enemy_damage(enemy_name, enemy_index, damage, attacker_pos, is_crit)
			else:
				# Client sends RPC to server
				game_world._request_enemy_damage.rpc_id(1, enemy_name, enemy_index, damage, attacker_pos, is_crit)
		else:
			# Fallback: try direct call (may fail if node path doesn't match)
			var enemy_peer_id = body.get_multiplayer_authority()
			if enemy_peer_id != 0:
				if multiplayer.is_server() and enemy_peer_id == multiplayer.get_unique_id():
					body.rpc_take_damage(damage, attacker_pos, is_crit)
				else:
					body.rpc_take_damage.rpc_id(enemy_peer_id, damage, attacker_pos, is_crit)
			else:
				body.rpc_take_damage.rpc(damage, attacker_pos, is_crit)
		
		# Slash hit effect (sync so all clients see it)
		var hit_pos = body.global_position
		if game_world and game_world.has_method("_sync_slash_hit_effect"):
			if multiplayer.is_server():
				game_world._sync_slash_hit_effect(hit_pos.x, hit_pos.y)
				if game_world.has_method("_rpc_to_ready_peers"):
					game_world._rpc_to_ready_peers("_sync_slash_hit_effect", [hit_pos.x, hit_pos.y])
			else:
				game_world._request_slash_hit_effect.rpc_id(1, hit_pos.x, hit_pos.y)
		
		# Debug print - handle null player_owner safely
		var owner_name: String = "none"
		var is_authority: bool = false
		if player_owner:
			owner_name = str(player_owner.name)
			is_authority = player_owner.is_multiplayer_authority()
		print("Sword projectile hit enemy: ", body.name, " for ", damage, " damage! (owner: ", owner_name, " is_authority: ", is_authority, ")")
		return # Don't apply generic knockback, take_damage handles it
	
	# Deal damage to boxes or other damageable objects
	elif "health" in body:
		#$SfxImpact.play()
		# Boxes have health property
		body.health -= damage
		if body.health <= 0:
			# Get object identifier - prefer consistent name (InteractableObject_X) or use object_index meta
			var obj_name = body.name
			var obj_index = -1
			
			# Check if object has object_index meta (set when spawned in dungeon)
			if body.has_meta("object_index"):
				obj_index = body.get_meta("object_index")
				# Use consistent naming if we have the index
				if obj_index >= 0:
					obj_name = "InteractableObject_%d" % obj_index
			
			# Fallback: if name doesn't start with "InteractableObject_", try to find consistent name
			if not obj_name.begins_with("InteractableObject_") and obj_index < 0:
				# Try to find object by position or other means
				# For now, just use the name as-is and log a warning
				print("Sword projectile: Warning - object ", body.name, " doesn't have consistent naming!")
			
			# Sync break to server (which will broadcast to all clients)
			# Use game_world to route the RPC to avoid node path resolution issues
			if multiplayer.has_multiplayer_peer():
				var game_world = get_tree().get_first_node_in_group("game_world")
				# Only send RPC if game_world exists and is valid
				if game_world and is_instance_valid(game_world) and game_world.is_inside_tree() and game_world.has_method("_sync_object_break"):
					if multiplayer.is_server():
						# Server: broadcast to ready clients (breaks locally too via _sync_break)
						if game_world.has_method("_rpc_to_ready_peers"):
							game_world._rpc_to_ready_peers("_sync_object_break", [obj_name])
						print("Sword projectile synced box break to all clients: ", obj_name)
					else:
						# Client: send request to server (server will break locally and broadcast to all)
						# Also send object_index if available for fallback lookup
						game_world._sync_object_break.rpc_id(1, obj_name)
						print("Sword projectile requested box break on server: ", obj_name, " (index: ", obj_index, ")")
				else:
					print("Sword projectile: GameWorld not ready, skipping box break sync for ", obj_name)
			
			# Break locally AFTER syncing (so node still exists for RPC path resolution)
			if body.has_method("_break_into_pieces"):
				body._break_into_pieces()
				print("Sword projectile broke box locally: ", body.name)
		print("Sword projectile hit object: ", body.name)
	
	# Push the hit target away slightly (only for non-enemies)
	if body is CharacterBody2D and not body.is_in_group("enemy"):
		var knockback_dir = (body.global_position - global_position).normalized()
		if body.is_in_group("player") and "is_dead" in body and body.is_dead:
			const CORPSE_KNOCKBACK: float = 50.0
			var pid = body.get_multiplayer_authority()
			if pid == multiplayer.get_unique_id():
				body.rpc_apply_corpse_knockback(knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK)
			elif pid != 0:
				body.rpc_apply_corpse_knockback.rpc_id(pid, knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK)
			else:
				body.rpc_apply_corpse_knockback.rpc(knockback_dir.x, knockback_dir.y, CORPSE_KNOCKBACK)
		else:
			body.velocity = knockback_dir * 200.0
