package com.tiedup.remake.minigame;

import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.IHasResistance;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState.TickResult;
import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState.UpdateType;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.minigame.PacketContinuousStruggleState;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;

/**
 * Manages continuous struggle mini-game sessions.
 *
 * <p>Extracted from {@link MiniGameSessionManager} (M15 split). Handles all
 * struggle variants: bind struggle, accessory struggle, V2 region struggle,
 * and furniture escape struggle.
 *
 * <p>Singleton, thread-safe via ConcurrentHashMap.
 */
public class StruggleSessionManager {

    private static final StruggleSessionManager INSTANCE =
        new StruggleSessionManager();

    /**
     * Active continuous struggle mini-game sessions by player UUID
     */
    private final Map<
        UUID,
        ContinuousStruggleMiniGameState
    > continuousSessions = new ConcurrentHashMap<>();

    /**
     * Mapping from legacy slot indices to V2 BodyRegionV2.
     * Used to convert legacy session targetSlot ordinals to V2 regions.
     * Index: 0=ARMS, 1=MOUTH, 2=EYES, 3=EARS, 4=NECK, 5=TORSO, 6=HANDS.
     */
    private static final BodyRegionV2[] SLOT_TO_REGION = {
        BodyRegionV2.ARMS, // 0 = BIND
        BodyRegionV2.MOUTH, // 1 = GAG
        BodyRegionV2.EYES, // 2 = BLINDFOLD
        BodyRegionV2.EARS, // 3 = EARPLUGS
        BodyRegionV2.NECK, // 4 = COLLAR
        BodyRegionV2.TORSO, // 5 = CLOTHES
        BodyRegionV2.HANDS, // 6 = MITTENS
    };

    private StruggleSessionManager() {}

    public static StruggleSessionManager getInstance() {
        return INSTANCE;
    }

    // ==================== SESSION START VARIANTS ====================

    /**
     * Start a new continuous struggle session for a player.
     *
     * @param player The server player
     * @param targetResistance Current bind resistance
     * @param isLocked Whether the bind is locked
     * @return The new session
     */
    public ContinuousStruggleMiniGameState startContinuousStruggleSession(
        ServerPlayer player,
        int targetResistance,
        boolean isLocked
    ) {
        UUID playerId = player.getUUID();

        // RISK-002 fix: reject if active session exists (prevents direction re-roll exploit)
        ContinuousStruggleMiniGameState existing = continuousSessions.get(
            playerId
        );
        if (existing != null) {
            TiedUpMod.LOGGER.debug(
                "[StruggleSessionManager] Rejected continuous session: active session already exists for {}",
                player.getName().getString()
            );
            return null;
        }

        // Create new session with configurable rate
        int ticksPerResistance =
            com.tiedup.remake.core.SettingsAccessor.getStruggleContinuousRate(
                player.level().getGameRules()
            );
        ContinuousStruggleMiniGameState session =
            new ContinuousStruggleMiniGameState(
                playerId,
                targetResistance,
                isLocked,
                null,
                ticksPerResistance
            );
        continuousSessions.put(playerId, session);

        TiedUpMod.LOGGER.info(
            "[StruggleSessionManager] Started continuous struggle session {} for {} (resistance: {}, locked: {})",
            session.getSessionId().toString().substring(0, 8),
            player.getName().getString(),
            targetResistance,
            isLocked
        );

        // Set struggle animation state and sync to client
        PlayerBindState state = PlayerBindState.getInstance(player);
        if (state != null) {
            state.setStruggling(true, player.level().getGameTime());
            SyncManager.syncStruggleState(player);
        }

        // Notify nearby kidnappers
        GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);

        return session;
    }

    /**
     * Start a new continuous struggle session for an accessory.
     *
     * @param player The server player
     * @param targetSlot Target accessory slot ordinal
     * @param lockResistance Current lock resistance
     * @return The new session
     */
    public ContinuousStruggleMiniGameState startContinuousAccessoryStruggleSession(
        ServerPlayer player,
        int targetSlot,
        int lockResistance
    ) {
        UUID playerId = player.getUUID();

        // RISK-002 fix: reject if active session exists (prevents direction re-roll exploit)
        ContinuousStruggleMiniGameState existing = continuousSessions.get(
            playerId
        );
        if (existing != null) {
            TiedUpMod.LOGGER.debug(
                "[StruggleSessionManager] Rejected accessory session: active session already exists for {}",
                player.getName().getString()
            );
            return null;
        }

        // Create new session with target slot and configurable rate
        int ticksPerResistance =
            com.tiedup.remake.core.SettingsAccessor.getStruggleContinuousRate(
                player.level().getGameRules()
            );
        ContinuousStruggleMiniGameState session =
            new ContinuousStruggleMiniGameState(
                playerId,
                lockResistance,
                true,
                targetSlot,
                ticksPerResistance
            );
        continuousSessions.put(playerId, session);

        TiedUpMod.LOGGER.info(
            "[StruggleSessionManager] Started continuous accessory struggle session {} for {} (slot: {}, resistance: {})",
            session.getSessionId().toString().substring(0, 8),
            player.getName().getString(),
            targetSlot,
            lockResistance
        );

        // Set struggle animation state and sync to client
        PlayerBindState state = PlayerBindState.getInstance(player);
        if (state != null) {
            state.setStruggling(true, player.level().getGameTime());
            SyncManager.syncStruggleState(player);
        }

        // Notify nearby kidnappers
        GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);

        return session;
    }

    /**
     * Start a new continuous struggle session for a V2 bondage item.
     *
     * @param player The server player
     * @param targetRegion V2 body region to struggle against
     * @param targetResistance Current resistance value
     * @param isLocked Whether the item is locked
     * @return The new session, or null if creation failed
     */
    public ContinuousStruggleMiniGameState startV2StruggleSession(
        ServerPlayer player,
        com.tiedup.remake.v2.BodyRegionV2 targetRegion,
        int targetResistance,
        boolean isLocked
    ) {
        UUID playerId = player.getUUID();

        // RISK-001 fix: reject if an active session already exists (prevents direction re-roll exploit)
        ContinuousStruggleMiniGameState existing = continuousSessions.get(
            playerId
        );
        if (existing != null) {
            TiedUpMod.LOGGER.debug(
                "[StruggleSessionManager] Rejected V2 session: active session already exists for {}",
                player.getName().getString()
            );
            return null;
        }

        int ticksPerResistance =
            com.tiedup.remake.core.SettingsAccessor.getStruggleContinuousRate(
                player.level().getGameRules()
            );
        ContinuousStruggleMiniGameState session =
            new ContinuousStruggleMiniGameState(
                playerId,
                targetResistance,
                isLocked,
                null,
                ticksPerResistance
            );
        session.setTargetRegion(targetRegion);
        continuousSessions.put(playerId, session);

        TiedUpMod.LOGGER.info(
            "[StruggleSessionManager] Started V2 struggle session {} for {} (region: {}, resistance: {}, locked: {})",
            session.getSessionId().toString().substring(0, 8),
            player.getName().getString(),
            targetRegion.name(),
            targetResistance,
            isLocked
        );

        // Set struggle animation state and sync to client
        PlayerBindState state = PlayerBindState.getInstance(player);
        if (state != null) {
            state.setStruggling(true, player.level().getGameTime());
            SyncManager.syncStruggleState(player);
        }

        GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);
        return session;
    }

    /**
     * Start a new continuous struggle session for a furniture seat escape.
     *
     * <p>The session behaves identically to a V2 struggle (direction-hold to
     * reduce resistance) but on completion it unlocks the seat and dismounts
     * the player instead of removing a bondage item.</p>
     *
     * @param player The server player (must be seated and locked)
     * @param furnitureEntityId Entity ID of the furniture
     * @param seatId The locked seat ID
     * @param totalDifficulty Combined resistance (seat base + item bonus)
     * @return The new session, or null if creation failed
     */
    public ContinuousStruggleMiniGameState startFurnitureStruggleSession(
        ServerPlayer player,
        int furnitureEntityId,
        String seatId,
        int totalDifficulty
    ) {
        UUID playerId = player.getUUID();

        // Reject if an active session already exists
        ContinuousStruggleMiniGameState existing = continuousSessions.get(
            playerId
        );
        if (existing != null) {
            TiedUpMod.LOGGER.debug(
                "[StruggleSessionManager] Rejected furniture session: active session already exists for {}",
                player.getName().getString()
            );
            return null;
        }

        int ticksPerResistance =
            com.tiedup.remake.core.SettingsAccessor.getStruggleContinuousRate(
                player.level().getGameRules()
            );
        ContinuousStruggleMiniGameState session =
            new ContinuousStruggleMiniGameState(
                playerId,
                totalDifficulty,
                true, // furniture seats are always "locked" in context
                null,
                ticksPerResistance
            );
        session.setFurnitureContext(furnitureEntityId, seatId);
        continuousSessions.put(playerId, session);

        TiedUpMod.LOGGER.info(
            "[StruggleSessionManager] Started furniture struggle session {} for {} (entity: {}, seat: '{}', difficulty: {})",
            session.getSessionId().toString().substring(0, 8),
            player.getName().getString(),
            furnitureEntityId,
            seatId,
            totalDifficulty
        );

        // Set struggle animation state and sync to client
        PlayerBindState state = PlayerBindState.getInstance(player);
        if (state != null) {
            state.setStruggling(true, player.level().getGameTime());
            SyncManager.syncStruggleState(player);
        }

        // Play struggle loop sound from furniture definition (plays once on start;
        // true looping sound would require client-side sound management -- future scope)
        net.minecraft.world.entity.Entity furnitureEntity = player
            .level()
            .getEntity(furnitureEntityId);
        if (
            furnitureEntity instanceof
                com.tiedup.remake.v2.furniture.EntityFurniture furniture
        ) {
            com.tiedup.remake.v2.furniture.FurnitureDefinition def =
                furniture.getDefinition();
            if (def != null && def.feedback().struggleLoopSound() != null) {
                player
                    .level()
                    .playSound(
                        null,
                        player.getX(),
                        player.getY(),
                        player.getZ(),
                        net.minecraft.sounds.SoundEvent.createVariableRangeEvent(
                            def.feedback().struggleLoopSound()
                        ),
                        SoundSource.PLAYERS,
                        0.6f,
                        1.0f
                    );
            }
        }

        GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);
        return session;
    }

    // ==================== SESSION QUERY ====================

    /**
     * Get active continuous struggle session for a player.
     *
     * @param playerId The player UUID
     * @return The session, or null if none active
     */
    @Nullable
    public ContinuousStruggleMiniGameState getContinuousStruggleSession(
        UUID playerId
    ) {
        ContinuousStruggleMiniGameState session = continuousSessions.get(
            playerId
        );
        if (session != null && session.isExpired()) {
            continuousSessions.remove(playerId);
            return null;
        }
        return session;
    }

    /**
     * Validate a continuous struggle session.
     *
     * @param playerId The player UUID
     * @param sessionId The session UUID to validate
     * @return true if session is valid and active
     */
    public boolean validateContinuousStruggleSession(
        UUID playerId,
        UUID sessionId
    ) {
        ContinuousStruggleMiniGameState session = getContinuousStruggleSession(
            playerId
        );
        if (session == null) {
            return false;
        }
        return session.getSessionId().equals(sessionId);
    }

    /**
     * End a continuous struggle session.
     *
     * @param playerId The player UUID
     * @param success Whether the session ended in success (escape)
     */
    public void endContinuousStruggleSession(UUID playerId, boolean success) {
        ContinuousStruggleMiniGameState session = continuousSessions.remove(
            playerId
        );
        if (session != null) {
            TiedUpMod.LOGGER.info(
                "[StruggleSessionManager] Ended continuous struggle session for player {} (success: {})",
                playerId.toString().substring(0, 8),
                success
            );

            // Clear struggle animation state
            // We need to find the player to clear the animation
            // This will be handled by the caller if they have access to the player
        }
    }

    // ==================== TICK ====================

    /**
     * Tick all continuous struggle sessions.
     * Should be called every server tick.
     *
     * @param server The server instance for player lookups
     * @param currentTick Current game tick
     */
    public void tickContinuousSessions(
        net.minecraft.server.MinecraftServer server,
        long currentTick
    ) {
        if (continuousSessions.isEmpty()) return;

        // Collect sessions to process (avoid ConcurrentModificationException)
        List<Map.Entry<UUID, ContinuousStruggleMiniGameState>> toProcess =
            new ArrayList<>(continuousSessions.entrySet());

        for (Map.Entry<
            UUID,
            ContinuousStruggleMiniGameState
        > entry : toProcess) {
            UUID playerId = entry.getKey();
            ContinuousStruggleMiniGameState session = entry.getValue();

            // Get player
            ServerPlayer player = server.getPlayerList().getPlayer(playerId);
            if (player == null) {
                // Player disconnected, clean up
                continuousSessions.remove(playerId);
                continue;
            }

            // Check for expired/completed sessions
            if (session.isExpired() || session.isComplete()) {
                continuousSessions.remove(playerId);
                clearStruggleAnimation(player);
                continue;
            }

            // Tick the session
            TickResult result = session.tick(currentTick);

            // Handle tick result
            switch (result) {
                case DIRECTION_CHANGE -> {
                    sendContinuousStruggleUpdate(
                        player,
                        session,
                        UpdateType.DIRECTION_CHANGE
                    );
                }
                case RESISTANCE_UPDATE -> {
                    // Update actual bind resistance
                    updateBindResistance(player, session);
                    // Send update to client immediately for real-time feedback
                    sendContinuousStruggleUpdate(
                        player,
                        session,
                        UpdateType.RESISTANCE_UPDATE
                    );
                }
                case ESCAPED -> {
                    handleStruggleEscape(player, session);
                }
                case NO_CHANGE -> {
                    // No action needed
                }
                default -> {
                }
            }

            // Check shock collar (separate from tick result)
            if (
                shouldCheckShockCollar(player) &&
                session.shouldTriggerShock(currentTick)
            ) {
                handleShockCollar(player, session);
            }

            // Check kidnapper notification
            if (session.shouldNotifyKidnappers(currentTick)) {
                GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(
                    player
                );
            }

            // Play struggle sound periodically
            if (session.shouldPlayStruggleSound(currentTick)) {
                playStruggleSound(player);
            }
        }
    }

    // ==================== PRIVATE HELPERS ====================

    /**
     * Play struggle sound for player and nearby entities.
     */
    private void playStruggleSound(ServerPlayer player) {
        // Play leather creaking sound - audible to player and nearby
        player
            .serverLevel()
            .playSound(
                null, // null = all players can hear
                player.getX(),
                player.getY(),
                player.getZ(),
                SoundEvents.ARMOR_EQUIP_LEATHER,
                SoundSource.PLAYERS,
                0.8f, // volume
                0.9f + player.getRandom().nextFloat() * 0.2f // slight pitch variation
            );
    }

    /**
     * Check if shock collar check is applicable for this player.
     */
    private boolean shouldCheckShockCollar(ServerPlayer player) {
        ItemStack collar = V2EquipmentHelper.getInRegion(
            player,
            BodyRegionV2.NECK
        );
        if (collar.isEmpty()) return false;

        // Only shock collars can trigger during struggle
        if (!CollarHelper.canShock(collar)) return false;

        // Must be locked
        if (collar.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
            return lockable.isLocked(collar);
        }
        return false;
    }

    /**
     * Handle shock collar trigger.
     */
    private void handleShockCollar(
        ServerPlayer player,
        ContinuousStruggleMiniGameState session
    ) {
        PlayerBindState state = PlayerBindState.getInstance(player);
        if (state != null) {
            state.shockKidnapped(" (Your struggle was interrupted!)", 2.0f);
        }

        session.triggerShock();
        sendContinuousStruggleUpdate(player, session, UpdateType.SHOCK);

        TiedUpMod.LOGGER.debug(
            "[StruggleSessionManager] Shock collar triggered for {} during struggle",
            player.getName().getString()
        );
    }

    /**
     * Update the actual bind resistance based on session state.
     */
    private void updateBindResistance(
        ServerPlayer player,
        ContinuousStruggleMiniGameState session
    ) {
        // V2 region-based resistance update
        if (session.isV2Struggle()) {
            com.tiedup.remake.v2.BodyRegionV2 region =
                session.getTargetRegion();
            ItemStack stack =
                com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.getInRegion(
                    player,
                    region
                );
            if (stack.isEmpty()) return;

            if (
                stack.getItem() instanceof
                    com.tiedup.remake.items.base.IHasResistance resistanceItem
            ) {
                resistanceItem.setCurrentResistance(
                    stack,
                    session.getCurrentResistance()
                );
                com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.sync(
                    player
                );
            }
            return;
        }

        if (session.isAccessoryStruggle()) {
            // Handle accessory resistance update (legacy slot-index path)
            Integer slotIndex = session.getTargetSlot();
            if (
                slotIndex == null ||
                slotIndex < 0 ||
                slotIndex >= SLOT_TO_REGION.length
            ) return;

            BodyRegionV2 region = SLOT_TO_REGION[slotIndex];
            ItemStack accessoryStack = V2EquipmentHelper.getInRegion(
                player,
                region
            );
            if (accessoryStack.isEmpty()) return;

            // Update accessory struggle resistance in dedicated NBT key
            com.tiedup.remake.util.ItemNBTHelper.setInt(
                accessoryStack,
                "accessoryStruggleResistance",
                session.getCurrentResistance()
            );
            // Sync V2 equipment state
            V2EquipmentHelper.sync(player);
            return;
        }

        // Update bind resistance
        ItemStack bindStack = V2EquipmentHelper.getInRegion(
            player,
            BodyRegionV2.ARMS
        );
        if (bindStack.isEmpty()) {
            return;
        }
        if (bindStack.getItem() instanceof IHasResistance resistanceItem) {
            resistanceItem.setCurrentResistance(bindStack, session.getCurrentResistance());
            V2EquipmentHelper.sync(player);
        }
    }

    /**
     * Handle successful escape from struggle.
     */
    private void handleStruggleEscape(
        ServerPlayer player,
        ContinuousStruggleMiniGameState session
    ) {
        TiedUpMod.LOGGER.info(
            "[StruggleSessionManager] Player {} escaped from struggle!",
            player.getName().getString()
        );

        // Send escape update to client
        sendContinuousStruggleUpdate(player, session, UpdateType.ESCAPE);

        // Clear animation state
        clearStruggleAnimation(player);

        // Furniture escape: unlock seat + dismount
        if (session.isFurnitureStruggle()) {
            handleFurnitureEscape(player, session);
            continuousSessions.remove(player.getUUID());
            return;
        }

        // V2 region-based escape
        if (session.isV2Struggle()) {
            com.tiedup.remake.v2.BodyRegionV2 region =
                session.getTargetRegion();
            // BUG-001 fix: break lock before unequip (consistent with legacy accessory escape)
            ItemStack stackInRegion =
                com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.getInRegion(
                    player,
                    region
                );
            if (
                !stackInRegion.isEmpty() &&
                stackInRegion.getItem() instanceof
                    com.tiedup.remake.items.base.ILockable lockable
            ) {
                lockable.breakLock(stackInRegion);
            }
            ItemStack removed =
                com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.unequipFromRegion(
                    player,
                    region,
                    true
                );
            if (!removed.isEmpty()) {
                if (!player.getInventory().add(removed)) {
                    player.drop(removed, false);
                }
            }
            continuousSessions.remove(player.getUUID());
            return;
        }

        // Remove the bind
        if (!session.isAccessoryStruggle()) {
            PlayerBindState state = PlayerBindState.getInstance(player);
            if (state != null) {
                ItemStack bind = state.unequip(BodyRegionV2.ARMS);
                if (!bind.isEmpty()) {
                    state.kidnappedDropItem(bind);
                }
            }
        } else {
            // Handle accessory escape (legacy slot-index path)
            Integer slotIndex = session.getTargetSlot();
            if (
                slotIndex != null &&
                slotIndex >= 0 &&
                slotIndex < SLOT_TO_REGION.length
            ) {
                BodyRegionV2 region = SLOT_TO_REGION[slotIndex];
                ItemStack accessoryStack = V2EquipmentHelper.getInRegion(
                    player,
                    region
                );
                if (!accessoryStack.isEmpty()) {
                    // Break the lock on the accessory
                    if (
                        accessoryStack.getItem() instanceof
                            com.tiedup.remake.items.base.ILockable lockable
                    ) {
                        lockable.breakLock(accessoryStack);
                    }
                    // Remove the accessory from the region and drop it
                    ItemStack removed = V2EquipmentHelper.unequipFromRegion(
                        player,
                        region,
                        true
                    );
                    if (!removed.isEmpty()) {
                        // Drop the item at player's feet
                        player.drop(removed, true);
                    }
                }
            }
        }

        // Remove session
        continuousSessions.remove(player.getUUID());
    }

    /**
     * Handle successful furniture escape: unlock the seat, dismount the player,
     * play sounds, and broadcast state to tracking clients.
     */
    private void handleFurnitureEscape(
        ServerPlayer player,
        ContinuousStruggleMiniGameState session
    ) {
        int furnitureEntityId = session.getFurnitureEntityId();
        String seatId = session.getFurnitureSeatId();

        net.minecraft.world.entity.Entity entity = player
            .level()
            .getEntity(furnitureEntityId);
        if (entity == null || entity.isRemoved()) {
            TiedUpMod.LOGGER.warn(
                "[StruggleSessionManager] Furniture entity {} no longer exists for escape",
                furnitureEntityId
            );
            // Player still needs to be freed even if entity is gone
            player.stopRiding();
            return;
        }

        if (
            !(entity instanceof
                    com.tiedup.remake.v2.furniture.ISeatProvider provider)
        ) {
            TiedUpMod.LOGGER.warn(
                "[StruggleSessionManager] Entity {} is not an ISeatProvider",
                furnitureEntityId
            );
            player.stopRiding();
            return;
        }

        // Unlock the seat
        provider.setSeatLocked(seatId, false);

        // Clear persistent data tag (reconnection system)
        net.minecraft.nbt.CompoundTag persistentData =
            player.getPersistentData();
        persistentData.remove("tiedup_locked_furniture");

        // Dismount the player
        player.stopRiding();

        // Play escape sound: prefer furniture-specific sound, fall back to CHAIN_BREAK
        net.minecraft.sounds.SoundEvent escapeSound =
            net.minecraft.sounds.SoundEvents.CHAIN_BREAK;
        if (
            entity instanceof
                com.tiedup.remake.v2.furniture.EntityFurniture furniture
        ) {
            com.tiedup.remake.v2.furniture.FurnitureDefinition def =
                furniture.getDefinition();
            if (def != null && def.feedback().escapeSound() != null) {
                escapeSound =
                    net.minecraft.sounds.SoundEvent.createVariableRangeEvent(
                        def.feedback().escapeSound()
                    );
            }
        }
        player
            .serverLevel()
            .playSound(
                null,
                player.getX(),
                player.getY(),
                player.getZ(),
                escapeSound,
                net.minecraft.sounds.SoundSource.PLAYERS,
                1.0f,
                1.0f
            );

        // Broadcast updated furniture state to all tracking clients
        if (
            entity instanceof
                com.tiedup.remake.v2.furniture.EntityFurniture furniture
        ) {
            com.tiedup.remake.v2.furniture.network.PacketSyncFurnitureState.sendToTracking(
                furniture
            );
        }

        TiedUpMod.LOGGER.info(
            "[StruggleSessionManager] {} escaped furniture {} seat '{}'",
            player.getName().getString(),
            furnitureEntityId,
            seatId
        );
    }

    /**
     * Send a continuous struggle state update to the client.
     */
    private void sendContinuousStruggleUpdate(
        ServerPlayer player,
        ContinuousStruggleMiniGameState session,
        UpdateType updateType
    ) {
        ModNetwork.sendToPlayer(
            new PacketContinuousStruggleState(
                session.getSessionId(),
                updateType,
                session.getCurrentDirectionIndex(),
                session.getCurrentResistance(),
                session.getMaxResistance(),
                session.isLocked()
            ),
            player
        );
    }

    /**
     * Clear struggle animation state for a player and sync to clients.
     */
    private void clearStruggleAnimation(ServerPlayer player) {
        PlayerBindState state = PlayerBindState.getInstance(player);
        if (state != null) {
            state.setStruggling(false, 0);
            SyncManager.syncStruggleState(player);
        }
    }

    // ==================== CLEANUP ====================

    /**
     * Clean up all struggle sessions for a player (called on disconnect).
     *
     * @param playerId The player UUID
     */
    public void cleanupPlayer(UUID playerId) {
        continuousSessions.remove(playerId);
    }

    /**
     * Periodic cleanup of expired struggle sessions.
     * Should be called from server tick handler.
     */
    public void tickCleanup(long currentTick) {
        // Only run every 100 ticks (5 seconds)
        if (currentTick % 100 != 0) {
            return;
        }

        continuousSessions
            .entrySet()
            .removeIf(entry -> entry.getValue().isExpired());
    }

    /**
     * Get count of active struggle sessions (for debugging).
     */
    public int getActiveSessionCount() {
        return continuousSessions.size();
    }
}
