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

import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.StuckDetector;
import com.tiedup.remake.items.ItemKey;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.action.PacketTying;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerState;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.KidnapperAIHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;

/**
 * AI Goal for EntityKidnapper to capture and enslave targets.
 *
 *
 * This goal:
 * 1. Moves towards the target
 * 2. When close enough, applies bind and gag
 * 3. Enslaves the target once fully restrained
 *
 * Based on original EntityAIEnslavingPlayer from 1.12.2
 */
public class KidnapperCaptureGoal extends Goal {

    private final EntityKidnapper kidnapper;
    private final double speedModifier;
    private LivingEntity target;
    private int captureProgress;
    private int pathRecalculateDelay;
    private int chaseTicks;

    // Capture states - progressive application order
    private static final int STATE_APPROACH = 0;
    private static final int STATE_BIND = 1;
    private static final int STATE_GAG = 2;
    private static final int STATE_MITTENS = 3;
    private static final int STATE_EARPLUGS = 4;
    private static final int STATE_BLINDFOLD = 5;
    private static final int STATE_COLLAR = 6;
    private static final int STATE_ENSLAVE = 7;
    private static final int STATE_DONE = 8;

    /** Radius for broadcasting dialogue to nearby players */
    private static final int DIALOGUE_RADIUS = 20;

    /** Cooldown between same dialogue category (in ticks) */
    private static final int DIALOGUE_COOLDOWN = 100; // 5 seconds

    // ========== Distance Constants ==========
    /** Distance at which target is considered escaped */
    private static final double ESCAPE_DISTANCE = 30.0;
    /** Distance needed to start/continue capture */
    private static final double CAPTURE_DISTANCE = 2.0;
    /** Distance at which capture progress resets */
    private static final double DISTANCE_RESET = 3.0;
    /** Look control speed (yaw/pitch max rotation per tick) */
    private static final float LOOK_SPEED = 30.0F;

    // ========== Timing Constants (in ticks) ==========
    /** Path recalculation interval during approach (10 ticks = 0.5s) */
    private static final int PATH_RECALC_APPROACH = 10;
    /** Path recalculation interval during capture (15 ticks = 0.75s) */
    private static final int PATH_RECALC_CAPTURE = 15;
    /** Time to apply mittens (~1.5 seconds) */
    private static final int MITTENS_APPLY_TIME = 30;
    /** Time to apply earplugs (~1 second) */
    private static final int EARPLUGS_APPLY_TIME = 20;
    /** Time to apply blindfold (~2 seconds) */
    private static final int BLINDFOLD_APPLY_TIME = 40;
    /** Time to apply collar (~2 seconds) */
    private static final int COLLAR_APPLY_TIME = 40;
    /** Maximum chase duration before giving up (60 seconds) */
    private static final int MAX_CHASE_TICKS = 1200;

    private int captureState;

    /** Flag to signal the goal system to stop this goal cleanly (avoids double-stop) */
    private boolean forceStop;

    /** Stuck detection for approach phase */
    private final StuckDetector stuckDetector = new StuckDetector(60, 3);

    /** Track last time each dialogue category was used */
    private final Map<DialogueCategory, Long> dialogueCooldowns =
        new HashMap<>();

    /**
     * Create a new capture goal.
     *
     * @param kidnapper The kidnapper entity
     */
    public KidnapperCaptureGoal(EntityKidnapper kidnapper) {
        this(kidnapper, 1.2);
    }

    /**
     * Create a new capture goal with custom speed.
     *
     * @param kidnapper The kidnapper entity
     * @param speedModifier Speed multiplier when chasing
     */
    public KidnapperCaptureGoal(
        EntityKidnapper kidnapper,
        double speedModifier
    ) {
        this.kidnapper = kidnapper;
        this.speedModifier = speedModifier;
        this.captureProgress = 0;
        this.pathRecalculateDelay = 0;
        this.captureState = STATE_APPROACH;
        this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
    }

    @Override
    public boolean canUse() {
        // Need a target to capture
        LivingEntity currentTarget = this.kidnapper.getTarget();
        if (currentTarget == null) {
            // Only log occasionally to avoid spam
            if (this.kidnapper.tickCount % 100 == 0) {
                TiedUpMod.LOGGER.debug(
                    "[KidnapperCaptureGoal] {} canUse=false: no target",
                    this.kidnapper.getNpcName()
                );
            }
            return false;
        }

        // Can't capture if tied up
        if (this.kidnapper.isTiedUp()) {
            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} canUse=false: kidnapper is tied",
                this.kidnapper.getNpcName()
            );
            return false;
        }

        // Can't capture if already have slave
        if (this.kidnapper.hasCaptives()) {
            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} canUse=false: already has captives",
                this.kidnapper.getNpcName()
            );
            return false;
        }

        // Target must be alive
        if (!currentTarget.isAlive()) {
            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} canUse=false: target {} is dead",
                this.kidnapper.getNpcName(),
                currentTarget.getName().getString()
            );
            return false;
        }

        // Must pass all suitability checks (grace period, labor, etc.)
        if (!this.kidnapper.isSuitableTarget(currentTarget)) {
            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} canUse=false: target {} is not suitable (protected)",
                this.kidnapper.getNpcName(),
                currentTarget.getName().getString()
            );
            // Clear the invalid target
            this.kidnapper.setTarget(null);
            return false;
        }

        TiedUpMod.LOGGER.debug(
            "[KidnapperCaptureGoal] {} canUse=TRUE for target {}",
            this.kidnapper.getNpcName(),
            currentTarget.getName().getString()
        );

        this.target = currentTarget;
        return true;
    }

    @Override
    public boolean canContinueToUse() {
        // Stop if no target or target died
        if (this.target == null || !this.target.isAlive()) {
            return false;
        }

        // Stop if kidnapper got tied up
        if (this.kidnapper.isTiedUp()) {
            return false;
        }

        // Stop if capture is done or force-stopped (e.g. chase timeout)
        if (this.captureState == STATE_DONE || this.forceStop) {
            return false;
        }

        // Stop if target escaped too far
        if (this.kidnapper.distanceTo(this.target) > ESCAPE_DISTANCE) {
            return false;
        }

        // Stop if target became protected (entered labor, grace period, etc.)
        // Use isTargetStillValidForChase() instead of isSuitableTarget() to avoid
        // abandoning chase when trees/obstacles temporarily block line-of-sight
        if (!this.kidnapper.isTargetStillValidForChase(this.target)) {
            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} stopping - target {} became protected",
                this.kidnapper.getNpcName(),
                this.target.getName().getString()
            );
            return false;
        }

        IBondageState state = KidnappedHelper.getKidnappedState(this.target);
        if (state != null && state.isCaptive()) {
            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} stopping - target {} already captured by another",
                this.kidnapper.getNpcName(),
                this.target.getName().getString()
            );
            return false;
        }

        return true;
    }

    @Override
    public void start() {
        this.captureState = STATE_APPROACH;
        this.captureProgress = 0;
        this.pathRecalculateDelay = 0;
        this.chaseTicks = 0;
        this.forceStop = false;
        this.stuckDetector.reset();

        // Set AI state to CAPTURE - prevents receiving alerts during capture
        this.kidnapper.setCurrentState(KidnapperState.CAPTURE);

        // Equip bind and gag items
        this.kidnapper.setUpHeldItems();

        // Broadcast to nearby players (includes target since they're close)
        broadcastWithCooldown(DialogueCategory.CAPTURE_START);

        TiedUpMod.LOGGER.debug(
            "[KidnapperCaptureGoal] {} STARTED pursuing {} (state: CAPTURE)",
            this.kidnapper.getNpcName(),
            this.target.getName().getString()
        );
    }

    @Override
    public void stop() {
        // Clear target if capture failed
        if (this.captureState != STATE_DONE) {
            this.kidnapper.setTarget(null);
            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} stopped pursuing target",
                this.kidnapper.getNpcName()
            );
        }

        // Reset AI state to IDLE when capture ends
        this.kidnapper.setCurrentState(KidnapperState.IDLE);

        // Hide held items when not chasing
        this.kidnapper.clearHeldItems();

        // Clear dialogue cooldowns to prevent memory leak
        this.dialogueCooldowns.clear();

        this.target = null;
        this.captureState = STATE_APPROACH;
        this.captureProgress = 0;
        this.stuckDetector.reset();
        this.kidnapper.getNavigation().stop();
    }

    @Override
    public void tick() {
        // TIMEOUT: Give up chase after MAX_CHASE_TICKS to prevent infinite pursuit
        this.chaseTicks++;
        if (this.chaseTicks > MAX_CHASE_TICKS) {
            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} chase timeout ({} ticks), giving up on {}",
                this.kidnapper.getNpcName(),
                MAX_CHASE_TICKS,
                this.target != null
                    ? this.target.getName().getString()
                    : "unknown"
            );
            // Set flag so canContinueToUse() returns false next tick,
            // letting the goal system call stop() exactly once.
            this.forceStop = true;
            return;
        }

        // Check target validity - could have died or been removed
        if (this.target == null || !this.target.isAlive()) {
            this.forceStop = true;
            return;
        }

        // Always look at target
        this.kidnapper.getLookControl().setLookAt(
            this.target,
            LOOK_SPEED,
            LOOK_SPEED
        );

        double distance = this.kidnapper.distanceTo(this.target);

        switch (this.captureState) {
            case STATE_APPROACH:
                tickApproach(distance);
                break;
            case STATE_BIND:
                tickBind();
                break;
            case STATE_GAG:
                tickGag();
                break;
            case STATE_MITTENS:
                tickMittens();
                break;
            case STATE_EARPLUGS:
                tickEarplugs();
                break;
            case STATE_BLINDFOLD:
                tickBlindfold();
                break;
            case STATE_COLLAR:
                tickCollar();
                break;
            case STATE_ENSLAVE:
                tickEnslave();
                break;
        }
    }

    /**
     * Approach the target.
     */
    private void tickApproach(double distance) {
        // Update stuck detection
        this.stuckDetector.update(this.kidnapper);

        // Handle stuck state
        if (this.stuckDetector.isStuck()) {
            if (this.stuckDetector.shouldTeleport()) {
                // Teleport near the target
                if (this.kidnapper.level() instanceof ServerLevel serverLevel) {
                    BlockPos teleportPos =
                        KidnapperAIHelper.findRandomGroundPos(
                            serverLevel,
                            this.target.blockPosition(),
                            3,
                            serverLevel.getRandom(),
                            10
                        );
                    if (teleportPos != null) {
                        this.kidnapper.teleportTo(
                            teleportPos.getX() + 0.5,
                            teleportPos.getY(),
                            teleportPos.getZ() + 0.5
                        );
                        TiedUpMod.LOGGER.debug(
                            "[KidnapperCaptureGoal] {} teleported near target to unblock",
                            this.kidnapper.getNpcName()
                        );
                    }
                }
                this.stuckDetector.reset();
            } else if (
                this.kidnapper.level() instanceof ServerLevel serverLevel
            ) {
                BlockPos detour = this.stuckDetector.tryDetour(
                    serverLevel,
                    this.kidnapper.blockPosition(),
                    5
                );
                if (detour != null) {
                    this.kidnapper.getNavigation().moveTo(
                        detour.getX() + 0.5,
                        detour.getY(),
                        detour.getZ() + 0.5,
                        this.speedModifier
                    );
                    TiedUpMod.LOGGER.debug(
                        "[KidnapperCaptureGoal] {} detouring to {} to bypass obstacle",
                        this.kidnapper.getNpcName(),
                        detour.toShortString()
                    );
                }
            }
            return;
        }

        // Skip normal navigation if in detour
        if (this.stuckDetector.isInDetour()) {
            return;
        }

        // Recalculate path frequently for smooth chasing
        if (--this.pathRecalculateDelay <= 0) {
            this.pathRecalculateDelay = PATH_RECALC_APPROACH;
            boolean pathFound = this.kidnapper.getNavigation().moveTo(
                this.target,
                this.speedModifier
            );
            if (!pathFound) {
                this.stuckDetector.onPathFailed();
            }
        }

        // Check if close enough to start capture
        if (distance <= CAPTURE_DISTANCE) {
            this.captureState = STATE_BIND;
            this.captureProgress = 0;
            this.stuckDetector.reset();

            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} reached target, starting bind",
                this.kidnapper.getNpcName()
            );
        }
    }

    /**
     * Apply bind to target.
     */
    private void tickBind() {
        IBondageState state = KidnappedHelper.getKidnappedState(this.target);
        if (state == null) {
            this.captureState = STATE_DONE;
            return;
        }

        // If already tied, move to gag
        if (state.isTiedUp()) {
            this.captureState = STATE_GAG;
            this.captureProgress = 0;
            return;
        }

        double distance = this.kidnapper.distanceTo(this.target);

        // Always keep following target while in capture mode
        if (--this.pathRecalculateDelay <= 0) {
            this.pathRecalculateDelay = PATH_RECALC_CAPTURE;
            this.kidnapper.getNavigation().moveTo(
                this.target,
                this.speedModifier
            );
        }

        // Check if target moved away too far - reset progress but keep following
        if (distance > DISTANCE_RESET) {
            this.captureProgress = 0;
            return; // Will keep following via pathRecalculateDelay
        }

        // Only progress if close enough, but keep following
        if (distance > CAPTURE_DISTANCE) {
            // Keep moving towards target
            this.kidnapper.getNavigation().moveTo(
                this.target,
                this.speedModifier
            );
            return;
        }

        // Simulate tying progress
        this.captureProgress++;

        // Taunt while tying (with cooldown)
        if (this.captureProgress == 1) {
            // Chat: broadcast dialogue to nearby players (includes target)
            broadcastWithCooldown(DialogueCategory.CAPTURE_TYING);
        }

        // Send progress packet to player target for progress bar display
        // Use ticks directly - progress calculation is state/maxState so unit doesn't matter
        if (this.target instanceof ServerPlayer serverPlayer) {
            int maxTicks = this.kidnapper.getCaptureBindTime();
            String npcName = this.kidnapper.getNpcName();
            PacketTying packet = new PacketTying(
                this.captureProgress,
                maxTicks,
                false,
                npcName
            );
            ModNetwork.sendToPlayer(packet, serverPlayer);
        }

        if (this.captureProgress >= this.kidnapper.getCaptureBindTime()) {
            ItemStack bind = this.kidnapper.getBindItem();
            state.equip(BodyRegionV2.ARMS, bind.copy());

            // Send completion packet to clear progress bar
            if (this.target instanceof ServerPlayer serverPlayer) {
                int maxTicks = this.kidnapper.getCaptureBindTime();
                String npcName = this.kidnapper.getNpcName();
                PacketTying completionPacket = new PacketTying(
                    -1,
                    maxTicks,
                    false,
                    npcName
                );
                ModNetwork.sendToPlayer(completionPacket, serverPlayer);
            }

            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} tied up {}",
                this.kidnapper.getNpcName(),
                this.target.getName().getString()
            );

            this.captureState = STATE_GAG;
            this.captureProgress = 0;
        }
    }

    /**
     * Apply gag to target.
     */
    private void tickGag() {
        IBondageState state = KidnappedHelper.getKidnappedState(this.target);
        if (state == null) {
            this.captureState = STATE_DONE;
            return;
        }

        // If already gagged, move to mittens
        if (state.isGagged()) {
            this.captureState = STATE_MITTENS;
            this.captureProgress = 0;
            return;
        }

        // Skip gag if kidnapper doesn't have one - move to mittens
        if (
            this.kidnapper.getGagItem() == null ||
            this.kidnapper.getGagItem().isEmpty()
        ) {
            this.captureState = STATE_MITTENS;
            this.captureProgress = 0;
            return;
        }

        // Must be tied first
        if (!state.isTiedUp()) {
            this.captureState = STATE_BIND;
            return;
        }

        double distance = this.kidnapper.distanceTo(this.target);

        // Always keep following target while in capture mode
        if (--this.pathRecalculateDelay <= 0) {
            this.pathRecalculateDelay = PATH_RECALC_CAPTURE;
            this.kidnapper.getNavigation().moveTo(
                this.target,
                this.speedModifier
            );
        }

        // Check if target moved away too far - reset progress but keep following
        if (distance > DISTANCE_RESET) {
            this.captureProgress = 0;
            return; // Will keep following via pathRecalculateDelay
        }

        // Only progress if close enough, but keep following
        if (distance > CAPTURE_DISTANCE) {
            // Keep moving towards target
            this.kidnapper.getNavigation().moveTo(
                this.target,
                this.speedModifier
            );
            return;
        }

        // Apply gag progress
        this.captureProgress++;

        // Taunt while gagging (with cooldown)
        if (this.captureProgress == 1) {
            // Chat: broadcast dialogue to nearby players (includes target)
            broadcastWithCooldown(DialogueCategory.CAPTURE_GAGGING);

            // Screen: action bar message to target
            SystemMessageManager.sendBeingGagged(this.kidnapper, this.target);
        }

        if (this.captureProgress >= this.kidnapper.getCaptureGagTime()) {
            ItemStack gag = this.kidnapper.getGagItem();
            state.equip(BodyRegionV2.MOUTH, gag.copy());

            // Screen: action bar message to target
            SystemMessageManager.sendGagged(this.kidnapper, this.target);

            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} gagged {}",
                this.kidnapper.getNpcName(),
                this.target.getName().getString()
            );

            this.captureState = STATE_MITTENS;
            this.captureProgress = 0;
        }
    }

    /**
     * Apply mittens to target.
     */
    private void tickMittens() {
        IBondageState state = KidnappedHelper.getKidnappedState(this.target);
        if (state == null) {
            this.captureState = STATE_DONE;
            return;
        }

        // Skip if kidnapper doesn't have mittens
        ItemStack mittens = this.kidnapper.getMittensItem();
        if (mittens == null || mittens.isEmpty()) {
            this.captureState = STATE_EARPLUGS;
            this.captureProgress = 0;
            return;
        }

        // Skip if already has mittens
        if (!state.getEquipment(BodyRegionV2.HANDS).isEmpty()) {
            this.captureState = STATE_EARPLUGS;
            this.captureProgress = 0;
            return;
        }

        double distance = this.kidnapper.distanceTo(this.target);

        // Keep following target
        if (--this.pathRecalculateDelay <= 0) {
            this.pathRecalculateDelay = PATH_RECALC_CAPTURE;
            this.kidnapper.getNavigation().moveTo(
                this.target,
                this.speedModifier
            );
        }

        if (distance > DISTANCE_RESET) {
            this.captureProgress = 0;
            return;
        }

        if (distance > CAPTURE_DISTANCE) {
            this.kidnapper.getNavigation().moveTo(
                this.target,
                this.speedModifier
            );
            return;
        }

        this.captureProgress++;

        // Apply mittens (faster than bind/gag)
        if (this.captureProgress >= MITTENS_APPLY_TIME) {
            state.equip(BodyRegionV2.HANDS, mittens.copy());

            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} put mittens on {}",
                this.kidnapper.getNpcName(),
                this.target.getName().getString()
            );

            this.captureState = STATE_EARPLUGS;
            this.captureProgress = 0;
        }
    }

    /**
     * Apply earplugs to target.
     */
    private void tickEarplugs() {
        IBondageState state = KidnappedHelper.getKidnappedState(this.target);
        if (state == null) {
            this.captureState = STATE_DONE;
            return;
        }

        // Skip if kidnapper doesn't have earplugs
        ItemStack earplugs = this.kidnapper.getEarplugsItem();
        if (earplugs == null || earplugs.isEmpty()) {
            this.captureState = STATE_BLINDFOLD;
            this.captureProgress = 0;
            return;
        }

        // Skip if already has earplugs
        if (!state.getEquipment(BodyRegionV2.EARS).isEmpty()) {
            this.captureState = STATE_BLINDFOLD;
            this.captureProgress = 0;
            return;
        }

        double distance = this.kidnapper.distanceTo(this.target);

        // Keep following target
        if (--this.pathRecalculateDelay <= 0) {
            this.pathRecalculateDelay = PATH_RECALC_CAPTURE;
            this.kidnapper.getNavigation().moveTo(
                this.target,
                this.speedModifier
            );
        }

        if (distance > DISTANCE_RESET) {
            this.captureProgress = 0;
            return;
        }

        if (distance > CAPTURE_DISTANCE) {
            this.kidnapper.getNavigation().moveTo(
                this.target,
                this.speedModifier
            );
            return;
        }

        this.captureProgress++;

        // Apply earplugs (fast)
        if (this.captureProgress >= EARPLUGS_APPLY_TIME) {
            state.equip(BodyRegionV2.EARS, earplugs.copy());

            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} put earplugs on {}",
                this.kidnapper.getNpcName(),
                this.target.getName().getString()
            );

            this.captureState = STATE_BLINDFOLD;
            this.captureProgress = 0;
        }
    }

    /**
     * Apply blindfold to target.
     */
    private void tickBlindfold() {
        IBondageState state = KidnappedHelper.getKidnappedState(this.target);
        if (state == null) {
            this.captureState = STATE_DONE;
            return;
        }

        // Skip if kidnapper doesn't have blindfold
        ItemStack blindfold = this.kidnapper.getBlindfoldItem();
        if (blindfold == null || blindfold.isEmpty()) {
            this.captureState = STATE_COLLAR;
            this.captureProgress = 0;
            return;
        }

        // Skip if already blindfolded
        if (!state.getEquipment(BodyRegionV2.EYES).isEmpty()) {
            this.captureState = STATE_COLLAR;
            this.captureProgress = 0;
            return;
        }

        double distance = this.kidnapper.distanceTo(this.target);

        // Keep following target
        if (--this.pathRecalculateDelay <= 0) {
            this.pathRecalculateDelay = PATH_RECALC_CAPTURE;
            this.kidnapper.getNavigation().moveTo(
                this.target,
                this.speedModifier
            );
        }

        if (distance > DISTANCE_RESET) {
            this.captureProgress = 0;
            return;
        }

        if (distance > CAPTURE_DISTANCE) {
            this.kidnapper.getNavigation().moveTo(
                this.target,
                this.speedModifier
            );
            return;
        }

        this.captureProgress++;

        // Apply blindfold (slower - most restrictive)
        if (this.captureProgress >= BLINDFOLD_APPLY_TIME) {
            state.equip(BodyRegionV2.EYES, blindfold.copy());

            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} blindfolded {}",
                this.kidnapper.getNpcName(),
                this.target.getName().getString()
            );

            this.captureState = STATE_COLLAR;
            this.captureProgress = 0;
        }
    }

    /**
     * Apply collar to target.
     */
    private void tickCollar() {
        IBondageState state = KidnappedHelper.getKidnappedState(this.target);
        if (state == null) {
            this.captureState = STATE_DONE;
            return;
        }

        // Skip if kidnapper doesn't have collar
        ItemStack collar = this.kidnapper.getCollarItem();
        if (collar == null || collar.isEmpty()) {
            this.captureState = STATE_ENSLAVE;
            this.captureProgress = 0;
            return;
        }

        // If already has collar, check ownership
        if (state.hasCollar()) {
            ItemStack existingCollar = state.getEquipment(BodyRegionV2.NECK);
            if (CollarHelper.isCollar(existingCollar)) {
                java.util.List<java.util.UUID> owners = CollarHelper.getOwners(
                    existingCollar
                );
                if (!owners.contains(this.kidnapper.getUUID())) {
                    // Filter out self-collar (owner == target = exploit, treat as uncollared)
                    java.util.List<java.util.UUID> realOwners = owners
                        .stream()
                        .filter(id -> !id.equals(this.target.getUUID()))
                        .toList();
                    // Check if owned by a DIFFERENT player — abort (don't steal from players)
                    if (
                        !realOwners.isEmpty() &&
                        this.kidnapper.level() instanceof
                            net.minecraft.server.level.ServerLevel sl
                    ) {
                        for (java.util.UUID ownerId : realOwners) {
                            if (
                                sl
                                    .getServer()
                                    .getPlayerList()
                                    .getPlayer(ownerId) !=
                                null
                            ) {
                                this.forceStop = true;
                                return;
                            }
                        }
                    }
                    // Self-collar or other kidnapper — transfer ownership
                    for (java.util.UUID oldOwner : new java.util.ArrayList<>(
                        owners
                    )) {
                        CollarHelper.removeOwner(existingCollar, oldOwner);
                    }
                    CollarHelper.addOwner(
                        existingCollar,
                        this.kidnapper.getUUID(),
                        this.kidnapper.getNpcName()
                    );
                }
            }
            this.captureState = STATE_ENSLAVE;
            this.captureProgress = 0;
            return;
        }

        double distance = this.kidnapper.distanceTo(this.target);

        // Keep following target
        if (--this.pathRecalculateDelay <= 0) {
            this.pathRecalculateDelay = PATH_RECALC_CAPTURE;
            this.kidnapper.getNavigation().moveTo(
                this.target,
                this.speedModifier
            );
        }

        if (distance > DISTANCE_RESET) {
            this.captureProgress = 0;
            return;
        }

        if (distance > CAPTURE_DISTANCE) {
            this.kidnapper.getNavigation().moveTo(
                this.target,
                this.speedModifier
            );
            return;
        }

        this.captureProgress++;

        // Apply collar
        if (this.captureProgress >= COLLAR_APPLY_TIME) {
            // Create a copy of the collar and configure it
            ItemStack collarCopy = collar.copy();
            if (CollarHelper.isCollar(collarCopy)) {
                // Add kidnapper as owner
                CollarHelper.addOwner(
                    collarCopy,
                    this.kidnapper.getUUID(),
                    this.kidnapper.getNpcName()
                );

                // Generate a key for this collar and link them via UUID
                ItemStack keyStack = new ItemStack(ModItems.COLLAR_KEY.get());
                if (keyStack.getItem() instanceof ItemKey keyItem) {
                    UUID keyUUID = keyItem.getKeyUUID(keyStack);
                    // Lock via ILockable interface (collar must implement it)
                    if (collarCopy.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
                        lockable.setLockedByKeyUUID(collarCopy, keyUUID);
                    }
                    // Store the key on the kidnapper for potential drop on death
                    this.kidnapper.addCollarKey(keyStack);
                } else {
                    // Fallback: just lock without a key
                    if (collarCopy.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
                        lockable.setLocked(collarCopy, true);
                    }
                }
            }

            state.equip(BodyRegionV2.NECK, collarCopy);

            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} collared and locked {}",
                this.kidnapper.getNpcName(),
                this.target.getName().getString()
            );

            this.captureState = STATE_ENSLAVE;
            this.captureProgress = 0;
        }
    }

    /**
     * Broadcast dialogue with cooldown check.
     * Returns true if dialogue was sent.
     */
    private boolean broadcastWithCooldown(DialogueCategory category) {
        long currentTick = this.kidnapper.level().getGameTime();
        Long lastUsed = dialogueCooldowns.get(category);

        if (lastUsed != null && (currentTick - lastUsed) < DIALOGUE_COOLDOWN) {
            return false; // Still on cooldown
        }

        dialogueCooldowns.put(category, currentTick);
        this.kidnapper.talkToPlayersInRadius(category, DIALOGUE_RADIUS);
        return true;
    }

    /**
     * Enslave the target.
     */
    private void tickEnslave() {
        IBondageState state = KidnappedHelper.getKidnappedState(this.target);
        if (state == null) {
            this.captureState = STATE_DONE;
            return;
        }

        // Must be tied to enslave
        if (!state.isTiedUp()) {
            this.captureState = STATE_BIND;
            return;
        }

        // Only require gag if kidnapper had one to use
        // This prevents infinite loop when kidnapper spawns without gag
        boolean kidnapperHasGag =
            this.kidnapper.getGagItem() != null &&
            !this.kidnapper.getGagItem().isEmpty();
        if (kidnapperHasGag && !state.isGagged()) {
            this.captureState = STATE_GAG;
            return;
        }

        // Attempt capture via PrisonerService (atomic: PrisonerManager + leash)
        boolean captured = false;
        if (
            this.kidnapper.level() instanceof
                net.minecraft.server.level.ServerLevel serverLevel
        ) {
            captured =
                com.tiedup.remake.prison.service.PrisonerService.get().capture(
                    serverLevel,
                    this.target,
                    this.kidnapper
                );
        }
        if (captured) {
            // Chat: broadcast dialogue to nearby players (includes target since they're close)
            broadcastWithCooldown(DialogueCategory.CAPTURE_ENSLAVED);

            // Screen: action bar message to target
            SystemMessageManager.sendEnslaved(this.kidnapper, this.target);

            TiedUpMod.LOGGER.debug(
                "[KidnapperCaptureGoal] {} successfully enslaved {}",
                this.kidnapper.getNpcName(),
                this.target.getName().getString()
            );

            this.captureState = STATE_DONE;
            this.kidnapper.setTarget(null);
        } else {
            TiedUpMod.LOGGER.warn(
                "[KidnapperCaptureGoal] {} failed to enslave {}",
                this.kidnapper.getNpcName(),
                this.target.getName().getString()
            );

            this.captureState = STATE_DONE;
            this.kidnapper.setTarget(null); // Clear target so goal doesn't restart
        }
    }
}
