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

import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.furniture.FurnitureDefinition;
import com.tiedup.remake.v2.furniture.FurnitureFeedback;
import com.tiedup.remake.v2.furniture.FurnitureRegistry;
import com.tiedup.remake.v2.furniture.SeatDefinition;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
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.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.fml.loading.FMLEnvironment;
import net.minecraftforge.network.NetworkEvent;
import net.minecraftforge.network.PacketDistributor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * Server-to-client packet that syncs ALL furniture definitions from the
 * {@link FurnitureRegistry} to a client.
 *
 * <p>Sent on player login and after {@code /reload}. The client handler
 * calls {@link FurnitureRegistry#reload(Map)} with the deserialized
 * definitions, replacing its entire local cache.</p>
 *
 * <p>Wire format: varint definition count, then for each definition
 * the full set of fields including nested {@link SeatDefinition} list
 * and {@link FurnitureFeedback} optional sounds.</p>
 *
 * <p>Direction: Server to Client (S2C)</p>
 */
public class PacketSyncFurnitureDefinitions {

    private static final Logger LOGGER = LogManager.getLogger(
        "PacketSyncFurnitureDefinitions"
    );

    /**
     * Safety cap on the number of definitions to prevent memory exhaustion
     * from malformed or malicious packets.
     */
    private static final int MAX_DEFINITIONS = 10_000;

    /**
     * Safety cap on the number of seats per furniture definition.
     */
    private static final int MAX_SEATS = 64;

    /**
     * Safety cap on the number of tint channels per definition.
     */
    private static final int MAX_TINT_CHANNELS = 32;

    /**
     * Safety cap on the number of blocked regions per seat.
     */
    private static final int MAX_BLOCKED_REGIONS = BodyRegionV2.values().length;

    private final Map<ResourceLocation, FurnitureDefinition> definitions;

    public PacketSyncFurnitureDefinitions(
        Map<ResourceLocation, FurnitureDefinition> definitions
    ) {
        this.definitions = definitions;
    }

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

    public static void encode(
        PacketSyncFurnitureDefinitions msg,
        FriendlyByteBuf buf
    ) {
        buf.writeVarInt(msg.definitions.size());

        for (FurnitureDefinition def : msg.definitions.values()) {
            // Identity
            buf.writeResourceLocation(def.id());
            buf.writeUtf(def.displayName());

            // Optional translation key
            boolean hasTranslationKey = def.translationKey() != null;
            buf.writeBoolean(hasTranslationKey);
            if (hasTranslationKey) {
                buf.writeUtf(def.translationKey());
            }

            // Model
            buf.writeResourceLocation(def.modelLocation());

            // Tint channels
            buf.writeVarInt(def.tintChannels().size());
            for (Map.Entry<String, Integer> entry : def
                .tintChannels()
                .entrySet()) {
                buf.writeUtf(entry.getKey());
                buf.writeInt(entry.getValue());
            }

            // Booleans and floats for placement/physics
            buf.writeBoolean(def.supportsColor());
            buf.writeFloat(def.hitboxWidth());
            buf.writeFloat(def.hitboxHeight());
            buf.writeBoolean(def.snapToWall());
            buf.writeBoolean(def.floorOnly());
            buf.writeBoolean(def.lockable());
            buf.writeFloat(def.breakResistance());
            buf.writeBoolean(def.dropOnBreak());

            // Seats
            buf.writeVarInt(def.seats().size());
            for (SeatDefinition seat : def.seats()) {
                encodeSeat(seat, buf);
            }

            // Feedback (6 optional ResourceLocations)
            encodeFeedback(def.feedback(), buf);

            // Category
            buf.writeUtf(def.category());

            // Icon (optional model ResourceLocation)
            writeOptionalRL(buf, def.icon());
        }
    }

    private static void encodeSeat(SeatDefinition seat, FriendlyByteBuf buf) {
        buf.writeUtf(seat.id());
        buf.writeUtf(seat.armatureName());

        // Blocked regions as string names
        buf.writeVarInt(seat.blockedRegions().size());
        for (BodyRegionV2 region : seat.blockedRegions()) {
            buf.writeUtf(region.name());
        }

        buf.writeBoolean(seat.lockable());
        buf.writeVarInt(seat.lockedDifficulty());
        buf.writeBoolean(seat.itemDifficultyBonus());
    }

    private static void encodeFeedback(
        FurnitureFeedback feedback,
        FriendlyByteBuf buf
    ) {
        writeOptionalRL(buf, feedback.mountSound());
        writeOptionalRL(buf, feedback.lockSound());
        writeOptionalRL(buf, feedback.unlockSound());
        writeOptionalRL(buf, feedback.struggleLoopSound());
        writeOptionalRL(buf, feedback.escapeSound());
        writeOptionalRL(buf, feedback.deniedSound());
    }

    private static void writeOptionalRL(
        FriendlyByteBuf buf,
        ResourceLocation rl
    ) {
        buf.writeBoolean(rl != null);
        if (rl != null) {
            buf.writeResourceLocation(rl);
        }
    }

    public static PacketSyncFurnitureDefinitions decode(FriendlyByteBuf buf) {
        int count = Math.min(buf.readVarInt(), MAX_DEFINITIONS);
        Map<ResourceLocation, FurnitureDefinition> defs = new HashMap<>(count);

        for (int i = 0; i < count; i++) {
            // Identity
            ResourceLocation id = buf.readResourceLocation();
            String displayName = buf.readUtf();

            // Optional translation key
            String translationKey = buf.readBoolean() ? buf.readUtf() : null;

            // Model
            ResourceLocation modelLocation = buf.readResourceLocation();

            // Tint channels
            int tintCount = Math.min(buf.readVarInt(), MAX_TINT_CHANNELS);
            Map<String, Integer> tintChannels = new HashMap<>(tintCount);
            for (int t = 0; t < tintCount; t++) {
                tintChannels.put(buf.readUtf(), buf.readInt());
            }

            // Booleans and floats
            boolean supportsColor = buf.readBoolean();
            float hitboxWidth = buf.readFloat();
            float hitboxHeight = buf.readFloat();
            boolean snapToWall = buf.readBoolean();
            boolean floorOnly = buf.readBoolean();
            boolean lockable = buf.readBoolean();
            float breakResistance = buf.readFloat();
            boolean dropOnBreak = buf.readBoolean();

            // Seats
            int seatCount = Math.min(buf.readVarInt(), MAX_SEATS);
            List<SeatDefinition> seats = new ArrayList<>(seatCount);
            for (int s = 0; s < seatCount; s++) {
                seats.add(decodeSeat(buf));
            }

            // Feedback
            FurnitureFeedback feedback = decodeFeedback(buf);

            // Category
            String category = buf.readUtf();

            // Icon (optional model ResourceLocation)
            ResourceLocation icon = readOptionalRL(buf);

            FurnitureDefinition def = new FurnitureDefinition(
                id,
                displayName,
                translationKey,
                modelLocation,
                Map.copyOf(tintChannels),
                supportsColor,
                hitboxWidth,
                hitboxHeight,
                snapToWall,
                floorOnly,
                lockable,
                breakResistance,
                dropOnBreak,
                List.copyOf(seats),
                feedback,
                category,
                icon
            );

            defs.put(id, def);
        }

        return new PacketSyncFurnitureDefinitions(defs);
    }

    private static SeatDefinition decodeSeat(FriendlyByteBuf buf) {
        String id = buf.readUtf();
        String armatureName = buf.readUtf();

        int regionCount = Math.min(buf.readVarInt(), MAX_BLOCKED_REGIONS);
        Set<BodyRegionV2> blockedRegions = new HashSet<>(regionCount);
        for (int r = 0; r < regionCount; r++) {
            BodyRegionV2 region = BodyRegionV2.fromName(buf.readUtf());
            if (region != null) {
                blockedRegions.add(region);
            }
            // Silently skip unknown region names for forward compatibility
        }

        boolean lockable = buf.readBoolean();
        int lockedDifficulty = buf.readVarInt();
        boolean itemDifficultyBonus = buf.readBoolean();

        return new SeatDefinition(
            id,
            armatureName,
            Set.copyOf(blockedRegions),
            lockable,
            lockedDifficulty,
            itemDifficultyBonus
        );
    }

    private static FurnitureFeedback decodeFeedback(FriendlyByteBuf buf) {
        ResourceLocation mountSound = readOptionalRL(buf);
        ResourceLocation lockSound = readOptionalRL(buf);
        ResourceLocation unlockSound = readOptionalRL(buf);
        ResourceLocation struggleLoopSound = readOptionalRL(buf);
        ResourceLocation escapeSound = readOptionalRL(buf);
        ResourceLocation deniedSound = readOptionalRL(buf);

        return new FurnitureFeedback(
            mountSound,
            lockSound,
            unlockSound,
            struggleLoopSound,
            escapeSound,
            deniedSound
        );
    }

    private static ResourceLocation readOptionalRL(FriendlyByteBuf buf) {
        return buf.readBoolean() ? buf.readResourceLocation() : null;
    }

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

    public static void handle(
        PacketSyncFurnitureDefinitions msg,
        Supplier<NetworkEvent.Context> ctxSupplier
    ) {
        NetworkEvent.Context ctx = ctxSupplier.get();
        ctx.enqueueWork(() -> {
            if (FMLEnvironment.dist == Dist.CLIENT) {
                handleOnClient(msg);
            }
        });
        ctx.setPacketHandled(true);
    }

    @OnlyIn(Dist.CLIENT)
    private static void handleOnClient(PacketSyncFurnitureDefinitions msg) {
        FurnitureRegistry.reload(msg.definitions);
        LOGGER.debug(
            "Client received {} furniture definitions from server",
            msg.definitions.size()
        );
    }

    // ==================== Server-side Helpers ====================

    /**
     * Send all current furniture definitions to a single player.
     * Call this on player login (PlayerLoggedInEvent) and after /reload.
     *
     * @param player the player to send definitions to
     */
    public static void sendToPlayer(ServerPlayer player) {
        ModNetwork.CHANNEL.send(
            PacketDistributor.PLAYER.with(() -> player),
            new PacketSyncFurnitureDefinitions(FurnitureRegistry.getAllMap())
        );
    }

    /**
     * Send all current furniture definitions to every connected player.
     * Call this after /reload completes.
     */
    public static void sendToAll() {
        ModNetwork.CHANNEL.send(
            PacketDistributor.ALL.noArg(),
            new PacketSyncFurnitureDefinitions(FurnitureRegistry.getAllMap())
        );
    }
}
