package com.tiedup.remake.entities.damsel.components;

import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
import com.tiedup.remake.v2.bondage.component.ComponentType;
import com.tiedup.remake.v2.bondage.component.GaggingComponent;
import com.tiedup.remake.v2.bondage.component.BlindingComponent;
import com.tiedup.remake.state.IRestrainableEntity;
import com.tiedup.remake.util.RestraintEffectUtils;
import com.tiedup.remake.util.TiedUpSounds;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.v2.bondage.capability.V2BondageEquipment;
import java.util.Map;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.Tag;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;

/**
 * Equipment CRUD, coercion, bulk operations, and permissions for NPC bondage items.
 *
 * <p>Extracted from DamselBondageManager (H9 split). Owns all equipment state
 * and delegates to V2BondageEquipment for storage. Does NOT own captivity state.</p>
 *
 * <p>Cross-cutting note: {@code untie()} stays in the facade because it calls
 * both equipment and captivity methods.</p>
 */
public class NpcEquipmentManager {

    // FIELDS

    private final AbstractTiedUpNpc entity;
    private final IBondageHost host;

    /** Maps legacy V1 NBT key names to V2 body regions for migration. */
    static final Map<String, BodyRegionV2> V1_TO_V2 = Map.of(
        "Bind",
        BodyRegionV2.ARMS,
        "Gag",
        BodyRegionV2.MOUTH,
        "Blindfold",
        BodyRegionV2.EYES,
        "Earplugs",
        BodyRegionV2.EARS,
        "Collar",
        BodyRegionV2.NECK,
        "Clothes",
        BodyRegionV2.TORSO,
        "Mittens",
        BodyRegionV2.HANDS
    );

    // CONSTRUCTOR

    public NpcEquipmentManager(AbstractTiedUpNpc entity, IBondageHost host) {
        this.entity = entity;
        this.host = host;
    }

    // V2 EQUIPMENT HELPERS

    /** Get the V2 equipment storage from the parent entity, cast to concrete type. */
    V2BondageEquipment getEquipment() {
        return (V2BondageEquipment) entity.getV2Equipment();
    }

    /** Get the item equipped in a V2 body region. */
    public ItemStack getInRegion(BodyRegionV2 region) {
        return getEquipment().getInRegion(region);
    }

    /** Serialize V2 equipment to synched entity data (copy-on-write). */
    void syncToEntityData() {
        entity.syncV2Equipment();
    }

    // STATE QUERIES (14 methods)

    public boolean isTiedUp() {
        return getEquipment().isRegionOccupied(BodyRegionV2.ARMS);
    }

    public boolean isGagged() {
        return getEquipment().isRegionOccupied(BodyRegionV2.MOUTH);
    }

    public boolean isBlindfolded() {
        return getEquipment().isRegionOccupied(BodyRegionV2.EYES);
    }

    public boolean hasEarplugs() {
        return getEquipment().isRegionOccupied(BodyRegionV2.EARS);
    }

    public boolean hasCollar() {
        return getEquipment().isRegionOccupied(BodyRegionV2.NECK);
    }

    public boolean hasClothes() {
        return getEquipment().isRegionOccupied(BodyRegionV2.TORSO);
    }

    public boolean hasMittens() {
        return getEquipment().isRegionOccupied(BodyRegionV2.HANDS);
    }

    public boolean isBoundAndGagged() {
        return isTiedUp() && isGagged();
    }

    public boolean hasGaggingEffect() {
        ItemStack gag = getCurrentGag();
        if (gag.isEmpty()) return false;
        return DataDrivenBondageItem.getComponent(gag, ComponentType.GAGGING, GaggingComponent.class) != null;
    }

    public boolean hasBlindingEffect() {
        ItemStack blindfold = getCurrentBlindfold();
        if (blindfold.isEmpty()) return false;
        return DataDrivenBondageItem.getComponent(blindfold, ComponentType.BLINDING, BlindingComponent.class) != null;
    }

    public boolean hasKnives() {
        return false; // NPCs don't have inventories for knives
    }

    public boolean hasClothesWithSmallArms() {
        // For damsels, delegate to appearance component
        return entity.hasSlimArms();
    }

    public boolean hasLockedCollar() {
        ItemStack collar = getCurrentCollar();
        if (collar.isEmpty()) return false;
        if (collar.getItem() instanceof ILockable lockable) {
            return lockable.isLocked(collar);
        }
        return false;
    }

    public boolean hasNamedCollar() {
        ItemStack collar = getCurrentCollar();
        if (collar.isEmpty()) return false;
        return collar.hasCustomHoverName();
    }

    // EQUIPMENT GETTERS (7 methods)

    public ItemStack getCurrentBind() {
        return getEquipment().getInRegion(BodyRegionV2.ARMS);
    }

    public ItemStack getCurrentGag() {
        return getEquipment().getInRegion(BodyRegionV2.MOUTH);
    }

    public ItemStack getCurrentBlindfold() {
        return getEquipment().getInRegion(BodyRegionV2.EYES);
    }

    public ItemStack getCurrentEarplugs() {
        return getEquipment().getInRegion(BodyRegionV2.EARS);
    }

    public ItemStack getCurrentCollar() {
        return getEquipment().getInRegion(BodyRegionV2.NECK);
    }

    public ItemStack getCurrentClothes() {
        return getEquipment().getInRegion(BodyRegionV2.TORSO);
    }

    public ItemStack getCurrentMittens() {
        return getEquipment().getInRegion(BodyRegionV2.HANDS);
    }

    // EQUIPMENT PUTTERS (7 methods)

    public void putBindOn(ItemStack bind) {
        if (bind.isEmpty()) return;

        boolean wasAlreadyTied = isTiedUp();
        getEquipment().setInRegion(BodyRegionV2.ARMS, bind.copy());
        syncToEntityData();

        // Call onEquipped hook
        if (bind.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onEquipped(bind, entity);
        }

        // Apply speed reduction
        RestraintEffectUtils.applyBindSpeedReduction(entity);

        // Play sound
        TiedUpSounds.playBindSound(entity);

        // Stop movement
        entity.getNavigation().stop();

        // Broadcast captured dialogue if just got tied
        if (!wasAlreadyTied && !entity.level().isClientSide()) {
            host.talkToPlayersInRadius(
                com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory.DAMSEL_CAPTURED,
                SettingsAccessor.getDialogueRadius()
            );
        }

        TiedUpMod.LOGGER.debug("[EntityDamsel] {} tied up", host.getNpcName());
    }

    public void putGagOn(ItemStack gag) {
        if (gag.isEmpty()) return;

        getEquipment().setInRegion(BodyRegionV2.MOUTH, gag.copy());
        syncToEntityData();

        if (gag.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onEquipped(gag, entity);
        }

        TiedUpMod.LOGGER.debug("[EntityDamsel] {} gagged", host.getNpcName());
    }

    public void putBlindfoldOn(ItemStack blindfold) {
        if (blindfold.isEmpty()) return;

        getEquipment().setInRegion(BodyRegionV2.EYES, blindfold.copy());
        syncToEntityData();

        if (blindfold.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onEquipped(blindfold, entity);
        }

        TiedUpMod.LOGGER.debug(
            "[EntityDamsel] {} blindfolded",
            host.getNpcName()
        );
    }

    public void putEarplugsOn(ItemStack earplugs) {
        if (earplugs.isEmpty()) return;

        getEquipment().setInRegion(BodyRegionV2.EARS, earplugs.copy());
        syncToEntityData();

        if (earplugs.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onEquipped(earplugs, entity);
        }

        TiedUpMod.LOGGER.debug(
            "[EntityDamsel] {} has earplugs",
            host.getNpcName()
        );
    }

    public void putCollarOn(ItemStack collar) {
        if (collar.isEmpty()) return;

        getEquipment().setInRegion(BodyRegionV2.NECK, collar.copy());
        syncToEntityData();

        if (collar.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onEquipped(collar, entity);
        }

        // Play lock sound
        TiedUpSounds.playLockSound(entity);

        TiedUpMod.LOGGER.debug("[EntityDamsel] {} collared", host.getNpcName());
    }

    public void putClothesOn(ItemStack clothes) {
        if (clothes.isEmpty()) return;

        getEquipment().setInRegion(BodyRegionV2.TORSO, clothes.copy());
        syncToEntityData();

        if (clothes.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onEquipped(clothes, entity);
        }

        TiedUpMod.LOGGER.debug(
            "[EntityDamsel] {} changed clothes",
            host.getNpcName()
        );
    }

    public void putMittensOn(ItemStack mittens) {
        if (mittens.isEmpty()) return;

        getEquipment().setInRegion(BodyRegionV2.HANDS, mittens.copy());
        syncToEntityData();

        if (mittens.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onEquipped(mittens, entity);
        }

        TiedUpMod.LOGGER.debug(
            "[EntityDamsel] {} has mittens",
            host.getNpcName()
        );
    }

    // EQUIPMENT REMOVERS (8 methods + 1 force-remove)

    /**
     * Force-remove the item from a region, bypassing ILockable lock checks.
     * Runs the same side effects as the per-region takeXOff methods (sync, onUnequipped, etc.).
     * Used by {@link DamselBondageManager#forceUnequip(BodyRegionV2)}.
     *
     * @param region The body region to force-remove from
     * @return The removed ItemStack, or {@link ItemStack#EMPTY} if the slot was empty
     */
    public ItemStack forceRemoveFromRegion(BodyRegionV2 region) {
        // NECK and TORSO already have correct force/no-lock handling
        if (region == BodyRegionV2.NECK) return takeCollarOff(true);
        if (region == BodyRegionV2.TORSO) return takeClothesOff();

        ItemStack current = getInRegion(region);
        if (current.isEmpty()) return ItemStack.EMPTY;

        // Clear slot + sync (no lock check — that's the point of force)
        getEquipment().setInRegion(region, ItemStack.EMPTY);
        syncToEntityData();

        if (current.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onUnequipped(current, entity);
        }

        // Region-specific side effects
        if (region == BodyRegionV2.ARMS) {
            RestraintEffectUtils.removeBindSpeedReduction(entity);
            TiedUpMod.LOGGER.debug(
                "[EntityDamsel] {} force-untied",
                host.getNpcName()
            );
        }

        return current;
    }

    public ItemStack takeBindOff() {
        ItemStack current = getCurrentBind();
        if (current.isEmpty()) return ItemStack.EMPTY;

        // Check if locked
        if (
            current.getItem() instanceof ILockable lockable &&
            lockable.isLocked(current)
        ) {
            return ItemStack.EMPTY;
        }

        getEquipment().setInRegion(BodyRegionV2.ARMS, ItemStack.EMPTY);
        syncToEntityData();

        if (current.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onUnequipped(current, entity);
        }

        // Remove speed reduction
        RestraintEffectUtils.removeBindSpeedReduction(entity);

        TiedUpMod.LOGGER.debug("[EntityDamsel] {} untied", host.getNpcName());

        return current;
    }

    public ItemStack takeGagOff() {
        ItemStack current = getCurrentGag();
        if (current.isEmpty()) return ItemStack.EMPTY;

        if (
            current.getItem() instanceof ILockable lockable &&
            lockable.isLocked(current)
        ) {
            return ItemStack.EMPTY;
        }

        getEquipment().setInRegion(BodyRegionV2.MOUTH, ItemStack.EMPTY);
        syncToEntityData();

        if (current.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onUnequipped(current, entity);
        }

        return current;
    }

    public ItemStack takeBlindfoldOff() {
        ItemStack current = getCurrentBlindfold();
        if (current.isEmpty()) return ItemStack.EMPTY;

        if (
            current.getItem() instanceof ILockable lockable &&
            lockable.isLocked(current)
        ) {
            return ItemStack.EMPTY;
        }

        getEquipment().setInRegion(BodyRegionV2.EYES, ItemStack.EMPTY);
        syncToEntityData();

        if (current.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onUnequipped(current, entity);
        }

        return current;
    }

    public ItemStack takeEarplugsOff() {
        ItemStack current = getCurrentEarplugs();
        if (current.isEmpty()) return ItemStack.EMPTY;

        if (
            current.getItem() instanceof ILockable lockable &&
            lockable.isLocked(current)
        ) {
            return ItemStack.EMPTY;
        }

        getEquipment().setInRegion(BodyRegionV2.EARS, ItemStack.EMPTY);
        syncToEntityData();

        if (current.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onUnequipped(current, entity);
        }

        return current;
    }

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

    public ItemStack takeCollarOff(boolean force) {
        ItemStack current = getCurrentCollar();
        if (current.isEmpty()) return ItemStack.EMPTY;

        if (
            !force &&
            current.getItem() instanceof ILockable lockable &&
            lockable.isLocked(current)
        ) {
            return ItemStack.EMPTY;
        }

        getEquipment().setInRegion(BodyRegionV2.NECK, ItemStack.EMPTY);
        syncToEntityData();

        if (current.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onUnequipped(current, entity);
        }

        // Play unlock sound
        TiedUpSounds.playUnlockSound(entity);

        return current;
    }

    public ItemStack takeClothesOff() {
        ItemStack current = getCurrentClothes();
        if (current.isEmpty()) return ItemStack.EMPTY;

        getEquipment().setInRegion(BodyRegionV2.TORSO, ItemStack.EMPTY);
        syncToEntityData();

        if (current.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onUnequipped(current, entity);
        }

        return current;
    }

    public ItemStack takeMittensOff() {
        ItemStack current = getCurrentMittens();
        if (current.isEmpty()) return ItemStack.EMPTY;

        if (
            current.getItem() instanceof ILockable lockable &&
            lockable.isLocked(current)
        ) {
            return ItemStack.EMPTY;
        }

        getEquipment().setInRegion(BodyRegionV2.HANDS, ItemStack.EMPTY);
        syncToEntityData();

        if (current.getItem() instanceof IV2BondageItem bondageItem) {
            bondageItem.onUnequipped(current, entity);
        }

        return current;
    }

    // EQUIPMENT REPLACERS (14 methods)

    public ItemStack replaceBind(ItemStack newBind) {
        ItemStack oldBind = takeBindOff();
        if (!oldBind.isEmpty() || !newBind.isEmpty()) {
            putBindOn(newBind);
        }
        return oldBind;
    }

    public ItemStack replaceBind(ItemStack newBind, boolean force) {
        ItemStack oldBind = force ? getCurrentBind() : takeBindOff();
        if (!oldBind.isEmpty() || !newBind.isEmpty()) {
            if (force && !oldBind.isEmpty()) {
                getEquipment().setInRegion(BodyRegionV2.ARMS, ItemStack.EMPTY);
                // Don't sync here -- putBindOn will sync
                if (oldBind.getItem() instanceof IV2BondageItem bondageItem) {
                    bondageItem.onUnequipped(oldBind, entity);
                }
            }
            putBindOn(newBind);
        }
        return oldBind;
    }

    public ItemStack replaceGag(ItemStack newGag) {
        return replaceGag(newGag, false);
    }

    public ItemStack replaceGag(ItemStack newGag, boolean force) {
        ItemStack oldGag = force ? getCurrentGag() : takeGagOff();
        if (!oldGag.isEmpty() || !newGag.isEmpty()) {
            if (force && !oldGag.isEmpty()) {
                getEquipment().setInRegion(BodyRegionV2.MOUTH, ItemStack.EMPTY);
                if (oldGag.getItem() instanceof IV2BondageItem bondageItem) {
                    bondageItem.onUnequipped(oldGag, entity);
                }
            }
            putGagOn(newGag);
        }
        return oldGag;
    }

    public ItemStack replaceBlindfold(ItemStack newBlindfold) {
        return replaceBlindfold(newBlindfold, false);
    }

    public ItemStack replaceBlindfold(ItemStack newBlindfold, boolean force) {
        ItemStack oldBlindfold = force
            ? getCurrentBlindfold()
            : takeBlindfoldOff();
        if (!oldBlindfold.isEmpty() || !newBlindfold.isEmpty()) {
            if (force && !oldBlindfold.isEmpty()) {
                getEquipment().setInRegion(BodyRegionV2.EYES, ItemStack.EMPTY);
                if (
                    oldBlindfold.getItem() instanceof IV2BondageItem bondageItem
                ) {
                    bondageItem.onUnequipped(oldBlindfold, entity);
                }
            }
            putBlindfoldOn(newBlindfold);
        }
        return oldBlindfold;
    }

    public ItemStack replaceEarplugs(ItemStack newEarplugs) {
        return replaceEarplugs(newEarplugs, false);
    }

    public ItemStack replaceEarplugs(ItemStack newEarplugs, boolean force) {
        ItemStack oldEarplugs = force
            ? getCurrentEarplugs()
            : takeEarplugsOff();
        if (!oldEarplugs.isEmpty() || !newEarplugs.isEmpty()) {
            if (force && !oldEarplugs.isEmpty()) {
                getEquipment().setInRegion(BodyRegionV2.EARS, ItemStack.EMPTY);
                if (
                    oldEarplugs.getItem() instanceof IV2BondageItem bondageItem
                ) {
                    bondageItem.onUnequipped(oldEarplugs, entity);
                }
            }
            putEarplugsOn(newEarplugs);
        }
        return oldEarplugs;
    }

    public ItemStack replaceCollar(ItemStack newCollar) {
        return replaceCollar(newCollar, false);
    }

    public ItemStack replaceCollar(ItemStack newCollar, boolean force) {
        ItemStack oldCollar = takeCollarOff(force);
        if (!oldCollar.isEmpty() || !newCollar.isEmpty()) {
            putCollarOn(newCollar);
        }
        return oldCollar;
    }

    public ItemStack replaceClothes(ItemStack newClothes) {
        return replaceClothes(newClothes, false);
    }

    public ItemStack replaceClothes(ItemStack newClothes, boolean force) {
        ItemStack oldClothes = takeClothesOff();
        if (!oldClothes.isEmpty() || !newClothes.isEmpty()) {
            putClothesOn(newClothes);
        }
        return oldClothes;
    }

    public ItemStack replaceMittens(ItemStack newMittens) {
        return replaceMittens(newMittens, false);
    }

    public ItemStack replaceMittens(ItemStack newMittens, boolean force) {
        ItemStack oldMittens = force ? getCurrentMittens() : takeMittensOff();
        if (!oldMittens.isEmpty() || !newMittens.isEmpty()) {
            if (force && !oldMittens.isEmpty()) {
                getEquipment().setInRegion(BodyRegionV2.HANDS, ItemStack.EMPTY);
                if (
                    oldMittens.getItem() instanceof IV2BondageItem bondageItem
                ) {
                    bondageItem.onUnequipped(oldMittens, entity);
                }
            }
            putMittensOn(newMittens);
        }
        return oldMittens;
    }

    // BULK OPERATIONS

    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);

        TiedUpMod.LOGGER.info(
            "[EntityDamsel] {} fully restrained",
            host.getNpcName()
        );
    }

    /**
     * Drop bondage items on the ground. Convenience overload.
     */
    public void dropBondageItems(boolean drop) {
        dropBondageItems(drop, true, true, true, true, true, true);
    }

    /**
     * Drop bondage items with optional bind control. Convenience overload.
     */
    public void dropBondageItems(boolean drop, boolean dropBind) {
        dropBondageItems(drop, dropBind, true, true, true, true, true);
    }

    /**
     * Drop bondage items with full granular control.
     *
     * @param drop master switch -- if false, nothing is dropped
     */
    public void dropBondageItems(
        boolean drop,
        boolean dropBind,
        boolean dropGag,
        boolean dropBlindfold,
        boolean dropEarplugs,
        boolean dropCollar,
        boolean dropClothes
    ) {
        if (!drop) return;

        if (dropBind) {
            ItemStack bind = takeBindOff();
            if (!bind.isEmpty()) host.dropItemStack(bind);
        }
        if (dropGag) {
            ItemStack gag = takeGagOff();
            if (!gag.isEmpty()) host.dropItemStack(gag);
        }
        if (dropBlindfold) {
            ItemStack blindfold = takeBlindfoldOff();
            if (!blindfold.isEmpty()) host.dropItemStack(blindfold);
        }
        if (dropEarplugs) {
            ItemStack earplugs = takeEarplugsOff();
            if (!earplugs.isEmpty()) host.dropItemStack(earplugs);
        }
        if (dropCollar) {
            ItemStack collar = takeCollarOff();
            if (!collar.isEmpty()) host.dropItemStack(collar);
        }
        if (dropClothes) {
            ItemStack clothes = takeClothesOff();
            if (!clothes.isEmpty()) host.dropItemStack(clothes);
        }
        // Always drop mittens if dropping
        ItemStack mittens = takeMittensOff();
        if (!mittens.isEmpty()) host.dropItemStack(mittens);
    }

    public void dropClothes() {
        ItemStack clothes = takeClothesOff();
        if (!clothes.isEmpty()) {
            host.dropItemStack(clothes);
        }
    }

    public int getBondageItemsWhichCanBeRemovedCount() {
        int count = 0;
        if (!getCurrentBind().isEmpty() && !isLocked(getCurrentBind())) count++;
        if (!getCurrentGag().isEmpty() && !isLocked(getCurrentGag())) count++;
        if (
            !getCurrentBlindfold().isEmpty() && !isLocked(getCurrentBlindfold())
        ) count++;
        if (
            !getCurrentEarplugs().isEmpty() && !isLocked(getCurrentEarplugs())
        ) count++;
        if (
            !getCurrentCollar().isEmpty() && !isLocked(getCurrentCollar())
        ) count++;
        if (
            !getCurrentClothes().isEmpty() && !isLocked(getCurrentClothes())
        ) count++;
        return count;
    }

    // ADVANCED QUERIES

    public boolean canBeTiedUp() {
        return !isTiedUp();
    }

    // PERMISSIONS

    public boolean canTakeOffClothes(Player player) {
        return true; // NPCs don't have permission restrictions
    }

    public boolean canChangeClothes(Player player) {
        return true;
    }

    public boolean canChangeClothes() {
        return true;
    }

    // COLLAR OWNER CHECK

    /**
     * Check if a player is an owner of this NPC's collar.
     * Returns true if no collar (anyone can interact with uncolored NPCs).
     */
    public boolean isCollarOwner(Player player) {
        if (!hasCollar()) return true;
        ItemStack collar = getCurrentCollar();
        if (!CollarHelper.isCollar(collar)) return true;
        return CollarHelper.isOwner(collar, player);
    }

    // COERCION

    public void tighten(Player tightener) {
        if (!isTiedUp()) return;

        TiedUpSounds.playSound(
            entity,
            net.minecraft.sounds.SoundEvents.PLAYER_ATTACK_NODAMAGE,
            1.0f
        );
        TiedUpMod.LOGGER.debug(
            "[EntityDamsel] {} tightened by {}",
            host.getNpcName(),
            tightener.getName().getString()
        );
    }

    public void applyChloroform(int duration) {
        RestraintEffectUtils.applyChloroformEffects(entity, duration);
    }

    public void shockKidnapped() {
        shockKidnapped("", 1.0f);
    }

    public void shockKidnapped(String messageAddon, float damage) {
        // Play shock sound
        TiedUpSounds.playSound(
            entity,
            net.minecraft.sounds.SoundEvents.LIGHTNING_BOLT_IMPACT,
            0.5f
        );

        // Apply damage
        entity.hurt(entity.damageSources().magic(), damage);

        TiedUpMod.LOGGER.debug(
            "[EntityDamsel] {} shocked ({} damage)",
            host.getNpcName(),
            damage
        );
    }

    public void takeBondageItemBy(IRestrainableEntity taker, int slotIndex) {
        ItemStack taken = ItemStack.EMPTY;

        switch (slotIndex) {
            case 0 -> taken = takeBindOff();
            case 1 -> taken = takeGagOff();
            case 2 -> taken = takeBlindfoldOff();
            case 3 -> taken = takeEarplugsOff();
            case 4 -> taken = takeCollarOff();
            case 5 -> taken = takeClothesOff();
        }

        if (!taken.isEmpty()) {
            taker.kidnappedDropItem(taken);
            TiedUpMod.LOGGER.debug(
                "[EntityDamsel] {} item taken by {}",
                host.getNpcName(),
                taker.getKidnappedName()
            );
        }
    }

    // INTERNAL - UNLOCK ALL

    /**
     * Unlock all locked bondage items on this entity.
     * Used by the facade in onDeathKidnapped.
     * Epic 4B: Uses V2 regions, syncs once at end.
     */
    public void unlockAllItems() {
        BodyRegionV2[] regions = {
            BodyRegionV2.ARMS,
            BodyRegionV2.MOUTH,
            BodyRegionV2.EYES,
            BodyRegionV2.EARS,
            BodyRegionV2.NECK,
        };
        boolean changed = false;
        for (BodyRegionV2 region : regions) {
            ItemStack stack = getEquipment().getInRegion(region);
            if (
                !stack.isEmpty() &&
                stack.getItem() instanceof ILockable lockable &&
                lockable.isLocked(stack)
            ) {
                lockable.setLocked(stack, false);
                changed = true;
            }
        }
        if (changed) {
            syncToEntityData();
        }
    }

    /**
     * Clear all V2 regions at once and sync. Used by untie(drop=false) in facade.
     */
    public void clearAllAndSync() {
        getEquipment().clearAll();
        syncToEntityData();
    }

    // PERSISTENCE

    /**
     * Save equipment-related state to NBT.
     * Epic 4B: Serializes V2BondageEquipment as a single CompoundTag.
     */
    public void saveEquipmentToTag(CompoundTag tag) {
        tag.put("V2Equipment", getEquipment().serializeNBT());
    }

    /**
     * Load equipment-related state from NBT.
     * Supports both V2 format and legacy V1 migration.
     */
    public void loadEquipmentFromTag(CompoundTag tag) {
        if (tag.contains("V2Equipment", Tag.TAG_COMPOUND)) {
            // V2 format: deserialize directly
            getEquipment().deserializeNBT(tag.getCompound("V2Equipment"));
        } else {
            // Legacy V1 migration: load individual item keys into V2 regions
            for (Map.Entry<String, BodyRegionV2> entry : V1_TO_V2.entrySet()) {
                if (tag.contains(entry.getKey(), Tag.TAG_COMPOUND)) {
                    ItemStack stack = ItemStack.of(
                        tag.getCompound(entry.getKey())
                    );
                    if (!stack.isEmpty()) {
                        getEquipment().setInRegion(entry.getValue(), stack);
                    }
                }
            }
        }
        syncToEntityData();
    }

    // PRIVATE HELPERS

    /**
     * Check if an item is locked (convenience -- no force parameter).
     */
    private boolean isLocked(ItemStack stack) {
        return (
            stack.getItem() instanceof ILockable lockable &&
            lockable.isLocked(stack)
        );
    }
}
