package com.tiedup.remake.prison.service;

import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.personality.PersonalityState;
import com.tiedup.remake.prison.LaborRecord;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.prison.PrisonerState;
import com.tiedup.remake.prison.service.BondageService;
import com.tiedup.remake.state.CollarRegistry;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.*;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;

/**
 * Centralized prisoner lifecycle service.
 *
 * Manages ALL prisoner state transitions atomically:
 * - PrisonerManager (SavedData state)
 * - CellRegistryV2 (cell assignment)
 * - Leash/captor refs (IBondageState / ICaptor)
 * - Escape detection (tick)
 *
 * Replaces EscapeService and unifies scattered capture/imprison/extract/return/transfer logic.
 *
 * AI goals manage: Navigation, Teleport, Equipment, Dialogue.
 * PrisonerService manages: State transitions + leash coordination.
 */
public class PrisonerService {

    private static final PrisonerService INSTANCE = new PrisonerService();

    public static PrisonerService get() {
        return INSTANCE;
    }

    private PrisonerService() {}

    // ==================== CONSTANTS (from EscapeService) ====================

    /** Maximum distance from cell for IMPRISONED prisoners (blocks) */
    public static final double CELL_ESCAPE_DISTANCE = 20.0;

    /** Maximum distance from camp for WORKING prisoners (blocks) */
    public static final double WORK_ESCAPE_DISTANCE = 100.0;

    /** Maximum time offline before escape (30 minutes in ticks) */
    public static final long OFFLINE_TIMEOUT_TICKS = 30 * 60 * 20L;

    /** Maximum time in WORKING state before forced return (10 minutes in ticks) */
    public static final long WORK_TIMEOUT_TICKS = 10 * 60 * 20L;

    /** Maximum time in RETURNING phase before escape (5 minutes in ticks) */
    public static final long RETURN_TIMEOUT_TICKS = 5 * 60 * 20L;

    /** Maximum time in PENDING_RETURN phase before forced return (2.5 minutes in ticks) */
    public static final long PENDING_RETURN_TIMEOUT_TICKS = 3000;

    /** Check interval in ticks (every 5 seconds) */
    public static final int CHECK_INTERVAL_TICKS = 100;

    // ==================== CAPTURE ====================

    /**
     * Capture a free entity.
     * FREE → CAPTURED + leash + captor refs.
     *
     * @param level Server level
     * @param captive The entity being captured (must have IBondageState state)
     * @param captor The capturing entity
     * @return true if capture succeeded
     */
    public boolean capture(
        ServerLevel level,
        LivingEntity captive,
        ICaptor captor
    ) {
        UUID captiveId = captive.getUUID();
        PrisonerManager manager = PrisonerManager.get(level);
        long gameTime = level.getGameTime();

        // 1. PrisonerManager state transition: FREE → CAPTURED
        if (
            !manager.capture(captiveId, captor.getEntity().getUUID(), gameTime)
        ) {
            TiedUpMod.LOGGER.warn(
                "[PrisonerService] capture() failed PrisonerManager transition for {}",
                captiveId.toString().substring(0, 8)
            );
            return false;
        }

        // 2. Leash attach
        IBondageState kidnappedState = KidnappedHelper.getKidnappedState(
            captive
        );
        if (kidnappedState == null) {
            // Rollback
            manager.escape(
                captiveId,
                gameTime,
                "capture rollback: no kidnapped state"
            );
            return false;
        }

        boolean leashed;
        if (captive instanceof AbstractTiedUpNpc npc) {
            // NPC: use forceCapturedBy to bypass canCapture() PrisonerManager check
            // (PrisonerManager state is already CAPTURED at this point)
            leashed = npc.forceCapturedBy(captor);
        } else {
            // Player: getCapturedBy checks isEnslavable() first (OR logic),
            // so canCapture() PrisonerManager check is bypassed if tied up.
            // Generic IBondageState: same path.
            leashed = kidnappedState.getCapturedBy(captor);
        }

        if (!leashed) {
            // Rollback PrisonerManager
            manager.escape(
                captiveId,
                gameTime,
                "capture rollback: leash failed"
            );
            TiedUpMod.LOGGER.warn(
                "[PrisonerService] capture() leash failed for {}, rolled back",
                captiveId.toString().substring(0, 8)
            );
            return false;
        }

        TiedUpMod.LOGGER.info(
            "[PrisonerService] Captured {} by {}",
            captive.getName().getString(),
            captor.getEntity().getName().getString()
        );
        return true;
    }

    // ==================== IMPRISONMENT ====================

    /**
     * Imprison a captured entity in a cell.
     * CAPTURED → IMPRISONED + unleash + CellRegistryV2.assign.
     *
     * The caller handles: teleport, confiscation, bind, dialogue.
     *
     * @param level Server level
     * @param captive The captured entity
     * @param captor The current captor
     * @param campId Camp UUID
     * @param cell The target cell
     * @return true if imprisonment succeeded
     */
    public boolean imprison(
        ServerLevel level,
        LivingEntity captive,
        ICaptor captor,
        UUID campId,
        CellDataV2 cell
    ) {
        if (cell == null) {
            TiedUpMod.LOGGER.warn(
                "[PrisonerService] imprison() called with null cell"
            );
            return false;
        }

        UUID captiveId = captive.getUUID();
        PrisonerManager manager = PrisonerManager.get(level);
        CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
        long gameTime = level.getGameTime();

        // 1. Assign to cell registry
        boolean assigned = cellRegistry.assignPrisoner(cell.getId(), captiveId);
        if (!assigned) {
            TiedUpMod.LOGGER.warn(
                "[PrisonerService] imprison() cell assignment failed for {}",
                captiveId.toString().substring(0, 8)
            );
            return false;
        }

        // Defense: if prisoner is FREE, capture first
        boolean defenseCaptured = false;
        PrisonerRecord record = manager.getRecord(captiveId);
        if (record.getState() == PrisonerState.FREE) {
            TiedUpMod.LOGGER.info(
                "[PrisonerService] {} is FREE, capturing before imprison",
                captive.getName().getString()
            );
            manager.capture(captiveId, captor.getEntity().getUUID(), gameTime);
            defenseCaptured = true;
        }

        // 2. PrisonerManager state transition: CAPTURED → IMPRISONED
        boolean imprisoned = manager.imprison(
            captiveId,
            campId,
            cell.getId(),
            gameTime
        );
        if (!imprisoned) {
            // Rollback cell assignment
            cellRegistry.releasePrisoner(
                cell.getId(),
                captiveId,
                level.getServer()
            );
            // Rollback defense auto-capture if we triggered it
            if (defenseCaptured) {
                manager.escape(captiveId, gameTime, "imprison_rollback");
            }
            TiedUpMod.LOGGER.warn(
                "[PrisonerService] imprison() PrisonerManager transition failed for {} (state={})",
                captiveId.toString().substring(0, 8),
                manager.getState(captiveId)
            );
            return false;
        }

        // 3. Unleash: remove captor refs and drop leash
        IBondageState kidnappedState = KidnappedHelper.getKidnappedState(
            captive
        );
        if (kidnappedState != null) {
            kidnappedState.free(false); // Drop leash without dialogue/items
            captor.removeCaptive(kidnappedState, false);
        }

        // 4. Sync PersonalityState cellId for damsels
        if (captive instanceof EntityDamsel damsel) {
            PersonalityState pState = damsel.getPersonalityState();
            if (pState != null) {
                pState.assignCell(cell.getId(), cell, damsel.getUUID());
            }
        }

        TiedUpMod.LOGGER.info(
            "[PrisonerService] Imprisoned {} in cell {} (camp={})",
            captive.getName().getString(),
            cell.getId().toString().substring(0, 8),
            campId != null ? campId.toString().substring(0, 8) : "null"
        );
        return true;
    }

    // ==================== EXTRACTION ====================

    /**
     * Extract a prisoner from their cell.
     * IMPRISONED → WORKING + CellRegistryV2.release + leash to new captor.
     *
     * For: dogwalk, maid fetch, labor extract.
     * Note: If the entity is not tracked by PrisonerManager (e.g., damsels),
     * the PrisonerManager step is skipped — only CellRegistryV2 + leash are handled.
     *
     * @param level Server level
     * @param captive The imprisoned entity
     * @param newCaptor The entity extracting (kidnapper or maid)
     * @param cell The cell to extract from
     * @return true if extraction succeeded
     */
    public boolean extractFromCell(
        ServerLevel level,
        LivingEntity captive,
        ICaptor newCaptor,
        CellDataV2 cell
    ) {
        UUID captiveId = captive.getUUID();
        PrisonerManager manager = PrisonerManager.get(level);
        CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
        long gameTime = level.getGameTime();

        // 1. PrisonerManager: IMPRISONED → WORKING (players only — damsels aren't tracked)
        boolean isPlayer = captive instanceof Player;
        if (isPlayer) {
            if (!manager.extract(captiveId, gameTime)) {
                TiedUpMod.LOGGER.warn(
                    "[PrisonerService] extractFromCell() PrisonerManager transition failed for {}",
                    captiveId.toString().substring(0, 8)
                );
                return false;
            }
        }

        // 2. Release from cell registry
        cellRegistry.releasePrisoner(
            cell.getId(),
            captiveId,
            level.getServer()
        );

        // 3. Leash to new captor — forceCapturedBy for damsels
        IBondageState kidnappedState = KidnappedHelper.getKidnappedState(
            captive
        );
        if (kidnappedState == null) {
            // Rollback
            if (isPlayer) {
                manager.returnToCell(captiveId, cell.getId(), gameTime);
            }
            cellRegistry.assignPrisoner(cell.getId(), captiveId);
            TiedUpMod.LOGGER.warn(
                "[PrisonerService] extractFromCell() no kidnapped state for {}, rolled back",
                captiveId.toString().substring(0, 8)
            );
            return false;
        }

        boolean leashed;
        if (captive instanceof AbstractTiedUpNpc npc) {
            leashed = npc.forceCapturedBy(newCaptor);
        } else {
            leashed = kidnappedState.getCapturedBy(newCaptor);
        }

        if (!leashed) {
            // Rollback: return to IMPRISONED + reassign cell
            if (isPlayer) {
                manager.returnToCell(captiveId, cell.getId(), gameTime);
            }
            cellRegistry.assignPrisoner(cell.getId(), captiveId);
            TiedUpMod.LOGGER.warn(
                "[PrisonerService] extractFromCell() leash failed for {}, rolled back",
                captiveId.toString().substring(0, 8)
            );
            return false;
        }

        TiedUpMod.LOGGER.info(
            "[PrisonerService] Extracted {} from cell {} by {}",
            captive.getName().getString(),
            cell.getId().toString().substring(0, 8),
            newCaptor.getEntity().getName().getString()
        );
        return true;
    }

    // ==================== RETURN TO CELL ====================

    /**
     * Return a prisoner to their cell.
     * WORKING → IMPRISONED + unleash + CellRegistryV2.assign.
     *
     * For: dogwalk return, labor return.
     * Note: If the entity is not tracked by PrisonerManager (e.g., damsels),
     * the PrisonerManager step is skipped — only unleash + CellRegistryV2 are handled.
     *
     * @param level Server level
     * @param captive The working entity
     * @param currentCaptor The current captor holding the leash
     * @param cell The cell to return to
     * @return true if return succeeded
     */
    public boolean returnToCell(
        ServerLevel level,
        LivingEntity captive,
        @Nullable ICaptor currentCaptor,
        CellDataV2 cell
    ) {
        UUID captiveId = captive.getUUID();
        PrisonerManager manager = PrisonerManager.get(level);
        CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
        long gameTime = level.getGameTime();

        // 1. Unleash
        IBondageState kidnappedState = KidnappedHelper.getKidnappedState(
            captive
        );
        if (kidnappedState != null && kidnappedState.isCaptive()) {
            kidnappedState.free(false);
        }
        if (currentCaptor != null && kidnappedState != null) {
            currentCaptor.removeCaptive(kidnappedState, false);
        }

        // 2. PrisonerManager: WORKING → IMPRISONED (players only — damsels aren't tracked)
        if (captive instanceof Player) {
            boolean returned = manager.returnToCell(
                captiveId,
                cell.getId(),
                gameTime
            );
            if (!returned) {
                // State might not be WORKING (escape during return, etc.)
                // Force-repair: try capture+imprison path
                PrisonerRecord record = manager.getRecord(captiveId);
                PrisonerState currentState = record.getState();
                TiedUpMod.LOGGER.warn(
                    "[PrisonerService] returnToCell() transition failed for {} (state={}). Force-repairing.",
                    captiveId.toString().substring(0, 8),
                    currentState
                );

                if (currentState == PrisonerState.FREE) {
                    UUID captorId =
                        currentCaptor != null
                            ? currentCaptor.getEntity().getUUID()
                            : captiveId;
                    manager.capture(captiveId, captorId, gameTime);
                    UUID campId = record.getCampId();
                    manager.imprison(captiveId, campId, cell.getId(), gameTime);
                } else if (currentState == PrisonerState.CAPTURED) {
                    UUID campId = record.getCampId();
                    manager.imprison(captiveId, campId, cell.getId(), gameTime);
                }
                // If IMPRISONED already — no transition needed, just reassign cell
            }
        }

        // 3. Reassign to cell registry
        cellRegistry.assignPrisoner(cell.getId(), captiveId);

        TiedUpMod.LOGGER.info(
            "[PrisonerService] Returned {} to cell {}",
            captive.getName().getString(),
            cell.getId().toString().substring(0, 8)
        );
        return true;
    }

    // ==================== TRANSFER ====================

    /**
     * Transfer a captive from one captor to another.
     * PrisonerManager state unchanged.
     *
     * For: maid→buyer, kidnapper→kidnapper.
     *
     * @param level Server level
     * @param captive The captive entity
     * @param oldCaptor The current captor
     * @param newCaptor The new captor
     * @return true if transfer succeeded
     */
    public boolean transferCaptive(
        ServerLevel level,
        LivingEntity captive,
        ICaptor oldCaptor,
        ICaptor newCaptor
    ) {
        IBondageState kidnappedState = KidnappedHelper.getKidnappedState(
            captive
        );
        if (kidnappedState == null) {
            TiedUpMod.LOGGER.warn(
                "[PrisonerService] transferCaptive() no kidnapped state for {}",
                captive.getName().getString()
            );
            return false;
        }

        // 1. Remove from old captor
        oldCaptor.removeCaptive(kidnappedState, false);

        // 2. Clear leash
        kidnappedState.free(false);

        // 3. Re-leash to new captor
        boolean leashed;
        if (captive instanceof AbstractTiedUpNpc npc) {
            leashed = npc.forceCapturedBy(newCaptor);
        } else {
            leashed = kidnappedState.getCapturedBy(newCaptor);
        }

        if (!leashed) {
            // Rollback: try to re-leash to old captor
            TiedUpMod.LOGGER.warn(
                "[PrisonerService] transferCaptive() re-leash to new captor failed, attempting rollback"
            );
            if (captive instanceof AbstractTiedUpNpc npc) {
                npc.forceCapturedBy(oldCaptor);
            } else {
                kidnappedState.getCapturedBy(oldCaptor);
            }
            return false;
        }

        TiedUpMod.LOGGER.info(
            "[PrisonerService] Transferred {} from {} to {}",
            captive.getName().getString(),
            oldCaptor.getEntity().getName().getString(),
            newCaptor.getEntity().getName().getString()
        );
        return true;
    }

    // ==================== ESCAPE (from EscapeService) ====================

    /**
     * CENTRAL ESCAPE METHOD - All escapes must go through here.
     *
     * Handles complete cleanup:
     * - State transition (PrisonerManager)
     * - Cell registry cleanup
     * - Guard despawn
     * - Restraints removal (if online)
     * - Inventory NOT restored (stays in camp chest as punishment)
     *
     * @param level The server level
     * @param playerId Player UUID
     * @param reason Reason for escape (for logging)
     * @return true if escape was processed successfully
     */
    public boolean escape(ServerLevel level, UUID playerId, String reason) {
        long currentTime = level.getGameTime();
        PrisonerManager manager = PrisonerManager.get(level);
        CellRegistryV2 cellRegistry = CellRegistryV2.get(level);

        TiedUpMod.LOGGER.info(
            "[PrisonerService] Processing escape for {} - reason: {}",
            playerId.toString().substring(0, 8),
            reason
        );

        // Step 1: Save guard ID BEFORE state transition (manager.escape() removes the LaborRecord)
        LaborRecord laborBeforeEscape = manager.getLaborRecord(playerId);
        UUID guardId = laborBeforeEscape.getGuardId();

        // Step 2: Transition prisoner state to FREE
        boolean stateChanged = manager.escape(playerId, currentTime, reason);
        if (!stateChanged) {
            TiedUpMod.LOGGER.warn(
                "[PrisonerService] Failed to change state for {} - invalid transition",
                playerId.toString().substring(0, 8)
            );
            return false;
        }

        // Step 3: Cleanup CellRegistryV2 - remove from all cells
        int cellsCleared = cellRegistry.releasePrisonerFromAllCells(playerId);
        if (cellsCleared > 0) {
            TiedUpMod.LOGGER.debug(
                "[PrisonerService] Cleared {} from {} cells",
                playerId.toString().substring(0, 8),
                cellsCleared
            );
        }

        // Step 4: Cleanup guard using saved reference (LaborRecord was removed by manager.escape())
        if (guardId != null) {
            net.minecraft.world.entity.Entity guardEntity = level.getEntity(
                guardId
            );
            if (guardEntity != null) {
                guardEntity.discard();
                TiedUpMod.LOGGER.debug(
                    "[PrisonerService] Despawned guard {} during escape",
                    guardId.toString().substring(0, 8)
                );
            }
        }

        // Step 5: Free from restraints (if player is online)
        // NOTE: Inventory is NOT restored on escape - items remain in camp chest as punishment
        ServerPlayer player = level
            .getServer()
            .getPlayerList()
            .getPlayer(playerId);
        if (player != null) {
            IBondageState cap = KidnappedHelper.getKidnappedState(player);
            if (cap != null) {
                cap.free(false);
                TiedUpMod.LOGGER.info(
                    "[PrisonerService] Freed {} from restraints (inventory remains in camp chest)",
                    player.getName().getString()
                );
            }
        } else {
            TiedUpMod.LOGGER.debug(
                "[PrisonerService] Player {} offline - restraints cleanup deferred",
                playerId.toString().substring(0, 8)
            );
        }

        TiedUpMod.LOGGER.info(
            "[PrisonerService] Escape complete for {} - items stay in camp chest",
            playerId.toString().substring(0, 8)
        );

        return true;
    }

    // ==================== RELEASE (from EscapeService) ====================

    /**
     * CENTRAL RELEASE METHOD - All legitimate releases must go through here.
     *
     * Similar to escape() but transitions to PROTECTED state with grace period.
     * Handles complete cleanup:
     * - State transition to PROTECTED (PrisonerManager)
     * - Cell registry cleanup
     * - Inventory restoration
     * - Restraints removal (if online)
     *
     * @param level The server level
     * @param playerId Player UUID
     * @param gracePeriodTicks Grace period before returning to FREE (0 = instant FREE)
     * @return true if release was processed successfully
     */
    public boolean release(
        ServerLevel level,
        UUID playerId,
        long gracePeriodTicks
    ) {
        long currentTime = level.getGameTime();
        PrisonerManager manager = PrisonerManager.get(level);
        CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
        com.tiedup.remake.cells.ConfiscatedInventoryRegistry inventoryRegistry =
            com.tiedup.remake.cells.ConfiscatedInventoryRegistry.get(level);

        TiedUpMod.LOGGER.info(
            "[PrisonerService] Processing release for {} - grace period: {} ticks",
            playerId.toString().substring(0, 8),
            gracePeriodTicks
        );

        // Step 1: Transition prisoner state to PROTECTED (or FREE if gracePeriod = 0)
        boolean stateChanged = manager.release(
            playerId,
            currentTime,
            gracePeriodTicks
        );
        if (!stateChanged) {
            TiedUpMod.LOGGER.warn(
                "[PrisonerService] Failed to change state for {} - invalid transition",
                playerId.toString().substring(0, 8)
            );
            return false;
        }

        // Step 2: Cleanup CellRegistryV2 - remove from all cells
        int cellsCleared = cellRegistry.releasePrisonerFromAllCells(playerId);
        if (cellsCleared > 0) {
            TiedUpMod.LOGGER.debug(
                "[PrisonerService] Cleared {} from {} cells",
                playerId.toString().substring(0, 8),
                cellsCleared
            );
        }

        // Step 3: Restore confiscated inventory (if player is online)
        ServerPlayer player = level
            .getServer()
            .getPlayerList()
            .getPlayer(playerId);
        if (player != null) {
            // Restore inventory
            if (inventoryRegistry.hasConfiscatedInventory(playerId)) {
                boolean restored = inventoryRegistry.restoreInventory(player);
                if (restored) {
                    TiedUpMod.LOGGER.info(
                        "[PrisonerService] Restored confiscated inventory for {}",
                        player.getName().getString()
                    );
                }
            }

            // Free from restraints
            IBondageState cap = KidnappedHelper.getKidnappedState(player);
            if (cap != null) {
                cap.free(false);
                TiedUpMod.LOGGER.debug(
                    "[PrisonerService] Freed {} from restraints",
                    player.getName().getString()
                );
            }
        } else {
            TiedUpMod.LOGGER.debug(
                "[PrisonerService] Player {} offline - inventory cleanup deferred",
                playerId.toString().substring(0, 8)
            );
        }

        TiedUpMod.LOGGER.info(
            "[PrisonerService] Release complete for {} - full cleanup done",
            playerId.toString().substring(0, 8)
        );

        return true;
    }

    // ==================== TICK (escape detection, from EscapeService) ====================

    /**
     * Tick escape detection.
     * Called from CampManagementHandler.
     *
     * @param server The server
     * @param currentTime Current game time
     */
    public void tick(MinecraftServer server, long currentTime) {
        if (currentTime % CHECK_INTERVAL_TICKS != 0) {
            return;
        }

        ServerLevel level = server.overworld();
        PrisonerManager manager = PrisonerManager.get(level);
        CellRegistryV2 cells = CellRegistryV2.get(level);
        CollarRegistry collars = CollarRegistry.get(level);

        List<EscapeCandidate> escapees = new ArrayList<>();

        for (UUID playerId : manager.getAllPrisonerIds()) {
            PrisonerRecord record = manager.getRecord(playerId);
            PrisonerState state = record.getState();

            if (!state.isCaptive()) {
                continue;
            }

            ServerPlayer player = server.getPlayerList().getPlayer(playerId);

            // === OFFLINE CHECK ===
            if (player == null) {
                long timeInState = record.getTimeInState(currentTime);
                if (timeInState > OFFLINE_TIMEOUT_TICKS) {
                    escapees.add(
                        new EscapeCandidate(playerId, "offline timeout")
                    );
                }
                continue;
            }

            // === COLLAR CHECK ===
            if (state != PrisonerState.CAPTURED) {
                boolean hasCollarOwners = collars.hasOwners(playerId);
                if (!hasCollarOwners) {
                    IBondageState cap = KidnappedHelper.getKidnappedState(
                        player
                    );
                    ItemStack collar =
                        cap != null
                            ? cap.getEquipment(BodyRegionV2.NECK)
                            : ItemStack.EMPTY;

                    if (
                        !collar.isEmpty() &&
                        collar.getItem() instanceof
                            com.tiedup.remake.items.base.ItemCollar collarItem
                    ) {
                        List<UUID> nbtOwners = collarItem.getOwners(collar);
                        if (!nbtOwners.isEmpty()) {
                            for (UUID ownerUUID : nbtOwners) {
                                collars.registerCollar(playerId, ownerUUID);
                            }
                            TiedUpMod.LOGGER.info(
                                "[PrisonerService] Re-synced collar registry for {} - found {} owners in NBT",
                                playerId.toString().substring(0, 8),
                                nbtOwners.size()
                            );
                            continue;
                        }
                    }

                    TiedUpMod.LOGGER.warn(
                        "[PrisonerService] Collar check failed for {} (state={}): collarEquipped={}, collarItem={}",
                        playerId.toString().substring(0, 8),
                        state,
                        !collar.isEmpty(),
                        collar.isEmpty() ? "none" : collar.getItem().toString()
                    );
                    escapees.add(
                        new EscapeCandidate(playerId, "collar removed")
                    );
                    continue;
                }
            }

            // === STATE-SPECIFIC CHECKS ===
            switch (state) {
                case IMPRISONED -> {
                    UUID cellId = record.getCellId();
                    if (cellId != null) {
                        CellDataV2 cell = cells.getCell(cellId);
                        if (cell == null) {
                            escapees.add(
                                new EscapeCandidate(
                                    playerId,
                                    "cell no longer exists"
                                )
                            );
                            TiedUpMod.LOGGER.warn(
                                "[PrisonerService] Prisoner {} has orphaned cellId {} - triggering escape",
                                playerId.toString().substring(0, 8),
                                cellId.toString().substring(0, 8)
                            );
                        } else {
                            double distance = Math.sqrt(
                                player
                                    .blockPosition()
                                    .distSqr(cell.getCorePos())
                            );
                            if (distance > CELL_ESCAPE_DISTANCE) {
                                escapees.add(
                                    new EscapeCandidate(
                                        playerId,
                                        String.format(
                                            "too far from cell (%.1f blocks)",
                                            distance
                                        )
                                    )
                                );
                            }
                        }
                    }
                }
                case WORKING -> {
                    LaborRecord labor = manager.getLaborRecord(playerId);

                    long timeInPhase = labor.getTimeInPhase(currentTime);
                    if (
                        labor.getPhase() == LaborRecord.WorkPhase.WORKING &&
                        timeInPhase > WORK_TIMEOUT_TICKS
                    ) {
                        labor.failTask(currentTime);
                        TiedUpMod.LOGGER.info(
                            "[PrisonerService] {} work timeout - forcing return",
                            playerId.toString().substring(0, 8)
                        );
                        continue;
                    }

                    if (
                        labor.getPhase() == LaborRecord.WorkPhase.RETURNING &&
                        timeInPhase > RETURN_TIMEOUT_TICKS
                    ) {
                        escapees.add(
                            new EscapeCandidate(playerId, "return timeout")
                        );
                        continue;
                    }

                    // PENDING_RETURN timeout: maid failed to pick up prisoner — force return to cell
                    if (
                        labor.getPhase() ==
                            LaborRecord.WorkPhase.PENDING_RETURN &&
                        timeInPhase > PENDING_RETURN_TIMEOUT_TICKS
                    ) {
                        PrisonerRecord rec = manager.getRecord(playerId);
                        UUID cellId = rec.getCellId();
                        if (cellId != null) {
                            CellDataV2 cell = cells.getCell(cellId);
                            if (cell != null) {
                                BlockPos teleportPos =
                                    cell.getSpawnPoint() != null
                                        ? cell.getSpawnPoint()
                                        : cell.getCorePos().above();
                                player.teleportTo(
                                    teleportPos.getX() + 0.5,
                                    teleportPos.getY(),
                                    teleportPos.getZ() + 0.5
                                );
                                IBondageState cap =
                                    KidnappedHelper.getKidnappedState(player);
                                if (cap != null) {
                                    CompoundTag snapshot =
                                        labor.getBondageSnapshot();
                                    if (snapshot != null) {
                                        BondageService.get().restoreSnapshot(
                                            cap,
                                            snapshot
                                        );
                                    }
                                }
                                returnToCell(level, player, null, cell);
                                labor.startRest(currentTime);
                                if (labor.getGuardId() != null) {
                                    net.minecraft.world.entity.Entity guardEntity =
                                        level.getEntity(labor.getGuardId());
                                    if (
                                        guardEntity != null
                                    ) guardEntity.discard();
                                }
                                player.sendSystemMessage(
                                    Component.literal(
                                        "You have been returned to your cell."
                                    ).withStyle(ChatFormatting.GRAY)
                                );
                                TiedUpMod.LOGGER.info(
                                    "[PrisonerService] Force-returned {} to cell (PENDING_RETURN timeout)",
                                    playerId.toString().substring(0, 8)
                                );
                                continue;
                            }
                        }
                        // Cell gone → trigger escape
                        escapees.add(
                            new EscapeCandidate(
                                playerId,
                                "pending_return timeout, cell gone"
                            )
                        );
                        continue;
                    }

                    if (labor.getGuardId() != null) {
                        net.minecraft.world.entity.Entity guardEntity =
                            level.getEntity(labor.getGuardId());
                        if (guardEntity != null && guardEntity.isAlive()) {
                            continue;
                        }
                    }

                    UUID campId = record.getCampId();
                    if (campId != null) {
                        List<CellDataV2> campCells = cells.getCellsByCamp(
                            campId
                        );
                        if (campCells.isEmpty()) {
                            escapees.add(
                                new EscapeCandidate(
                                    playerId,
                                    "camp no longer exists"
                                )
                            );
                            TiedUpMod.LOGGER.warn(
                                "[PrisonerService] Worker {} has orphaned campId {} with no cells - triggering escape",
                                playerId.toString().substring(0, 8),
                                campId.toString().substring(0, 8)
                            );
                        } else {
                            BlockPos campCenter = campCells.get(0).getCorePos();
                            double distance = Math.sqrt(
                                player.blockPosition().distSqr(campCenter)
                            );
                            if (distance > WORK_ESCAPE_DISTANCE) {
                                escapees.add(
                                    new EscapeCandidate(
                                        playerId,
                                        String.format(
                                            "too far from camp (%.1f blocks)",
                                            distance
                                        )
                                    )
                                );
                            }
                        }
                    }
                }
                case CAPTURED -> {
                    // During transport - handled by kidnapper goals
                }
                default -> {
                    // Other states don't need distance checks
                }
            }
        }

        for (EscapeCandidate candidate : escapees) {
            escape(level, candidate.playerId, candidate.reason);
        }
    }

    // ==================== VALIDATION ====================

    /**
     * Check if a player should be considered escaped.
     * Called on-demand (e.g., when player moves).
     */
    @Nullable
    public String checkEscape(ServerLevel level, ServerPlayer player) {
        PrisonerManager manager = PrisonerManager.get(level);
        CellRegistryV2 cells = CellRegistryV2.get(level);
        CollarRegistry collars = CollarRegistry.get(level);

        UUID playerId = player.getUUID();
        PrisonerRecord record = manager.getRecord(playerId);
        PrisonerState state = record.getState();

        if (!state.isCaptive()) {
            return null;
        }

        if (!collars.hasOwners(playerId)) {
            return "collar removed";
        }

        switch (state) {
            case IMPRISONED -> {
                UUID cellId = record.getCellId();
                if (cellId != null) {
                    CellDataV2 cell = cells.getCell(cellId);
                    if (cell != null) {
                        double distance = Math.sqrt(
                            player.blockPosition().distSqr(cell.getCorePos())
                        );
                        if (distance > CELL_ESCAPE_DISTANCE) {
                            return String.format(
                                "too far from cell (%.1f blocks)",
                                distance
                            );
                        }
                    }
                }
            }
            case WORKING -> {
                UUID campId = record.getCampId();
                if (campId != null) {
                    List<CellDataV2> campCells = cells.getCellsByCamp(campId);
                    if (!campCells.isEmpty()) {
                        BlockPos campCenter = campCells.get(0).getCorePos();
                        double distance = Math.sqrt(
                            player.blockPosition().distSqr(campCenter)
                        );
                        if (distance > WORK_ESCAPE_DISTANCE) {
                            return String.format(
                                "too far from camp (%.1f blocks)",
                                distance
                            );
                        }
                    }
                }
            }
            default -> {
                // Other states don't have distance limits
            }
        }

        return null;
    }

    /**
     * Handle player movement event.
     * Called from event handler to check distance-based escapes.
     */
    public void onPlayerMove(ServerPlayer player, ServerLevel level) {
        String escapeReason = checkEscape(level, player);
        if (escapeReason != null) {
            escape(level, player.getUUID(), escapeReason);
        }
    }

    // ==================== HELPER ====================

    private record EscapeCandidate(UUID playerId, String reason) {}
}
