extends CharacterBody2D

# Base Enemy Class - All enemies inherit from this

@export var max_health: float = 50.0
@export var move_speed: float = 80.0
@export var damage: float = 10.0
@export var attack_cooldown: float = 1.0

var current_health: float = 50.0
var is_dead: bool = false
var target_player: Node = null
var attack_timer: float = 0.0
var killer_player: Node = null  # Track who killed this enemy (for kill credit)

# Knockback
var is_knocked_back: bool = false
var knockback_time: float = 0.0
var knockback_duration: float = 0.3
var knockback_force: float = 125.0 # Scaled down for 1x scale

# Z-axis for flying enemies
var position_z: float = 0.0
var velocity_z: float = 0.0

# Animation
enum Direction {DOWN = 0, LEFT = 1, RIGHT = 2, UP = 3, DOWN_LEFT = 4, DOWN_RIGHT = 5, UP_LEFT = 6, UP_RIGHT = 7}
var current_direction: Direction = Direction.DOWN
var anim_frame: int = 0
var anim_time: float = 0.0
var anim_speed: float = 0.15 # Seconds per frame

@onready var sprite = get_node_or_null("Sprite2D")
@onready var shadow = get_node_or_null("Shadow")
@onready var collision_shape = get_node_or_null("CollisionShape2D")

func _ready():
	current_health = max_health
	add_to_group("enemy")
	
	# Setup shadow (if it exists - some enemies like humanoids don't have a Shadow node)
	if shadow:
		shadow.modulate = Color(0, 0, 0, 0.5)
		shadow.z_index = -1
	
	# Top-down physics
	motion_mode = MOTION_MODE_FLOATING
	
	# CRITICAL: Set collision mask to include interactable objects (layer 2) and walls (layer 7)
	# This allows enemies to collide with interactable objects so they can path around them
	# Walls are on layer 7 (bit 6 = 64), not layer 4!
	collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)

func _physics_process(delta):
	if is_dead:
		# Even when dead, allow knockback to continue briefly
		if is_knocked_back:
			knockback_time += delta
			if knockback_time >= knockback_duration:
				is_knocked_back = false
				knockback_time = 0.0
				velocity = Vector2.ZERO
			else:
				# Apply friction to slow down knockback
				velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
				move_and_slide()
		return
	
	# Only server (authority) runs AI and physics
	if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
		# Clients only interpolate position (handled by _sync_position)
		return
	
	# Update attack timer
	if attack_timer > 0:
		attack_timer -= delta
	
	# Handle knockback
	if is_knocked_back:
		knockback_time += delta
		if knockback_time >= knockback_duration:
			is_knocked_back = false
			knockback_time = 0.0
			velocity = Vector2.ZERO
		else:
			# Apply friction to slow down knockback
			velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
	
	# Enemy AI (override in subclasses) - only if not knocked back
	if not is_knocked_back:
		_ai_behavior(delta)
	
	# Move
	move_and_slide()
	
	# Check collisions with players
	_check_player_collision()
	
	# Check collisions with interactable objects
	_check_interactable_object_collision()
	
	# Sync position and animation to clients (only server sends)
	if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
		# Get state value if enemy has a state variable (for bats/slimes)
		var state_val = -1
		if "state" in self:
			state_val = get("state") as int
		# Only send RPC if we're in the scene tree
		if is_inside_tree():
			# Get enemy name/index for identification
			var enemy_name = name
			var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
			
			# Use game_world to send RPC instead of rpc() on node instance
			# This avoids node path resolution issues when clients haven't spawned yet
			var game_world = get_tree().get_first_node_in_group("game_world")
			if game_world and game_world.has_method("_sync_enemy_position"):
				# Send via game_world using enemy name/index and position for identification
				game_world._sync_enemy_position.rpc(enemy_name, enemy_index, position, velocity, position_z, current_direction, anim_frame, "", 0, state_val)
			else:
				# Fallback: try direct rpc (may fail if node path doesn't match)
				rpc("_sync_position", position, velocity, position_z, current_direction, anim_frame, "", 0, state_val)

func _ai_behavior(_delta):
	# Override in subclasses
	pass

func _check_player_collision():
	# Check if touching a player to deal damage
	for i in get_slide_collision_count():
		var collision = get_slide_collision(i)
		var collider = collision.get_collider()
		
		if collider and collider.is_in_group("player"):
			_attack_player(collider)

func _check_interactable_object_collision():
	# Check collisions with interactable objects and handle pathfinding around them
	var blocked_objects = []
	
	for i in get_slide_collision_count():
		var collision = get_slide_collision(i)
		var collider = collision.get_collider()
		
		if collider and collider.is_in_group("interactable_object"):
			var obj = collider
			
			# CRITICAL: Enemies cannot move objects that cannot be lifted
			# If object is not liftable, enemy should try to path around it
			if obj.has_method("can_be_lifted") and not obj.can_be_lifted():
				# Object cannot be lifted - store for pathfinding
				blocked_objects.append({"object": obj, "collision": collision})
			# If object is liftable but not currently being held, we can still try to push it
			# but enemies don't actively push liftable objects (only players do)
			elif obj.has_method("is_being_held") and obj.is_being_held():
				# Object is being held by someone - treat as obstacle
				blocked_objects.append({"object": obj, "collision": collision})
	
	# Handle pathfinding around blocked objects
	if blocked_objects.size() > 0 and not is_knocked_back:
		var collision_normal = blocked_objects[0].collision.get_normal()
		var _obj_pos = blocked_objects[0].object.global_position
		
		# Try to path around the object by moving perpendicular to collision normal
		# This creates a side-stepping behavior to go around obstacles
		var perpendicular = Vector2(-collision_normal.y, collision_normal.x) # Rotate 90 degrees
		
		# Choose perpendicular direction that moves toward target (if we have one)
		if target_player and is_instance_valid(target_player):
			var to_target = (target_player.global_position - global_position).normalized()
			# If perpendicular dot product with target direction is negative, flip it
			if perpendicular.dot(to_target) < 0:
				perpendicular = - perpendicular
		
		# Apply perpendicular movement (side-step around object)
		var side_step_velocity = perpendicular * move_speed * 0.7 # 70% of move speed for side-step
		velocity = velocity.lerp(side_step_velocity, 0.3) # Smoothly blend with existing velocity
		
		# Also add some push-away from object to create clearance
		var push_away = collision_normal * move_speed * 0.3
		velocity = velocity + push_away
		
		# Limit total velocity to move_speed
		if velocity.length() > move_speed:
			velocity = velocity.normalized() * move_speed
		
		# For humanoid enemies, sometimes try to destroy the object
		if has_method("_try_attack_object") and randf() < 0.1: # 10% chance per frame when blocked
			call("_try_attack_object", blocked_objects[0].object)

func _attack_player(player):
	# Attack cooldown
	if attack_timer > 0:
		return
	
	# Deal damage - send RPC to player's authority peer
	if player.has_method("rpc_take_damage"):
		var player_peer_id = player.get_multiplayer_authority()
		if player_peer_id != 0:
			# If target peer is the same as server (us), call directly
			# rpc_id() might not execute locally when called to same peer
			if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id():
				# Call directly on the same peer
				player.rpc_take_damage(damage, global_position)
			else:
				# Send RPC to remote peer
				player.rpc_take_damage.rpc_id(player_peer_id, damage, global_position)
		else:
			# Fallback: broadcast if we can't get peer_id
			player.rpc_take_damage.rpc(damage, global_position)
		attack_timer = attack_cooldown
		print(name, " attacked ", player.name, " (peer: ", player_peer_id, ", server: ", multiplayer.get_unique_id(), ")")

func _find_nearest_player() -> Node:
	var players = get_tree().get_nodes_in_group("player")
	var nearest = null
	var nearest_dist = INF
	
	for player in players:
		if player and is_instance_valid(player) and not player.is_dead:
			var dist = global_position.distance_to(player.global_position)
			if dist < nearest_dist:
				nearest_dist = dist
				nearest = player
	
	return nearest

func _find_nearest_player_in_range(max_range: float) -> Node:
	# Find nearest player within specified range
	var players = get_tree().get_nodes_in_group("player")
	var nearest = null
	var nearest_dist = INF
	
	for player in players:
		if player and is_instance_valid(player) and not player.is_dead:
			var dist = global_position.distance_to(player.global_position)
			if dist <= max_range and dist < nearest_dist:
				nearest_dist = dist
				nearest = player
	
	return nearest

func _find_nearest_player_to_position(pos: Vector2, max_range: float = 100.0) -> Node:
	# Find the nearest player to a specific position (used to find attacker)
	var players = get_tree().get_nodes_in_group("player")
	if players.is_empty():
		return null
	
	var nearest: Node = null
	var nearest_dist = max_range
	
	for player in players:
		if not is_instance_valid(player):
			continue
		var dist = pos.distance_to(player.global_position)
		if dist <= max_range and dist < nearest_dist:
			nearest_dist = dist
			nearest = player
	
	return nearest

func take_damage(amount: float, from_position: Vector2):
	# Only process damage on server/authority
	if not is_multiplayer_authority():
		return
	
	if is_dead:
		return
	
	# Find the nearest player to the attack position (likely the attacker)
	# This allows us to credit kills correctly
	var nearest_player = _find_nearest_player_to_position(from_position)
	if nearest_player:
		killer_player = nearest_player  # Update killer to the most recent attacker
	
	current_health -= amount
	print(name, " took ", amount, " damage! Health: ", current_health)
	
	# Calculate knockback direction (away from attacker)
	var knockback_direction = (global_position - from_position).normalized()
	velocity = knockback_direction * knockback_force
	is_knocked_back = true
	knockback_time = 0.0
	
	_on_take_damage()
	
	# Flash red (even if dying, show the hit)
	_flash_damage()
	
	# Show damage number (red, using dmg_numbers.png font) above enemy
	# Only show if damage > 0
	if amount > 0:
		_show_damage_number(amount, from_position)
	
	# Sync damage visual to clients
	# Use game_world to route damage visual sync instead of direct RPC to avoid node path issues
	if multiplayer.has_multiplayer_peer() and is_inside_tree():
		var enemy_name = name
		var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
		var game_world = get_tree().get_first_node_in_group("game_world")
		if game_world and game_world.has_method("_sync_enemy_damage_visual"):
			game_world._sync_enemy_damage_visual.rpc(enemy_name, enemy_index)
		else:
			# Fallback: try direct RPC (may fail if node path doesn't match)
			_sync_damage_visual.rpc()
	
	if current_health <= 0:
		# Prevent multiple death triggers
		if is_dead:
			return  # Already dying
		
		# Don't set is_dead here - let _die() set it to avoid early return bug
		# Mark as dead in _die() function instead of here
		
		# Delay death slightly so knockback is visible
		call_deferred("_die")
		
		# Notify doors that an enemy has died (if spawned from spawner)
		# This needs to happen after _die() sets is_dead, so defer it
		if has_meta("spawned_from_spawner") and get_meta("spawned_from_spawner"):
			call_deferred("_notify_doors_enemy_died")

@rpc("any_peer", "reliable")
func rpc_take_damage(amount: float, from_position: Vector2):
	# RPC version - only process on server/authority
	if is_multiplayer_authority():
		take_damage(amount, from_position)

func _show_damage_number(amount: float, from_position: Vector2):
	# Show damage number (red, using dmg_numbers.png font) above enemy
	# Only show if damage > 0
	if amount <= 0:
		return
	
	var damage_number_scene = preload("res://scenes/damage_number.tscn")
	if not damage_number_scene:
		return
	
	var damage_label = damage_number_scene.instantiate()
	if not damage_label:
		return
	
	# Set damage text and red color
	damage_label.label = str(int(amount))
	damage_label.color = Color.RED
	
	# Calculate direction from attacker (slight upward variation)
	var direction_from_attacker = (global_position - from_position).normalized()
	# Add slight upward bias
	direction_from_attacker = direction_from_attacker.lerp(Vector2(0, -1), 0.5).normalized()
	damage_label.direction = direction_from_attacker
	
	# Position above enemy's head
	var game_world = get_tree().get_first_node_in_group("game_world")
	if game_world:
		var entities_node = game_world.get_node_or_null("Entities")
		if entities_node:
			entities_node.add_child(damage_label)
			damage_label.global_position = global_position + Vector2(0, -16) # Above enemy head
		else:
			get_tree().current_scene.add_child(damage_label)
			damage_label.global_position = global_position + Vector2(0, -16)
	else:
		get_tree().current_scene.add_child(damage_label)
		damage_label.global_position = global_position + Vector2(0, -16)

func _flash_damage():
	# Flash red visual effect
	if sprite:
		var tween = create_tween()
		tween.tween_property(sprite, "modulate", Color.RED, 0.1)
		tween.tween_property(sprite, "modulate", Color.WHITE, 0.1)

func _update_client_visuals():
	# Override in subclasses to update sprite frame and visuals based on synced state
	# Base implementation just updates Z position
	if sprite:
		sprite.position.y = - position_z * 0.5
	if shadow:
		var shadow_scale = 1.0 - (position_z / 50.0) * 0.5
		shadow.scale = Vector2.ONE * max(0.3, shadow_scale)
		shadow.modulate.a = 0.5 - (position_z / 50.0) * 0.2

func _on_take_damage():
	# Override in subclasses for custom damage reactions
	pass

func _notify_doors_enemy_died():
	# Notify all doors that require enemies to check puzzle state
	# This ensures doors open immediately when the last enemy dies
	if not has_meta("spawned_from_spawner") or not get_meta("spawned_from_spawner"):
		return # Only notify if this enemy was spawned from a spawner
	
	# Find all doors in the scene that require enemies
	for door in get_tree().get_nodes_in_group("blocking_door"):
		if not is_instance_valid(door):
			continue
		
		if not door.has_method("_check_puzzle_state"):
			continue
		
		# Check if this door requires enemies (requires_enemies is a property defined in door.gd)
		# Access property directly - it's always defined in door.gd class
		if door.requires_enemies:
			# Trigger puzzle state check immediately (doors will verify if all enemies are dead)
			door.call_deferred("_check_puzzle_state")
			print("Enemy: Notified door ", door.name, " to check puzzle state after enemy death")

func _set_animation(_anim_name: String):
	# Virtual function - override in subclasses that use animation state system
	# (e.g., enemy_humanoid.gd uses player-like animation system)
	pass

func _die():
	if is_dead:
		return
	
	is_dead = true
	print(name, " died!")
	
	# Credit kill to the player who dealt the fatal damage
	if killer_player and is_instance_valid(killer_player) and killer_player.character_stats:
		killer_player.character_stats.kills += 1
		print(name, " kill credited to ", killer_player.name, " (total kills: ", killer_player.character_stats.kills, ")")
		
		# Sync kill update to client if this player belongs to a client
		# Only sync if we're on the server and the killer is a client's player
		if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
			var killer_peer_id = killer_player.get_multiplayer_authority()
			# Only sync if killer is a client (not server's own player)
			if killer_peer_id != 0 and killer_peer_id != multiplayer.get_unique_id() and killer_player.has_method("_sync_stats_update"):
				# Server is updating a client's player stats - sync to the client
				var coins = killer_player.character_stats.coin if "coin" in killer_player.character_stats else 0
				print(name, " syncing kill stats to client peer_id=", killer_peer_id, " kills=", killer_player.character_stats.kills, " coins=", coins)
				killer_player._sync_stats_update.rpc_id(killer_peer_id, killer_player.character_stats.kills, coins)
	
	# Spawn loot immediately (before death animation)
	_spawn_loot()
	
	# Sync death to all clients (only server sends RPC)
	# Use game_world to route death sync instead of direct RPC to avoid node path issues
	if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and is_inside_tree():
		var enemy_name = name
		var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
		var game_world = get_tree().get_first_node_in_group("game_world")
		if game_world and game_world.has_method("_sync_enemy_death"):
			game_world._sync_enemy_death.rpc(enemy_name, enemy_index)
		else:
			# Fallback: try direct RPC (may fail if node path doesn't match)
			_sync_death.rpc()
	
	# Play death animation (override in subclasses)
	_play_death_animation()

func _play_death_animation():
	# Override in subclasses
	await get_tree().create_timer(0.5).timeout
	queue_free()

func _spawn_loot():
	# Only spawn loot on server/authority
	if not is_multiplayer_authority():
		print(name, " _spawn_loot() called but not authority, skipping")
		return
	
	print(name, " _spawn_loot() called on authority")
	
	# Spawn random loot at enemy position
	var loot_scene = preload("res://scenes/loot.tscn")
	if not loot_scene:
		print(name, " ERROR: loot_scene is null!")
		return
	
	# Random chance to drop loot (70% chance)
	var loot_chance = randf()
	print(name, " loot chance roll: ", loot_chance, " (need > 0.3)")
	if loot_chance > 0.3:
		# Random loot type
		var loot_type = 0
		
		# 50% chance for coin
		if randf() < 0.5:
			loot_type = 0 # COIN
		# 50% chance for food item
		else:
			var food_types = [1, 2, 3] # APPLE, BANANA, CHERRY
			loot_type = food_types[randi() % food_types.size()]
		
		# Generate random velocity values (same on all clients)
		var random_angle = randf() * PI * 2
		var random_force = randf_range(50.0, 100.0)
		var random_velocity_z = randf_range(80.0, 120.0)
		
		# Generate initial velocity (same on all clients via RPC)
		var initial_velocity = Vector2(cos(random_angle), sin(random_angle)) * random_force
		
		# Find safe spawn position (on floor tile, not in walls)
		var game_world = get_tree().get_first_node_in_group("game_world")
		var safe_spawn_pos = global_position
		if game_world and game_world.has_method("_find_nearby_safe_spawn_position"):
			safe_spawn_pos = game_world._find_nearby_safe_spawn_position(global_position, 64.0)
		
		# Spawn on server
		var loot = loot_scene.instantiate()
		var entities_node = get_parent()
		if entities_node:
			entities_node.add_child(loot)
			loot.global_position = safe_spawn_pos
			loot.loot_type = loot_type
			# Set initial velocity before _ready() processes
			loot.velocity = initial_velocity
			loot.velocity_z = random_velocity_z
			loot.velocity_set_by_spawner = true
			loot.is_airborne = true
			print(name, " ✓ dropped loot: ", loot_type, " at ", safe_spawn_pos, " (original enemy pos: ", global_position, ")")
			
			# Sync loot spawn to all clients (use safe position)
			if multiplayer.has_multiplayer_peer():
				# Reuse game_world variable from above
				if game_world:
					# Generate unique loot ID
					if not "loot_id_counter" in game_world:
						game_world.loot_id_counter = 0
					var loot_id = game_world.loot_id_counter
					game_world.loot_id_counter += 1
					# Store loot ID on server loot instance
					loot.set_meta("loot_id", loot_id)
					# Sync to clients with ID
					game_world._sync_loot_spawn.rpc(safe_spawn_pos, loot_type, initial_velocity, random_velocity_z, loot_id)
					print(name, " ✓ synced loot spawn to clients")
				else:
					print(name, " ERROR: game_world not found for loot sync!")
		else:
			print(name, " ERROR: entities_node is null! Cannot spawn loot!")
	else:
		print(name, " loot chance failed (", loot_chance, " <= 0.3), no loot dropped")

# This function can be called directly (not just via RPC) when game_world routes the update
func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, dir: int = 0, frame: int = 0, anim: String = "", frame_num: int = 0, state_value: int = -1):
	# Clients receive position and animation updates from server
	# Only process if we're not the authority (i.e., we're a client)
	if is_multiplayer_authority():
		return # Server ignores its own updates
	
	# Debug: Log when client receives position update (first few times)
	if not has_meta("position_sync_count"):
		set_meta("position_sync_count", 0)
		print("Enemy ", name, " (client) RECEIVED first position sync! pos=", pos, " authority: ", get_multiplayer_authority(), " is_authority: ", is_multiplayer_authority(), " in_tree: ", is_inside_tree())
	
	var sync_count = get_meta("position_sync_count") + 1
	set_meta("position_sync_count", sync_count)
	if sync_count <= 3: # Log first 3 syncs
		print("Enemy ", name, " (client) received position sync #", sync_count, ": pos=", pos)
	
	# Update position and state
	position = pos
	velocity = vel
	position_z = z_pos
	current_direction = dir as Direction
	
	# Update state if provided (for enemies with state machines like bats/slimes)
	if state_value != -1 and "state" in self:
		set("state", state_value)
	
	# Update animation if provided (for humanoid enemies with player-like animation system)
	if anim != "":
		_set_animation(anim)
		if "current_frame" in self:
			set("current_frame", frame_num)
	else:
		# Default: use frame number for simple enemies
		anim_frame = frame
	
	# Update visual representation (override in subclasses for custom animation)
	_update_client_visuals()

# This function can be called directly (not just via RPC) when game_world routes the update
func _sync_damage_visual():
	# Clients receive damage visual sync
	# Only process if we're not the authority (i.e., we're a client)
	if is_multiplayer_authority():
		return # Server ignores its own updates
	
	_flash_damage()

# This function can be called directly (not just via RPC) when game_world routes the update
func _sync_death():
	# Clients receive death sync and play death animation locally
	# Only process if we're not the authority (i.e., we're a client)
	if is_multiplayer_authority():
		return # Server ignores its own updates
	
	if not is_dead:
		is_dead = true
		print(name, " received death sync, dying on client")
		
		# Remove collision layer so they don't collide with players, but still collide with walls
		# This matches what happens on the server when rats/slimes die
		set_collision_layer_value(2, false) # Remove from enemy collision layer (layer 2)
		
		# Immediately mark as dead and stop AI/physics
		# This prevents "inactive" enemies that are already dead
		_play_death_animation()
	else:
		# Already dead, but make sure collision is removed and it's removed from scene
		print(name, " received death sync but already dead, ensuring removal")
		
		# Remove collision layer if not already removed
		if get_collision_layer_value(2):
			set_collision_layer_value(2, false)
		
		if not is_queued_for_deletion():
			queue_free()

func _get_direction_from_vector(vec: Vector2) -> Direction:
	if vec.length() < 0.1:
		return current_direction
	
	var angle = vec.angle()
	
	# Convert angle to 8 directions
	if angle >= -PI / 8 and angle < PI / 8:
		return Direction.RIGHT
	elif angle >= PI / 8 and angle < 3 * PI / 8:
		return Direction.DOWN_RIGHT
	elif angle >= 3 * PI / 8 and angle < 5 * PI / 8:
		return Direction.DOWN
	elif angle >= 5 * PI / 8 and angle < 7 * PI / 8:
		return Direction.DOWN_LEFT
	elif angle >= 7 * PI / 8 or angle < -7 * PI / 8:
		return Direction.LEFT
	elif angle >= -7 * PI / 8 and angle < -5 * PI / 8:
		return Direction.UP_LEFT
	elif angle >= -5 * PI / 8 and angle < -3 * PI / 8:
		return Direction.UP
	return Direction.UP_RIGHT
