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

import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.prison.PrisonerState;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.RestraintApplicator;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.Random;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;

/**
 * KidnapperCaptiveManager - Manages captive lifecycle and ICaptor implementation.
 *
 * Handles:
 * - ICaptor interface implementation (10 methods)
 * - Captive tracking (currentCaptive, pendingCaptiveUUID)
 * - UUID lazy resolution after chunk reload
 * - Lifecycle hooks (putBindOn, die, remove, tick)
 * - Abandon/keep captive logic (solo mode fallback)
 * - Struggle and attack punishment
 * - Leash physics (visual positioning)
 * - NBT serialization (backward compatible SlaveUUID → CaptiveUUID)
 *
 * <p><b>VERY HIGH complexity</b> - Critical captive state management with UUID resolution,
 * leash physics, and punishment flows. Bugs here cause captive loss or state corruption.</p>
 */
public class KidnapperCaptiveManager {

    // FIELDS

    /** Host callbacks */
    private final ICaptiveHost host;

    /** Entity reference (needed for leash attachment and UUID lookups) */
    private final EntityKidnapper entity;

    /** Entity data accessor for has-captive sync */
    private final EntityDataAccessor<Boolean> dataHasCaptive;

    /** Current captive being held */
    @Nullable
    private IRestrainable currentCaptive;

    /** UUID of captive to restore after chunk/server reload */
    @Nullable
    private UUID pendingCaptiveUUID;

    /** Retry counter for pending captive restoration (ticks) */
    private int pendingCaptiveRetryTicks = 0;

    /** Max ticks to retry before giving up (30 seconds = 600 ticks) */
    private static final int MAX_PENDING_CAPTIVE_RETRY_TICKS = 600;

    /** Flag to allow captive transfer (prevents re-capture during transfer) */
    private boolean allowCaptiveTransferFlag = false;

    /** Target for struggle punishment (shock/tighten) */
    @Nullable
    private LivingEntity strugglePunishmentTarget;

    // CONSTRUCTOR

    public KidnapperCaptiveManager(
        EntityKidnapper entity,
        ICaptiveHost host,
        EntityDataAccessor<Boolean> dataHasCaptive
    ) {
        this.entity = entity;
        this.host = host;
        this.dataHasCaptive = dataHasCaptive;
    }

    // I_KIDNAPPER IMPLEMENTATION

    /**
     * Add a captive to this kidnapper.
     * For NPCs: Attach vanilla leash.
     * For Players: Leash already attached by PlayerBindState mixin.
     */
    public void addCaptive(IRestrainable captive) {
        if (captive == null) return;

        // Guard: don't overwrite an existing captive - prevents state corruption
        if (this.currentCaptive != null && this.currentCaptive != captive) {
            TiedUpMod.LOGGER.warn(
                "[KidnapperCaptiveManager] {} tried to capture {} but already has captive {} - rejecting",
                host.getNpcName(),
                captive.getKidnappedName(),
                this.currentCaptive.getKidnappedName()
            );
            return;
        }

        // For Players: Leash is already attached by PlayerBindState.getCapturedBy()
        // For NPCs: Attach vanilla leash directly
        LivingEntity captiveEntity = captive.asLivingEntity();
        if (captiveEntity instanceof Mob mob) {
            // NPC captive - use vanilla leash
            mob.setLeashedTo(this.entity, true);
        }
        // Player captives have LeashProxyEntity attached via mixin - no action needed here

        this.currentCaptive = captive;
        host.setEntityData(dataHasCaptive, true);

        TiedUpMod.LOGGER.info(
            "[KidnapperCaptiveManager] {} captured {}",
            host.getNpcName(),
            captive.getKidnappedName()
        );
    }

    /**
     * Remove a captive from this kidnapper.
     * For NPCs: Detach vanilla leash.
     * For Players: Leash detached by PlayerBindState mixin.
     *
     * @param captive The captive to remove
     * @param transportState true if captive is being transported (no leash drop item)
     */
    public void removeCaptive(IRestrainable captive, boolean transportState) {
        if (captive == null || captive != this.currentCaptive) return;

        // For Players: Leash detached by PlayerBindState.free()
        // For NPCs: Detach vanilla leash without dropping item
        LivingEntity captiveEntity = captive.asLivingEntity();
        if (captiveEntity instanceof Mob mob && mob.isLeashed()) {
            mob.dropLeash(false, false); // Don't drop leash item when imprisoning
        }
        // Player captives have LeashProxyEntity removed via mixin free() - no action needed here

        String captiveName = this.currentCaptive.getKidnappedName();
        this.currentCaptive = null;
        host.setEntityData(dataHasCaptive, false);

        TiedUpMod.LOGGER.info(
            "[KidnapperCaptiveManager] {} released captive {}",
            host.getNpcName(),
            captiveName
        );
    }

    /**
     * Check if this kidnapper can capture a target.
     * Validates collar ownership if target has collar.
     * Also checks PrisonerManager state to prevent capturing already-imprisoned players.
     */
    public boolean canCapture(IRestrainable target) {
        if (target == null) return false;

        // Check if target is already in any non-FREE state
        // Only FREE players can be targeted by kidnappers
        if (host.level() instanceof ServerLevel serverLevel) {
            PrisonerManager manager = PrisonerManager.get(serverLevel);
            PrisonerState state = manager.getState(
                target.asLivingEntity().getUUID()
            );

            if (!state.isTargetable()) {
                TiedUpMod.LOGGER.debug(
                    "[KidnapperCaptiveManager] {} can't capture {} - not targetable (state={})",
                    host.getNpcName(),
                    target.getKidnappedName(),
                    state
                );
                return false;
            }
        }

        // If target has collar, verify we own it
        if (target.hasCollar()) {
            ItemStack collar = target.getEquipment(BodyRegionV2.NECK);
            if (CollarHelper.isCollar(collar)) {
                // Check if THIS kidnapper is owner
                if (!CollarHelper.getOwners(collar).contains(host.getUUID())) {
                    TiedUpMod.LOGGER.debug(
                        "[KidnapperCaptiveManager] {} can't capture {} - collar owned by someone else",
                        host.getNpcName(),
                        target.getKidnappedName()
                    );
                    return false; // Not our collar
                }
            }
        }

        return true;
    }

    /**
     * Check if captive can be released.
     */
    public boolean canRelease(IRestrainable captive) {
        return captive != null && captive == this.currentCaptive;
    }

    /**
     * Check if captive transfer is allowed.
     * Used to prevent re-capture during transfer between kidnappers.
     */
    public boolean allowCaptiveTransfer() {
        // Can transfer if explicitly flagged OR if we're tied up (forced release)
        return this.allowCaptiveTransferFlag || host.isTiedUp();
    }

    /**
     * Get the captive transfer flag.
     */
    public boolean getAllowCaptiveTransferFlag() {
        return this.allowCaptiveTransferFlag;
    }

    /**
     * Set the captive transfer flag.
     */
    public void setAllowCaptiveTransferFlag(boolean flag) {
        this.allowCaptiveTransferFlag = flag;
    }

    /**
     * Check if multiple captives are allowed.
     */
    public boolean allowMultipleCaptives() {
        return false; // Kidnappers only hold one captive at a time
    }

    /**
     * Called when a captive (player) logs out.
     * Releases the captive gracefully.
     */
    public void onCaptiveLogout(IRestrainable captive) {
        if (captive == this.currentCaptive) {
            TiedUpMod.LOGGER.info(
                "[KidnapperCaptiveManager] {} captive {} logged out - releasing",
                host.getNpcName(),
                captive.getKidnappedName()
            );
            removeCaptive(captive, false);
        }
    }

    /**
     * Called when a captive is released/freed.
     * Tracks escape, broadcasts alert, and transitions to IDLE.
     */
    public void onCaptiveReleased(IRestrainable releasedCaptive) {
        if (releasedCaptive != this.currentCaptive) return;

        TiedUpMod.LOGGER.info(
            "[KidnapperCaptiveManager] {} captive {} was released",
            host.getNpcName(),
            releasedCaptive.getKidnappedName()
        );

        LivingEntity escapedEntity = releasedCaptive.asLivingEntity();

        // Track escapee for potential recapture (30 seconds)
        host.getAggressionSystem().onCaptiveEscaped(escapedEntity);

        // Broadcast alert to nearby kidnappers
        host.broadcastAlert(escapedEntity);

        // Clear captive reference
        this.currentCaptive = null;
        host.setEntityData(dataHasCaptive, false);

        // Return to IDLE state
        host.setCurrentState(KidnapperState.IDLE);
    }

    /**
     * Called when a captive struggles.
     * Currently no special handling needed.
     */
    public void onCaptiveStruggle(IRestrainable captive) {
        // No-op - struggle punishment handled by onStruggleDetected() separately
    }

    /**
     * Check if this kidnapper has captives.
     */
    public boolean hasCaptives() {
        return this.currentCaptive != null;
    }

    /**
     * Get the entity implementing ICaptor.
     */
    public LivingEntity getEntity() {
        return this.entity;
    }

    // CAPTIVE ACCESS

    /**
     * Get the current captive.
     */
    @Nullable
    public IRestrainable getCaptive() {
        return this.currentCaptive;
    }

    /**
     * Transfer all captives to another kidnapper.
     * Updates PrisonerManager with new captor.
     */
    public void transferAllCaptivesTo(ICaptor target) {
        if (!this.hasCaptives() || target == null) return;

        // Update captivity state before transfer
        if (
            host.level() instanceof ServerLevel serverLevel &&
            target.getEntity() != null
        ) {
            PrisonerManager manager = PrisonerManager.get(serverLevel);
            UUID captiveId = this.currentCaptive.asLivingEntity().getUUID();
            PrisonerRecord record = manager.getRecord(captiveId);
            // Update captor ID
            record.setCaptorId(target.getEntity().getUUID());
        }

        // Transfer captive
        this.allowCaptiveTransferFlag = true;
        this.currentCaptive.transferCaptivityTo(target);
        this.allowCaptiveTransferFlag = false;
    }

    // UUID RESTORATION (CRITICAL)

    /**
     * Restore captive reference from UUID after chunk/server reload.
     * Called from customServerAiStep() when pendingCaptiveUUID is set but currentCaptive is null.
     * This is CRITICAL for maintaining captive state across save/load cycles.
     *
     * FIX: Now retries for up to 30 seconds before giving up, to handle:
     * - Players temporarily offline
     * - Chunks not yet loaded
     * - Entity loading delays
     */
    public void restoreCaptiveFromUUID() {
        if (this.pendingCaptiveUUID == null) return;
        if (!(host.level() instanceof ServerLevel serverLevel)) return;

        // Find entity by UUID
        net.minecraft.world.entity.Entity foundEntity = serverLevel.getEntity(
            this.pendingCaptiveUUID
        );

        if (foundEntity == null) {
            // Entity not found - increment retry counter
            this.pendingCaptiveRetryTicks++;

            if (
                this.pendingCaptiveRetryTicks >= MAX_PENDING_CAPTIVE_RETRY_TICKS
            ) {
                // Timeout reached - give up
                TiedUpMod.LOGGER.warn(
                    "[KidnapperCaptiveManager] {} giving up on captive UUID {} after {} ticks",
                    host.getNpcName(),
                    this.pendingCaptiveUUID,
                    this.pendingCaptiveRetryTicks
                );
                this.pendingCaptiveUUID = null;
                this.pendingCaptiveRetryTicks = 0;
            } else if (this.pendingCaptiveRetryTicks % 100 == 0) {
                // Log every 5 seconds at debug level
                TiedUpMod.LOGGER.debug(
                    "[KidnapperCaptiveManager] {} waiting for captive UUID {} ({}/{})",
                    host.getNpcName(),
                    this.pendingCaptiveUUID,
                    this.pendingCaptiveRetryTicks,
                    MAX_PENDING_CAPTIVE_RETRY_TICKS
                );
            }
            return;
        }

        if (!(foundEntity instanceof LivingEntity livingEntity)) {
            TiedUpMod.LOGGER.warn(
                "[KidnapperCaptiveManager] {} found captive entity {} but it's not LivingEntity",
                host.getNpcName(),
                foundEntity.getName().getString()
            );
            this.pendingCaptiveUUID = null;
            this.pendingCaptiveRetryTicks = 0;
            return;
        }

        // Get kidnapped state
        IRestrainable kidnappedState = KidnappedHelper.getKidnappedState(
            livingEntity
        );
        if (kidnappedState == null) {
            TiedUpMod.LOGGER.warn(
                "[KidnapperCaptiveManager] {} found captive {} but it doesn't have IRestrainable state",
                host.getNpcName(),
                livingEntity.getName().getString()
            );
            this.pendingCaptiveUUID = null;
            this.pendingCaptiveRetryTicks = 0;
            return;
        }

        // Verify captive is still marked as captured by this kidnapper
        ICaptor captor = kidnappedState.getCaptor();
        UUID captorUUID = (captor != null && captor.getEntity() != null)
            ? captor.getEntity().getUUID()
            : null;
        if (
            !kidnappedState.isCaptive() ||
            captorUUID == null ||
            !captorUUID.equals(host.getUUID())
        ) {
            TiedUpMod.LOGGER.warn(
                "[KidnapperCaptiveManager] {} found captive {} but it's not captured by us (captive={}, captor={})",
                host.getNpcName(),
                livingEntity.getName().getString(),
                kidnappedState.isCaptive(),
                captorUUID
            );
            this.pendingCaptiveUUID = null;
            this.pendingCaptiveRetryTicks = 0;
            return;
        }

        // Restore reference - success!
        this.currentCaptive = kidnappedState;
        host.setEntityData(dataHasCaptive, true);
        this.pendingCaptiveUUID = null;
        this.pendingCaptiveRetryTicks = 0;

        TiedUpMod.LOGGER.info(
            "[KidnapperCaptiveManager] {} restored captive {} from UUID",
            host.getNpcName(),
            kidnappedState.getKidnappedName()
        );
    }

    /**
     * Tick processing for UUID restoration and captive validation.
     * Call this from EntityKidnapper.customServerAiStep().
     */
    public void tick() {
        // FIX: Check if current captive is still alive (died, disconnected, etc.)
        // Without this, kidnapper keeps reference to dead captive, blocking all goals
        if (this.currentCaptive != null) {
            LivingEntity captiveEntity = this.currentCaptive.asLivingEntity();
            if (captiveEntity == null || !captiveEntity.isAlive()) {
                TiedUpMod.LOGGER.info(
                    "[KidnapperCaptiveManager] {} captive died or was removed - clearing reference",
                    host.getNpcName()
                );
                // Clear reference without dropping leash (entity is already gone)
                this.currentCaptive = null;
                host.setEntityData(dataHasCaptive, false);
                host.setCurrentState(KidnapperState.IDLE);
            }
        }

        // Restore captive from UUID after chunk/server reload
        if (this.pendingCaptiveUUID != null && this.currentCaptive == null) {
            restoreCaptiveFromUUID();
        }
    }

    // LIFECYCLE HOOKS

    /**
     * Called when kidnapper gets bound/tied up.
     * Releases captive and updates PrisonerManager.
     */
    public void onPutBindOn() {
        if (!this.hasCaptives()) return;

        // Capture local reference - escape() may trigger onCaptiveReleased() which nulls the field
        IRestrainable captive = this.currentCaptive;
        if (captive == null) return;

        TiedUpMod.LOGGER.info(
            "[KidnapperCaptiveManager] {} got tied up - releasing captive {}",
            host.getNpcName(),
            captive.getKidnappedName()
        );

        // Release prisoner from captivity state
        if (host.level() instanceof ServerLevel serverLevel) {
            UUID captiveUUID = captive.asLivingEntity().getUUID();
            PrisonerManager manager = PrisonerManager.get(serverLevel);
            PrisonerRecord record = manager.getPrisoner(captiveUUID);
            if (record != null && record.isImprisoned()) {
                // Clear captivity state - prisoner escaped
                // Use centralized escape service for complete cleanup
                com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
                    serverLevel,
                    captiveUUID,
                    "kidnapper tied up"
                );
            }
        }

        // Free the captive
        captive.free();
    }

    /**
     * Called when kidnapper dies.
     * Releases captive and updates PrisonerManager.
     */
    public void onDie() {
        if (!this.hasCaptives()) return;

        // Capture local reference - escape() may trigger onCaptiveReleased() which nulls the field
        IRestrainable captive = this.currentCaptive;
        if (captive == null) return;

        TiedUpMod.LOGGER.info(
            "[KidnapperCaptiveManager] {} died - releasing captive {}",
            host.getNpcName(),
            captive.getKidnappedName()
        );

        // Release prisoner from captivity state
        if (host.level() instanceof ServerLevel serverLevel) {
            UUID captiveUUID = captive.asLivingEntity().getUUID();
            PrisonerManager manager = PrisonerManager.get(serverLevel);
            PrisonerRecord record = manager.getPrisoner(captiveUUID);
            if (record != null && record.isImprisoned()) {
                // Clear captivity state - captor died
                // Use centralized escape service for complete cleanup
                com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
                    serverLevel,
                    captiveUUID,
                    "kidnapper died"
                );
            }
        }

        // Free the captive
        captive.free();
    }

    /**
     * Called when kidnapper is removed (despawn/chunk unload).
     * Conditional leash drop based on removal reason.
     */
    public void onRemove(
        net.minecraft.world.entity.Entity.RemovalReason reason
    ) {
        if (!this.hasCaptives()) return;

        // Graceful chunk unload - don't drop leash item
        boolean transportState = (reason ==
            net.minecraft.world.entity.Entity.RemovalReason.UNLOADED_TO_CHUNK);

        TiedUpMod.LOGGER.debug(
            "[KidnapperCaptiveManager] {} removed (reason={}), transport={}",
            host.getNpcName(),
            reason,
            transportState
        );

        removeCaptive(this.currentCaptive, transportState);
    }

    // SOLO MODE FALLBACK

    /**
     * Keep captive (solo mode fallback when no buyers available).
     * DecideNextActionGoal will handle job assignment or cell transport.
     */
    public void keepCaptive() {
        if (!this.hasCaptives()) {
            TiedUpMod.LOGGER.warn(
                "[KidnapperCaptiveManager] {} tried to keep captive but has none",
                host.getNpcName()
            );
            return;
        }

        TiedUpMod.LOGGER.info(
            "[KidnapperCaptiveManager] {} keeping captive {} for themselves",
            host.getNpcName(),
            this.currentCaptive.getKidnappedName()
        );

        // DecideNextActionGoal will take over and assign a job or bring to cell
        // No need to do anything else here - just don't set getOutState
    }

    /**
     * Abandon captive (solo mode fallback when no buyers and no cells).
     * Blindfolds, teleports to safe position, optionally removes restraints.
     */
    public void abandonCaptive() {
        if (!this.hasCaptives()) {
            TiedUpMod.LOGGER.warn(
                "[KidnapperCaptiveManager] {} tried to abandon captive but has none",
                host.getNpcName()
            );
            return;
        }

        LivingEntity captiveEntity = this.currentCaptive.asLivingEntity();

        TiedUpMod.LOGGER.info(
            "[KidnapperCaptiveManager] {} abandoning captive {}",
            host.getNpcName(),
            this.currentCaptive.getKidnappedName()
        );

        // Apply blindfold if enabled in config
        if (
            SettingsAccessor.isAbandonKeepsBlindfold()
        ) {
            net.minecraft.world.item.ItemStack blindfold = getBlindfoldItem();
            if (
                blindfold != null &&
                !blindfold.isEmpty() &&
                !this.currentCaptive.isBlindfolded()
            ) {
                this.currentCaptive.equip(BodyRegionV2.EYES, blindfold.copy());
            }
        }

        // Find safe position and teleport
        BlockPos safePos = findRandomSafePosition();
        if (safePos != null) {
            captiveEntity.teleportTo(
                safePos.getX() + 0.5,
                safePos.getY(),
                safePos.getZ() + 0.5
            );
        }

        // Remove restraints if NOT configured to keep them
        boolean keepBinds =
            SettingsAccessor.isAbandonKeepsBinds();
        if (!keepBinds) {
            // Full release including binds
            this.currentCaptive.untie(true);
            this.currentCaptive.free(true);
        } else {
            // Just release from leash, keep binds
            this.currentCaptive.free(false);
        }

        // Release from captivity state
        if (host.level() instanceof ServerLevel serverLevel) {
            UUID captiveUUID = captiveEntity.getUUID();
            PrisonerManager manager = PrisonerManager.get(serverLevel);
            PrisonerRecord record = manager.getPrisoner(captiveUUID);
            if (record != null && record.isImprisoned()) {
                // Clear captivity state - prisoner freed
                // Use centralized escape service for complete cleanup
                com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
                    serverLevel,
                    captiveUUID,
                    "abandoned by kidnapper"
                );
            }
        }

        // Enter flee state
        host.setGetOutState(true);
    }

    /**
     * Get blindfold item to use for abandon.
     */
    @Nullable
    private net.minecraft.world.item.ItemStack getBlindfoldItem() {
        return this.entity.getBlindfoldItem();
    }

    /**
     * Find a random safe position near kidnapper for abandoning captive.
     */
    @Nullable
    private BlockPos findRandomSafePosition() {
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            int offsetX = random.nextInt(21) - 10;
            int offsetZ = random.nextInt(21) - 10;
            BlockPos testPos = host.blockPosition().offset(offsetX, 0, offsetZ);
            if (host.level().getBlockState(testPos.below()).isSolid()) {
                return testPos;
            }
        }
        return host.blockPosition();
    }

    // PUNISHMENT SYSTEM

    /** Maximum distance to HEAR struggle (through bars, no line of sight needed) */
    private static final double STRUGGLE_HEARING_RANGE = 6.0;

    /** Maximum distance to SEE struggle (requires line of sight) */
    private static final double STRUGGLE_VISION_RANGE = 15.0;

    /**
     * Called when struggle noise is detected from a prisoner.
     * Uses HYBRID detection: HEARING (close range) + VISION (far range).
     *
     * Detection modes:
     * - Within 6 blocks: Can HEAR through bars/fences (no line of sight needed)
     * - Within 15 blocks: Can SEE if line of sight is clear
     *
     * Validates prisoner belongs to us, then applies shock + tighten punishment.
     */
    public void onStruggleDetected(LivingEntity source, BlockPos strugglePos) {
        // Ignore if not in GUARD state or already has punishment target
        if (
            host.getCurrentState() != KidnapperState.GUARD ||
            this.strugglePunishmentTarget != null
        ) {
            return;
        }

        // HYBRID DETECTION: Hearing (close) + Vision (far)
        double distance = host.distanceTo(source);

        // HEARING: Close range - can hear through bars/fences (no LOS needed)
        boolean canHear = distance <= STRUGGLE_HEARING_RANGE;

        // VISION: Longer range - requires clear line of sight
        boolean canSee =
            distance <= STRUGGLE_VISION_RANGE && host.hasLineOfSight(source);

        if (!canHear && !canSee) {
            return; // Can't detect the struggle
        }

        // Verify this prisoner belongs to a cell we're guarding
        IRestrainable sourceState = KidnappedHelper.getKidnappedState(source);
        if (sourceState == null || !sourceState.isCaptive()) {
            return;
        }

        // Check if prisoner is in a cell
        CellDataV2 cell = host.findCellContainingPrisoner(source.getUUID());
        if (cell == null) {
            return;
        }

        String detectionMethod = canHear ? "heard" : "saw";
        TiedUpMod.LOGGER.info(
            "[KidnapperCaptiveManager] {} {} struggle from prisoner {} (distance: {})",
            host.getNpcName(),
            detectionMethod,
            source.getName().getString(),
            distance
        );

        // Apply shock punishment
        sourceState.shockKidnapped(" Stop struggling!", 2.0f);

        // Tighten binds
        tightenBinds(source);

        // Set punishment target
        this.strugglePunishmentTarget = source;
    }

    /**
     * Punish a prisoner for attacking.
     * Used when prisoner attacks while in cell/camp.
     * Stronger punishment: shock (4.0f) + tighten.
     */
    public boolean punishAttackingPrisoner(Player player) {
        if (player == null) return false;

        IRestrainable playerState = KidnappedHelper.getKidnappedState(player);
        if (playerState == null) return false;

        // Check if captive (leashed) OR imprisoned/working in PrisonerManager
        if (!playerState.isCaptive()) {
            if (
                !(host.level() instanceof
                        net.minecraft.server.level.ServerLevel serverLevel)
            ) {
                return false;
            }
            PrisonerManager pm = PrisonerManager.get(serverLevel);
            PrisonerState pmState = pm.getState(player.getUUID());
            if (
                pmState != PrisonerState.IMPRISONED &&
                pmState != PrisonerState.WORKING
            ) {
                return false;
            }
        }

        boolean shouldPunish = false;

        // Check if prisoner is in one of our cells
        CellDataV2 cell = host.findCellContainingPrisoner(player.getUUID());
        if (cell != null) {
            shouldPunish = true;
        }

        // Also check camp ownership for camp kidnappers
        if (!shouldPunish && host.level() instanceof ServerLevel serverLevel) {
            UUID campId = entity.getAssociatedStructure();
            if (campId != null) {
                // Check if this prisoner is in a cell owned by our camp
                com.tiedup.remake.cells.CellRegistryV2 cellRegistry =
                    com.tiedup.remake.cells.CellRegistryV2.get(serverLevel);
                java.util.List<CellDataV2> campCells =
                    cellRegistry.getCellsByCamp(campId);

                for (CellDataV2 campCell : campCells) {
                    if (campCell.hasPrisoner(player.getUUID())) {
                        shouldPunish = true;
                        break;
                    }
                }
            }
        }

        if (shouldPunish) {
            TiedUpMod.LOGGER.info(
                "[KidnapperCaptiveManager] {} punishing attacking prisoner {}",
                host.getNpcName(),
                player.getName().getString()
            );

            // Stronger shock for attacking
            playerState.shockKidnapped(" How DARE you!", 4.0f);

            // Tighten binds (also re-applies if player struggled free)
            tightenBinds(player);

            // Re-apply gag and blindfold if missing
            RestraintApplicator.applyGagIfMissing(player, entity.getGagItem());
            RestraintApplicator.applyBlindfoldIfMissing(
                player,
                entity.getBlindfoldItem()
            );

            // Trigger dialogue
            host.talkToPlayersInRadius(
                EntityDialogueManager.DialogueCategory.PUNISH,
                20
            );

            return true;
        }

        return false;
    }

    /**
     * Tighten binds on a target.
     */
    private void tightenBinds(LivingEntity target) {
        IRestrainable targetState = KidnappedHelper.getKidnappedState(target);
        if (targetState != null) {
            RestraintApplicator.tightenBind(targetState, target);
        }
    }

    /**
     * Set struggle punishment target.
     */
    public void setStrugglePunishmentTarget(LivingEntity target) {
        this.strugglePunishmentTarget = target;
    }

    /**
     * Get struggle punishment target.
     */
    @Nullable
    public LivingEntity getStrugglePunishmentTarget() {
        return this.strugglePunishmentTarget;
    }

    /**
     * Clear struggle punishment target.
     */
    public void clearStrugglePunishmentTarget() {
        this.strugglePunishmentTarget = null;
    }

    // LEASH PHYSICS (VISUAL)

    /**
     * Get the position where the rope/leash is held.
     * Overrides default chest position to use right hand.
     * Custom visual physics - positions leash at right hand with body rotation.
     */
    public Vec3 getRopeHoldPosition(float partialTicks) {
        // Interpolate position for smooth rendering
        double x = entity.xo + (entity.getX() - entity.xo) * partialTicks;
        double y = entity.yo + (entity.getY() - entity.yo) * partialTicks;
        double z = entity.zo + (entity.getZ() - entity.zo) * partialTicks;

        // Get body yaw rotation
        float bodyYaw =
            entity.yBodyRotO +
            (entity.yBodyRot - entity.yBodyRotO) * partialTicks;
        double yawRad = Math.toRadians(bodyYaw);

        // Calculate offset in kidnapper's local space (right hand position)
        double lateralOffset = 0.4; // Right side
        double heightOffset = 1.3; // Shoulder height
        double forwardOffset = 0.2; // Slightly forward

        // Rotate offset by body yaw
        double offsetX =
            -Math.sin(yawRad) * forwardOffset +
            Math.cos(yawRad) * lateralOffset;
        double offsetZ =
            Math.cos(yawRad) * forwardOffset + Math.sin(yawRad) * lateralOffset;

        return new Vec3(x + offsetX, y + heightOffset, z + offsetZ);
    }

    // NBT SERIALIZATION

    /**
     * Save captive manager data to NBT.
     * Saves captive UUID for lazy restoration.
     * Backward compatible: saves as "CaptiveUUID" (old: "SlaveUUID").
     */
    public void saveToNBT(net.minecraft.nbt.CompoundTag tag) {
        // Save captive UUID if we have one
        if (this.currentCaptive != null) {
            tag.putUUID(
                "CaptiveUUID",
                this.currentCaptive.getKidnappedUniqueId()
            );
        }
    }

    /**
     * Load captive manager data from NBT.
     * Loads captive UUID for lazy restoration.
     * Backward compatible: reads both "CaptiveUUID" and old "SlaveUUID".
     */
    public void loadFromNBT(net.minecraft.nbt.CompoundTag tag) {
        // Load captive UUID (will be resolved later in tick)
        // Support both old "SlaveUUID" and new "CaptiveUUID" for backward compatibility
        if (tag.contains("CaptiveUUID")) {
            this.pendingCaptiveUUID = tag.getUUID("CaptiveUUID");
        } else if (tag.contains("SlaveUUID")) {
            this.pendingCaptiveUUID = tag.getUUID("SlaveUUID");
        }
    }
}
