# src/player.py
import pygame
from client.resource_path import get_asset_path
import pytmx
import os
from client.data import config
from client.src.player_animation import load_player_animations

class Player:
    # Structure: WEAPON_OFFSETS[weapon_name][anim_key][direction] = (dx, dy)
    # dx positive => move right, dy positive => move down

    WEAPON_OFFSETS = {
        "sword_01": {
            "idle": {
                "right": (-120, 115),
                "left":  (-120, 115),
            },
            "halfslash": {
                "right": (10, 75),
                "left":  (-10, 75),
            },
        },
    }

    def __init__(self, name="", gender="Male", hair_style="Medium 01 - Page", hair_color=None,
                 clothing_style="Clothing1", x=None, y=None, stats=None, char_id=None, spawned=False):
        self.id = char_id

        # Ensure stats is always a dict to avoid .get() attribute errors
        self.stats = stats or {}
        self.level = self.stats.get("Level", 0)

        # Basic identity / appearance
        self.name = name
        self.gender = gender
        self.hair_style = hair_style
        self.hair_color = hair_color
        self.clothing_style = clothing_style
        self.x = x if x is not None else 0
        self.y = y if y is not None else 0
        
        self.spawned = spawned

        # Network interpolation
        self.target_x = self.x
        self.target_y = self.y

        self.collision_width = 128
        self.collision_height = 128
        self.speed = self.stats.get("SPD", 1.0)
        self.jump_strength = -self.stats.get("JMP", 8.0)
        self.gravity = self.stats.get("GRAVITY", 0.6)
        self.dx = 0
        self.dy = 0
        self.on_ground = False
        self.inventory = []
        self.equipment = {}
        self.items = {}
        self.inventory_rows = 6
        self.inventory_cols = 10
        self.current_frame = 0
        self.frame_timer = 0
        self.direction = "right"
        self.attacking = False
        self.current_state = "idle"
        self.attack_idle_timer = 0
        self.attack_idle_duration = 10.0
        # Track equipped weapon info
        self.equipped_weapon_sprite = None  # e.g., "sword_01", "axe_01"
        self.equipped_weapon_type = None    # e.g., "sword", "axe", "bow"
        self.equipped_weapon_range = 100    # Attack range from weapon stats

        # Debug toggle: set to True to draw attack hitbox on-screen
        self.debug_draw_hitbox = False
        
        # Server debug data for comparison - receives debug data from server
        # when server.client_handler.debug_draw_hitbox = True
        # Shows both client (red) and server (blue) hitboxes for comparison
        self.server_debug_data = None

        # Load animations
        self.animations = load_player_animations(self)

        # Determine base sprite dimensions safely (use a sample frame if available)
        sample_layer = (self.animations.get("walk", {})
                                       .get("body", []))
        try:
            sample_surface = sample_layer[0][0]
            base_w, base_h = sample_surface.get_width(), sample_surface.get_height()
        except Exception:
            base_w, base_h = (64 * 3, 64 * 3)
        
        # Store base dimensions for character positioning
        self.base_sprite_width = base_w
        self.base_sprite_height = base_h

        max_weapon_w = 0
        max_weapon_h = 0
        weapons = self.animations.get("weapons", {}) or {}
        for weapon_name, weapon_anims in weapons.items():
            if not weapon_anims:
                continue
            for anim_key, anim_rows in weapon_anims.items():
                if not anim_rows:
                    continue
                for row in anim_rows:
                    if not row:
                        continue
                    for surf in row:
                        if surf:
                            try:
                                w, h = surf.get_size()
                                if w > max_weapon_w:
                                    max_weapon_w = w
                                if h > max_weapon_h:
                                    max_weapon_h = h
                            except Exception:
                                continue

        # Choose padding and compute a composite surface that can hold body + weapon
        # Keep a left_padding so the weapon can extend left of the character anchor if needed.
        left_padding = 100
        right_padding = max(100, max_weapon_w - (base_w // 2)) if max_weapon_w else 100

        # Composite width and height: ensure weapon fits horizontally and vertically
        comp_w = left_padding + base_w + right_padding
        comp_h = max(base_h, max_weapon_h)

        # Fallback if sizes are too small
        if comp_w < base_w + 200:
            comp_w = base_w + 200
        if comp_h < base_h:
            comp_h = base_h

        self.left_padding = left_padding
        self.right_padding = right_padding
        self.image = pygame.Surface((comp_w, comp_h), pygame.SRCALPHA)

        self.game = None
        self.update_movement_stats()
        self.calculate_max_hp()

    @classmethod
    def from_dict(cls, data: dict):
        """
        Single robust from_dict that supports both legacy flat dicts and
        nested 'appearance' blocks.
        """
        appearance = data.get("appearance", {})
        # prefer appearance.* if present, else flat keys
        gender = appearance.get("gender", data.get("gender", "Male"))
        hair_style = appearance.get("hair", data.get("hair_style", "Medium 01 - Page"))
        hair_color = appearance.get("hair_color", data.get("hair_color", None))

        return cls(
            name=data.get("name", ""),
            gender=gender,
            hair_style=hair_style,
            hair_color=hair_color,
            clothing_style=data.get("clothing_style", "Clothing1"),
            x=data.get("x", 0),
            y=data.get("y", 0),
            stats=data.get("stats", {}),
            char_id=data.get("id"),
            spawned=data.get("spawned", False)
        )

    def set_game(self, gameloop):
        """Called by GameLoop to provide collision/map context"""
        self.game = gameloop

        # Keep collision box at fixed 128x128 to match server
        # (Tile-based collision would require server sync)
        # if hasattr(gameloop, "current_map"):
        #     tile_w = gameloop.current_map.tilewidth
        #     tile_h = gameloop.current_map.tileheight
        #     self.collision_width = tile_w
        #     self.collision_height = tile_h

        self.update_movement_stats()

    def handle_input(self, keys):
        """
        Handles player control + movement + collisions.
        (Animation state changes are set here; frame updates are done in update_animation())
        """
        # reset horizontal velocity (basic approach)
        self.dx = 0

        # --- Horizontal movement ---
        if not self.attacking:
            if keys[pygame.K_LEFT] or keys[pygame.K_a]:
                self.dx = -self.speed
                self.direction = "left"
            elif keys[pygame.K_RIGHT] or keys[pygame.K_d]:
                self.dx = self.speed
                self.direction = "right"
        else:
            self.dx = 0

        # --- Jumping ---
        if keys[pygame.K_SPACE] and self.on_ground:
            self.dy = self.jump_strength
            self.on_ground = False

        # --- Simple Attack ---
        if keys[pygame.K_LCTRL] and not self.attacking:
            self.attacking = True
            self.current_state = "attack"
            self.current_frame = 0
            self.frame_timer = 0
            self.attack_idle_timer = 0

            # Only send combat if fully initialized
            if (hasattr(self, 'game') and 
                hasattr(self.game, 'enemy_manager') and 
                hasattr(self.game, 'client') and
                self.spawned):  # ← ADD spawned check
                
                target_id = self.get_nearest_enemy_in_range(self.game.enemy_manager, self.equipped_weapon_range)
                if target_id:
                    from client.src.send_json import send_attack
                    # Include current position and direction for server sync debugging
                    send_attack(self.game.client, target_id,
                              player_pos=(int(self.x), int(self.y)),
                              player_direction=self.direction)
        
        # Cancel attack idle if moving or jumping
        if self.dx != 0 or not self.on_ground:
            self.attack_idle_timer = 0

        # --- Horizontal collision / movement ---
        new_x = self.x + self.dx
        if not self._collides(new_x, self.y, self.dx, 0):
            self.x = new_x

        # --- Apply gravity only if not on ground ---
        if not self.on_ground:
            self.dy += self.gravity
            if self.dy > 12:  # terminal velocity
                self.dy = 12

        # --- Vertical movement and collision ---
        new_y = self.y + self.dy

        if self.dy > 0:  # Falling
            bottom_left = (self.x, new_y + self.collision_height)
            bottom_right = (self.x + self.collision_width - 1, new_y + self.collision_height)

            if self._collides(*bottom_left, 0, self.dy) or self._collides(*bottom_right, 0, self.dy):
                self.on_ground = True
                self.dy = 0

                feet_y_pos = new_y + self.collision_height
                tile_y = int(feet_y_pos // self.game.current_map.tileheight)

                offset_correction = self.game.current_map.tileheight - 1 #+63
                self.y = (tile_y * self.game.current_map.tileheight) - self.collision_height + offset_correction
            else:
                self.y = new_y
                self.on_ground = False

        elif self.dy < 0:  # Jumping / moving up
            top_left = (self.x, new_y)
            top_right = (self.x + self.collision_width - 1, new_y)

            if self._collides(*top_left, 0, self.dy) or self._collides(*top_right, 0, self.dy):
                self.dy = 0  # hit ceiling
                self.y = new_y
            else:
                self.y = new_y

        # --- Prevent falling out of the map ---
        if self.game and getattr(self.game, "current_map", None):
            map_px_height = self.game.current_map.height * self.game.current_map.tileheight
            if self.y > map_px_height + 100:  # add a small buffer
                print("[DEBUG] Player fell off map — respawning at safe height")
                self.y = 750
                self.x = max(1110, min(self.x, self.game.current_map.width * self.game.current_map.tilewidth - self.collision_width))
                self.dy = 0
                self.on_ground = False

        # Update current_state if not attacking (attack state must finish first)
        # Also preserve attack_idle state while timer is active
        if not self.attacking and self.attack_idle_timer <= 0:
            if not self.on_ground:
                self.current_state = "jump"
            elif self.dx != 0:
                self.current_state = "walk"
            else:
                self.current_state = "idle"
        
        # --- Attack idle timer ---
        if self.attack_idle_timer > 0:
            self.attack_idle_timer -= self.game.clock.get_time() / 1000.0  # convert ms → seconds
            if self.attack_idle_timer <= 0:
                self.attack_idle_timer = 0
                if not self.attacking and self.on_ground:
                    self.current_state = "idle"

        # Finally: advance animation (use current_state which respects attacking)
        self.update_animation()

    def get_nearest_enemy_in_range(self, enemy_manager, attack_range=100):
        """Find the nearest enemy within attack range using an actual attack hitbox.
        
        Uses collision-based hitbox to match server-side calculations.
        This replaces the previous naive distance-only check with a collision
        test between the weapon hitbox (or fallback directional range) and the
        enemy bounding boxes.
        """
        if not enemy_manager or not getattr(enemy_manager, "enemies", None):
            return None
        
        # Use collision-based hitbox to match server calculations
        attack_rect = self.get_collision_center_attack_hitbox(attack_range)
        if not attack_rect:
            return None

        nearest_enemy = None
        nearest_distance = float('inf')

        for enemy_id, enemy in enemy_manager.enemies.items():
            try:
                enemy_rect = pygame.Rect(int(enemy.x), int(enemy.y), int(getattr(enemy, "collision_width", 32)), int(getattr(enemy, "collision_height", 32)))
            except Exception:
                # if enemy lacks attributes, skip
                continue

            if attack_rect.colliderect(enemy_rect):
                # choose the closest by horizontal distance as a tie-breaker
                dist = abs((enemy.x + enemy_rect.width/2) - (self.x + self.collision_width/2))
                if dist < nearest_distance:
                    nearest_distance = dist
                    nearest_enemy = enemy_id

        return nearest_enemy

    def get_attack_hitbox(self, attack_range=100):
        """
        Compute the world-space rectangle representing the player's attack hitbox.
        Uses sprite-edge based calculation to match server-side validation.
        Returns: pygame.Rect in world coordinates.
        """
        # Always use sprite-edge calculation (matches server logic)
        # The sprite is rendered with its composite image, need to find where sprite actually appears
        composite_width = self.image.get_width()
        
        # This is where the composite image starts in world coords (from draw() method)
        composite_left_world = self.x - (composite_width - self.collision_width) // 2
        
        # The character sprite is drawn at char_offset_x inside the composite
        char_offset_x = getattr(self, "left_padding", 100)
        sprite_left_world = composite_left_world + char_offset_x
        sprite_right_world = sprite_left_world + getattr(self, "base_sprite_width", 179)
        
        # ADJUST THESE VALUES to move hitbox left (negative) or right (positive)
        hitbox_offset_right = -90  # Adjust for right-facing attacks
        hitbox_offset_left = 90   # Adjust for left-facing attacks
        
        if self.direction == "right":
            # Attack starts from right edge of sprite + offset
            rect_x = int(sprite_right_world + hitbox_offset_right)
        else:
            # Attack ends at left edge of sprite + offset
            rect_x = int(sprite_left_world - attack_range + hitbox_offset_left)
            
        return pygame.Rect(rect_x, int(self.y), attack_range, self.collision_height)

    def update_animation(self):
        """
        Advance animation frames for the current_state. Safe against missing frames.
        """
        state = "attack" if self.attacking else self.current_state

        frames_dict = self.animations.get(state, self.animations.get("idle", {}))
        body_rows = frames_dict.get("body", [])

        # Row selection (left/right -> different rows in sheet)
        row_map = {"left": 1, "right": 3}
        preferred_row = row_map.get(self.direction, 3)
        # choose a usable row that exists
        if body_rows and preferred_row < len(body_rows):
            row = preferred_row
        elif body_rows and len(body_rows) > 0:
            row = min(preferred_row, len(body_rows) - 1)
        else:
            row = 0

        num_frames = 1
        if body_rows and row < len(body_rows):
            num_frames = max(1, len(body_rows[row]))

        # increment timer
        self.frame_timer += 1

        if state == "walk":
            if self.frame_timer >= 8:
                self.current_frame = (self.current_frame + 1) % num_frames
                self.frame_timer = 0

        elif state == "jump":
            if self.frame_timer >= 8:
                if self.current_frame < num_frames - 1:
                    self.current_frame += 1
                self.frame_timer = 0

        elif state == "attack":
            # attack should play through once then stop
            base_attack_delay = 8.0  # baseline frame delay
            attack_spd = max(0.1, float(self.stats.get("ATT_SPD", 1.0)))  # prevent divide-by-zero
            frame_delay = max(1, int(base_attack_delay / attack_spd))

            if self.frame_timer >= frame_delay:
                self.current_frame += 1
                self.frame_timer = 0

                if self.current_frame >= num_frames:
                    # Finished attack animation
                    self.current_frame = 0
                    self.attacking = False
                    self.current_state = "attack_idle"
                    self.attack_idle_timer = self.attack_idle_duration

        elif state == "attack_idle":
            # Slightly slower breathing/idle stance during post-attack period
            if self.frame_timer >= 24:
                self.current_frame = (self.current_frame + 1) % num_frames
                self.frame_timer = 0

        else:  # idle
            self.current_frame = 0
            self.frame_timer = 0

    def get_nearby_loot_target(self, max_distance=64):
        if not hasattr(self, "game") or not self.game:
            return None
        return None

    def _collides(self, px, py, dx, dy):
        if not self.game or not getattr(self.game, "current_map", None):
            return False

        tilewidth = self.game.current_map.tilewidth
        tileheight = self.game.current_map.tileheight

        for layer in self.game.current_map.visible_layers:
            # Only check tile layers with the "collision" property
            if isinstance(layer, pytmx.TiledTileLayer) and layer.properties.get("collision"):
                for cx in (px, px + self.collision_width - 1):
                    for cy in (py, py + self.collision_height - 1):
                        tile_x = int(cx // tilewidth)
                        tile_y = int(cy // tileheight)

                        # Check if within bounds
                        if 0 <= tile_x < layer.width and 0 <= tile_y < layer.height:
                            if layer.data[tile_y][tile_x] != 0:  # non-empty tile = collision
                                return True
        return False
    
    def update_equipped_weapon(self, weapon_data):
        """
        Update the player's equipped weapon from inventory data.
        Call this when inventory changes.
        
        Args:
            weapon_data: dict with 'item_id' and 'seed_data'
        """
        if not weapon_data:
            self.equipped_weapon_sprite = None
            self.equipped_weapon_type = None
            self.equipped_weapon_range = 100  # Reset to default
            return
        
        seed_data = weapon_data.get("seed_data", {})
        subtype = seed_data.get("subtype", "")  # "sword", "axe", "bow"
        
        # Extract attack_range from weapon stats
        stats = seed_data.get("stats", {})
        self.equipped_weapon_range = stats.get("attack_range", 100)
        
        # Map weapon subtype to sprite folder
        weapon_sprite_map = {
            "sword": "sword_01",
            "axe": "axe_01",
            "bow": "bow_01",
            "dagger": "dagger_01",
            "staff": "staff_01",
        }
        
        self.equipped_weapon_type = subtype
        self.equipped_weapon_sprite = weapon_sprite_map.get(subtype, "sword_01")
        
        print(f"[Player] Equipped weapon: {seed_data.get('name')} ({self.equipped_weapon_sprite}) Range: {self.equipped_weapon_range}")
    
    def get_weapon_surface(self):
        """
        Get the current weapon sprite frame based on player state.
        Returns the appropriate weapon frame for idle vs attacking.
        """
        if not self.equipped_weapon_sprite:
            #print("[Weapon Debug] No equipped weapon sprite")
            return None
        
        # Only show weapon during attack or attack_idle states
        if self.current_state not in ["attack", "attack_idle"] and not self.attacking:
            #print(f"[Weapon Debug] Wrong state: {self.current_state}, attacking: {self.attacking}")
            return None
        
        weapons = self.animations.get("weapons", {})
        weapon_anims = weapons.get(self.equipped_weapon_sprite, {})
        
        if not weapon_anims:
            #print(f"[Weapon Debug] No weapon anims for {self.equipped_weapon_sprite}")
            return None
        
        # Choose animation based on state
        if self.attacking:
            weapon_frames = weapon_anims.get("halfslash", [])
            row_map = {"right": 3, "left": 1}
            #print(f"[Weapon Debug] Attacking - halfslash frames: {len(weapon_frames)} rows")
        elif self.current_state == "attack_idle":
            weapon_frames = weapon_anims.get("idle", [])
            # Use the same left/right row mapping as body sprites
            row_map = {"left": 1, "right": 3}
            #print(f"[Weapon Debug] Attack_idle - idle frames: {len(weapon_frames)} rows")
        else:
            # No weapon for normal idle/walk
            return None
        
        if not weapon_frames:
            #print("[Weapon Debug] No weapon frames!")
            return None
        
        # Get correct row based on direction and state
        row = row_map.get(self.direction, 0)
        
        # Safety checks
        if row >= len(weapon_frames):
            row = len(weapon_frames) - 1
            #print(f"[Weapon Debug] Row out of bounds, using row {row}")
        
        if not weapon_frames[row]:
            #print(f"[Weapon Debug] Empty row {row}")
            return None
        
        # Get current frame (use player's animation frame)
        frame_index = min(self.current_frame, len(weapon_frames[row]) - 1)
        
        #print(f"[Weapon Debug] Direction: {self.direction}, Row: {row}, Frame: {frame_index}, State: {self.current_state}")
        
        try:
            surf = weapon_frames[row][frame_index]
            #print(f"[Weapon Debug] Returning surface: {surf.get_size()}")
            return surf
        except (IndexError, KeyError) as e:
            #print(f"[Weapon Debug] Error getting surface: {e}")
            return None

    def draw(self, screen, offset_x=0, offset_y=0):
        sprite_offset_y = 10

        state = "attack" if self.attacking else self.current_state

        frames_dict = self.animations.get(state, self.animations.get("idle", {}))
        body_rows = frames_dict.get("body", [])

        # Determine row
        row_map = {"left": 1, "right": 3}
        preferred_row = row_map.get(self.direction, 3)
        if body_rows and preferred_row < len(body_rows):
            row = preferred_row
        elif body_rows and len(body_rows) > 0:
            row = min(preferred_row, len(body_rows) - 1)
        else:
            row = 0

        # Clear previous composite
        self.image.fill((0, 0, 0, 0))

        # Use computed left padding so weapon & body stay aligned
        char_offset_x = getattr(self, "left_padding", 100)
        char_offset_y = self.image.get_height() - getattr(self, "base_sprite_height", self.image.get_height())

        # Draw body layers
        for layer in ["body", "legs", "feet", "torso", "head", "hair"]:
            layer_frames = frames_dict.get(layer, [])
            if not layer_frames:
                continue
            if row >= len(layer_frames):
                use_row = min(row, len(layer_frames) - 1)
            else:
                use_row = row

            if not layer_frames[use_row]:
                continue

            max_frame_index = len(layer_frames[use_row]) - 1
            frame_index = min(self.current_frame, max_frame_index)
            self.image.blit(layer_frames[use_row][frame_index], (char_offset_x, char_offset_y))

        # Draw weapon if equipped
        if self.equipped_weapon_sprite:
            weapon_surf = self.get_weapon_surface()
            if weapon_surf:
                weapon_w, weapon_h = weapon_surf.get_size()

                # Optional scale-down safety (keep if you want automatic fits)
                if weapon_h > self.image.get_height() or weapon_w > self.image.get_width():
                    scale_factor = min(self.image.get_width() / weapon_w, self.image.get_height() / weapon_h, 1.0)
                    new_w = max(1, int(weapon_w * scale_factor))
                    new_h = max(1, int(weapon_h * scale_factor))
                    weapon_surf = pygame.transform.scale(weapon_surf, (new_w, new_h))
                    weapon_w, weapon_h = weapon_surf.get_size()

                # Choose which animation we are using for the weapon
                anim_key = "halfslash" if self.attacking else "idle"

                # Horizontal anchor:
                # - idle: align the weapon's left to the char_offset_x (keeps your idle tuning)
                # - halfslash: center the weapon over the character (so swings originate from body)
                char_center_x = char_offset_x + (getattr(self, "base_sprite_width", 0) // 2)
                if anim_key == "halfslash":
                    # center the weapon over the character by default
                    weapon_x = char_center_x - (weapon_w // 2)
                else:
                    # idle: keep the same left-based anchor you used earlier
                    weapon_x = char_offset_x

                # Vertical baseline alignment (same baseline approach you used)
                weapon_y = char_offset_y + getattr(self, "base_sprite_height", self.image.get_height()) - weapon_h

                # Apply per-weapon, per-animation, per-direction offsets (if present)
                dx, dy = (0, 0)
                dx = dy = 0
                try:
                    dx, dy = self.WEAPON_OFFSETS.get(self.equipped_weapon_sprite, {}) \
                                                .get(anim_key, {}) \
                                                .get(self.direction, (0, 0))
                except Exception:
                    dx, dy = (0, 0)

                weapon_x += dx
                weapon_y += dy

                #print(f"[Weapon Draw] Blitting weapon at ({weapon_x}, {weapon_y}), canvas size: {self.image.get_size()}, weapon size: {(weapon_w, weapon_h)}, anim={anim_key}, dir={self.direction}")
                self.image.blit(weapon_surf, (weapon_x, weapon_y))

        # Calculate screen position
        # Keep previous behavior of anchoring composite bottom to the collision box so baseline is stable.
        x_offset_corrected = self.x + offset_x - (self.image.get_width() - self.collision_width) // 2
        y_offset_corrected = self.y + offset_y - (self.image.get_height() - self.collision_height) + sprite_offset_y

        # Draw final composite
        screen.blit(self.image, (x_offset_corrected, y_offset_corrected))

        # Debug: draw the attack hitbox (world -> screen by adding offset)
        if self.debug_draw_hitbox:
            try:
                # CLIENT-SIDE HITBOX (RED) - Visual/sprite-based calculation
                attack_rect = self.get_attack_hitbox(attack_range=self.equipped_weapon_range)
                if attack_rect:
                    screen_rect = attack_rect.move(offset_x, offset_y)
                    # semi-transparent fill
                    s = pygame.Surface((screen_rect.width, screen_rect.height), pygame.SRCALPHA)
                    s.fill((255, 0, 0, 60))  # red translucent
                    screen.blit(s, (screen_rect.x, screen_rect.y))
                    # outline
                    pygame.draw.rect(screen, (255, 0, 0), screen_rect, 2)
                    # draw center crosshair for quick reference
                    cx = screen_rect.x + screen_rect.width // 2
                    cy = screen_rect.y + screen_rect.height // 2
                    pygame.draw.line(screen, (255, 255, 0), (cx - 6, cy), (cx + 6, cy), 2)
                    pygame.draw.line(screen, (255, 255, 0), (cx, cy - 6), (cx, cy + 6), 2)
                
                # CLIENT-SIDE COLLISION HITBOX (ORANGE) - Collision-based calculation (should match server)
                collision_rect = self.get_collision_center_attack_hitbox(attack_range=100)
                if collision_rect:
                    collision_screen_rect = collision_rect.move(offset_x, offset_y)
                    # Orange semi-transparent fill
                    s_collision = pygame.Surface((collision_screen_rect.width, collision_screen_rect.height), pygame.SRCALPHA)
                    s_collision.fill((255, 128, 0, 40))  # orange translucent
                    screen.blit(s_collision, (collision_screen_rect.x, collision_screen_rect.y))
                    # Orange outline
                    pygame.draw.rect(screen, (255, 128, 0), collision_screen_rect, 2)
                
                # SERVER-SIDE HITBOX (BLUE) - if we have server debug data
                if hasattr(self, 'server_debug_data') and self.server_debug_data:
                    server_attack = self.server_debug_data.get('attack_rect', {})
                    server_enemy = self.server_debug_data.get('enemy_rect', {})
                    
                    # Draw server attack hitbox in blue
                    if server_attack:
                        server_rect = pygame.Rect(
                            server_attack['x'] + offset_x,
                            server_attack['y'] + offset_y,
                            server_attack['w'],
                            server_attack['h']
                        )
                        # Blue semi-transparent fill
                        s_server = pygame.Surface((server_rect.width, server_rect.height), pygame.SRCALPHA)
                        s_server.fill((0, 0, 255, 40))  # blue translucent
                        screen.blit(s_server, (server_rect.x, server_rect.y))
                        # Blue outline
                        pygame.draw.rect(screen, (0, 0, 255), server_rect, 3)
                        
                        # Label
                        font = pygame.font.Font(None, 16)
                        visual_label = font.render("VISUAL", True, (255, 0, 0))
                        collision_label = font.render("COLLISION", True, (255, 128, 0))
                        server_label = font.render("SERVER", True, (0, 0, 255))
                        screen.blit(visual_label, (screen_rect.x, screen_rect.y - 35))
                        screen.blit(collision_label, (collision_screen_rect.x, collision_screen_rect.y - 20))
                        screen.blit(server_label, (server_rect.x, server_rect.y - 20))
                    
                    # Draw server enemy hitbox in green
                    if server_enemy:
                        enemy_rect = pygame.Rect(
                            server_enemy['x'] + offset_x,
                            server_enemy['y'] + offset_y,
                            server_enemy['w'],
                            server_enemy['h']
                        )
                        # Green outline for enemy
                        pygame.draw.rect(screen, (0, 255, 0), enemy_rect, 2)
                        
                        # Enemy label
                        font = pygame.font.Font(None, 16)
                        enemy_label = font.render("ENEMY", True, (0, 255, 0))
                        screen.blit(enemy_label, (enemy_rect.x, enemy_rect.y - 20))
                
                # Clear server debug data after a short time to avoid stale data
                import time
                if hasattr(self, 'server_debug_data') and self.server_debug_data:
                    timestamp = self.server_debug_data.get('timestamp', 0)
                    current_time = int(time.time() * 1000)
                    if current_time - timestamp > 5000:  # Clear after 5 seconds
                        print("[DEBUG] Clearing stale server debug data")
                        self.server_debug_data = None
                        
            except Exception as e:
                # don't break rendering because of debug draw
                print(f"[DEBUG] Error drawing attack hitbox: {e}")

    def draw_hp_bar(self, screen, x=20, y=20, width=200, height=20):
        hp = getattr(self, "current_hp", self.stats.get("HP", 50))
        max_hp = getattr(self, "max_hp", self.stats.get("HP", 50))
        hp_ratio = max(0, min(hp / max_hp, 1))

        bg_rect = pygame.Rect(x, y, width, height)
        pygame.draw.rect(screen, (50, 50, 50), bg_rect, border_radius=6)

        if hp_ratio > 0.5:
            fill_color = (0, 255, 0)
        elif hp_ratio > 0.25:
            fill_color = (255, 255, 0)
        else:
            fill_color = (255, 0, 0)

        fill_width = int(width * hp_ratio)
        fill_rect = pygame.Rect(x, y, fill_width, height)
        pygame.draw.rect(screen, fill_color, fill_rect, border_radius=6)

        shine = pygame.Surface((fill_width, height), pygame.SRCALPHA)
        pygame.draw.rect(shine, (255, 255, 255, 50), (0, 0, fill_width, height // 2), border_radius=6)
        screen.blit(shine, (x, y))

        pygame.draw.rect(screen, (200, 200, 200), bg_rect, 2, border_radius=6)

        font = pygame.font.Font(None, 22)
        text = f"{int(hp)}/{int(max_hp)}"
        text_surf = font.render(text, True, (255, 255, 255))
        text_rect = text_surf.get_rect(center=bg_rect.center)
        screen.blit(text_surf, text_rect)

    def calculate_max_hp(self):
        self.max_hp = self.stats.get("HP", 50)
        self.current_hp = self.stats.get("HP", self.max_hp)

        if getattr(self, "current_hp", None) is None:
            self.current_hp = self.max_hp
        else:
            self.current_hp = min(self.current_hp, self.max_hp)

    def take_damage(self, amount):
        """Reduce HP and handle death if HP reaches 0."""
        self.current_hp = max(self.current_hp - amount, 0)
        # Keep stats["HP"] roughly in sync
        self.stats["HP"] = self.current_hp
        if self.current_hp == 0:
            self.die()

    def heal(self, amount):
        """Restore HP, capped at MaxHP."""
        self.current_hp = min(self.current_hp + amount, getattr(self, "max_hp", self.stats.get("HP", 100)))
        self.stats["HP"] = self.current_hp

    def die(self):
        # Placeholder - hook for game over / respawn logic
        print(f"{self.name} died.")

    def update_movement_stats(self):
        self.speed = self.stats.get("SPD", 1.0)
        self.jump_strength = -self.stats.get("JMP", 8.0)
        self.gravity = self.stats.get("GRAVITY", 0.6)

    def update_server_debug_data(self, debug_data):
        """Update server debug data for hitbox comparison"""
        self.server_debug_data = debug_data
        
        # Log the comparison for debugging
        if debug_data:
            server_attack = debug_data.get('attack_rect', {})
            client_attack = self.get_attack_hitbox(attack_range=self.equipped_weapon_range)
            
            if server_attack and client_attack:
                print(f"[HITBOX_COMPARE] Client: ({client_attack.x}, {client_attack.y}, {client_attack.width}, {client_attack.height})")
                print(f"[HITBOX_COMPARE] Server: ({server_attack['x']}, {server_attack['y']}, {server_attack['w']}, {server_attack['h']})")
                
                # Calculate differences
                dx = abs(client_attack.x - server_attack['x'])
                dy = abs(client_attack.y - server_attack['y'])
                dw = abs(client_attack.width - server_attack['w'])
                dh = abs(client_attack.height - server_attack['h'])
                
                print(f"[HITBOX_COMPARE] Differences: dx={dx}, dy={dy}, dw={dw}, dh={dh}")
                
                # Debug coordinate system differences
                print(f"[COORD_DEBUG] Player collision box: ({self.x}, {self.y}, {self.collision_width}, {self.collision_height})")
                print(f"[COORD_DEBUG] Server player pos: ({debug_data.get('player_pos', {}).get('x', 'N/A')}, {debug_data.get('player_pos', {}).get('y', 'N/A')})")
                print(f"[COORD_DEBUG] Direction: client={self.direction}, server={debug_data.get('player_dir', 'N/A')}")
                
                # Calculate expected server coordinates using client's method
                collision_center_x = self.x + self.collision_width // 2
                collision_center_y = self.y + self.collision_height // 2
                print(f"[COORD_DEBUG] Client collision center: ({collision_center_x}, {collision_center_y})")

    def get_collision_center_attack_hitbox(self, attack_range=100):
        """
        Sprite-edge based hitbox - extends from where sprite visually appears
        """
        # The sprite is rendered with its composite image, need to find where sprite actually appears
        composite_width = self.image.get_width()
        
        # This is where the composite image starts in world coords (from draw() method)
        composite_left_world = self.x - (composite_width - self.collision_width) // 2
        
        # The character sprite is drawn at char_offset_x inside the composite
        char_offset_x = getattr(self, "left_padding", 100)
        sprite_left_world = composite_left_world + char_offset_x
        sprite_right_world = sprite_left_world + getattr(self, "base_sprite_width", 179)
        
        # ADJUST THESE VALUES to move hitbox left (negative) or right (positive)
        hitbox_offset_right = -90  # Adjust for right-facing attacks
        hitbox_offset_left = 90   # Adjust for left-facing attacks
        
        if self.direction == "right":
            # Attack starts from right edge of sprite + offset
            rect_x = int(sprite_right_world + hitbox_offset_right)
        else:
            # Attack ends at left edge of sprite + offset
            rect_x = int(sprite_left_world - attack_range + hitbox_offset_left)
            
        return pygame.Rect(rect_x, int(self.y), attack_range, self.collision_height)