package com.tiedup.remake.events.restriction;

import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.items.ItemLockpick;
import com.tiedup.remake.items.base.IKnife;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.GameConstants;
import com.tiedup.remake.util.KidnappedHelper;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.ButtonBlock;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.FenceGateBlock;
import net.minecraft.world.level.block.LeverBlock;
import net.minecraft.world.level.block.TrapDoorBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.living.LivingEntityUseItemEvent;
import net.minecraftforge.event.entity.living.LivingEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.event.entity.player.AttackEntityEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.level.BlockEvent;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;

/**
 * Unified event handler for all bondage item restrictions.
 *
 * Leg Binding: Separate arm/leg restrictions
 *
 * This handler manages restrictions based on equipped bondage items:
 *
 * === LEGS BOUND (hasLegsBound) ===
 * - No sprinting
 * - No climbing ladders (can descend)
 * - No elytra flying
 * - Reduced swim speed (50%)
 *
 * === ARMS BOUND (hasArmsBound) ===
 * - No block breaking
 * - No block placing
 * - No attacking
 * - No item usage
 * - No block interaction (except allowed blocks)
 *
 * === MITTENS ===
 * - Additional hand restrictions
 * - Allowed: buttons, levers, doors, trapdoors, fence gates (when arms bound only)
 *
 * === BLINDFOLDED ===
 * - Vision effects (handled client-side)
 *
 * === GAGGED ===
 * - Chat muffling (handled in ChatEventHandler)
 *
 * @see RestraintTaskTickHandler for task tick progression (untying, tying, force feeding)
 */
@Mod.EventBusSubscriber(
    modid = TiedUpMod.MOD_ID,
    bus = Mod.EventBusSubscriber.Bus.FORGE
)
public class BondageItemRestrictionHandler {

    /** Cooldown between restriction messages (in milliseconds) */
    private static final long MESSAGE_COOLDOWN_MS = 2000; // 2 seconds

    /** Swim speed multiplier when tied (from config) */

    /** Per-player, per-category message cooldowns */
    private static final Map<
        UUID,
        Map<MessageCategory, Long>
    > messageCooldowns = new HashMap<>();

    // MOVEMENT RESTRICTIONS

    /**
     * Movement restrictions per tick (throttled to every 5 ticks).
     * - Prevent sprinting when tied
     * - Prevent ladder climbing when tied (can descend)
     * - Reduce swim speed when tied
     */
    @SubscribeEvent
    public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
        if (event.side.isClient() || event.phase != TickEvent.Phase.END) {
            return;
        }

        Player player = event.player;

        // Throttle: only check every 5 ticks (0.25 seconds) - per-player timing
        if (player.tickCount % 5 != 0) {
            return;
        }
        IRestrainable state = KidnappedHelper.getKidnappedState(player);
        if (state == null) return;

        // Movement restrictions only apply when LEGS are bound
        if (!state.hasLegsBound()) return;

        // === SPRINT RESTRICTION ===
        if (player.isSprinting()) {
            player.setSprinting(false);
        }

        // === LADDER RESTRICTION ===
        // Can descend but not climb
        if (player.onClimbable()) {
            Vec3 motion = player.getDeltaMovement();
            if (motion.y > 0) {
                player.setDeltaMovement(motion.x, 0, motion.z);
            }
        }

        // === SWIM SPEED RESTRICTION ===
        if (player.isInWater() && player.isSwimming()) {
            Vec3 motion = player.getDeltaMovement();
            player.setDeltaMovement(
                motion.scale(
                    SettingsAccessor.getTiedSwimSpeedMultiplier()
                )
            );
        }
    }

    /**
     * Prevent elytra flying when tied.
     */
    @SubscribeEvent
    public static void onLivingTick(LivingEvent.LivingTickEvent event) {
        if (!(event.getEntity() instanceof Player player)) return;
        if (player.level().isClientSide) return;

        // Cheap check FIRST: only proceed if player is flying
        if (!player.isFallFlying()) return;

        IRestrainable state = KidnappedHelper.getKidnappedState(player);
        if (state == null || !state.hasLegsBound()) return;

        // Elytra restriction only applies when LEGS are bound
        player.stopFallFlying();
        sendRestrictionMessage(player, MessageCategory.NO_ELYTRA);
    }

    // INTERACTION RESTRICTIONS

    /**
     * Block breaking restrictions.
     * - Tied: Cannot break blocks
     * - Mittens: Cannot break blocks (hands covered)
     */
    @SubscribeEvent(priority = EventPriority.HIGH)
    public static void onBlockBreak(BlockEvent.BreakEvent event) {
        Player player = event.getPlayer();
        if (player == null) return;

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

        // Block breaking requires ARMS to be free
        if (state.hasArmsBound()) {
            event.setCanceled(true);
            sendRestrictionMessage(player, MessageCategory.CANT_BREAK_TIED);
        } else if (state.hasMittens()) {
            event.setCanceled(true);
            sendRestrictionMessage(player, MessageCategory.CANT_BREAK_MITTENS);
        }
    }

    /**
     * Block placing restrictions.
     * - Arms bound: Cannot place blocks
     * - Mittens: Cannot place blocks (hands covered)
     */
    @SubscribeEvent(priority = EventPriority.HIGH)
    public static void onBlockPlace(BlockEvent.EntityPlaceEvent event) {
        if (!(event.getEntity() instanceof Player player)) return;

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

        // Block placing requires ARMS to be free
        if (state.hasArmsBound()) {
            event.setCanceled(true);
            sendRestrictionMessage(player, MessageCategory.CANT_PLACE_TIED);
        } else if (state.hasMittens()) {
            event.setCanceled(true);
            sendRestrictionMessage(player, MessageCategory.CANT_PLACE_MITTENS);
        }
    }

    /**
     * Block interaction restrictions.
     * - Arms bound: Cannot interact with most blocks
     * - Mittens: Cannot interact (hands covered)
     * - Exception: Buttons, levers, doors (can be pressed/opened with body when arms bound, but not with mittens)
     */
    @SubscribeEvent(priority = EventPriority.HIGH)
    public static void onRightClickBlock(
        PlayerInteractEvent.RightClickBlock event
    ) {
        Player player = event.getEntity();

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

        boolean hasArmsBound = state.hasArmsBound();
        boolean hasMittens = state.hasMittens();

        if (!hasArmsBound && !hasMittens) return;

        // Get the block being interacted with
        BlockState blockState = event.getLevel().getBlockState(event.getPos());

        // Allow specific block interactions (can press with body/head) - only when arms bound WITHOUT mittens
        // Mittens block ALL interactions since you can't use buttons with covered hands
        if (
            hasArmsBound && !hasMittens && isAllowedTiedInteraction(blockState)
        ) {
            return; // Allow this interaction
        }

        // Block all other interactions
        event.setCanceled(true);
        if (hasArmsBound) {
            sendRestrictionMessage(player, MessageCategory.CANT_INTERACT_TIED);
        } else {
            sendRestrictionMessage(
                player,
                MessageCategory.CANT_INTERACT_MITTENS
            );
        }
    }

    /**
     * Item usage restrictions.
     * - Arms bound: Cannot use items (except knife/lockpick without mittens)
     * - Mittens: Cannot use items (hands covered)
     *
     * v2.5: Allow knife and lockpick usage when tied (but NOT with mittens).
     * This enables the player to cut/lockpick their binds while restrained.
     */
    @SubscribeEvent(priority = EventPriority.HIGH)
    public static void onRightClickItem(
        PlayerInteractEvent.RightClickItem event
    ) {
        Player player = event.getEntity();
        ItemStack heldItem = event.getItemStack();

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

        // v2.5: Allow knife and lockpick usage when tied (but NOT with mittens)
        if (state.hasArmsBound() && !state.hasMittens()) {
            // Allow knives - they have special cutting logic
            if (heldItem.getItem() instanceof IKnife) {
                return; // Don't block
            }
            // Allow lockpicks - they open the struggle choice screen
            if (heldItem.getItem() instanceof ItemLockpick) {
                return; // Don't block
            }
        }

        // Item usage requires ARMS to be free (except above exceptions)
        if (state.hasArmsBound()) {
            event.setCanceled(true);
            sendRestrictionMessage(player, MessageCategory.CANT_USE_ITEM_TIED);
        } else if (state.hasMittens()) {
            event.setCanceled(true);
            sendRestrictionMessage(
                player,
                MessageCategory.CANT_USE_ITEM_MITTENS
            );
        }
    }

    /**
     * Attack restrictions.
     * - Captive/slave attacking their kidnapper: Cannot attack, gets shocked
     * - Arms bound: Cannot attack at all
     * - Mittens only: Can punch but damage is zeroed in onLivingHurt()
     */
    @SubscribeEvent(priority = EventPriority.HIGH)
    public static void onAttack(AttackEntityEvent event) {
        Player player = event.getEntity();
        Entity target = event.getTarget();

        // Check if player is attacking a kidnapper
        if (target instanceof EntityKidnapper kidnapper) {
            // Check if the attacker is this kidnapper's captive or job worker
            boolean isThisCaptive = false;
            boolean isThisJobWorker = false;

            // Check if player is the current captive
            IRestrainable captive = kidnapper.getCaptive();
            if (captive != null) {
                UUID captiveUUID = captive.getKidnappedUniqueId();
                if (
                    captiveUUID != null && captiveUUID.equals(player.getUUID())
                ) {
                    isThisCaptive = true;
                }
            }

            // Check if player is the job worker
            UUID workerUUID = kidnapper.getJobWorkerUUID();
            if (workerUUID != null && workerUUID.equals(player.getUUID())) {
                isThisJobWorker = true;
            }

            // Only block if player is THIS kidnapper's captive or job worker
            if (isThisCaptive || isThisJobWorker) {
                event.setCanceled(true);

                // Kidnapper responds with dialogue
                kidnapper.talkTo(player, DialogueCategory.ATTACK_SLAVE);

                // Shock the captive/worker
                IRestrainable playerState = KidnappedHelper.getKidnappedState(
                    player
                );
                if (playerState != null) {
                    playerState.shockKidnapped(
                        " (You cannot attack your master!)",
                        2.0f
                    );
                }

                TiedUpMod.LOGGER.debug(
                    "[RESTRICTION] {} tried to attack master {} - shocked (captive={}, worker={})",
                    player.getName().getString(),
                    kidnapper.getName().getString(),
                    isThisCaptive,
                    isThisJobWorker
                );
                return;
            }
            // Other players CAN attack this kidnapper - don't block
        }

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

        // Arms bound: cannot attack at all
        if (state.hasArmsBound()) {
            event.setCanceled(true);
            return;
        }

        // Mittens only (not arms bound): allow punch animation
        // Damage will be zeroed in onLivingHurt()
    }

    /**
     * Damage reduction for mittens.
     * When a player with mittens (but not arms bound) attacks, damage is reduced to 0.
     * The punch animation still plays, but no damage is dealt.
     */
    @SubscribeEvent(priority = EventPriority.HIGHEST)
    public static void onLivingHurt(LivingHurtEvent event) {
        // Check if the attacker is a player with mittens
        if (!(event.getSource().getEntity() instanceof Player player)) return;

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

        // Mittens only (not arms bound): zero damage
        if (state.hasMittens() && !state.hasArmsBound()) {
            event.setAmount(0);
        }
    }

    /**
     * Fall damage protection for captive players on a leash.
     * When being led by a kidnapper/master, the player has no control over movement
     * and should not take fall damage from terrain the captor drags them through.
     */
    @SubscribeEvent(priority = EventPriority.HIGHEST)
    public static void onCaptiveFallDamage(LivingHurtEvent event) {
        if (!(event.getEntity() instanceof Player player)) return;
        if (
            !event
                .getSource()
                .is(net.minecraft.world.damagesource.DamageTypes.FALL)
        ) return;

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

        if (state.isCaptive()) {
            event.setCanceled(true);
        }
    }

    /**
     * Notify the Master when their pet takes damage.
     * This allows the Master to react (e.g., get up from human chair).
     */
    @SubscribeEvent
    public static void onPetHurt(LivingHurtEvent event) {
        if (!(event.getEntity() instanceof ServerPlayer pet)) return;
        if (event.getAmount() <= 0) return;

        // Find if this player has a nearby Master who owns them
        var masters = pet
            .level()
            .getEntitiesOfClass(
                com.tiedup.remake.entities.EntityMaster.class,
                pet.getBoundingBox().inflate(32.0),
                m ->
                    m.isAlive() &&
                    m.hasPet() &&
                    pet.getUUID().equals(m.getStateManager().getPetPlayerUUID())
            );

        for (var master : masters) {
            master.onPetHurt(event.getSource(), event.getAmount());
        }
    }

    // ========== BREAK SPEED REDUCTION ==========

    /**
     * Apply break speed reduction when tied up.
     * This makes mining extremely slow (10% speed) when restrained.
     */
    @SubscribeEvent
    public static void onCalculateSpeed(PlayerEvent.BreakSpeed event) {
        Player player = event.getEntity();
        IBondageState state = KidnappedHelper.getKidnappedState(player);

        if (state != null && state.isTiedUp()) {
            event.setNewSpeed(
                event.getNewSpeed() * GameConstants.TIED_BREAK_SPEED_MULTIPLIER
            );
        }
    }

    // ========== BLOCK SELF-EATING WHEN GAGGED ==========

    /**
     * Block gagged players from eating food themselves.
     */
    @SubscribeEvent
    public static void onGaggedPlayerEat(LivingEntityUseItemEvent.Start event) {
        if (!(event.getEntity() instanceof Player player)) {
            return;
        }

        if (!event.getItem().getItem().isEdible()) {
            return;
        }

        IBondageState state = KidnappedHelper.getKidnappedState(player);
        if (state != null && state.isGagged()) {
            event.setCanceled(true);
            player.displayClientMessage(
                Component.translatable("msg.tiedup.event.cant_eat_gagged").withStyle(
                    ChatFormatting.RED
                ),
                true
            );
        }
    }

    // HELPER METHODS

    /**
     * Check if a block interaction should be allowed when tied.
     * These blocks can be activated with body/head, not hands.
     */
    private static boolean isAllowedTiedInteraction(BlockState blockState) {
        // Buttons - can press with head/body
        if (blockState.getBlock() instanceof ButtonBlock) return true;

        // Levers - can push with body
        if (blockState.getBlock() instanceof LeverBlock) return true;

        // Doors - can push open with body
        if (blockState.getBlock() instanceof DoorBlock) return true;

        // Trapdoors - debatable, but allow for now
        if (blockState.getBlock() instanceof TrapDoorBlock) return true;

        // Fence gates - can push open
        if (blockState.getBlock() instanceof FenceGateBlock) return true;

        return false;
    }

    /**
     * Send a restriction message to the player.
     * Uses per-category cooldown to prevent spam.
     */
    private static void sendRestrictionMessage(
        Player player,
        MessageCategory category
    ) {
        // Only send on server side
        if (player.level().isClientSide) return;

        UUID playerId = player.getUUID();
        long now = System.currentTimeMillis();

        // Get or create player's cooldown map
        Map<MessageCategory, Long> playerCooldowns =
            messageCooldowns.computeIfAbsent(playerId, k -> new HashMap<>());

        // Check cooldown for this category
        Long lastSent = playerCooldowns.get(category);
        if (lastSent != null && (now - lastSent) < MESSAGE_COOLDOWN_MS) {
            return; // Still on cooldown
        }

        // Update cooldown and send message
        playerCooldowns.put(category, now);
        SystemMessageManager.sendRestriction(player, category);
    }

    /**
     * Clean up cooldowns for a player (call when they disconnect).
     */
    public static void clearCooldowns(UUID playerId) {
        messageCooldowns.remove(playerId);
    }
}
