package com.tiedup.remake.items.base;

import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.action.PacketTying;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.tasks.TyingPlayerTask;
import com.tiedup.remake.tasks.TyingTask;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.RestraintEffectUtils;
import com.tiedup.remake.util.TiedUpSounds;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.List;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;

/**
 * Base class for binding/restraint items (ropes, chains, straitjacket, etc.)
 * These items restrain a player's movement and actions when equipped.
 *
 * <p>Implements {@link IHasResistance} for the struggle/escape system.
 * <p>Implements {@link ILockable} for the padlock system.
 *
 * Based on original ItemBind from 1.12.2
 *
 */
public abstract class ItemBind
    extends Item
    implements IBondageItem, IHasResistance, ILockable
{

    // ========== Leg Binding: Bind Mode NBT Key ==========
    private static final String NBT_BIND_MODE = "bindMode";

    public ItemBind(Properties properties) {
        super(properties);
    }

    @Override
    public BodyRegionV2 getBodyRegion() {
        return BodyRegionV2.ARMS;
    }

    // ========== Leg Binding: Bind Mode Methods ==========

    // String constants matching NBT values
    public static final String BIND_MODE_FULL = "full";
    private static final String MODE_FULL = BIND_MODE_FULL;
    private static final String MODE_ARMS = "arms";
    private static final String MODE_LEGS = "legs";
    private static final String[] MODE_CYCLE = {
        MODE_FULL,
        MODE_ARMS,
        MODE_LEGS,
    };
    private static final java.util.Map<String, String> MODE_TRANSLATION_KEYS =
        java.util.Map.of(
            MODE_FULL,
            "tiedup.bindmode.full",
            MODE_ARMS,
            "tiedup.bindmode.arms",
            MODE_LEGS,
            "tiedup.bindmode.legs"
        );

    /**
     * Get the bind mode ID string from the stack's NBT.
     * @param stack The bind ItemStack
     * @return "full", "arms", or "legs" (defaults to "full" if absent)
     */
    public static String getBindModeId(ItemStack stack) {
        if (stack.isEmpty()) return MODE_FULL;
        CompoundTag tag = stack.getTag();
        if (tag == null || !tag.contains(NBT_BIND_MODE)) return MODE_FULL;
        String value = tag.getString(NBT_BIND_MODE);
        if (
            MODE_FULL.equals(value) ||
            MODE_ARMS.equals(value) ||
            MODE_LEGS.equals(value)
        ) {
            return value;
        }
        return MODE_FULL;
    }

    /**
     * Check if arms are bound (mode is "arms" or "full").
     * @param stack The bind ItemStack
     * @return true if arms are restrained
     */
    public static boolean hasArmsBound(ItemStack stack) {
        String mode = getBindModeId(stack);
        return MODE_ARMS.equals(mode) || MODE_FULL.equals(mode);
    }

    /**
     * Check if legs are bound (mode is "legs" or "full").
     * @param stack The bind ItemStack
     * @return true if legs are restrained
     */
    public static boolean hasLegsBound(ItemStack stack) {
        String mode = getBindModeId(stack);
        return MODE_LEGS.equals(mode) || MODE_FULL.equals(mode);
    }

    /**
     * Cycle bind mode: full -> arms -> legs -> full.
     * @param stack The bind ItemStack
     * @return the new mode ID string
     */
    public static String cycleBindModeId(ItemStack stack) {
        String current = getBindModeId(stack);
        String next = MODE_FULL;
        for (int i = 0; i < MODE_CYCLE.length; i++) {
            if (MODE_CYCLE[i].equals(current)) {
                next = MODE_CYCLE[(i + 1) % MODE_CYCLE.length];
                break;
            }
        }
        stack.getOrCreateTag().putString(NBT_BIND_MODE, next);
        return next;
    }

    /**
     * Get the translation key for the current bind mode.
     * @param stack The bind ItemStack
     * @return the i18n key for the mode
     */
    public static String getBindModeTranslationKey(ItemStack stack) {
        return MODE_TRANSLATION_KEYS.getOrDefault(
            getBindModeId(stack),
            "tiedup.bindmode.full"
        );
    }

    /**
     * Called when player right-clicks in air with bind item.
     * Sneak+click cycles the bind mode.
     */
    @Override
    public InteractionResultHolder<ItemStack> use(
        Level level,
        Player player,
        InteractionHand hand
    ) {
        ItemStack stack = player.getItemInHand(hand);

        // Sneak+click in air cycles bind mode
        if (player.isShiftKeyDown()) {
            if (!level.isClientSide) {
                String newModeId = cycleBindModeId(stack);

                // Play feedback sound
                player.playSound(SoundEvents.CHAIN_STEP, 0.5f, 1.2f);

                // Show action bar message
                player.displayClientMessage(
                    Component.translatable(
                        "tiedup.message.bindmode_changed",
                        Component.translatable(getBindModeTranslationKey(stack))
                    ),
                    true
                );

                TiedUpMod.LOGGER.debug(
                    "[ItemBind] {} cycled bind mode to {}",
                    player.getName().getString(),
                    newModeId
                );
            }
            return InteractionResultHolder.sidedSuccess(
                stack,
                level.isClientSide
            );
        }

        return super.use(level, player, hand);
    }

    @Override
    public void appendHoverText(
        ItemStack stack,
        @Nullable Level level,
        List<Component> tooltip,
        TooltipFlag flag
    ) {
        super.appendHoverText(stack, level, tooltip, flag);

        // Show bind mode
        tooltip.add(
            Component.translatable(
                "item.tiedup.tooltip.bindmode",
                Component.translatable(getBindModeTranslationKey(stack))
            ).withStyle(ChatFormatting.GRAY)
        );

        // Show lock status
        if (isLockable(stack)) {
            if (isLocked(stack)) {
                tooltip.add(
                    Component.translatable(
                        "item.tiedup.tooltip.locked"
                    ).withStyle(ChatFormatting.RED)
                );
            } else {
                tooltip.add(
                    Component.translatable(
                        "item.tiedup.tooltip.lockable"
                    ).withStyle(ChatFormatting.GOLD)
                );
            }
        }
    }

    /**
     * Called when the bind is equipped on an entity.
     * Applies movement speed reduction only if legs are bound.
     *
     * Leg Binding: Speed reduction conditional on mode
     * Based on original ItemBind.onEquipped() (1.12.2)
     */
    @Override
    public void onEquipped(ItemStack stack, LivingEntity entity) {
        String modeId = getBindModeId(stack);

        // Only apply speed reduction if legs are bound
        if (hasLegsBound(stack)) {
            // H6 fix: For players, speed is handled exclusively by MovementStyleManager
            // (V2 tick-based system) via MovementStyleResolver V1 fallback.
            // Applying V1 RestraintEffectUtils here would cause double stacking (different
            // UUIDs, ADDITION vs MULTIPLY_BASE) leading to quasi-immobility.
            if (entity instanceof Player) {
                TiedUpMod.LOGGER.debug(
                    "[ItemBind] Applied bind (mode={}, pose={}) to player {} - speed delegated to MovementStyleManager",
                    modeId,
                    getPoseType().getAnimationId(),
                    entity.getName().getString()
                );
            } else {
                // NPCs: MovementStyleManager only handles ServerPlayer, so NPCs
                // still need the legacy RestraintEffectUtils speed modifier.
                PoseType poseType = getPoseType();
                boolean fullImmobilization =
                    poseType == PoseType.WRAP ||
                    poseType == PoseType.LATEX_SACK;

                RestraintEffectUtils.applyBindSpeedReduction(
                    entity,
                    fullImmobilization
                );
                TiedUpMod.LOGGER.debug(
                    "[ItemBind] Applied bind (mode={}, pose={}) to NPC {} - speed reduced (full={})",
                    modeId,
                    poseType.getAnimationId(),
                    entity.getName().getString(),
                    fullImmobilization
                );
            }
        } else {
            TiedUpMod.LOGGER.debug(
                "[ItemBind] Applied bind (mode={}) to {} - no speed reduction",
                modeId,
                entity.getName().getString()
            );
        }
    }

    /**
     * Called when the bind is unequipped from an entity.
     * Restores normal movement speed for all entities.
     *
     * Based on original ItemBind.onUnequipped() (1.12.2)
     */
    @Override
    public void onUnequipped(ItemStack stack, LivingEntity entity) {
        // H6 fix: For players, speed cleanup is handled by MovementStyleManager
        // (V2 tick-based system). On the next tick, the resolver will see the item
        // is gone, deactivate the style, and remove the modifier automatically.
        // NPCs still need the legacy RestraintEffectUtils cleanup.
        if (!(entity instanceof Player)) {
            RestraintEffectUtils.removeBindSpeedReduction(entity);
        }

        IHasResistance.super.resetCurrentResistance(stack);

        TiedUpMod.LOGGER.debug(
            "[ItemBind] Removed bind from {} - speed {} resistance reset",
            entity.getName().getString(),
            entity instanceof Player
                ? "delegated to MovementStyleManager,"
                : "restored,"
        );
    }

    // ========== Tying Interaction ==========

    /**
     * Called when player right-clicks another entity with this bind item.
     * Starts or continues a tying task to tie up the target entity.
     *
     * - Players: Uses tying task with progress bar
     * - NPCs: Instant bind (no tying mini-game)
     *
     * Based on original ItemBind.itemInteractionForEntity() (1.12.2)
     *
     * @param stack The item stack
     * @param player The player using the item (kidnapper)
     * @param target The entity being interacted with
     * @param hand The hand holding the item
     * @return SUCCESS if tying started/continued, PASS otherwise
     */
    @Override
    public InteractionResult interactLivingEntity(
        ItemStack stack,
        Player player,
        LivingEntity target,
        InteractionHand hand
    ) {
        // Only run on server side
        if (player.level().isClientSide) {
            return InteractionResult.SUCCESS;
        }

        IBondageState targetState = KidnappedHelper.getKidnappedState(target);
        if (targetState == null) {
            return InteractionResult.PASS; // Target cannot be restrained
        }

        // Get kidnapper state (player using the item)
        IBondageState kidnapperState = KidnappedHelper.getKidnappedState(
            player
        );
        if (kidnapperState == null) {
            return InteractionResult.FAIL;
        }

        // Already tied - try to swap binds (if not locked)
        // Check stack.isEmpty() first to prevent accidental unbinding when
        // the original stack was consumed (e.g., rapid clicks after tying completes)
        if (targetState.isTiedUp()) {
            if (stack.isEmpty()) {
                // No bind in hand - can't swap, just pass
                return InteractionResult.PASS;
            }
            ItemStack oldBind = targetState.replaceEquipment(
                BodyRegionV2.ARMS,
                stack.copy(),
                false
            );
            if (!oldBind.isEmpty()) {
                stack.shrink(1);
                targetState.kidnappedDropItem(oldBind);
                TiedUpMod.LOGGER.debug(
                    "[ItemBind] Swapped bind on {} - dropped old bind",
                    target.getName().getString()
                );
                return InteractionResult.SUCCESS;
            }
            // Locked or failed - can't swap
            return InteractionResult.PASS;
        }

        if (kidnapperState.isTiedUp()) {
            TiedUpMod.LOGGER.debug(
                "[ItemBind] {} tried to tie but is tied themselves",
                player.getName().getString()
            );
            return InteractionResult.PASS;
        }

        // SECURITY: Distance and line-of-sight validation (skip for self-tying)
        boolean isSelfTying = player.equals(target);
        if (!isSelfTying) {
            double maxTieDistance = 4.0; // Max distance to tie (blocks)
            double distance = player.distanceTo(target);
            if (distance > maxTieDistance) {
                TiedUpMod.LOGGER.warn(
                    "[ItemBind] {} tried to tie {} from too far away ({} blocks)",
                    player.getName().getString(),
                    target.getName().getString(),
                    String.format("%.1f", distance)
                );
                return InteractionResult.PASS;
            }

            // Check line-of-sight (must be able to see target)
            if (!player.hasLineOfSight(target)) {
                TiedUpMod.LOGGER.warn(
                    "[ItemBind] {} tried to tie {} without line of sight",
                    player.getName().getString(),
                    target.getName().getString()
                );
                return InteractionResult.PASS;
            }
        }

        return handleTying(stack, player, target, targetState);
    }

    /**
     * Handle tying any target entity (Player or NPC).
     *
     * Uses progress-based system:
     * - update() marks the tick as active
     * - tick() in RestraintTaskTickHandler.onPlayerTick() handles progress increment/decrement
     */
    private InteractionResult handleTying(
        ItemStack stack,
        Player player,
        LivingEntity target,
        IBondageState targetState
    ) {
        // Get kidnapper's state to track the tying task
        PlayerBindState kidnapperState = PlayerBindState.getInstance(player);
        if (kidnapperState == null) {
            return InteractionResult.FAIL;
        }

        // Get tying duration from GameRule (default: 5 seconds)
        int tyingSeconds = getTyingDuration(player);

        // Get current tying task (if any)
        TyingTask currentTask = kidnapperState.getCurrentTyingTask();

        // Check if we should start a new task or continue existing one
        if (
            currentTask == null ||
            !currentTask.isSameTarget(target) ||
            currentTask.isStopped() ||
            !ItemStack.matches(currentTask.getBind(), stack)
        ) {
            // Create new tying task (works for both Players and NPCs)
            TyingPlayerTask newTask = new TyingPlayerTask(
                stack.copy(),
                targetState,
                target,
                tyingSeconds,
                player.level(),
                player // Pass kidnapper for SystemMessage
            );

            // FIX: Store the inventory slot for consumption when task completes
            // This prevents duplication AND allows refund if task is cancelled
            int sourceSlot = player.getInventory().selected;
            newTask.setSourceSlot(sourceSlot);
            newTask.setSourcePlayer(player);

            // Start new task
            kidnapperState.setCurrentTyingTask(newTask);
            newTask.setUpTargetState(); // Initialize target's restraint state (only for players)
            newTask.start();
            currentTask = newTask;

            TiedUpMod.LOGGER.debug(
                "[ItemBind] {} started tying {} ({} seconds, slot={})",
                player.getName().getString(),
                target.getName().getString(),
                tyingSeconds,
                sourceSlot
            );
        } else {
            // Continue existing task - ensure kidnapper is set
            if (currentTask instanceof TyingPlayerTask playerTask) {
                playerTask.setKidnapper(player);
            }
        }

        // Mark this tick as active (progress will increase in onPlayerTick)
        // The tick() method in RestraintTaskTickHandler.onPlayerTick handles progress increment/decrement
        currentTask.update();

        return InteractionResult.SUCCESS;
    }

    /**
     * Called when player right-clicks with the bind item (not targeting an entity).
     * Cancels any ongoing tying task.
     *
     * Based on original ItemBind.onItemRightClick() (1.12.2)
     *
     * @param context The use context
     * @return FAIL to cancel the action
     */
    @Override
    public InteractionResult useOn(UseOnContext context) {
        // Only run on server side
        if (context.getLevel().isClientSide) {
            return InteractionResult.SUCCESS;
        }

        Player player = context.getPlayer();
        if (player == null) {
            return InteractionResult.FAIL;
        }

        // Cancel any ongoing tying task
        PlayerBindState state = PlayerBindState.getInstance(player);
        if (state == null) {
            return InteractionResult.FAIL;
        }

        // Check for active tying task (unified for both players and NPCs)
        TyingTask task = state.getCurrentTyingTask();
        if (task != null) {
            task.stop();
            state.setCurrentTyingTask(null);

            LivingEntity target = task.getTargetEntity();
            String targetName =
                target != null ? target.getName().getString() : "???";
            String kidnapperName = player.getName().getString();

            // Send cancellation packet to kidnapper
            if (player instanceof ServerPlayer serverPlayer) {
                PacketTying packet = new PacketTying(
                    -1,
                    task.getMaxSeconds(),
                    true,
                    targetName
                );
                ModNetwork.sendToPlayer(packet, serverPlayer);
            }

            // Send cancellation packet to target (if it's a player)
            if (target instanceof ServerPlayer serverTarget) {
                PacketTying packet = new PacketTying(
                    -1,
                    task.getMaxSeconds(),
                    false,
                    kidnapperName
                );
                ModNetwork.sendToPlayer(packet, serverTarget);
            }

            TiedUpMod.LOGGER.debug(
                "[ItemBind] {} cancelled tying task",
                player.getName().getString()
            );
        }

        return InteractionResult.FAIL;
    }

    /**
     * Get the tying duration in seconds from GameRule.
     *
     * @param player The player (for accessing world/GameRules)
     * @return Duration in seconds (default: 5)
     */
    private int getTyingDuration(Player player) {
        return SettingsAccessor.getTyingPlayerTime(
            player.level().getGameRules()
        );
    }

    // ========== Resistance System (via IHasResistance) ==========

    /**
     * Get the item name for GameRule lookup.
     * Each subclass must implement this to return its identifier (e.g., "rope", "chain", etc.)
     *
     * @return Item name for resistance GameRule lookup
     */
    public abstract String getItemName();

    // ========== Pose System ==========

    /**
     * Get the pose type for this bind item.
     * Determines which animation/pose is applied when this item is equipped.
     *
     * Override in subclasses for special poses (straitjacket, wrap, latex_sack).
     *
     * @return PoseType for this bind (default: STANDARD)
     */
    public PoseType getPoseType() {
        return PoseType.STANDARD;
    }

    /**
     * Implementation of IHasResistance.getResistanceId().
     * Delegates to getItemName() for backward compatibility with subclasses.
     *
     * @return Item identifier for resistance lookup
     */
    @Override
    public String getResistanceId() {
        return getItemName();
    }

    /**
     * Called when the entity struggles against this bind.
     * Plays struggle sound and shows message.
     *
     * Based on original ItemBind struggle notification (1.12.2)
     *
     * @param entity The entity struggling
     */
    @Override
    public void notifyStruggle(LivingEntity entity) {
        // Play struggle sound
        TiedUpSounds.playStruggleSound(entity);

        // Log the struggle attempt
        TiedUpMod.LOGGER.debug(
            "[ItemBind] {} is struggling against bind",
            entity.getName().getString()
        );

        // Notify nearby players if the entity is a player
        if (entity instanceof ServerPlayer serverPlayer) {
            serverPlayer.displayClientMessage(
                Component.translatable("tiedup.message.struggling"),
                true // Action bar
            );
        }
    }

    // ILockable implementation inherited from interface default methods
}
