package com.tiedup.remake.network.selfbondage;

import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.*;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.network.action.PacketTying;
import com.tiedup.remake.network.sync.SyncManager;
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.tasks.V2TyingPlayerTask;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.network.NetworkEvent;

/**
 * Packet sent from client to server when player attempts self-bondage.
 * Triggered by left-click with bondage item in hand.
 *
 * Self-bondage rules:
 * - Binds: Requires tying task (same duration as normal tying)
 * - Accessories (gag, blindfold, mittens, earplugs): Instant, can be equipped anytime
 * - Collar: NOT allowed (cannot self-collar)
 */
public class PacketSelfBondage {

    private final InteractionHand hand;

    public PacketSelfBondage(InteractionHand hand) {
        this.hand = hand;
    }

    public static void encode(PacketSelfBondage msg, FriendlyByteBuf buf) {
        buf.writeEnum(msg.hand);
    }

    public static PacketSelfBondage decode(FriendlyByteBuf buf) {
        return new PacketSelfBondage(buf.readEnum(InteractionHand.class));
    }

    public static void handle(
        PacketSelfBondage msg,
        Supplier<NetworkEvent.Context> ctx
    ) {
        ctx
            .get()
            .enqueueWork(() -> {
                ServerPlayer player = ctx.get().getSender();
                if (player == null) return;

                // Rate limiting (dedicated bucket: 15 tokens, 6/sec refill for continuous 5 pkt/sec)
                if (!PacketRateLimiter.allowPacket(player, "selfbondage")) {
                    return;
                }

                ItemStack stack = player.getItemInHand(msg.hand);
                if (stack.isEmpty()) return;

                Item item = stack.getItem();

                // Get player's kidnapped state
                IBondageState state = KidnappedHelper.getKidnappedState(player);
                if (state == null) return;

                // Route to appropriate handler based on item type
                // V2 bondage items — use tying task with V2 equip
                if (item instanceof IV2BondageItem v2Item) {
                    handleV2SelfBondage(player, stack, v2Item, state);
                    return;
                }

                // V1 routes below (legacy)
                if (item instanceof ItemBind bind) {
                    handleSelfBind(player, stack, bind, state);
                } else if (item instanceof ItemGag) {
                    handleSelfAccessory(
                        player,
                        stack,
                        state,
                        "gag",
                        s -> s.isGagged(),
                        s -> s.getEquipment(BodyRegionV2.MOUTH),
                        s -> s.unequip(BodyRegionV2.MOUTH),
                        (s, i) -> s.equip(BodyRegionV2.MOUTH, i)
                    );
                } else if (item instanceof ItemBlindfold) {
                    handleSelfAccessory(
                        player,
                        stack,
                        state,
                        "blindfold",
                        s -> s.isBlindfolded(),
                        s -> s.getEquipment(BodyRegionV2.EYES),
                        s -> s.unequip(BodyRegionV2.EYES),
                        (s, i) -> s.equip(BodyRegionV2.EYES, i)
                    );
                } else if (item instanceof ItemMittens) {
                    handleSelfAccessory(
                        player,
                        stack,
                        state,
                        "mittens",
                        s -> s.hasMittens(),
                        s -> s.getEquipment(BodyRegionV2.HANDS),
                        s -> s.unequip(BodyRegionV2.HANDS),
                        (s, i) -> s.equip(BodyRegionV2.HANDS, i)
                    );
                } else if (item instanceof ItemEarplugs) {
                    handleSelfAccessory(
                        player,
                        stack,
                        state,
                        "earplugs",
                        s -> s.hasEarplugs(),
                        s -> s.getEquipment(BodyRegionV2.EARS),
                        s -> s.unequip(BodyRegionV2.EARS),
                        (s, i) -> s.equip(BodyRegionV2.EARS, i)
                    );
                }
                // ItemCollar: NOT handled - cannot self-collar
            });

        ctx.get().setPacketHandled(true);
    }

    /**
     * Handle self-binding with a bind item (rope, chain, etc.).
     * Uses tying task system - requires holding left-click.
     */
    private static void handleSelfBind(
        ServerPlayer player,
        ItemStack stack,
        ItemBind bind,
        IBondageState state
    ) {
        // Can't self-tie if already tied
        if (state.isTiedUp()) {
            TiedUpMod.LOGGER.debug(
                "[SelfBondage] {} tried to self-tie but is already tied",
                player.getName().getString()
            );
            return;
        }

        // Get player's bind state for tying task management
        PlayerBindState playerState = PlayerBindState.getInstance(player);
        if (playerState == null) return;

        // Get tying duration from GameRule
        int tyingSeconds = SettingsAccessor.getTyingPlayerTime(
            player.level().getGameRules()
        );

        // Create self-tying task (target == kidnapper)
        TyingPlayerTask newTask = new TyingPlayerTask(
            stack.copy(),
            state,
            player, // Target is self
            tyingSeconds,
            player.level(),
            player // Kidnapper is also self
        );

        // Get current tying task
        TyingTask currentTask = playerState.getCurrentTyingTask();

        // Check if we should start a new task or continue existing one
        if (
            currentTask == null ||
            !currentTask.isSameTarget(player) ||
            currentTask.isOutdated() ||
            !ItemStack.matches(currentTask.getBind(), stack)
        ) {
            // Start new self-tying task
            playerState.setCurrentTyingTask(newTask);
            newTask.start();

            TiedUpMod.LOGGER.debug(
                "[SelfBondage] {} started self-tying ({} seconds)",
                player.getName().getString(),
                tyingSeconds
            );
        } else {
            // Continue existing task
            newTask = (TyingPlayerTask) currentTask;
        }

        // Update task progress
        newTask.update();

        // Check if task completed
        if (newTask.isStopped()) {
            // Self-tying complete! Consume the item
            stack.shrink(1);
            playerState.setCurrentTyingTask(null);

            TiedUpMod.LOGGER.info(
                "[SelfBondage] {} successfully self-tied",
                player.getName().getString()
            );
        }
    }

    /**
     * Handle self-bondage with a V2 bondage item.
     * Uses V2TyingPlayerTask for progress, V2EquipmentHelper for equip.
     */
    private static void handleV2SelfBondage(
        ServerPlayer player,
        ItemStack stack,
        IV2BondageItem v2Item,
        IBondageState state
    ) {
        // Check if all target regions are already occupied or blocked
        boolean allBlocked = true;
        for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) {
            if (
                !V2EquipmentHelper.isRegionOccupied(player, region) &&
                !V2EquipmentHelper.isRegionBlocked(player, region)
            ) {
                allBlocked = false;
                break;
            }
        }
        if (allBlocked) {
            TiedUpMod.LOGGER.debug(
                "[SelfBondage] {} tried V2 self-equip but all regions occupied",
                player.getName().getString()
            );
            return;
        }

        PlayerBindState playerState = PlayerBindState.getInstance(player);
        if (playerState == null) return;

        int tyingSeconds = SettingsAccessor.getTyingPlayerTime(
            player.level().getGameRules()
        );

        // Create V2 tying task (uses V2EquipmentHelper on completion, NOT putBindOn)
        V2TyingPlayerTask newTask = new V2TyingPlayerTask(
            stack.copy(), // copy for display/matching
            stack, // live reference for consumption
            state,
            player, // target is self
            tyingSeconds,
            player.level(),
            player // kidnapper is also self
        );

        TyingTask currentTask = playerState.getCurrentTyingTask();

        if (
            currentTask == null ||
            !currentTask.isSameTarget(player) ||
            currentTask.isOutdated() ||
            !ItemStack.matches(currentTask.getBind(), stack)
        ) {
            // Start new task
            playerState.setCurrentTyingTask(newTask);
            newTask.start();

            TiedUpMod.LOGGER.debug(
                "[SelfBondage] {} started V2 self-tying ({} seconds)",
                player.getName().getString(),
                tyingSeconds
            );
        } else {
            // Continue existing task — just mark active
            currentTask.update();
        }

        // If we started a new task, mark it active too
        if (playerState.getCurrentTyingTask() == newTask) {
            newTask.update();
        }
    }

    /**
     * Handle self-equipping an accessory (gag, blindfold, mittens, earplugs).
     * Can be used anytime (no need to be tied).
     * Blocked only if arms are fully bound.
     * Supports swapping: if same type already equipped (and not locked), swap them.
     */
    private static void handleSelfAccessory(
        ServerPlayer player,
        ItemStack stack,
        IBondageState state,
        String itemType,
        java.util.function.Predicate<IBondageState> isEquipped,
        java.util.function.Function<IBondageState, ItemStack> getCurrent,
        java.util.function.Function<IBondageState, ItemStack> takeOff,
        java.util.function.BiConsumer<IBondageState, ItemStack> putOn
    ) {
        // Can't equip if arms are fully bound (need hands to put on accessories)
        ItemStack currentBind = state.getEquipment(BodyRegionV2.ARMS);
        if (!currentBind.isEmpty()) {
            if (ItemBind.hasArmsBound(currentBind)) {
                TiedUpMod.LOGGER.debug(
                    "[SelfBondage] {} can't self-{} - arms are bound",
                    player.getName().getString(),
                    itemType
                );
                return;
            }
        }

        // Already equipped? Try to swap
        if (isEquipped.test(state)) {
            ItemStack currentItem = getCurrent.apply(state);

            // Check if current item is locked
            if (
                currentItem.getItem() instanceof ILockable lockable &&
                lockable.isLocked(currentItem)
            ) {
                TiedUpMod.LOGGER.debug(
                    "[SelfBondage] {} can't swap {} - current is locked",
                    player.getName().getString(),
                    itemType
                );
                return;
            }

            // Remove current and drop it
            ItemStack removed = takeOff.apply(state);
            if (!removed.isEmpty()) {
                state.kidnappedDropItem(removed);
                TiedUpMod.LOGGER.debug(
                    "[SelfBondage] {} swapping {} - dropped old one",
                    player.getName().getString(),
                    itemType
                );
            }
        }

        // Equip new item on self
        putOn.accept(state, stack.copy());
        stack.shrink(1);

        // Sync to client
        SyncManager.syncInventory(player);

        TiedUpMod.LOGGER.info(
            "[SelfBondage] {} self-equipped {}",
            player.getName().getString(),
            itemType
        );
    }
}
