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

import com.tiedup.remake.cells.CampOwnership;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.cells.ConfiscatedInventoryRegistry;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.kidnapper.CaptivePriority;
import com.tiedup.remake.entities.ai.StuckDetector;
import com.tiedup.remake.entities.ai.WaypointNavigator;
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.IRestrainable;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.KidnapperAIHelper;
import com.tiedup.remake.util.teleport.TeleportHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
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.Entity;
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;
import org.jetbrains.annotations.Nullable;

/**
 * AI Goal for EntityKidnapper to bring captured players to nearby cells.
 *
 * Phase: Kidnapper Revamp - Cell System
 *
 * This goal:
 * 1. Finds the nearest cell (BlockMarker with cellId) within range
 * 2. Walks the kidnapper towards the cell
 * 3. When close enough, teleports the prisoner to the cell spawn point
 * 4. Registers the prisoner in CellRegistry
 */
public class KidnapperBringToCellGoal extends Goal {

    private final EntityKidnapper kidnapper;

    /** Maximum distance to search for cells */
    private static final int SEARCH_RADIUS = 64;

    /** Distance at which to teleport prisoner to cell */
    private static final double TELEPORT_DISTANCE = 5.0;

    /** Dialogue radius */
    private static final int DIALOGUE_RADIUS = 20;

    /** Target cell data */
    @Nullable
    private CellDataV2 targetCell;

    /** Target cell position */
    @Nullable
    private BlockPos targetPos;

    /** Waypoint navigator for complex paths */
    @Nullable
    private WaypointNavigator waypointNav;

    /** Whether we've announced intention */
    private boolean hasAnnounced;

    /** Ticks since we started moving */
    private int moveTicks;

    /** Max ticks before giving up on pathfinding */
    private static final int MAX_MOVE_TICKS = 600; // 30 seconds

    /** Max horizontal distance (XZ) to consider "at delivery point" */
    private static final double DELIVERY_XZ_DISTANCE = 4.0; // Allow kidnapper to be 3-4 blocks away

    /** Max vertical distance (Y) to consider "at delivery point" */
    private static final double DELIVERY_Y_DISTANCE = 5.0; // Allow up to 4 blocks vertical difference

    /** Consecutive path failures before forced teleport (waypoint mode) */
    private static final int PATH_FAIL_THRESHOLD = 5;

    /** Counter for consecutive path failures */
    private int pathFailCounter = 0;

    /** Stuck detection for vanilla navigation mode */
    private final StuckDetector stuckDetector = new StuckDetector(60, 3);

    public KidnapperBringToCellGoal(EntityKidnapper kidnapper) {
        this.kidnapper = kidnapper;
        this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
    }

    /** Tick counter for periodic diagnostic logging */
    private int diagCounter = 0;

    @Override
    public boolean canUse() {
        // Periodic diagnostic: log which check blocks every 10 seconds
        boolean diag = (this.kidnapper.hasCaptives() &&
            ++diagCounter % 200 == 0);

        // Don't interrupt dogwalk - WalkPrisonerGoal uses captive mechanism
        // and BringToCellGoal (P5) would preempt it (P7), causing instant release
        if (this.kidnapper.isDogwalking()) {
            if (diag) TiedUpMod.LOGGER.warn(
                "[KidnapperBringToCellGoal] {} BLOCKED: isDogwalking=true",
                this.kidnapper.getNpcName()
            );
            return false;
        }

        // Common transport preconditions
        if (!KidnapperAIHelper.canKidnapperTransport(this.kidnapper)) {
            if (diag) {
                IRestrainable captive = this.kidnapper.getCaptive();
                TiedUpMod.LOGGER.warn(
                    "[KidnapperBringToCellGoal] {} BLOCKED: canKidnapperTransport=false " +
                        "(tiedUp={}, hasCaptives={}, getOut={}, forSell={}, waitingJob={})",
                    this.kidnapper.getNpcName(),
                    this.kidnapper.isTiedUp(),
                    this.kidnapper.hasCaptives(),
                    this.kidnapper.isGetOutState(),
                    captive != null ? captive.isForSell() : "noCaptive",
                    this.kidnapper.isWaitingForJobToBeCompleted()
                );
            }
            return false;
        }

        // Check if associated camp is still alive before starting
        UUID campId = this.kidnapper.getAssociatedStructure();
        if (
            campId != null &&
            this.kidnapper.level() instanceof ServerLevel serverLevel
        ) {
            CampOwnership ownership = CampOwnership.get(serverLevel);
            if (!ownership.isCampAlive(campId)) {
                CampOwnership.CampData campData = ownership.getCamp(campId);
                if (campData != null) {
                    // Camp was registered but trader was killed — block normally
                    if (diag) TiedUpMod.LOGGER.warn(
                        "[KidnapperBringToCellGoal] {} BLOCKED: camp {} is dead (trader killed)",
                        this.kidnapper.getNpcName(),
                        campId.toString().substring(0, 8)
                    );
                    return false;
                }
                // Camp was NEVER registered — orphaned grid ID from structure
                // boundary split. Auto-relink to nearest alive camp.
                CampOwnership.CampData nearestCamp =
                    ownership.findNearestAliveCamp(
                        this.kidnapper.blockPosition(),
                        100
                    );
                if (nearestCamp != null) {
                    UUID newCampId = nearestCamp.getCampId();
                    this.kidnapper.setAssociatedStructure(newCampId);
                    ownership.linkKidnapperToCamp(
                        newCampId,
                        this.kidnapper.getUUID()
                    );
                    TiedUpMod.LOGGER.info(
                        "[KidnapperBringToCellGoal] {} auto-relinked from orphan camp {} to alive camp {}",
                        this.kidnapper.getNpcName(),
                        campId.toString().substring(0, 8),
                        newCampId.toString().substring(0, 8)
                    );
                    // Continue with the new camp — don't return false
                } else {
                    if (diag) TiedUpMod.LOGGER.warn(
                        "[KidnapperBringToCellGoal] {} BLOCKED: orphan camp {}, no alive camp nearby",
                        this.kidnapper.getNpcName(),
                        campId.toString().substring(0, 8)
                    );
                    return false;
                }
            }
        }

        // Find nearest cell (prioritizes camp cells if kidnapper is linked to a camp)
        this.targetCell = findNearestCell();
        if (this.targetCell == null) {
            if (diag) TiedUpMod.LOGGER.warn(
                "[KidnapperBringToCellGoal] {} BLOCKED: findNearestCell=null (campId={})",
                this.kidnapper.getNpcName(),
                campId != null ? campId.toString().substring(0, 8) : "wild"
            );
            return false;
        }

        // Use DELIVERY point if available, otherwise fall back to spawn point
        this.targetPos = getDeliveryOrSpawnPoint(this.targetCell);

        if (diag) TiedUpMod.LOGGER.warn(
            "[KidnapperBringToCellGoal] {} canUse=TRUE, cell={}, target={}",
            this.kidnapper.getNpcName(),
            this.targetCell.getId().toString().substring(0, 8),
            this.targetPos.toShortString()
        );
        return true;
    }

    /**
     * Get the delivery point for a cell, or fall back to spawn point.
     * DELIVERY markers are used for prisoner drop-off/pickup navigation.
     *
     * FIX: Uses smart floor detection instead of blindly calling .above().
     * This handles both floor-placed markers and air-placed markers correctly.
     */
    private BlockPos getDeliveryOrSpawnPoint(CellDataV2 cell) {
        return KidnapperAIHelper.getDeliveryOrSpawnPoint(
            cell,
            this.kidnapper.level()
        );
    }

    @Override
    public boolean canContinueToUse() {
        if (!this.kidnapper.hasCaptives()) {
            return false;
        }

        if (this.kidnapper.isTiedUp()) {
            return false;
        }

        if (this.targetCell == null || this.targetPos == null) {
            return false;
        }

        // Give up after max ticks
        if (this.moveTicks > MAX_MOVE_TICKS) {
            return false;
        }

        // Check if associated camp is still alive
        UUID campId = this.kidnapper.getAssociatedStructure();
        if (
            campId != null &&
            this.kidnapper.level() instanceof ServerLevel serverLevel
        ) {
            CampOwnership ownership = CampOwnership.get(serverLevel);
            if (!ownership.isCampAlive(campId)) {
                CampOwnership.CampData campData = ownership.getCamp(campId);
                if (campData != null) {
                    // Camp was registered but trader killed — free captive
                    TiedUpMod.LOGGER.debug(
                        "[KidnapperBringToCellGoal] {} - camp {} is dead, freeing captive and aborting",
                        this.kidnapper.getNpcName(),
                        campId.toString().substring(0, 8)
                    );
                    IRestrainable captive = this.kidnapper.getCaptive();
                    if (captive != null) {
                        CollarHelper.runWithSuppressedAlert(() ->
                            captive.free(false)
                        );
                        this.kidnapper.removeCaptive(captive, false);
                    }
                    return false;
                }
                // Camp never registered (orphan) — canUse() should have
                // auto-relinked, but if not, don't free the captive
            }
        }

        // Check captive is still captive and leashed to this kidnapper
        IRestrainable captive = this.kidnapper.getCaptive();
        if (captive == null) {
            // Captive reference lost (leash broke) - onCaptiveReleased() handles alerting
            return false;
        }
        if (!captive.isCaptive()) {
            // Captive freed themselves while we still have reference - record escape
            this.kidnapper.onCaptiveEscaped(captive);
            this.kidnapper.removeCaptive(captive, false); // Clear reference to stop spam
            return false;
        }

        return true;
    }

    @Override
    public void start() {
        this.hasAnnounced = false;
        this.moveTicks = 0;
        this.pathFailCounter = 0;
        this.stuckDetector.reset();

        // Build waypoint path if cell has waypoints
        if (targetCell != null && !targetCell.getPathWaypoints().isEmpty()) {
            List<BlockPos> fullPath = new ArrayList<>(
                targetCell.getPathWaypoints()
            );
            fullPath.add(targetPos); // Add DELIVERY as final waypoint
            this.waypointNav = new WaypointNavigator(kidnapper, fullPath, 1.0);
            TiedUpMod.LOGGER.debug(
                "[KidnapperBringToCellGoal] {} using {} waypoints for navigation to cell",
                this.kidnapper.getNpcName(),
                fullPath.size()
            );
        } else {
            this.waypointNav = null; // Fallback to vanilla pathfinding
        }

        TiedUpMod.LOGGER.debug(
            "[KidnapperBringToCellGoal] {} starting cell delivery to {}",
            this.kidnapper.getNpcName(),
            this.targetPos
        );
    }

    @Override
    public void stop() {
        // Cancel cell reservation if we're giving up
        if (
            this.targetCell != null &&
            this.kidnapper.level() instanceof ServerLevel serverLevel
        ) {
            CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
            registry.cancelReservation(
                this.targetCell.getId(),
                this.kidnapper.getUUID()
            );
        }

        this.targetCell = null;
        this.targetPos = null;
        this.waypointNav = null;
        this.hasAnnounced = false;
        this.moveTicks = 0;
        this.pathFailCounter = 0;
        this.stuckDetector.reset();

        // Stop navigation
        this.kidnapper.getNavigation().stop();
    }

    @Override
    public void tick() {
        IRestrainable captive = this.kidnapper.getCaptive();
        if (
            captive == null || this.targetPos == null || this.targetCell == null
        ) {
            return;
        }

        // Verify captive is still leashed to this kidnapper
        if (!captive.isCaptive()) {
            // Captive freed themselves - record escape and alert
            this.kidnapper.onCaptiveEscaped(captive);
            this.kidnapper.broadcastAlert(captive.asLivingEntity());
            this.kidnapper.removeCaptive(captive, false); // Clear reference to stop spam
            return;
        }

        this.moveTicks++;

        // Announce intention once
        if (!this.hasAnnounced) {
            announceIntention(captive);
            this.hasAnnounced = true;
        }

        // WAYPOINT NAVIGATION MODE
        if (waypointNav != null && waypointNav.hasWaypoints()) {
            waypointNav.tick();

            if (waypointNav.isComplete()) {
                // Reached final destination (delivery point)
                executeCellDelivery(captive);
                return;
            }

            // Navigate to current waypoint
            if (this.moveTicks % 20 == 0) {
                boolean pathFound = waypointNav.navigateToCurrentWaypoint();
                if (!pathFound) {
                    this.pathFailCounter++;
                    this.stuckDetector.onPathFailed();

                    BlockPos wp = waypointNav.getCurrentWaypoint();
                    if (this.pathFailCounter >= PATH_FAIL_THRESHOLD) {
                        // Teleport to current waypoint if stuck (waypoints are safe passage points)
                        if (wp != null) {
                            TiedUpMod.LOGGER.warn(
                                "[KidnapperBringToCellGoal] {} stuck on waypoint {}/{}, teleporting to unblock",
                                this.kidnapper.getNpcName(),
                                waypointNav.getCurrentIndex() + 1,
                                waypointNav.getTotalWaypoints()
                            );
                            this.kidnapper.teleportTo(
                                wp.getX() + 0.5,
                                wp.getY(),
                                wp.getZ() + 0.5
                            );
                        }
                        this.pathFailCounter = 0;
                    }
                } else {
                    this.pathFailCounter = 0;
                }
            }

            // Look at current waypoint
            BlockPos currentWp = waypointNav.getCurrentWaypoint();
            if (currentWp != null && !this.kidnapper.getNavigation().isDone()) {
                this.kidnapper.getLookControl().setLookAt(
                    currentWp.getX() + 0.5,
                    currentWp.getY() + 1,
                    currentWp.getZ() + 0.5
                );
            }
            return;
        }

        // VANILLA NAVIGATION MODE (no waypoints defined)
        // Check if at delivery point using separate XZ/Y distance checks
        if (isAtDeliveryPoint(this.targetPos)) {
            // Close enough - teleport and imprison
            executeCellDelivery(captive);
        } else {
            // Update stuck detection
            this.stuckDetector.update(this.kidnapper);

            // Handle stuck state
            if (this.stuckDetector.isStuck()) {
                if (this.stuckDetector.shouldTeleport()) {
                    TiedUpMod.LOGGER.warn(
                        "[KidnapperBringToCellGoal] {} stuck after detour attempts, teleporting to cell and delivering",
                        this.kidnapper.getNpcName()
                    );
                    teleportKidnapperToCell();
                    executeCellDelivery(captive);
                    this.stuckDetector.reset();
                    return;
                } 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,
                            1.0
                        );
                        TiedUpMod.LOGGER.debug(
                            "[KidnapperBringToCellGoal] {} detouring to {} to bypass obstacle",
                            this.kidnapper.getNpcName(),
                            detour.toShortString()
                        );
                    }
                }
                return;
            }

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

            // Keep walking towards cell
            if (this.moveTicks % 20 == 0) {
                boolean pathFound = this.kidnapper.getNavigation().moveTo(
                    this.targetPos.getX() + 0.5,
                    this.targetPos.getY(),
                    this.targetPos.getZ() + 0.5,
                    1.0
                );

                if (!pathFound) {
                    this.stuckDetector.onPathFailed();
                }
            }

            // Only look at target if we have an active path (not stuck)
            if (!this.kidnapper.getNavigation().isDone()) {
                this.kidnapper.getLookControl().setLookAt(
                    this.targetPos.getX() + 0.5,
                    this.targetPos.getY() + 1,
                    this.targetPos.getZ() + 0.5
                );
            }
        }
    }

    /**
     * Check if kidnapper is at the delivery point using separate XZ and Y distance checks.
     * This prevents premature teleport when kidnapper is on wrong floor (Y different).
     *
     * @param deliveryPos The target delivery position
     * @return true if kidnapper is close enough in both XZ and Y dimensions
     */
    private boolean isAtDeliveryPoint(BlockPos deliveryPos) {
        double dx = this.kidnapper.getX() - (deliveryPos.getX() + 0.5);
        double dz = this.kidnapper.getZ() - (deliveryPos.getZ() + 0.5);
        double distXZ = Math.sqrt(dx * dx + dz * dz);
        double distY = Math.abs(this.kidnapper.getY() - deliveryPos.getY());

        return distXZ < DELIVERY_XZ_DISTANCE && distY < DELIVERY_Y_DISTANCE;
    }

    /**
     * Teleport the kidnapper (and leashed captive) near the target cell.
     * Called when pathfinding fails repeatedly — the kidnapper can't walk there
     * (e.g., large vertical difference, no accessible path).
     */
    private void teleportKidnapperToCell() {
        if (this.targetCell == null) return;

        BlockPos spawnPoint =
            this.targetCell.getSpawnPoint() != null
                ? this.targetCell.getSpawnPoint()
                : this.targetCell.getCorePos().above();
        // Teleport 2 blocks away from spawn point (so kidnapper doesn't stand on captive)
        this.kidnapper.teleportTo(
            spawnPoint.getX() + 2.5,
            spawnPoint.getY() + 1,
            spawnPoint.getZ() + 0.5
        );

        TiedUpMod.LOGGER.info(
            "[KidnapperBringToCellGoal] Teleported {} to cell area at {}",
            this.kidnapper.getNpcName(),
            spawnPoint
        );
    }

    /**
     * Find the nearest cell with an available spot.
     * Priority order:
     * 1. Recaptured ESCAPED prisoner → return to their ORIGINAL camp (captive's campId)
     * 2. Kidnapper's camp cells (if kidnapper is linked to a camp)
     * 3. Any nearby cells (fallback)
     *
     * May trigger prisoner replacement if cells are full and captive is higher priority.
     */
    @Nullable
    private CellDataV2 findNearestCell() {
        if (!(this.kidnapper.level() instanceof ServerLevel serverLevel)) {
            return null;
        }

        BlockPos kidnapperPos = this.kidnapper.blockPosition();
        CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
        CampOwnership campOwnership = CampOwnership.get(serverLevel);

        // Get captive priority
        IRestrainable captive = this.kidnapper.getCaptive();
        CaptivePriority captivePriority =
            captive != null
                ? CaptivePriority.fromEntity(captive.asLivingEntity())
                : CaptivePriority.DAMSEL;

        // ESCAPED SYSTEM: Check if captive has a campId (recaptured escaped prisoner)
        // If so, return them to their ORIGINAL camp, not the kidnapper's camp
        UUID captiveCampId = null;
        if (captive != null && captive.asLivingEntity() != null) {
            PrisonerManager manager = PrisonerManager.get(serverLevel);
            PrisonerRecord record = manager.getRecord(
                captive.asLivingEntity().getUUID()
            );
            captiveCampId = record.getCampId();

            if (captiveCampId != null) {
                // Check if captive's camp is alive
                CampOwnership.CampData captiveCamp = campOwnership.getCamp(
                    captiveCampId
                );
                if (captiveCamp != null && captiveCamp.isAlive()) {
                    // Try to find a cell in the captive's ORIGINAL camp
                    CellDataV2 campCell = findCampCellByIndex(
                        registry,
                        captiveCampId
                    );
                    if (campCell != null) {
                        TiedUpMod.LOGGER.info(
                            "[KidnapperBringToCellGoal] {} returning recaptured prisoner to original camp {}",
                            this.kidnapper.getNpcName(),
                            captiveCampId.toString().substring(0, 8)
                        );
                        return campCell;
                    }

                    // All cells full - check for replacement
                    // Allow replacement for PLAYER (x3) and DAMSEL_SHINY (x2)
                    if (captivePriority.isHigherThan(CaptivePriority.DAMSEL)) {
                        CellDataV2 replacementCell =
                            tryReleaseLowerPriorityPrisonerFromCamp(
                                serverLevel,
                                registry,
                                captiveCampId,
                                captivePriority
                            );
                        if (replacementCell != null) {
                            TiedUpMod.LOGGER.info(
                                "[KidnapperBringToCellGoal] {} found replacement cell in captive's camp {} via prisoner eviction (priority: {})",
                                this.kidnapper.getNpcName(),
                                captiveCampId.toString().substring(0, 8),
                                captivePriority
                            );
                            return replacementCell;
                        }
                    }
                }
                // Captive's camp is dead or has no cells - fall through to normal search
            }
        }

        // If kidnapper is linked to a camp, try camp cells first using the camp index
        UUID associatedCamp = this.kidnapper.getAssociatedStructure();
        if (associatedCamp != null) {
            CampOwnership.CampData campData = campOwnership.getCamp(
                associatedCamp
            );
            if (campData != null && campData.isAlive()) {
                // Use the new cellsByCamp index for efficient lookup
                CellDataV2 campCell = findCampCellByIndex(
                    registry,
                    associatedCamp
                );
                if (campCell != null) {
                    return campCell;
                }

                // All camp cells are full - check if we can replace a lower-priority prisoner
                // Allow replacement for PLAYER (x3) and DAMSEL_SHINY (x2)
                if (captivePriority.isHigherThan(CaptivePriority.DAMSEL)) {
                    CellDataV2 replacementCell =
                        tryReleaseLowerPriorityPrisonerFromCamp(
                            serverLevel,
                            registry,
                            associatedCamp,
                            captivePriority
                        );
                    if (replacementCell != null) {
                        TiedUpMod.LOGGER.info(
                            "[KidnapperBringToCellGoal] {} found replacement cell in camp {} via prisoner eviction (priority: {})",
                            this.kidnapper.getNpcName(),
                            associatedCamp.toString().substring(0, 8),
                            captivePriority
                        );
                        return replacementCell;
                    }
                }
            }
        }

        // For wild kidnappers (no camp): find nearest alive camp and use its cells
        // This covers archers and other wild kidnappers who capture far from camps
        if (associatedCamp == null) {
            CampOwnership.CampData nearestCamp =
                campOwnership.findNearestAliveCamp(
                    kidnapperPos,
                    200 // 200-block radius covers archers who shoot from distance
                );
            if (nearestCamp != null) {
                CellDataV2 campCell = findCampCellByIndex(
                    registry,
                    nearestCamp.getCampId()
                );
                if (campCell != null) {
                    TiedUpMod.LOGGER.info(
                        "[KidnapperBringToCellGoal] {} (wild) found cell in nearest camp {}",
                        this.kidnapper.getNpcName(),
                        nearestCamp.getCampId().toString().substring(0, 8)
                    );
                    return campCell;
                }

                // Check for replacement if captive is high priority
                if (captivePriority.isHigherThan(CaptivePriority.DAMSEL)) {
                    CellDataV2 replacementCell =
                        tryReleaseLowerPriorityPrisonerFromCamp(
                            serverLevel,
                            registry,
                            nearestCamp.getCampId(),
                            captivePriority
                        );
                    if (replacementCell != null) {
                        TiedUpMod.LOGGER.info(
                            "[KidnapperBringToCellGoal] {} (wild) found replacement cell in nearest camp {} via prisoner eviction (priority: {})",
                            this.kidnapper.getNpcName(),
                            nearestCamp.getCampId().toString().substring(0, 8),
                            captivePriority
                        );
                        return replacementCell;
                    }
                }
            }
        }

        // Fall back to finding any nearby cell using CellRegistry (spatial lookup)
        List<CellDataV2> nearbyCells = registry.findCellsNear(
            kidnapperPos,
            SEARCH_RADIUS
        );

        CellDataV2 nearestCell = null;
        double nearestDistSq = Double.MAX_VALUE;

        for (CellDataV2 cell : nearbyCells) {
            // Skip full cells
            if (cell.isFull()) {
                continue;
            }

            // Skip cells reserved by other kidnappers
            if (
                registry.isReservedByOther(
                    cell.getId(),
                    this.kidnapper.getUUID(),
                    this.kidnapper.level().getGameTime()
                )
            ) {
                continue;
            }

            // Skip cells belonging to dead camps
            if (cell.isCampOwned()) {
                UUID cellCampId = cell.getCampId();
                if (
                    cellCampId != null && !campOwnership.isCampAlive(cellCampId)
                ) {
                    continue;
                }
            }

            double distSq = kidnapperPos.distSqr(cell.getCorePos());
            if (distSq < nearestDistSq) {
                nearestDistSq = distSq;
                nearestCell = cell;
            }
        }

        // Reserve the cell if found to prevent race condition
        if (nearestCell != null) {
            boolean reserved = registry.reserveCell(
                nearestCell.getId(),
                this.kidnapper.getUUID(),
                this.kidnapper.level().getGameTime()
            );
            if (!reserved) {
                // Someone else reserved it in the meantime - retry
                TiedUpMod.LOGGER.debug(
                    "[KidnapperBringToCellGoal] {} failed to reserve cell {}, will retry",
                    this.kidnapper.getNpcName(),
                    nearestCell.getId().toString().substring(0, 8)
                );
                return null;
            }
            TiedUpMod.LOGGER.debug(
                "[KidnapperBringToCellGoal] {} reserved cell {} (searched {} nearby cells)",
                this.kidnapper.getNpcName(),
                nearestCell.getId().toString().substring(0, 8),
                nearbyCells.size()
            );
        }

        return nearestCell;
    }

    /**
     * Find an available cell from the camp's registered cells using the camp index.
     * Reserves the cell before returning to prevent race conditions.
     *
     * @param registry The cell registry
     * @param campId   The camp UUID
     * @return An available cell (reserved), or null if all are full/reserved
     */
    @Nullable
    private CellDataV2 findCampCellByIndex(
        CellRegistryV2 registry,
        UUID campId
    ) {
        List<CellDataV2> campCells = registry.getCellsByCamp(campId);

        for (CellDataV2 cell : campCells) {
            // Skip full cells
            if (cell.isFull()) {
                continue;
            }

            // Skip cells reserved by other kidnappers
            if (
                registry.isReservedByOther(
                    cell.getId(),
                    this.kidnapper.getUUID(),
                    this.kidnapper.level().getGameTime()
                )
            ) {
                continue;
            }

            // Try to reserve this cell
            if (
                registry.reserveCell(
                    cell.getId(),
                    this.kidnapper.getUUID(),
                    this.kidnapper.level().getGameTime()
                )
            ) {
                return cell;
            }
        }

        return null;
    }

    /**
     * Try to release a lower-priority prisoner to make room for a higher-priority captive.
     * Only releases NPCs (Damsel/DamselShiny), never players.
     * Uses the camp index to find cells belonging to the camp.
     *
     * @param serverLevel     The server level
     * @param registry        The cell registry
     * @param campId          The camp UUID
     * @param captivePriority The priority of the captive we want to place
     * @return A cell that now has room, or null if no replacement possible
     */
    @Nullable
    private CellDataV2 tryReleaseLowerPriorityPrisonerFromCamp(
        ServerLevel serverLevel,
        CellRegistryV2 registry,
        UUID campId,
        CaptivePriority captivePriority
    ) {
        // Use camp index for efficient lookup
        List<CellDataV2> campCells = registry.getCellsByCamp(campId);

        // Find a cell with a lower-priority NPC prisoner
        for (CellDataV2 cell : campCells) {
            if (!cell.isOccupied()) continue;

            // Check each prisoner in the cell
            for (UUID prisonerId : cell.getPrisonerIds()) {
                Entity entity = serverLevel.getEntity(prisonerId);
                if (entity instanceof LivingEntity livingPrisoner) {
                    CaptivePriority prisonerPriority =
                        CaptivePriority.fromEntity(livingPrisoner);

                    // Only replace NPCs with lower priority (never release players)
                    if (
                        captivePriority.isHigherThan(prisonerPriority) &&
                        !(livingPrisoner instanceof Player)
                    ) {
                        // Release this prisoner
                        releasePrisonerForReplacement(
                            serverLevel,
                            registry,
                            cell,
                            prisonerId,
                            livingPrisoner
                        );

                        TiedUpMod.LOGGER.debug(
                            "[KidnapperBringToCellGoal] Released {} (priority {}) to make room for priority {}",
                            livingPrisoner.getName().getString(),
                            prisonerPriority,
                            captivePriority
                        );

                        return cell;
                    }
                }
            }
        }

        return null;
    }

    /**
     * Release a prisoner NPC from a cell to make room.
     * The NPC is freed and allowed to wander away.
     */
    private void releasePrisonerForReplacement(
        ServerLevel serverLevel,
        CellRegistryV2 registry,
        CellDataV2 cell,
        UUID prisonerId,
        LivingEntity prisoner
    ) {
        // Remove from cell registry (HIGH FIX: pass server for state cleanup)
        registry.releasePrisoner(
            cell.getId(),
            prisonerId,
            serverLevel.getServer()
        );

        // Release from captivity state (clear imprisonment status)
        PrisonerManager manager = PrisonerManager.get(serverLevel);
        PrisonerRecord record = manager.getRecord(prisonerId);
        if (record.isImprisoned()) {
            // Release prisoner with grace period
            manager.release(prisonerId, serverLevel.getGameTime());
        }

        // Free the NPC if it has kidnapped state
        IRestrainable kidnappedState = KidnappedHelper.getKidnappedState(
            prisoner
        );
        if (kidnappedState != null) {
            kidnappedState.free(false); // Free without dropping leash
        }

        // If it's an NPC, despawn it (replaced by higher priority captive)
        if (prisoner instanceof AbstractTiedUpNpc npc) {
            TiedUpMod.LOGGER.info(
                "[KidnapperBringToCellGoal] Despawning NPC {} (priority {}) - replaced by higher priority captive",
                npc.getNpcName(),
                CaptivePriority.fromEntity(npc)
            );
            // Despawn the NPC (remove from world)
            npc.discard();
        }
    }

    /**
     * Announce that we're taking the captive to a cell.
     */
    private void announceIntention(IRestrainable captive) {
        this.kidnapper.talkToPlayersInRadius(
            DialogueCategory.SLAVE_TRANSPORT,
            DIALOGUE_RADIUS
        );

        if (captive.asLivingEntity() instanceof Player player) {
            this.kidnapper.talkTo(player, DialogueCategory.SLAVE_TRANSPORT);
        }

        TiedUpMod.LOGGER.debug(
            "[KidnapperBringToCellGoal] {} taking {} to cell at {}",
            this.kidnapper.getNpcName(),
            captive.getKidnappedName(),
            this.targetPos
        );
    }

    /**
     * Execute the cell delivery - imprison and teleport.
     *
     * CRITICAL FIX: Teleport AFTER successful imprisonment to prevent state desync.
     * Previous order: teleport → imprison → (fail) → player stuck in cell but logically free
     * New order: assign → imprison → (success) → teleport
     */
    private void executeCellDelivery(IRestrainable captive) {
        if (this.targetCell == null || this.targetPos == null) {
            return;
        }

        if (!(this.kidnapper.level() instanceof ServerLevel serverLevel)) {
            return;
        }

        // Validation: must be captive by this kidnapper
        if (!captive.isCaptive()) {
            TiedUpMod.LOGGER.warn(
                "[KidnapperBringToCellGoal] Cannot deliver - captive is not in captive state"
            );
            return;
        }

        // Validation: must have this kidnapper as holder
        if (
            !this.kidnapper.hasCaptives() ||
            this.kidnapper.getCaptive() != captive
        ) {
            TiedUpMod.LOGGER.warn(
                "[KidnapperBringToCellGoal] Cannot deliver - captive mismatch"
            );
            return;
        }

        CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
        UUID prisonerId = captive.asLivingEntity().getUUID();
        PrisonerManager manager = PrisonerManager.get(serverLevel);

        // Defense-in-depth: if prisoner is already in a cell (IMPRISONED state specifically),
        // skip imprison and just release the leash.
        PrisonerRecord existingRecord = manager.getRecord(prisonerId);
        if (existingRecord != null && existingRecord.getState().isInCell()) {
            TiedUpMod.LOGGER.warn(
                "[KidnapperBringToCellGoal] {} is already in cell (state={}, cell={}) - " +
                    "releasing leash without destructive rollback",
                captive.getKidnappedName(),
                existingRecord.getState(),
                existingRecord.getCellId() != null
                    ? existingRecord.getCellId().toString().substring(0, 8)
                    : "none"
            );

            // Cancel reservation
            registry.cancelReservation(
                this.targetCell.getId(),
                this.kidnapper.getUUID()
            );

            // Free from kidnapper leash (they're already imprisoned in their cell)
            captive.free(false);
            this.kidnapper.removeCaptive(captive, false);

            this.targetCell = null;
            this.targetPos = null;
            return;
        }

        // Get campId from cell (authoritative), fallback to kidnapper's camp
        UUID campId = this.targetCell.getCampId();
        if (campId == null) {
            campId = this.kidnapper.getAssociatedStructure();
        }

        // Imprison via PrisonerService (atomic: cell assign + PrisonerManager + unleash)
        boolean imprisoned =
            com.tiedup.remake.prison.service.PrisonerService.get().imprison(
                serverLevel,
                captive.asLivingEntity(),
                this.kidnapper,
                campId,
                this.targetCell
            );

        if (!imprisoned) {
            TiedUpMod.LOGGER.error(
                "[KidnapperBringToCellGoal] PrisonerService.imprison() failed for {} - aborting",
                captive.getKidnappedName()
            );

            // Cancel reservation since we're not using the cell
            registry.cancelReservation(
                this.targetCell.getId(),
                this.kidnapper.getUUID()
            );

            // Clear targets
            this.targetCell = null;
            this.targetPos = null;
            return;
        }

        // Consume cell reservation
        boolean hadReservation = registry.consumeReservation(
            this.targetCell.getId(),
            this.kidnapper.getUUID()
        );
        if (!hadReservation) {
            TiedUpMod.LOGGER.debug(
                "[KidnapperBringToCellGoal] {} delivered without reservation (may have expired)",
                this.kidnapper.getNpcName()
            );
        }

        // Teleport after successful imprisonment (prevents state desync)
        BlockPos spawnPoint =
            this.targetCell.getSpawnPoint() != null
                ? this.targetCell.getSpawnPoint()
                : this.targetCell.getCorePos().above();
        com.tiedup.remake.util.teleport.Position position =
            new com.tiedup.remake.util.teleport.Position(
                spawnPoint,
                serverLevel.dimension()
            );
        TeleportHelper.teleportEntity(captive.asLivingEntity(), position);

        // Update the captive's collar with the cell ID
        ItemStack collar = captive.getEquipment(BodyRegionV2.NECK);
        if (
            !collar.isEmpty() &&
            CollarHelper.isCollar(collar)
        ) {
            CollarHelper.setCellId(collar, this.targetCell.getId());
        }

        TiedUpMod.LOGGER.info(
            "[KidnapperBringToCellGoal] {} delivered {} to cell at {}",
            this.kidnapper.getNpcName(),
            captive.getKidnappedName(),
            spawnPoint
        );

        // Confiscate inventory
        if (captive.asLivingEntity() instanceof ServerPlayer serverPlayer) {
            confiscateInventory(serverPlayer, serverLevel, this.targetCell);
        }

        // Mark NPCs for sale immediately
        if (captive.asLivingEntity() instanceof AbstractTiedUpNpc npc) {
            com.tiedup.remake.util.tasks.ItemTask salePrice =
                new com.tiedup.remake.util.tasks.ItemTask(
                    net.minecraft.world.item.Items.EMERALD,
                    12
                );
            npc.putForSale(salePrice);
        }

        // Enter "get out" state ONLY if not a camp guard (wild kidnapper)
        if (this.kidnapper.getAssociatedStructure() == null) {
            this.kidnapper.setGetOutState(true);
        }

        // Clear target
        this.targetCell = null;
        this.targetPos = null;
    }

    /**
     */
    private void confiscateInventory(
        ServerPlayer player,
        ServerLevel serverLevel,
        CellDataV2 cell
    ) {
        // Find LOOT chest via CampData (fast path) or cell markers (fallback)
        UUID campId = cell.getOwnerId();
        BlockPos lootPos = null;
        if (campId != null) {
            lootPos =
                com.tiedup.remake.prison.service.ItemService.get().findCampChest(
                    serverLevel,
                    campId
                );
        }

        // Last resort: offset from core position (unlikely to have a chest)
        if (lootPos == null) {
            lootPos = cell.getCorePos().offset(2, 0, 0);
            TiedUpMod.LOGGER.warn(
                "[KidnapperBringToCellGoal] No LOOT chest found for cell {} (campId={}) - confiscation will likely fail",
                cell.getId().toString().substring(0, 8),
                campId != null ? campId.toString().substring(0, 8) : "null"
            );
        }

        // Check if player's inventory is empty (nothing to confiscate)
        if (player.getInventory().isEmpty()) {
            TiedUpMod.LOGGER.debug(
                "[KidnapperBringToCellGoal] {} has empty inventory, skipping confiscation",
                player.getName().getString()
            );
            return;
        }

        // Confiscate inventory
        ConfiscatedInventoryRegistry registry =
            ConfiscatedInventoryRegistry.get(serverLevel);
        boolean success = registry.confiscate(player, lootPos, cell.getId());

        if (success) {
            TiedUpMod.LOGGER.debug(
                "[KidnapperBringToCellGoal] Confiscated inventory from {} into chest at {}",
                player.getName().getString(),
                lootPos.toShortString()
            );
        } else {
            TiedUpMod.LOGGER.warn(
                "[KidnapperBringToCellGoal] Failed to confiscate inventory from {}",
                player.getName().getString()
            );
        }
    }
}
