package com.tiedup.remake.entities;

import com.tiedup.remake.cells.CampLifecycleManager;
import com.tiedup.remake.cells.CampOwnership;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.SpeakerType;
// Prison system v2 goals
import com.tiedup.remake.entities.ai.trader.goals.TraderIdleGoal;
import com.tiedup.remake.entities.ai.trader.goals.TraderSellGoal;
import com.tiedup.remake.entities.skins.Gender;
import com.tiedup.remake.entities.skins.TraderSkinManager;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.IHasResistance;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.trader.PacketOpenTraderScreen;
import com.tiedup.remake.network.trader.PacketOpenTraderScreen.CaptiveOfferData;
import com.tiedup.remake.personality.PersonalityType;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.tasks.ItemTask;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.annotation.Nullable;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.ai.goal.*;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;

/**
 * EntitySlaveTrader - Boss of a camp who sells captives.
 *
 * Slave Trader & Maid System
 *
 * Characteristics:
 * - Stats like Elite (high HP, fast, resistant)
 * - Does NOT capture (stays at camp)
 * - Manages sale of captives in camp cells
 * - Has a Maid who follows orders
 * - Token required to interact peacefully
 *
 * When killed:
 * - Camp becomes permanently dead
 * - Maid becomes neutral
 */
public class EntitySlaveTrader extends EntityKidnapperElite {

    // CONSTANTS

    public static final double TRADER_MAX_HEALTH = 50.0D;
    public static final double TRADER_MOVEMENT_SPEED = 0.30D;
    public static final double TRADER_KNOCKBACK_RESISTANCE = 0.95D;
    public static final double TRADER_FOLLOW_RANGE = 40.0D;
    public static final double TRADER_ATTACK_DAMAGE = 10.0D;
    public static final int TRADER_NAME_COLOR = 0xFFD700; // Gold

    // STATE

    /** UUID of the maid that serves this trader */
    @Nullable
    private UUID maidUUID;

    /** UUID of the camp this trader manages */
    @Nullable
    private UUID campUUID;

    /** Spawn position (marker location) */
    @Nullable
    private net.minecraft.core.BlockPos spawnPos;

    /** Grace period: warned player UUID and tick */
    @Nullable
    private UUID warnedPlayerUUID;

    private long warningTick;
    private static final int GRACE_PERIOD_TICKS = 100; // 5 seconds before warning expires

    /** Chase limit: tick when target was set, and max chase duration */
    private long chaseStartTick;
    private static final int MAX_CHASE_TICKS = 200; // 10 seconds max chase
    private static final double MAX_CHASE_DISTANCE_FROM_SPAWN = 30.0;

    /** Hostility cooldown (ticks remaining before auto-reset) */
    private int hostileCooldownTicks = 0;
    private static final int HOSTILE_DURATION_TICKS = 600; // 30 seconds max hostility

    // CONSTRUCTOR

    public EntitySlaveTrader(
        EntityType<? extends EntitySlaveTrader> type,
        Level level
    ) {
        super(type, level);
    }

    // ATTRIBUTES

    public static AttributeSupplier.Builder createAttributes() {
        return Mob.createMobAttributes()
            .add(Attributes.MAX_HEALTH, TRADER_MAX_HEALTH)
            .add(Attributes.MOVEMENT_SPEED, TRADER_MOVEMENT_SPEED)
            .add(Attributes.KNOCKBACK_RESISTANCE, TRADER_KNOCKBACK_RESISTANCE)
            .add(Attributes.FOLLOW_RANGE, TRADER_FOLLOW_RANGE)
            .add(Attributes.ATTACK_DAMAGE, TRADER_ATTACK_DAMAGE);
    }

    // AI GOALS

    @Override
    protected void registerGoals() {
        // Trader has different AI - doesn't hunt, stays at camp

        // Priority 0: Always swim
        this.goalSelector.addGoal(0, new FloatGoal(this));

        // Priority 1: Melee attack if hostile (trader fights but doesn't carry captives)
        this.goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.2D, false));

        // Priority 4: Sell captives (interact with buyers)
        this.goalSelector.addGoal(4, new TraderSellGoal(this));

        // Priority 5: Idle/patrol behavior
        this.goalSelector.addGoal(5, new TraderIdleGoal(this));

        // These were non-functional before (Traders have no PersonalityState).
        // Proper fix: Create TraderCommandGoals that use collar owner instead.
        // DamselAIController.registerCommandGoals(this.goalSelector, this, 6);

        // Priority 10: Look at players
        this.goalSelector.addGoal(
            10,
            new LookAtPlayerGoal(this, Player.class, 8.0F)
        );

        // Priority 11: Random look around
        this.goalSelector.addGoal(11, new RandomLookAroundGoal(this));

        // Priority 12: Wander occasionally
        this.goalSelector.addGoal(
            12,
            new WaterAvoidingRandomStrollGoal(this, 0.6D)
        );
    }

    // INTERACTION

    @Override
    public InteractionResult mobInteract(Player player, InteractionHand hand) {
        // Enslaved: use base NPC behavior (feeding, conversation via EntityDamsel)
        if (this.isTiedUp()) {
            return super.mobInteract(player, hand);
        }

        if (this.level().isClientSide) {
            return InteractionResult.SUCCESS;
        }

        // Only process main hand to avoid double-triggering
        if (hand != InteractionHand.MAIN_HAND) {
            return InteractionResult.PASS;
        }

        // Check if player has token
        if (hasTokenInInventory(player)) {
            // Open trading screen
            if (player instanceof ServerPlayer serverPlayer) {
                openTradingScreen(serverPlayer);
            }
            return InteractionResult.SUCCESS;
        } else {
            // No token - grace period before becoming hostile
            if (player instanceof ServerPlayer serverPlayer) {
                if (
                    warnedPlayerUUID == null ||
                    !warnedPlayerUUID.equals(player.getUUID())
                ) {
                    // First warning — do NOT attack yet
                    warnedPlayerUUID = player.getUUID();
                    warningTick = this.tickCount;
                    serverPlayer.sendSystemMessage(
                        Component.literal("[" + this.getNpcName() + "] ")
                            .withStyle(Style.EMPTY.withColor(TRADER_NAME_COLOR))
                            .append(
                                Component.translatable(
                                    "entity.tiedup.trader.no_token_warning"
                                ).withStyle(ChatFormatting.RED)
                            )
                    );
                    return InteractionResult.SUCCESS;
                }
                // Second click → attack directly
                this.hostileCooldownTicks = HOSTILE_DURATION_TICKS;
                this.setTarget(player);
            }
            return InteractionResult.SUCCESS;
        }
    }

    /**
     * Open the trading screen for a player.
     * Gathers available captives and sends packet to client.
     */
    private void openTradingScreen(ServerPlayer player) {
        if (!(this.level() instanceof ServerLevel serverLevel)) {
            return;
        }

        // Gather captive offers from camp cells
        List<CaptiveOfferData> offers = gatherCaptiveOffers(serverLevel);

        TiedUpMod.LOGGER.info(
            "[EntitySlaveTrader] {} opening trading screen for {} with {} offers",
            this.getNpcName(),
            player.getName().getString(),
            offers.size()
        );

        // Send packet to open screen
        ModNetwork.sendToPlayer(
            new PacketOpenTraderScreen(this.getId(), this.getNpcName(), offers),
            player
        );
    }

    /**
     * Gather captive offers from camp cells.
     * Supports both Players and NPCs (Damsels) as captives.
     */
    private List<CaptiveOfferData> gatherCaptiveOffers(ServerLevel level) {
        List<CaptiveOfferData> offers = new ArrayList<>();

        if (campUUID == null) {
            return offers;
        }

        CampOwnership ownership = CampOwnership.get(level);
        CampOwnership.CampData campData = ownership.getCamp(campUUID);

        if (campData == null || campData.getCenter() == null) {
            return offers;
        }

        CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
        List<CellDataV2> cells = cellRegistry.findCellsNear(
            campData.getCenter(),
            50.0
        );

        for (CellDataV2 cell : cells) {
            if (!cell.isOccupied()) {
                continue;
            }

            // Iterate over ALL prisoners in the cell (not just first)
            for (UUID captiveId : cell.getPrisonerIds()) {
                // Try to find the captive - first as Player, then as Entity
                net.minecraft.world.entity.LivingEntity captiveEntity = null;
                String captiveName = null;

                // Try player first
                ServerPlayer captivePlayer = level
                    .getServer()
                    .getPlayerList()
                    .getPlayer(captiveId);
                if (captivePlayer != null) {
                    captiveEntity = captivePlayer;
                    captiveName = captivePlayer.getName().getString();
                } else {
                    // Try as entity (e.g., Damsel)
                    net.minecraft.world.entity.Entity entity = level.getEntity(
                        captiveId
                    );
                    if (
                        entity instanceof
                            net.minecraft.world.entity.LivingEntity living
                    ) {
                        captiveEntity = living;
                        captiveName = living.getName().getString();
                    }
                }

                if (captiveEntity == null) {
                    continue;
                }

                IRestrainable kidnappedState =
                    KidnappedHelper.getKidnappedState(captiveEntity);
                if (kidnappedState == null || !kidnappedState.isForSell()) {
                    continue;
                }

                ItemTask price = kidnappedState.getSalePrice();
                if (price == null) {
                    continue;
                }

                String priceDescription =
                    price.getAmount() +
                    "x " +
                    (price.getItem() != null
                        ? price.getItem().getDescription().getString()
                        : "???");

                offers.add(
                    new CaptiveOfferData(
                        captiveId,
                        captiveName,
                        priceDescription,
                        price.getAmount(),
                        price.getItemId()
                    )
                );
            }
        }

        return offers;
    }

    // Uses EntityKidnapper.hasTokenInInventory(player) - inherited from parent

    // TARGETING - Trader doesn't capture

    @Override
    public void setTarget(@Nullable LivingEntity target) {
        super.setTarget(target);
        if (target != null) {
            this.chaseStartTick = this.tickCount;
        }
    }

    @Override
    public boolean isSuitableTarget(
        net.minecraft.world.entity.LivingEntity entity
    ) {
        // Trader doesn't actively hunt - only attacks if:
        // 1. Entity attacked us (getLastAttacker)
        // 2. Entity has no token

        if (entity instanceof Player player) {
            // Don't target players with tokens
            if (hasTokenInInventory(player)) {
                return false;
            }
        }

        // Only target if they attacked us
        return entity == this.getLastAttacker();
    }

    // MAID & CAMP MANAGEMENT

    @Nullable
    public UUID getMaidUUID() {
        return maidUUID;
    }

    public void setMaidUUID(@Nullable UUID maidUUID) {
        this.maidUUID = maidUUID;
    }

    @Nullable
    public UUID getCampUUID() {
        return campUUID;
    }

    public void setCampUUID(@Nullable UUID campUUID) {
        this.campUUID = campUUID;
    }

    @Nullable
    public net.minecraft.core.BlockPos getSpawnPos() {
        return spawnPos;
    }

    public void setSpawnPos(@Nullable net.minecraft.core.BlockPos spawnPos) {
        this.spawnPos = spawnPos;
    }

    /**
     * Clear all hostility state and return to neutral.
     */
    private void clearHostility() {
        this.setTarget(null);
        this.hostileCooldownTicks = 0;
        this.warnedPlayerUUID = null;
    }

    /**
     * Get the maid entity if loaded.
     */
    @Nullable
    public EntityMaid getMaid() {
        if (
            maidUUID == null ||
            !(this.level() instanceof ServerLevel serverLevel)
        ) {
            return null;
        }
        var entity = serverLevel.getEntity(maidUUID);
        return entity instanceof EntityMaid maid ? maid : null;
    }

    /**
     * Order the maid to deliver a captive to a buyer.
     */
    public void orderMaidDeliverCaptive(IRestrainable captive, Player buyer) {
        EntityMaid maid = getMaid();
        if (maid != null) {
            maid.startDeliverCaptive(captive, buyer);
        }
    }

    /**
     * Order the maid to collect ransom/items.
     */
    public void orderMaidCollectItems(BlockPos target) {
        EntityMaid maid = getMaid();
        if (maid != null) {
            maid.startCollectItems(target);
        }
    }

    // DEATH HANDLING

    /** Guard against double-cleanup (die() triggers remove(KILLED)) */
    private boolean cleanedUp = false;

    private void performCleanup() {
        if (cleanedUp) return;
        cleanedUp = true;

        if (
            !this.level().isClientSide &&
            this.level() instanceof ServerLevel serverLevel
        ) {
            // Mark camp as dead and perform full cleanup
            // This will: cancel ransoms, free prisoners, unlock collars, clear labor states
            if (campUUID != null) {
                CampLifecycleManager.markCampDead(campUUID, serverLevel);
                TiedUpMod.LOGGER.info(
                    "[EntitySlaveTrader] {} removed, camp {} marked as dead and prisoners freed",
                    this.getNpcName(),
                    campUUID.toString().substring(0, 8)
                );
            }

            // Free the maid (becomes neutral/capturable)
            EntityMaid maid = getMaid();
            if (maid != null) {
                maid.onTraderDeath();
            }
        }
    }

    @Override
    public void die(
        net.minecraft.world.damagesource.DamageSource damageSource
    ) {
        performCleanup();
        super.die(damageSource);
    }

    @Override
    public void remove(Entity.RemovalReason reason) {
        performCleanup();
        super.remove(reason);
    }

    // VARIANT SYSTEM

    @Override
    public KidnapperVariant lookupVariantById(String variantId) {
        return TraderSkinManager.CORE.getVariant(variantId);
    }

    @Override
    public KidnapperVariant computeVariantForEntity(UUID entityUUID) {
        return TraderSkinManager.CORE.getVariantForEntity(
            entityUUID,
            Gender.FEMALE
        );
    }

    @Override
    public String getVariantTextureFolder() {
        return "textures/entity/kidnapper/trader/";
    }

    @Override
    public String getDefaultVariantId() {
        return "trader_default";
    }

    @Override
    public String getVariantNBTKey() {
        return "TraderVariantId";
    }

    @Override
    public void applyVariantName(KidnapperVariant variant) {
        // Numbered variants (trader_mob_1, trader_mob_2, etc.) get random names
        // Named variants (trader_default, trader_noble, etc.) use their default name
        if (variant.id().startsWith("trader_mob_")) {
            this.setNpcName(
                com.tiedup.remake.util.NameGenerator.getRandomTraderName()
            );
        } else {
            this.setNpcName(variant.defaultName());
        }
    }

    // DISPLAY

    @Override
    public Component getDisplayName() {
        return Component.literal(this.getNpcName()).withStyle(
            Style.EMPTY.withColor(TRADER_NAME_COLOR)
        );
    }

    // NBT PERSISTENCE

    @Override
    public void addAdditionalSaveData(CompoundTag tag) {
        super.addAdditionalSaveData(tag);

        if (maidUUID != null) {
            tag.putUUID("MaidUUID", maidUUID);
        }
        if (campUUID != null) {
            tag.putUUID("CampUUID", campUUID);
        }
        if (spawnPos != null) {
            tag.putInt("SpawnX", spawnPos.getX());
            tag.putInt("SpawnY", spawnPos.getY());
            tag.putInt("SpawnZ", spawnPos.getZ());
        }
        if (hostileCooldownTicks > 0) {
            tag.putInt("HostileCooldown", hostileCooldownTicks);
        }
    }

    @Override
    public void readAdditionalSaveData(CompoundTag tag) {
        super.readAdditionalSaveData(tag);

        if (tag.contains("MaidUUID")) {
            maidUUID = tag.getUUID("MaidUUID");
        }
        if (tag.contains("CampUUID")) {
            campUUID = tag.getUUID("CampUUID");
        }
        if (
            tag.contains("SpawnX") &&
            tag.contains("SpawnY") &&
            tag.contains("SpawnZ")
        ) {
            spawnPos = new net.minecraft.core.BlockPos(
                tag.getInt("SpawnX"),
                tag.getInt("SpawnY"),
                tag.getInt("SpawnZ")
            );
        }
        if (tag.contains("HostileCooldown")) {
            hostileCooldownTicks = tag.getInt("HostileCooldown");
        }
    }

    // CAPTURE DETECTION

    /**
     * Check if trader has been captured and removed from camp.
     * If captured and taken far from camp (>100 blocks), the camp dies.
     */
    @Override
    public void tick() {
        super.tick();

        // Server-side only
        if (
            !this.level().isClientSide &&
            this.level() instanceof ServerLevel serverLevel
        ) {
            // Grace period expiry — just reset warning (player gets a fresh warning next click)
            if (
                warnedPlayerUUID != null &&
                this.tickCount - warningTick > GRACE_PERIOD_TICKS
            ) {
                warnedPlayerUUID = null;
            }

            // Chase limit — give up if too far from spawn or chasing too long
            if (this.getTarget() != null) {
                boolean shouldClearTarget = false;

                // Timer-based limit (always works, even if spawnPos is null)
                boolean tooLong =
                    this.tickCount - chaseStartTick > MAX_CHASE_TICKS;
                if (tooLong) {
                    shouldClearTarget = true;
                }

                // Distance-from-spawn limit (when spawnPos is available)
                if (!shouldClearTarget && spawnPos != null) {
                    double distFromSpawn = this.distanceToSqr(
                        spawnPos.getX() + 0.5,
                        spawnPos.getY(),
                        spawnPos.getZ() + 0.5
                    );
                    if (
                        distFromSpawn >
                        MAX_CHASE_DISTANCE_FROM_SPAWN *
                        MAX_CHASE_DISTANCE_FROM_SPAWN
                    ) {
                        shouldClearTarget = true;
                    }
                }

                // Token forgiveness — stop chasing if target acquired a token
                if (
                    !shouldClearTarget &&
                    this.getTarget() instanceof Player targetPlayer &&
                    hasTokenInInventory(targetPlayer)
                ) {
                    shouldClearTarget = true;
                }

                if (shouldClearTarget) {
                    clearHostility();
                }
            }

            // Hostility cooldown — auto-reset after duration expires
            if (hostileCooldownTicks > 0) {
                hostileCooldownTicks--;
                if (hostileCooldownTicks <= 0 && this.getTarget() != null) {
                    clearHostility();
                }
            }

            // Check every second (20 ticks)
            if (this.tickCount % 20 == 0) {
                checkIfCapturedAndRemoved(serverLevel);
            }
        }
    }

    private void checkIfCapturedAndRemoved(ServerLevel level) {
        // Must be tied up
        if (!isTiedUp()) return;

        // Must have a camp
        UUID campId = getCampUUID();
        if (campId == null) return;

        CampOwnership ownership = CampOwnership.get(level);
        CampOwnership.CampData camp = ownership.getCamp(campId);
        if (camp == null || camp.getCenter() == null) return;

        // Check distance from camp center
        BlockPos campCenter = camp.getCenter();
        double distanceSq = this.distanceToSqr(
            campCenter.getX(),
            campCenter.getY(),
            campCenter.getZ()
        );

        // If > 100 blocks away → camp destroyed
        double CAPTURE_DISTANCE_THRESHOLD = 100.0;
        if (
            distanceSq > CAPTURE_DISTANCE_THRESHOLD * CAPTURE_DISTANCE_THRESHOLD
        ) {
            TiedUpMod.LOGGER.warn(
                "[EntitySlaveTrader] {} captured and removed {} blocks from camp {} - marking camp dead",
                this.getNpcName(),
                (int) Math.sqrt(distanceSq),
                campId.toString().substring(0, 8)
            );

            // Mark camp as dead (frees all prisoners, etc.)
            CampLifecycleManager.markCampDead(campId, level);

            // Clear our camp reference
            this.setCampUUID(null);

            // Broadcast destruction message
            broadcastCampDestruction(level, campCenter);
        }
    }

    private void broadcastCampDestruction(
        ServerLevel level,
        BlockPos campCenter
    ) {
        Component message = Component.translatable(
            "entity.tiedup.trader.camp_destroyed"
        ).withStyle(ChatFormatting.GOLD, ChatFormatting.BOLD);

        // Send to all players within 200 blocks
        for (ServerPlayer player : level.players()) {
            double distSq = player.distanceToSqr(
                campCenter.getX(),
                campCenter.getY(),
                campCenter.getZ()
            );

            if (distSq < 200 * 200) {
                player.sendSystemMessage(message);
            }
        }
    }

    // STRUGGLE DETECTION

    /** Target prisoner to approach after catching them struggling */
    @Nullable
    private ServerPlayer strugglePunishmentTarget;

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

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

    /**
     * Called when a prisoner is detected struggling nearby.
     * Uses HYBRID detection: HEARING (close range) + VISION (far range).
     *
     * Detection modes:
     * - Within 6 blocks: Can HEAR through bars/fences (no line of sight needed)
     * - Within 15 blocks: Can SEE if line of sight is clear
     *
     * If detected:
     * 1. Shock the prisoner (punishment - harder than maid!)
     * 2. Approach to tighten their binds (reset resistance)
     *
     * @param prisoner The player who is struggling
     */
    public void onStruggleDetected(ServerPlayer prisoner) {
        // HYBRID DETECTION: Hearing (close) + Vision (far)
        double distance = this.distanceTo(prisoner);

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

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

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

        // Check if this player is a prisoner of our camp
        if (campUUID == null) {
            return;
        }

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

        PrisonerManager manager = PrisonerManager.get(serverLevel);
        PrisonerRecord record = manager.getPrisoner(prisoner.getUUID());
        if (record == null || !campUUID.equals(record.getCampId())) {
            return; // Not our prisoner
        }

        IRestrainable state = KidnappedHelper.getKidnappedState(prisoner);
        if (state == null || !state.isTiedUp()) {
            return;
        }

        String detectionMethod = canHear ? "heard" : "saw";
        TiedUpMod.LOGGER.info(
            "[EntitySlaveTrader] {} {} {} struggling! Punishing... (distance: {})",
            this.getNpcName(),
            detectionMethod,
            prisoner.getName().getString(),
            distance
        );

        // PUNISHMENT: Shock the prisoner
        state.shockKidnapped(" Don't even think about it.", 3.0f); // Trader shocks harder

        // TIGHTEN BINDS: Reset resistance to maximum
        tightenBinds(state, prisoner);

        // Look at the prisoner menacingly
        this.getLookControl().setLookAt(prisoner, 30.0f, 30.0f);

        // Set as target to approach
        this.strugglePunishmentTarget = prisoner;
    }

    /**
     * Tighten the prisoner's binds by resetting their resistance to maximum.
     * This happens when a guard catches someone struggling.
     *
     * @param state The prisoner's kidnapped state
     * @param prisoner The prisoner entity
     */
    private void tightenBinds(IRestrainable state, LivingEntity prisoner) {
        com.tiedup.remake.util.RestraintApplicator.tightenBind(state, prisoner);
    }

    /**
     * Get the current struggle punishment target.
     * @return The prisoner to approach, or null if none
     */
    @Nullable
    public ServerPlayer getStrugglePunishmentTarget() {
        return this.strugglePunishmentTarget;
    }

    /**
     * Clear the struggle punishment target (after approaching or timeout).
     */
    public void clearStrugglePunishmentTarget() {
        this.strugglePunishmentTarget = null;
    }

    // DIALOGUE SPEAKER (Trader-specific)

    @Override
    public SpeakerType getSpeakerType() {
        return SpeakerType.TRADER;
    }

    @Override
    public PersonalityType getSpeakerPersonality() {
        // Traders are greedy business-oriented
        return PersonalityType.PROUD;
    }

    @Override
    public int getSpeakerMood() {
        // Mood based on business (captives to sell)
        // Count captives in camp cells
        int captiveCount = 0;
        if (
            campUUID != null && this.level() instanceof ServerLevel serverLevel
        ) {
            CampOwnership ownership = CampOwnership.get(serverLevel);
            CampOwnership.CampData campData = ownership.getCamp(campUUID);
            if (campData != null && campData.getCenter() != null) {
                CellRegistryV2 cellRegistry = CellRegistryV2.get(serverLevel);
                List<CellDataV2> cells = cellRegistry.findCellsNear(
                    campData.getCenter(),
                    50.0
                );
                for (CellDataV2 cell : cells) {
                    if (cell.isOccupied()) {
                        captiveCount += cell.getPrisonerIds().size();
                    }
                }
            }
        }
        if (captiveCount > 2) {
            return 85; // Excellent business!
        } else if (captiveCount > 0) {
            return 70; // Good business
        }
        return 50; // Waiting for captives
    }

    @Override
    public String getTargetRelation(Player player) {
        // Check if player has trader token (friendly)
        // For now, just return customer if interacting
        return "customer";
    }
}
