package com.tiedup.remake.v2.furniture.network;

import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemLockpick;
import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState;
import com.tiedup.remake.minigame.LockpickMiniGameState;
import com.tiedup.remake.minigame.LockpickSessionManager;
import com.tiedup.remake.minigame.StruggleSessionManager;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.PacketRateLimiter;
import com.tiedup.remake.network.minigame.PacketContinuousStruggleState;
import com.tiedup.remake.network.minigame.PacketLockpickMiniGameState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import com.tiedup.remake.v2.furniture.EntityFurniture;
import com.tiedup.remake.v2.furniture.ISeatProvider;
import com.tiedup.remake.v2.furniture.SeatDefinition;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.network.NetworkEvent;

/**
 * Client-to-server packet: seated player initiates a struggle escape, or a
 * third party standing nearby initiates a lockpick escape.
 *
 * <p>Escape methods:
 * <ul>
 *   <li><b>STRUGGLE (0)</b>: Sender must BE the seated passenger. Always
 *       allowed regardless of blocked regions.</li>
 *   <li><b>LOCKPICK (1)</b>: Sender must NOT be the seated passenger (third
 *       party standing nearby). Must have a lockpick in their inventory.</li>
 * </ul>
 *
 * <p>Difficulty is computed as: {@code total = min(seat.lockedDifficulty + itemBonus, 600)},
 * where {@code itemBonus} is the sum of {@link IV2BondageItem#getEscapeDifficulty(ItemStack)}
 * for all equipped items on NON-blocked body regions.</p>
 *
 * <p>Wire format: int furnitureEntityId (4) + byte escapeMethod (1)</p>
 *
 * <p>Direction: Client to Server (C2S)</p>
 */
public class PacketFurnitureEscape {

    /** Escape method: the seated player struggles to break free. */
    public static final byte METHOD_STRUGGLE = 0;
    /** Escape method: a third party lockpicks the seat lock. */
    public static final byte METHOD_LOCKPICK = 1;

    /** Maximum total escape difficulty (cap). */
    private static final int MAX_DIFFICULTY = 600;

    private final int furnitureEntityId;
    private final byte escapeMethod;

    public PacketFurnitureEscape(int furnitureEntityId, byte escapeMethod) {
        this.furnitureEntityId = furnitureEntityId;
        this.escapeMethod = escapeMethod;
    }

    // ==================== Codec ====================

    public static void encode(PacketFurnitureEscape msg, FriendlyByteBuf buf) {
        buf.writeInt(msg.furnitureEntityId);
        buf.writeByte(msg.escapeMethod);
    }

    public static PacketFurnitureEscape decode(FriendlyByteBuf buf) {
        return new PacketFurnitureEscape(buf.readInt(), buf.readByte());
    }

    // ==================== Handler ====================

    public static void handle(
        PacketFurnitureEscape msg,
        Supplier<NetworkEvent.Context> ctxSupplier
    ) {
        NetworkEvent.Context ctx = ctxSupplier.get();
        ctx.enqueueWork(() -> handleOnServer(msg, ctx));
        ctx.setPacketHandled(true);
    }

    private static void handleOnServer(
        PacketFurnitureEscape msg,
        NetworkEvent.Context ctx
    ) {
        ServerPlayer sender = ctx.getSender();
        if (sender == null) return;

        // Rate limit: prevent escape spam
        if (!PacketRateLimiter.allowPacket(sender, "struggle")) return;

        // Resolve the furniture entity
        Entity entity = sender.level().getEntity(msg.furnitureEntityId);
        if (entity == null) return;
        if (!(entity instanceof ISeatProvider provider)) return;
        if (sender.distanceTo(entity) > 5.0) return;
        if (!entity.isAlive() || entity.isRemoved()) return;

        // Validate escape method
        if (
            msg.escapeMethod != METHOD_STRUGGLE &&
            msg.escapeMethod != METHOD_LOCKPICK
        ) {
            TiedUpMod.LOGGER.warn(
                "[PacketFurnitureEscape] Invalid escape method {} from {}",
                msg.escapeMethod,
                sender.getName().getString()
            );
            return;
        }

        if (msg.escapeMethod == METHOD_STRUGGLE) {
            handleStruggle(sender, entity, provider);
        } else {
            handleLockpick(sender, entity, provider);
        }
    }

    // ==================== Struggle ====================

    /**
     * Sender must BE the seated passenger. Always allowed regardless of blocked
     * regions.
     */
    private static void handleStruggle(
        ServerPlayer sender,
        Entity furnitureEntity,
        ISeatProvider provider
    ) {
        // Sender must be riding this furniture
        if (!furnitureEntity.hasPassenger(sender)) {
            TiedUpMod.LOGGER.debug(
                "[PacketFurnitureEscape] Struggle: {} is not a passenger of furniture {}",
                sender.getName().getString(),
                furnitureEntity.getId()
            );
            return;
        }

        // Find sender's seat
        SeatDefinition seat = provider.getSeatForPassenger(sender);
        if (seat == null) {
            TiedUpMod.LOGGER.debug(
                "[PacketFurnitureEscape] Struggle: {} has no assigned seat",
                sender.getName().getString()
            );
            return;
        }

        // Seat must be locked (no point struggling if unlocked — just dismount)
        if (!provider.isSeatLocked(seat.id())) {
            TiedUpMod.LOGGER.debug(
                "[PacketFurnitureEscape] Struggle: seat '{}' is not locked, no struggle needed",
                seat.id()
            );
            return;
        }

        // Compute difficulty. Clamp to [0, MAX] so a misconfigured
        // IV2BondageItem returning a negative bonus can't underflow the
        // instant-escape check below and slip into the minigame with a
        // negative resistance state.
        int baseDifficulty = provider.getLockedDifficulty(seat.id());
        int itemBonus = computeItemDifficultyBonus(sender, provider, seat);
        int totalDifficulty = Math.max(
            0,
            Math.min(baseDifficulty + itemBonus, MAX_DIFFICULTY)
        );

        TiedUpMod.LOGGER.debug(
            "[PacketFurnitureEscape] Struggle: {} on seat '{}' — difficulty {} (base {} + items {})",
            sender.getName().getString(),
            seat.id(),
            totalDifficulty,
            baseDifficulty,
            itemBonus
        );

        // Difficulty 0: immediate escape (no minigame needed)
        if (totalDifficulty == 0) {
            provider.setSeatLocked(seat.id(), false);
            sender.getPersistentData().remove("tiedup_locked_furniture");
            sender.stopRiding();

            // Broadcast updated state
            if (furnitureEntity instanceof EntityFurniture furniture) {
                PacketSyncFurnitureState.sendToTracking(furniture);
            }

            TiedUpMod.LOGGER.info(
                "[PacketFurnitureEscape] {} escaped furniture {} (difficulty was 0)",
                sender.getName().getString(),
                furnitureEntity.getId()
            );
            return;
        }

        // Respect server config: if struggle minigame is disabled, skip
        if (
            !SettingsAccessor.isStruggleMiniGameEnabled()
        ) {
            TiedUpMod.LOGGER.debug(
                "[PacketFurnitureEscape] Struggle minigame disabled by server config"
            );
            return;
        }

        // Launch struggle minigame session via StruggleSessionManager
        StruggleSessionManager manager = StruggleSessionManager.getInstance();
        ContinuousStruggleMiniGameState session =
            manager.startFurnitureStruggleSession(
                sender,
                furnitureEntity.getId(),
                seat.id(),
                totalDifficulty
            );

        if (session != null) {
            // Send START packet to open the struggle GUI on the client
            ModNetwork.sendToPlayer(
                new PacketContinuousStruggleState(
                    session.getSessionId(),
                    ContinuousStruggleMiniGameState.UpdateType.START,
                    session.getCurrentDirection().getIndex(),
                    session.getCurrentResistance(),
                    session.getMaxResistance(),
                    true // locked context
                ),
                sender
            );

            // Set furniture animation state to STRUGGLE
            if (furnitureEntity instanceof EntityFurniture furniture) {
                furniture.setAnimState(EntityFurniture.STATE_STRUGGLE);
                PacketSyncFurnitureState.sendToTracking(furniture);
            }
        }
    }

    // ==================== Lockpick ====================

    /**
     * Sender must NOT be the seated passenger (third party standing nearby).
     * Must have a lockpick item in inventory. Targets the locked occupied seat
     * closest to the sender's look direction.
     */
    private static void handleLockpick(
        ServerPlayer sender,
        Entity furnitureEntity,
        ISeatProvider provider
    ) {
        // Sender must NOT be riding this furniture (third party assistance)
        if (furnitureEntity.hasPassenger(sender)) {
            TiedUpMod.LOGGER.debug(
                "[PacketFurnitureEscape] Lockpick: {} cannot lockpick while seated",
                sender.getName().getString()
            );
            return;
        }

        // Sender must have a lockpick in their inventory
        if (!hasLockpickInInventory(sender)) {
            TiedUpMod.LOGGER.debug(
                "[PacketFurnitureEscape] Lockpick: {} has no lockpick",
                sender.getName().getString()
            );
            return;
        }

        // Use look-direction-based seat targeting (same vector math as
        // EntityFurniture.findNearestOccupiedLockableSeat) instead of
        // blindly picking the first locked seat.
        SeatDefinition targetSeat = findNearestLockedOccupiedSeat(
            sender,
            provider,
            furnitureEntity
        );
        if (targetSeat == null) {
            TiedUpMod.LOGGER.debug(
                "[PacketFurnitureEscape] Lockpick: no locked occupied seat found"
            );
            return;
        }

        // Find the passenger in this seat (needed for item bonus computation)
        Entity passenger = findPassengerInSeat(
            provider,
            furnitureEntity,
            targetSeat.id()
        );

        // Compute difficulty
        int baseDifficulty = provider.getLockedDifficulty(targetSeat.id());
        int itemBonus = 0;
        if (passenger instanceof LivingEntity livingPassenger) {
            itemBonus = computeItemDifficultyBonus(
                livingPassenger,
                provider,
                targetSeat
            );
        }
        // Clamp to [0, MAX] — same rationale as handleStruggle: a misconfigured
        // item returning negative getEscapeDifficulty could push totalDifficulty
        // below 0, skipping the == 0 instant-success branch below and starting
        // a minigame with negative difficulty (sweet spot wider than intended).
        int totalDifficulty = Math.max(
            0,
            Math.min(baseDifficulty + itemBonus, MAX_DIFFICULTY)
        );

        TiedUpMod.LOGGER.debug(
            "[PacketFurnitureEscape] Lockpick: {} on seat '{}' -- difficulty {} (base {} + items {})",
            sender.getName().getString(),
            targetSeat.id(),
            totalDifficulty,
            baseDifficulty,
            itemBonus
        );

        // Difficulty 0: immediate success — unlock + dismount + consume lockpick
        if (totalDifficulty == 0) {
            completeLockpickSuccess(
                sender,
                furnitureEntity,
                provider,
                targetSeat,
                passenger
            );
            return;
        }

        // Respect server config: if lockpick minigame is disabled, skip
        if (
            !SettingsAccessor.isLockpickMiniGameEnabled()
        ) {
            TiedUpMod.LOGGER.debug(
                "[PacketFurnitureEscape] Lockpick minigame disabled by server config"
            );
            return;
        }

        // Find the lockpick to determine remaining uses and sweet spot width
        ItemStack lockpickStack = ItemLockpick.findLockpickInInventory(sender);
        if (lockpickStack.isEmpty()) return; // double-check; hasLockpickInInventory passed above

        int remainingUses =
            lockpickStack.getMaxDamage() - lockpickStack.getDamageValue();

        // Sweet spot width scales inversely with difficulty: harder locks = narrower sweet spot.
        // Base width 0.15 at difficulty 1, down to 0.03 at MAX_DIFFICULTY.
        float sweetSpotWidth = Math.max(
            0.03f,
            0.15f - (totalDifficulty / (float) MAX_DIFFICULTY) * 0.12f
        );

        // Start a FURNITURE_SEAT-kind session; PacketLockpickAttempt branches
        // on the kind to run the furniture-specific completion.
        LockpickSessionManager lockpickManager =
            LockpickSessionManager.getInstance();
        LockpickMiniGameState session = lockpickManager.startLockpickSession(
            sender,
            com.tiedup.remake.minigame.LockpickTargetKind.FURNITURE_SEAT,
            furnitureEntity.getId(),
            sweetSpotWidth
        );

        if (session == null) {
            TiedUpMod.LOGGER.warn(
                "[PacketFurnitureEscape] Failed to create lockpick session for {}",
                sender.getName().getString()
            );
            return;
        }

        session.setRemainingUses(remainingUses);

        // Store seat_id in persistent data so the completion callback can
        // find it (the session carries furniture id via targetData, but
        // seat id has nowhere else to live yet).
        net.minecraft.nbt.CompoundTag ctx = new net.minecraft.nbt.CompoundTag();
        ctx.putInt("furniture_id", furnitureEntity.getId());
        ctx.putString("seat_id", targetSeat.id());
        sender.getPersistentData().put("tiedup_furniture_lockpick_ctx", ctx);

        // Send initial lockpick state to open the minigame GUI on the client
        ModNetwork.sendToPlayer(
            new PacketLockpickMiniGameState(
                session.getSessionId(),
                session.getSweetSpotCenter(),
                session.getSweetSpotWidth(),
                session.getCurrentPosition(),
                session.getRemainingUses()
            ),
            sender
        );

        TiedUpMod.LOGGER.info(
            "[PacketFurnitureEscape] {} started lockpick on seat '{}' of furniture {} (difficulty {}, sweet spot width {})",
            sender.getName().getString(),
            targetSeat.id(),
            furnitureEntity.getId(),
            totalDifficulty,
            sweetSpotWidth
        );
    }

    /**
     * Complete a successful lockpick: unlock the seat, clear reconnection tag,
     * dismount the passenger, consume/damage the lockpick, and broadcast state.
     */
    private static void completeLockpickSuccess(
        ServerPlayer sender,
        Entity furnitureEntity,
        ISeatProvider provider,
        SeatDefinition targetSeat,
        Entity passenger
    ) {
        provider.setSeatLocked(targetSeat.id(), false);
        if (passenger instanceof ServerPlayer passengerPlayer) {
            passengerPlayer
                .getPersistentData()
                .remove("tiedup_locked_furniture");
        }
        if (passenger != null) {
            passenger.stopRiding();
        }

        // Damage the lockpick (1 durability per successful pick)
        ItemStack lockpickStack = ItemLockpick.findLockpickInInventory(sender);
        if (!lockpickStack.isEmpty()) {
            lockpickStack.setDamageValue(lockpickStack.getDamageValue() + 1);
            if (
                lockpickStack.getDamageValue() >= lockpickStack.getMaxDamage()
            ) {
                lockpickStack.shrink(1);
            }
        }

        // Broadcast updated state
        if (furnitureEntity instanceof EntityFurniture furniture) {
            PacketSyncFurnitureState.sendToTracking(furniture);
        }

        TiedUpMod.LOGGER.info(
            "[PacketFurnitureEscape] {} lockpicked seat '{}' on furniture {} (difficulty was 0)",
            sender.getName().getString(),
            targetSeat.id(),
            furnitureEntity.getId()
        );
    }

    // ==================== Helpers ====================

    /**
     * Compute the item difficulty bonus: sum of {@code getEscapeDifficulty(stack)}
     * for all V2 bondage items equipped on NON-blocked body regions.
     *
     * <p>Only applies if the seat's {@code itemDifficultyBonus} flag is true.</p>
     */
    private static int computeItemDifficultyBonus(
        LivingEntity passenger,
        ISeatProvider provider,
        SeatDefinition seat
    ) {
        if (!provider.hasItemDifficultyBonus(seat.id())) {
            return 0;
        }

        Set<BodyRegionV2> blockedRegions = provider.getBlockedRegions(
            seat.id()
        );
        Map<BodyRegionV2, ItemStack> equipped =
            V2EquipmentHelper.getAllEquipped(passenger);

        int bonus = 0;
        for (Map.Entry<BodyRegionV2, ItemStack> entry : equipped.entrySet()) {
            // Skip items on blocked regions (those are "held" by the furniture)
            if (blockedRegions.contains(entry.getKey())) continue;

            ItemStack stack = entry.getValue();
            if (stack.getItem() instanceof IV2BondageItem bondageItem) {
                bonus += bondageItem.getEscapeDifficulty(stack);
            }
        }

        return bonus;
    }

    /**
     * Check if the sender has a lockpick item anywhere in their inventory.
     */
    private static boolean hasLockpickInInventory(ServerPlayer player) {
        for (ItemStack stack : player.getInventory().items) {
            if (stack.getItem() instanceof ItemLockpick) return true;
        }
        // Also check offhand
        if (
            player.getOffhandItem().getItem() instanceof ItemLockpick
        ) return true;
        return false;
    }

    /**
     * Find the locked and occupied seat whose approximate world position has the
     * smallest angle to the player's look direction.
     *
     * <p>Uses the same vector math as
     * {@link EntityFurniture#findNearestOccupiedLockableSeat} to ensure
     * consistent targeting between key interactions and lockpick attempts.</p>
     *
     * @param player the player performing the lockpick (provides look direction)
     * @param provider the seat provider (furniture entity)
     * @param furnitureEntity the furniture entity (provides position and yaw)
     * @return the best matching seat, or null if no locked occupied seats exist
     */
    private static SeatDefinition findNearestLockedOccupiedSeat(
        ServerPlayer player,
        ISeatProvider provider,
        Entity furnitureEntity
    ) {
        List<SeatDefinition> seats = provider.getSeats();
        if (seats.isEmpty()) return null;

        Vec3 playerPos = player.getEyePosition();
        Vec3 lookDir = player.getLookAngle();
        Vec3 right = com.tiedup.remake.v2.furniture.FurnitureSeatGeometry.rightAxis(
            furnitureEntity.getYRot()
        );

        SeatDefinition best = null;
        double bestScore = Double.MAX_VALUE;
        int seatCount = seats.size();

        for (int i = 0; i < seatCount; i++) {
            SeatDefinition seat = seats.get(i);

            // Only consider locked seats that have a passenger
            if (!provider.isSeatLocked(seat.id())) continue;
            boolean hasPassenger = false;
            for (Entity p : furnitureEntity.getPassengers()) {
                SeatDefinition ps = provider.getSeatForPassenger(p);
                if (ps != null && ps.id().equals(seat.id())) {
                    hasPassenger = true;
                    break;
                }
            }
            if (!hasPassenger) continue;

            // Approximate seat world position along the right axis
            double offset = com.tiedup.remake.v2.furniture.FurnitureSeatGeometry.seatOffset(
                i, seatCount
            );
            Vec3 seatWorldPos = new Vec3(
                furnitureEntity.getX() + right.x * offset,
                furnitureEntity.getY() + 0.5,
                furnitureEntity.getZ() + right.z * offset
            );

            Vec3 toSeat = seatWorldPos.subtract(playerPos);
            double distSq = toSeat.lengthSqr();
            if (distSq < 1e-6) {
                best = seat;
                bestScore = -1.0;
                continue;
            }

            Vec3 toSeatNorm = toSeat.normalize();
            double dot = lookDir.dot(toSeatNorm);
            double score = -dot; // lower = better (looking more directly at seat)

            if (score < bestScore) {
                bestScore = score;
                best = seat;
            }
        }

        return best;
    }

    /**
     * Find the passenger entity sitting in a specific seat.
     */
    private static Entity findPassengerInSeat(
        ISeatProvider provider,
        Entity furnitureEntity,
        String seatId
    ) {
        for (Entity passenger : furnitureEntity.getPassengers()) {
            SeatDefinition passengerSeat = provider.getSeatForPassenger(
                passenger
            );
            if (passengerSeat != null && passengerSeat.id().equals(seatId)) {
                return passenger;
            }
        }
        return null;
    }
}
