package com.tiedup.remake.state;

import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.LeashProxyEntity;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.components.PlayerCaptivity;
import com.tiedup.remake.state.components.PlayerClothesPermission;
import com.tiedup.remake.state.components.PlayerDataRetrieval;
import com.tiedup.remake.state.components.PlayerEquipment;
import com.tiedup.remake.state.components.PlayerLifecycle;
import com.tiedup.remake.state.components.PlayerMovement;
import com.tiedup.remake.state.components.PlayerSale;
import com.tiedup.remake.state.components.PlayerShockCollar;
import com.tiedup.remake.state.components.PlayerSpecialActions;
import com.tiedup.remake.state.components.PlayerStateQuery;
import com.tiedup.remake.state.components.PlayerStruggle;
import com.tiedup.remake.state.components.PlayerTaskManagement;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.util.RestraintEffectUtils;
import com.tiedup.remake.util.teleport.Position;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;

/**
 * Core state management for player restraints and bondage equipment.
 * Singleton pattern - one instance per player, tracked by UUID.
 *
 * Based on original PlayerBindState from 1.12.2.
 *
 * Responsibilities:
 * - Track player restraint states (tied, gagged, blindfolded, etc.)
 * - Manage bondage equipment lifecycle (put on/take off items)
 * - Lifecycle management (connection, death, respawn)
 * - Enslavement lifecycle (can be enslaved, act as master)
 * - Advanced collar features (shocks, GPS tracking)
 *
 * Thread Safety: All mutating methods must be called from the server thread
 * (via enqueueWork for packet handlers). The only cross-thread access is
 * the StruggleSnapshot (atomic volatile record) and the Player reference (volatile).
 * The instances map uses ConcurrentHashMap for thread-safe lookup.
 *
 * Refactoring: Component-Host pattern
 */
public class PlayerBindState implements IRestrainable, IPlayerBindStateHost {

    // ========== Singleton Pattern ==========

    /** Server-side instances - one per player. Thread-safe map with atomic computeIfAbsent. */
    private static final Map<UUID, PlayerBindState> instances =
        new ConcurrentHashMap<>();
    /** Client-side instances - one per player. Thread-safe map with atomic computeIfAbsent. */
    private static final Map<UUID, PlayerBindState> instancesClient =
        new ConcurrentHashMap<>();

    // Note: playersBeingFreed removed - no longer needed with proxy-based leash system

    /**
     * Get or create a PlayerBindState instance for a player.
     *
     * @param player The player entity
     * @return The state instance associated with this player
     */
    @Nullable
    public static PlayerBindState getInstance(Player player) {
        if (player == null) return null;
        Map<UUID, PlayerBindState> map = player.level().isClientSide
            ? instancesClient
            : instances;
        UUID uuid = player.getUUID();
        PlayerBindState state = map.computeIfAbsent(uuid, k ->
            new PlayerBindState(player)
        );

        // Refresh the cached reference: an observer reconnect creates a
        // new remote-player Entity object with the same UUID, and the
        // cached state otherwise keeps pointing at the old (removed) one.
        if (state.player != player) {
            state.player = player;
        }

        return state;
    }

    /**
     * Clears all client-side instances.
     * Must be called on world unload to prevent memory leaks from stale player references.
     */
    public static void clearClientInstances() {
        instancesClient.clear();
    }

    /**
     * Cleans up the instance map when a player leaves the server.
     */
    public static void removeInstance(UUID uuid, boolean isClient) {
        Map<UUID, PlayerBindState> map = isClient ? instancesClient : instances;
        map.remove(uuid);
    }

    // ========== Instance Fields ==========

    private final UUID playerUUID;
    // volatile: accessed from multiple threads (network, tick, render)
    private volatile Player player;
    private boolean online;

    // ========== Core Components ==========
    private final PlayerTaskManagement taskManagement;
    private final PlayerStateQuery stateQuery;
    private final PlayerDataRetrieval dataRetrieval;
    private final PlayerSale sale;

    // ========== Struggle Components ==========
    private final PlayerSpecialActions specialActions;
    private final PlayerClothesPermission clothesPermission;

    // ========== Animation Components ==========
    private final PlayerEquipment equipment;
    private final PlayerStruggle struggle;
    private final PlayerShockCollar shockCollar;

    // ========== Captivity Components ==========
    private final PlayerLifecycle lifecycle;
    private final PlayerCaptivity captivity;

    private ICaptor captor;
    private PlayerCaptorManager captorManager;
    // Note: transport field removed - now using IPlayerLeashAccess mixin

    // Struggle animation state — atomic snapshot for cross-thread visibility.
    // Server thread writes via setStruggling(), render thread reads via isStruggling()/getStruggleStartTick().
    // Single volatile reference guarantees both fields are visible atomically.
    private record StruggleSnapshot(boolean struggling, long startTick) {}
    private volatile StruggleSnapshot struggleState = new StruggleSnapshot(false, 0);

    // ========== Movement Style State ==========
    // Encapsulated in PlayerMovement component. Managed exclusively by MovementStyleManager.
    private final PlayerMovement movement;

    /** Access movement style state (hop, crawl, position tracking, etc.). */
    public PlayerMovement getMovement() {
        return movement;
    }

    /** Access task management component (tying, untying, feeding tasks). */
    public PlayerTaskManagement tasks() {
        return taskManagement;
    }

    /** Access struggle component (struggle, tighten, cooldowns). */
    public PlayerStruggle getStruggle() {
        return struggle;
    }

    /**
     * Resets all movement style state to defaults.
     * Called on death, logout, and dimension change to ensure clean re-activation.
     */
    public void clearMovementState() {
        movement.clear();
    }

    // ========== Constructor ==========

    private PlayerBindState(Player player) {
        this.playerUUID = player.getUUID();
        this.player = player;
        this.online = true;

        // Initialize core components
        this.taskManagement = new PlayerTaskManagement();
        this.stateQuery = new PlayerStateQuery(this);
        this.dataRetrieval = new PlayerDataRetrieval(this);
        this.sale = new PlayerSale();

        // Initialize struggle components
        this.specialActions = new PlayerSpecialActions(this);
        this.clothesPermission = new PlayerClothesPermission(this);

        // Initialize animation components
        this.equipment = new PlayerEquipment(this);
        this.struggle = new PlayerStruggle(this);
        this.shockCollar = new PlayerShockCollar(this);

        // Initialize captivity components
        this.lifecycle = new PlayerLifecycle(this);
        this.captivity = new PlayerCaptivity(this);

        // Initialize movement component
        this.movement = new PlayerMovement();

        this.captor = null;
        this.captorManager = new PlayerCaptorManager(player);
    }

    // ========== State Query Methods ==========
    // Delegated to PlayerStateQuery component

    /** Check if player has ropes/ties equipped. */
    @Override
    public boolean isTiedUp() {
        return stateQuery.isTiedUp();
    }

    /** Check if player is currently gagged. */
    public boolean isGagged() {
        return stateQuery.isGagged();
    }

    /** Check if player is blindfolded. */
    public boolean isBlindfolded() {
        return stateQuery.isBlindfolded();
    }

    /** Check if player has earplugs. */
    public boolean hasEarplugs() {
        return stateQuery.hasEarplugs();
    }

    public boolean isEarplugged() {
        return stateQuery.isEarplugged();
    }

    /** Check if player is wearing a collar. */
    @Override
    public boolean hasCollar() {
        return stateQuery.hasCollar();
    }

    /** Returns the current collar ItemStack, or empty if none. */
    public ItemStack getCurrentCollar() {
        return stateQuery.getCurrentCollar();
    }

    public boolean hasClothes() {
        return stateQuery.hasClothes();
    }

    /** Check if player has mittens equipped. Mittens system */
    public boolean hasMittens() {
        return stateQuery.hasMittens();
    }

    // ========== Item Management Methods ==========
    // Delegated to PlayerEquipment component

    /** Equips a bind item and applies speed reduction. */
    public void putBindOn(ItemStack bind) {
        equipment.putBindOn(bind);
    }

    /** Equips a gag (enables gag talk if implemented). Issue #14 fix: now calls onEquipped. */
    public void putGagOn(ItemStack gag) {
        equipment.putGagOn(gag);
    }

    /** Equips a blindfold (restricts vision). Issue #14 fix: now calls onEquipped. */
    public void putBlindfoldOn(ItemStack blindfold) {
        equipment.putBlindfoldOn(blindfold);
    }

    /** Equips a collar (starts GPS/Shock monitoring). Issue #14 fix: now calls onEquipped. */
    public void putCollarOn(ItemStack collar) {
        equipment.putCollarOn(collar);
    }

    /** Issue #14 fix: now calls onEquipped. Syncs clothes config to all clients. */
    public void putClothesOn(ItemStack clothes) {
        equipment.putClothesOn(clothes);
    }

    /** Equips mittens (blocks hand interactions). Mittens system. Issue #14 fix: now calls onEquipped. */
    public void putMittensOn(ItemStack mittens) {
        equipment.putMittensOn(mittens);
    }

    /** Removes binds and restores speed. */
    public ItemStack takeBindOff() {
        return equipment.takeBindOff();
    }

    public ItemStack takeGagOff() {
        return equipment.takeGagOff();
    }

    public ItemStack takeBlindfoldOff() {
        return equipment.takeBlindfoldOff();
    }

    /** Removes mittens. Mittens system */
    public ItemStack takeMittensOff() {
        return equipment.takeMittensOff();
    }

    /** Helper to drop an item at the kidnapped player's feet. */
    public void kidnappedDropItem(ItemStack stack) {
        equipment.kidnappedDropItem(stack);
    }

    // ========== Lifecycle Methods ==========

    /**
     * Resets the player instance upon reconnection or respawn.
     * Delegated to PlayerLifecycle component.
     *
     * IMPORTANT: Handle transport restoration for pole binding.
     * With proxy-based leash system, leashes are not persisted through disconnection.
     * The leash proxy is ephemeral and will be recreated if needed.
     *
     * Leg Binding: Speed reduction based on hasLegsBound(), not isTiedUp().
     */
    public void resetNewConnection(Player player) {
        // Update player reference (must be done here in host)
        this.player = player;

        // Clear movement style state so it re-resolves on the new entity
        // (transient AttributeModifiers are lost when the entity is recreated,
        // e.g., after dimension change or respawn)
        clearMovementState();

        // Delegate to lifecycle component
        lifecycle.resetNewConnection(player);
    }

    // onDeathKidnapped() removed - consolidated into IRestrainable override version below

    // ========== IPlayerBindStateHost Implementation ==========
    // Note: Some methods are implemented elsewhere in the class with @Override

    @Override
    public boolean isOnline() {
        return online;
    }

    @Override
    public void setOnline(boolean online) {
        this.online = online;
    }

    @Override
    public Player getPlayer() {
        return player;
    }

    @Override
    public UUID getPlayerUUID() {
        return playerUUID;
    }

    @Override
    public Level getLevel() {
        return player != null ? player.level() : null;
    }

    @Override
    public boolean isClientSide() {
        Level level = getLevel();
        return level != null && level.isClientSide;
    }

    @Override
    public void syncClothesConfig() {
        SyncManager.syncClothesConfig(player);
    }

    @Override
    public void syncEnslavement() {
        SyncManager.syncEnslavement(player);
    }

    @Override
    public IRestrainable getKidnapped() {
        return this; // PlayerBindState implements IRestrainable
    }

    @Override
    public void setCaptor(ICaptor captor) {
        this.captor = captor;
    }

    // Note: getCaptor(), getCaptorManager(), setStruggling(), setStrugglingClient(),
    // isStruggling(), getStruggleStartTick() are implemented elsewhere in the class

    // ========== Struggle & Resistance Methods ==========
    // Non-delegation methods only — delegation removed, use getStruggle() accessor.
    // tighten(Player) is interface-mandated by ICoercible — cannot be removed.

    /** Restores resistance to base values when a master tightens the ties. */
    public void tighten(Player tightener) {
        struggle.tighten(tightener);
    }

    // ========== v2.5: Knife Cut Target Methods ==========
    // Delegated to PlayerSpecialActions component

    /**
     * Set the body region target for knife cutting.
     * Used when player selects "Cut" from StruggleChoiceScreen.
     *
     * @param region The body region to cut (NECK, MOUTH, etc.)
     */
    public void setKnifeCutTarget(BodyRegionV2 region) {
        specialActions.setKnifeCutTarget(region);
    }

    /**
     * Get the current knife cut target region.
     *
     * @return The target region, or null if none
     */
    public BodyRegionV2 getKnifeCutTarget() {
        return specialActions.getKnifeCutTarget();
    }

    /**
     * Clear the knife cut target.
     */
    public void clearKnifeCutTarget() {
        specialActions.clearKnifeCutTarget();
    }

    /**
     * Check if player is currently playing struggle animation.
     * Thread-safe: reads from atomic StruggleSnapshot.
     */
    @Override
    public boolean isStruggling() {
        return struggleState.struggling();
    }

    /**
     * Get the tick when struggle animation started.
     * Thread-safe: reads from atomic StruggleSnapshot.
     */
    @Override
    public long getStruggleStartTick() {
        return struggleState.startTick();
    }

    /**
     * Set struggle animation state (server-side).
     * Single volatile write ensures both fields are visible atomically to render thread.
     */
    @Override
    public void setStruggling(boolean struggling, long currentTick) {
        this.struggleState = new StruggleSnapshot(
            struggling,
            struggling ? currentTick : struggleState.startTick()
        );
    }

    /**
     * Set struggle animation flag (client-side only).
     * Used by network sync - does NOT update timer (server manages timer).
     */
    @Override
    public void setStrugglingClient(boolean struggling) {
        StruggleSnapshot current = this.struggleState;
        this.struggleState = new StruggleSnapshot(struggling, current.startTick());
    }

    /**
     * Delegated to PlayerEquipment component
     */
    @Override
    public int getCurrentBindResistance() {
        return equipment.getCurrentBindResistance();
    }

    /**
     * Delegated to PlayerEquipment component.
     * Thread safety: must be called from the server thread.
     */
    @Override
    public void setCurrentBindResistance(int resistance) {
        equipment.setCurrentBindResistance(resistance);
    }

    /**
     * Delegated to PlayerEquipment component.
     * Thread safety: must be called from the server thread.
     */
    @Override
    public int getCurrentCollarResistance() {
        return equipment.getCurrentCollarResistance();
    }

    /**
     * Delegated to PlayerEquipment component.
     * Thread safety: must be called from the server thread.
     */
    @Override
    public void setCurrentCollarResistance(int resistance) {
        equipment.setCurrentCollarResistance(resistance);
    }

    /**
     * Initiates the capture process by a captor.
     * Uses the proxy-based leash system (player is NOT mounted).
     * Delegated to PlayerCaptivity component.
     */
    @Override
    public boolean getCapturedBy(ICaptor newCaptor) {
        return captivity.getCapturedBy(newCaptor);
    }

    @Override
    public void free() {
        captivity.free();
    }

    /** Ends captivity and detaches the leash proxy.
     * Delegated to PlayerCaptivity component. */
    @Override
    public void free(boolean dropLead) {
        captivity.free(dropLead);
    }

    /**
     * Delegated to PlayerCaptivity component.
     */
    @Override
    public void transferCaptivityTo(ICaptor newCaptor) {
        captivity.transferCaptivityTo(newCaptor);
    }

    @Override
    public boolean isEnslavable() {
        return captivity.isEnslavable();
    }

    /**
     * Delegated to PlayerCaptivity component.
     */
    @Override
    public boolean isCaptive() {
        return captivity.isCaptive();
    }

    /**
     * Also implements IPlayerBindStateHost.
     */
    @Override
    public ICaptor getCaptor() {
        return captor;
    }

    @Override
    public LeashProxyEntity getTransport() {
        return captivity.getTransport();
    }

    public ItemStack takeCollarOff() {
        return takeCollarOff(false);
    }

    /**
     * Tries to remove the collar. Fails if locked unless forced.
     * Delegated to PlayerEquipment component
     */
    public ItemStack takeCollarOff(boolean force) {
        return equipment.takeCollarOff(force);
    }

    /** Checks if the wearer has a collar with the 'locked' NBT flag set. */
    @Override
    public boolean hasLockedCollar() {
        ItemStack collar = getCurrentCollar();
        return (
            !collar.isEmpty() &&
            collar.getItem() instanceof ILockable lockable &&
            lockable.isLocked(collar)
        );
    }

    @Override
    public UUID getKidnappedUniqueId() {
        return playerUUID;
    }

    @Override
    public String getKidnappedName() {
        return player.getName().getString();
    }

    // Delegated to PlayerShockCollar component

    @Override
    public void shockKidnapped() {
        shockCollar.shockKidnapped();
    }

    /**
     * Triggers a visual and auditory shock effect.
     * Damage is applied (shock can kill).
     */
    @Override
    public void shockKidnapped(@Nullable String messageAddon, float damage) {
        shockCollar.shockKidnapped(messageAddon, damage);
    }

    /**
     * Periodically monitors captivity validity.
     * Simplified: If any condition is invalid, free the captive immediately.
     * Delegated to PlayerCaptivity component.
     */
    public void checkStillCaptive() {
        captivity.checkStillCaptive();
    }

    /**
     * Periodic check for Auto-Shock intervals and GPS Safe Zones.
     * Called from RestraintTaskTickHandler every player tick.
     * Delegated to PlayerShockCollar component.
     */
    public void checkAutoShockCollar() {
        shockCollar.checkAutoShockCollar();
    }

    /**
     * Force-stops and clears any active shock timers.
     * Delegated to PlayerShockCollar component.
     */
    public void resetAutoShockTimer() {
        shockCollar.resetAutoShockTimer();
    }

    /**
     * Manager for capturing other entities (acting as captor).
     * Also implements IPlayerBindStateHost.
     */
    @Override
    public PlayerCaptorManager getCaptorManager() {
        return captorManager;
    }

    // IRestrainable Missing Methods

    @Override
    public void teleportToPosition(Position position) {
        if (player == null || position == null) return;

        // Check if dimension change is needed
        if (!player.level().dimension().equals(position.getDimension())) {
            // Cross-dimension teleport
            net.minecraft.server.level.ServerPlayer serverPlayer =
                (net.minecraft.server.level.ServerPlayer) player;
            net.minecraft.server.level.ServerLevel targetLevel =
                serverPlayer.server.getLevel(position.getDimension());

            if (targetLevel != null) {
                serverPlayer.teleportTo(
                    targetLevel,
                    position.getX(),
                    position.getY(),
                    position.getZ(),
                    serverPlayer.getYRot(),
                    serverPlayer.getXRot()
                );
            }
        } else {
            // Same dimension teleport
            player.teleportTo(
                position.getX(),
                position.getY(),
                position.getZ()
            );
        }

        TiedUpMod.LOGGER.debug(
            "[PlayerBindState] Teleported {} to {}",
            player.getName().getString(),
            position
        );
    }

    @Override
    public String getNameFromCollar() {
        return dataRetrieval.getNameFromCollar();
    }

    // V2 Region-Based Equipment Access

    @Override
    public ItemStack getEquipment(BodyRegionV2 region) {
        return V2EquipmentHelper.getInRegion(player, region);
    }

    @Override
    public void equip(BodyRegionV2 region, ItemStack stack) {
        switch (region) {
            case ARMS -> equipment.putBindOn(stack);
            case MOUTH -> equipment.putGagOn(stack);
            case EYES -> equipment.putBlindfoldOn(stack);
            case EARS -> equipment.putEarplugsOn(stack);
            case NECK -> equipment.putCollarOn(stack);
            case TORSO -> equipment.putClothesOn(stack);
            case HANDS -> equipment.putMittensOn(stack);
            default -> {
            }
        }
    }

    @Override
    public ItemStack unequip(BodyRegionV2 region) {
        return switch (region) {
            case ARMS -> equipment.takeBindOff();
            case MOUTH -> equipment.takeGagOff();
            case EYES -> equipment.takeBlindfoldOff();
            case EARS -> equipment.takeEarplugsOff();
            case NECK -> equipment.takeCollarOff(false);
            case TORSO -> equipment.takeClothesOff();
            case HANDS -> equipment.takeMittensOff();
            default -> ItemStack.EMPTY;
        };
    }

    @Override
    public ItemStack forceUnequip(BodyRegionV2 region) {
        if (region == BodyRegionV2.NECK) {
            return equipment.takeCollarOff(true);
        }
        if (region == BodyRegionV2.TORSO) {
            // takeClothesOff has no lock check but has syncClothesConfig side effect
            return equipment.takeClothesOff();
        }
        // All other regions: bypass both V1 isLocked and V2 canUnequip checks
        // by using V2EquipmentHelper directly with force=true
        return V2EquipmentHelper.unequipFromRegion(player, region, true);
    }

    // IRestrainable State Queries
    // Delegated to PlayerStateQuery component

    @Override
    public boolean canBeTiedUp() {
        return stateQuery.canBeTiedUp();
    }

    @Override
    public boolean isBoundAndGagged() {
        return stateQuery.isBoundAndGagged();
    }

    @Override
    public boolean hasKnives() {
        return stateQuery.hasKnives();
    }

    // Sale System - Delegated to PlayerSale component

    @Override
    public boolean isForSell() {
        return sale.isForSell();
    }

    @Override
    public com.tiedup.remake.util.tasks.ItemTask getSalePrice() {
        return sale.getSalePrice();
    }

    @Override
    public void putForSale(com.tiedup.remake.util.tasks.ItemTask price) {
        sale.putForSale(price);
    }

    @Override
    public void cancelSale() {
        sale.cancelSale();
    }

    @Override
    public boolean isTiedToPole() {
        // Check if leash proxy is attached to a fence knot (pole)
        if (!(player instanceof IPlayerLeashAccess access)) return false;
        if (!access.tiedup$isLeashed()) return false;

        net.minecraft.world.entity.Entity leashHolder =
            access.tiedup$getLeashHolder();
        return (
            leashHolder instanceof
                net.minecraft.world.entity.decoration.LeashFenceKnotEntity
        );
    }

    @Override
    public boolean tieToClosestPole(int searchRadius) {
        if (player == null) return false;
        return RestraintEffectUtils.tieToClosestPole(player, searchRadius);
    }

    @Override
    public boolean canBeKidnappedByEvents() {
        return stateQuery.canBeKidnappedByEvents();
    }

    @Override
    public boolean hasNamedCollar() {
        return dataRetrieval.hasNamedCollar();
    }

    @Override
    public boolean hasClothesWithSmallArms() {
        return dataRetrieval.hasClothesWithSmallArms();
    }

    @Override
    public boolean hasGaggingEffect() {
        return stateQuery.hasGaggingEffect();
    }

    @Override
    public boolean hasBlindingEffect() {
        return stateQuery.hasBlindingEffect();
    }

    // Equipment Take Off (local helpers)
    // Delegated to PlayerEquipment component

    public ItemStack takeEarplugsOff() {
        return equipment.takeEarplugsOff();
    }

    public ItemStack takeClothesOff() {
        return equipment.takeClothesOff();
    }

    // IRestrainable Equipment Put On
    // Delegated to PlayerEquipment component

    /** Equips earplugs (muffles sounds). Issue #14 fix: now calls onEquipped. */
    public void putEarplugsOn(ItemStack earplugs) {
        equipment.putEarplugsOn(earplugs);
    }

    // IRestrainable Equipment Replacement (V2 region-based)
    // Delegated to PlayerEquipment component (except TORSO - handled by PlayerClothesPermission)

    @Override
    public ItemStack replaceEquipment(
        BodyRegionV2 region,
        ItemStack newStack,
        boolean force
    ) {
        // Thread safety: must be called from the server thread.
        return switch (region) {
            case ARMS -> equipment.replaceBind(newStack, force);
            case MOUTH -> equipment.replaceGag(newStack, force);
            case EYES -> equipment.replaceBlindfold(newStack, force);
            case EARS -> equipment.replaceEarplugs(newStack, force);
            case NECK -> equipment.replaceCollar(newStack, force);
            case TORSO -> clothesPermission.replaceClothes(newStack, force);
            case HANDS -> equipment.replaceMittens(newStack, force);
            default -> ItemStack.EMPTY;
        };
    }

    // IRestrainable Bulk Operations

    @Override
    public void untie(boolean drop) {
        if (drop) {
            dropBondageItems(true);
        } else {
            // Clear all V2 equipment slots without dropping (no lifecycle hooks)
            var v2Equip = V2EquipmentHelper.getEquipment(player);
            if (v2Equip != null) {
                v2Equip.clearAll();
                V2EquipmentHelper.sync(player);
            }
        }

        // V1 speed reduction handled by MovementStyleManager (V2 tick-based).
        // See H6 fix — removing V1 calls prevents double stacking.

        if (isCaptive()) {
            free();
        }
        // Also detach leash if tied to pole (no captor but still leashed)
        else if (
            player instanceof IPlayerLeashAccess access &&
            access.tiedup$isLeashed()
        ) {
            access.tiedup$detachLeash();
            access.tiedup$dropLeash();
        }
    }

    @Override
    public void dropBondageItems(boolean drop) {
        if (!drop) return;
        dropBondageItems(true, true, true, true, true, true, true);
    }

    @Override
    public void dropBondageItems(boolean drop, boolean dropBind) {
        if (!drop) return;
        if (dropBind) kidnappedDropItem(takeBindOff());
    }

    @Override
    public void dropBondageItems(
        boolean drop,
        boolean dropBind,
        boolean dropGag,
        boolean dropBlindfold,
        boolean dropEarplugs,
        boolean dropCollar,
        boolean dropClothes
    ) {
        if (!drop) return;

        if (dropBind) takeBondageItemIfUnlocked(
            getEquipment(BodyRegionV2.ARMS),
            this::takeBindOff
        );
        if (dropGag) takeBondageItemIfUnlocked(
            getEquipment(BodyRegionV2.MOUTH),
            this::takeGagOff
        );
        if (dropBlindfold) takeBondageItemIfUnlocked(
            getEquipment(BodyRegionV2.EYES),
            this::takeBlindfoldOff
        );
        if (dropEarplugs) takeBondageItemIfUnlocked(
            getEquipment(BodyRegionV2.EARS),
            this::takeEarplugsOff
        );
        if (dropCollar) takeBondageItemIfUnlocked(
            getEquipment(BodyRegionV2.NECK),
            this::takeCollarOff
        );
        if (dropClothes) kidnappedDropItem(takeClothesOff());
    }

    @Override
    public void dropClothes() {
        ItemStack clothes = takeClothesOff();
        if (!clothes.isEmpty()) {
            kidnappedDropItem(clothes);
        }
    }

    @Override
    public void applyBondage(
        ItemStack bind,
        ItemStack gag,
        ItemStack blindfold,
        ItemStack earplugs,
        ItemStack collar,
        ItemStack clothes
    ) {
        if (!bind.isEmpty()) putBindOn(bind);
        if (!gag.isEmpty()) putGagOn(gag);
        if (!blindfold.isEmpty()) putBlindfoldOn(blindfold);
        if (!earplugs.isEmpty()) putEarplugsOn(earplugs);
        if (!collar.isEmpty()) putCollarOn(collar);
        if (!clothes.isEmpty()) putClothesOn(clothes);
    }

    @Override
    public int getBondageItemsWhichCanBeRemovedCount() {
        int count = 0;
        if (!isLocked(getEquipment(BodyRegionV2.ARMS), false)) count++;
        if (!isLocked(getEquipment(BodyRegionV2.MOUTH), false)) count++;
        if (!isLocked(getEquipment(BodyRegionV2.EYES), false)) count++;
        if (!isLocked(getEquipment(BodyRegionV2.EARS), false)) count++;
        if (!isLocked(getEquipment(BodyRegionV2.NECK), false)) count++;
        if (!getEquipment(BodyRegionV2.TORSO).isEmpty()) count++;
        return count;
    }

    // IRestrainable Callbacks
    // Delegated to PlayerEquipment component

    @Override
    public void checkGagAfterApply() {
        equipment.checkGagAfterApply();
    }

    @Override
    public void checkBlindfoldAfterApply() {
        equipment.checkBlindfoldAfterApply();
    }

    @Override
    public void checkEarplugsAfterApply() {
        equipment.checkEarplugsAfterApply();
    }

    @Override
    public void checkCollarAfterApply() {
        equipment.checkCollarAfterApply();
    }

    // IRestrainable Special Interactions
    // Delegated to PlayerSpecialActions component

    @Override
    public void applyChloroform(int duration) {
        specialActions.applyChloroform(duration);
    }

    /**
     * C6-V2: Narrowed to IRestrainableEntity
     */
    @Override
    public void takeBondageItemBy(IRestrainableEntity taker, int slotIndex) {
        specialActions.takeBondageItemBy(taker, slotIndex);
    }

    // IRestrainable Clothes Permissions
    // Delegated to PlayerClothesPermission component

    @Override
    public boolean canTakeOffClothes(Player player) {
        return clothesPermission.canTakeOffClothes(player);
    }

    @Override
    public boolean canChangeClothes(Player player) {
        return clothesPermission.canChangeClothes(player);
    }

    @Override
    public boolean canChangeClothes() {
        return clothesPermission.canChangeClothes();
    }

    // IRestrainable Lifecycle

    @Override
    public boolean onDeathKidnapped(net.minecraft.world.level.Level world) {
        // Clear movement style state so onActivate() re-fires on respawn
        // (PlayerBindState instance survives respawn via computeIfAbsent)
        clearMovementState();

        // Reset shock timers
        resetAutoShockTimer();

        // Unlock all locked items
        ItemStack collar = getEquipment(BodyRegionV2.NECK);
        if (
            !collar.isEmpty() &&
            collar.getItem() instanceof ILockable lockable
        ) {
            lockable.setLocked(collar, false);
        }

        // Drop all items
        dropBondageItems(true);

        if (isCaptive()) {
            free();
        }

        // Delegate to lifecycle component for registry cleanup and offline status
        return lifecycle.onDeathKidnapped(world);
    }

    // IRestrainable Entity Communication
    // Delegated to PlayerDataRetrieval component

    @Override
    public net.minecraft.world.entity.LivingEntity asLivingEntity() {
        return dataRetrieval.asLivingEntity();
    }
}
