package com.tiedup.remake.network;

import com.tiedup.remake.compat.mca.MCABondageManager;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.sync.PacketSyncBindState;
import com.tiedup.remake.network.sync.PacketSyncCollarRegistry;
import com.tiedup.remake.network.sync.PacketSyncEnslavement;
import com.tiedup.remake.network.sync.PacketSyncStruggleState;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.v2.furniture.EntityFurniture;
import com.tiedup.remake.v2.furniture.ISeatProvider;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket;
import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.jetbrains.annotations.Nullable;

/**
 * Handles network synchronization for bondage equipment in multiplayer.
 *
 * <h2>Sync Events</h2>
 * <ul>
 *   <li><b>onStartTracking:</b> When player A enters player B's view range,
 *       sync B's bondage inventory to A so they see the correct model layers.</li>
 *   <li><b>onPlayerLoggedIn:</b> Sync all players' states to the newly joined player,
 *       and sync the new player's state to everyone else.</li>
 * </ul>
 *
 * <h2>Position Sync (MC-262715 Fix)</h2>
 * When a player's state changes (freed from leash, etc.), other clients may have
 * stale data. We send delayed position/passenger packets to correct this.
 *
 * @see SyncManager
 * @see PacketSyncBindState
 */
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
public class NetworkEventHandler {

    /**
     * Called when a player starts tracking an entity (another player enters their view range).
     * We sync the tracked player's bondage inventory to the tracker.
     *
     * CRITICAL FIX: Also sync riding state and position to fix MC-262715 desync bug.
     * When a tracker reconnects, they may have stale data about the tracked player's
     * riding status, causing the player to appear frozen.
     */
    @SubscribeEvent
    public static void onStartTracking(PlayerEvent.StartTracking event) {
        if (!(event.getEntity() instanceof ServerPlayer tracker)) return;

        // Handle MCA villagers - sync their bondage state to the tracker
        if (
            event.getTarget() instanceof LivingEntity target &&
            MCACompat.isMCALoaded() &&
            MCACompat.isMCAVillager(target)
        ) {
            syncMCAVillagerToTracker(target, tracker);
            return;
        }

        // Handle players
        if (!(event.getTarget() instanceof Player trackedPlayer)) return;

        // Sync tracked player's V2 equipment to the tracker (so they see the bondage layers)
        com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.syncTo(
            trackedPlayer,
            tracker
        );

        // Also sync state flags
        PacketSyncBindState statePacket = PacketSyncBindState.fromPlayer(
            trackedPlayer
        );
        if (statePacket != null) {
            ModNetwork.sendToPlayer(statePacket, tracker);
        }

        // Sync struggle state (needed for animations)
        PacketSyncStruggleState strugglePacket =
            PacketSyncStruggleState.fromPlayer(trackedPlayer);
        if (strugglePacket != null) {
            ModNetwork.sendToPlayer(strugglePacket, tracker);
        }

        // CRITICAL FIX: Sync enslavement state (needed for leash visibility)
        PacketSyncEnslavement enslavementPacket =
            PacketSyncEnslavement.fromPlayer(trackedPlayer);
        if (enslavementPacket != null) {
            ModNetwork.sendToPlayer(enslavementPacket, tracker);
        }

        // Workaround for MC-262715: vanilla doesn't resync riding state or
        // position when a tracker reconnects, so a tracked player who was
        // freed from a vehicle between the two connect events renders
        // frozen until they next move.
        if (trackedPlayer instanceof ServerPlayer trackedServerPlayer) {
            syncRidingStateAndPosition(trackedServerPlayer, tracker);
        }
    }

    /**
     * Sync MCA villager's bondage state to a specific tracker.
     * Called when a player starts tracking an MCA villager.
     * Delegates to MCABondageManager.
     */
    private static void syncMCAVillagerToTracker(
        LivingEntity villager,
        ServerPlayer tracker
    ) {
        MCABondageManager.getInstance().syncBondageStateTo(villager, tracker);
    }

    /** Delay before sending position sync (in ticks) - allows entity spawn to complete */
    private static final int POSITION_SYNC_DELAY = 5;

    /**
     * Sync the riding state and position of a player to a specific tracker.
     *
     * This handles edge cases where the tracker has stale data about the tracked player's
     * riding status (e.g., tracker reconnects after tracked player was freed from transport).
     *
     * The delay ensures the entity spawn packet is processed before we send corrections.
     */
    private static void syncRidingStateAndPosition(
        ServerPlayer trackedPlayer,
        ServerPlayer tracker
    ) {
        var server = tracker.getServer();
        if (server == null) return;

        server.tell(
            new net.minecraft.server.TickTask(
                server.getTickCount() + POSITION_SYNC_DELAY,
                () -> {
                    if (!trackedPlayer.isAlive() || !tracker.isAlive()) return;
                    if (
                        trackedPlayer.isRemoved() || tracker.isRemoved()
                    ) return;

                    var vehicle = trackedPlayer.getVehicle();
                    boolean hasValidVehicle =
                        vehicle != null &&
                        vehicle.isAlive() &&
                        !vehicle.isRemoved();

                    if (!trackedPlayer.isPassenger() || !hasValidVehicle) {
                        // Not riding - send position to clear stale riding state on client
                        tracker.connection.send(
                            new ClientboundTeleportEntityPacket(trackedPlayer)
                        );

                        // Fix orphaned passenger state
                        if (trackedPlayer.isPassenger() && !hasValidVehicle) {
                            trackedPlayer.stopRiding();
                        }
                    } else {
                        // Riding valid vehicle - sync passenger relationship
                        tracker.connection.send(
                            new ClientboundSetPassengersPacket(vehicle)
                        );
                    }
                }
            )
        );
    }

    /**
     * Called when a player logs in.
     * We sync:
     * 1. The player's own inventory to themselves
     * 2. The player's inventory to all other players (so they see the new player)
     * 3. All other players' inventories to the new player (so they see everyone)
     * 4. The player's collar registry
     */
    @SubscribeEvent
    public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) {
        if (!(event.getEntity() instanceof ServerPlayer player)) return;

        // Sync this player's state to all others, and all others' states to this player
        SyncManager.syncAll(player);
        SyncManager.syncAllPlayersTo(player);

        syncCollarRegistry(player);

        // Sync furniture definitions
        com.tiedup.remake.v2.furniture.network.PacketSyncFurnitureDefinitions.sendToPlayer(
            player
        );

        // Check for furniture reconnection (player was locked in a seat and disconnected)
        handleFurnitureReconnection(player);

        TiedUpMod.LOGGER.debug(
            "[Network] Player {} logged in - sync complete",
            player.getName().getString()
        );
    }

    /**
     * Sync the collar registry to a player.
     * Sends all slaves (collar wearers) owned by this player.
     */
    private static void syncCollarRegistry(ServerPlayer player) {
        var server = player.getServer();
        if (server == null) return;

        var registry = com.tiedup.remake.state.CollarRegistry.get(server);
        if (registry == null) return;

        java.util.Set<java.util.UUID> slaves = registry.getSlaves(
            player.getUUID()
        );
        ModNetwork.sendToPlayer(new PacketSyncCollarRegistry(slaves), player);

        TiedUpMod.LOGGER.debug(
            "[Network] Synced {} slaves to {}",
            slaves.size(),
            player.getName().getString()
        );
    }

    /**
     * Handle furniture reconnection for a player who was locked in a seat when
     * they disconnected. Reads the {@code tiedup_locked_furniture} tag from
     * persistent data, finds the furniture entity, and re-mounts the player.
     *
     * <p>The re-mount is deferred by a few ticks to ensure the player's entity
     * is fully spawned and the furniture's chunk is loaded.</p>
     *
     * @param player the player who just logged in
     */
    private static void handleFurnitureReconnection(ServerPlayer player) {
        CompoundTag persistentData = player.getPersistentData();
        if (!persistentData.contains("tiedup_locked_furniture", 10)) return;

        CompoundTag tag = persistentData.getCompound("tiedup_locked_furniture");

        // Read stored data
        if (
            !tag.contains("x") ||
            !tag.contains("y") ||
            !tag.contains("z") ||
            !tag.contains("dim") ||
            !tag.contains("furniture_uuid") ||
            !tag.contains("seat_id")
        ) {
            TiedUpMod.LOGGER.warn(
                "[Network] Malformed furniture reconnection tag for {}, removing",
                player.getName().getString()
            );
            persistentData.remove("tiedup_locked_furniture");
            return;
        }

        int x = tag.getInt("x");
        int y = tag.getInt("y");
        int z = tag.getInt("z");
        String dimStr = tag.getString("dim");
        String furnitureUuidStr = tag.getString("furniture_uuid");
        String seatId = tag.getString("seat_id");

        // Validate furniture UUID
        UUID furnitureUuid;
        try {
            furnitureUuid = UUID.fromString(furnitureUuidStr);
        } catch (IllegalArgumentException e) {
            TiedUpMod.LOGGER.warn(
                "[Network] Invalid furniture UUID '{}' in reconnection tag for {}, removing",
                furnitureUuidStr,
                player.getName().getString()
            );
            persistentData.remove("tiedup_locked_furniture");
            return;
        }

        // Resolve the dimension (guard against corrupted NBT)
        ResourceLocation dimLoc;
        try {
            dimLoc = ResourceLocation.parse(dimStr);
        } catch (net.minecraft.ResourceLocationException e) {
            TiedUpMod.LOGGER.warn("Invalid dimension in furniture reconnect NBT: '{}', clearing", dimStr);
            persistentData.remove("tiedup_locked_furniture");
            return;
        }
        ResourceKey<Level> dimKey = ResourceKey.create(
            net.minecraft.core.registries.Registries.DIMENSION,
            dimLoc
        );

        var server = player.getServer();
        if (server == null) {
            persistentData.remove("tiedup_locked_furniture");
            return;
        }

        ServerLevel targetLevel = server.getLevel(dimKey);
        if (targetLevel == null) {
            TiedUpMod.LOGGER.warn(
                "[Network] Dimension '{}' not found for furniture reconnection, removing tag",
                dimStr
            );
            persistentData.remove("tiedup_locked_furniture");
            return;
        }

        // Defer the re-mount to ensure the player entity is fully spawned
        BlockPos furniturePos = new BlockPos(x, y, z);

        server.tell(
            new net.minecraft.server.TickTask(
                server.getTickCount() + FURNITURE_RECONNECT_DELAY,
                () -> {
                    if (!player.isAlive() || player.isRemoved()) {
                        persistentData.remove("tiedup_locked_furniture");
                        return;
                    }

                    // Force-load the target chunk before the search so
                    // cross-dimension reconnects don't silently fail on an
                    // unloaded chunk — that was a "disconnect then cross-dim
                    // travel to escape" hole where the null-furniture path
                    // below cleared the tag.
                    targetLevel.getChunk(
                        furniturePos.getX() >> 4,
                        furniturePos.getZ() >> 4
                    );

                    // Search for the furniture entity near the stored position
                    Entity furniture = findFurnitureEntity(
                        targetLevel,
                        furniturePos,
                        furnitureUuid
                    );

                    if (
                        furniture == null ||
                        !(furniture instanceof ISeatProvider provider)
                    ) {
                        TiedUpMod.LOGGER.info(
                            "[Network] Furniture entity {} not found at {} for reconnection of {}. " +
                                "Teleporting player to last furniture position.",
                            furnitureUuidStr,
                            furniturePos,
                            player.getName().getString()
                        );
                        // Teleport to furniture position to prevent "disconnect to escape"
                        teleportPlayerTo(
                            player,
                            targetLevel,
                            x + 0.5,
                            y,
                            z + 0.5
                        );
                        persistentData.remove("tiedup_locked_furniture");
                        return;
                    }

                    // Verify the seat is still locked
                    if (!provider.isSeatLocked(seatId)) {
                        TiedUpMod.LOGGER.info(
                            "[Network] Seat '{}' is no longer locked on furniture {}. Freeing {}.",
                            seatId,
                            furnitureUuidStr,
                            player.getName().getString()
                        );
                        persistentData.remove("tiedup_locked_furniture");
                        return;
                    }

                    // Teleport to furniture dimension/position if needed
                    if (
                        player.level() != targetLevel ||
                        player.distanceToSqr(furniture) > 25.0
                    ) {
                        teleportPlayerTo(
                            player,
                            targetLevel,
                            furniture.getX(),
                            furniture.getY(),
                            furniture.getZ()
                        );
                    }

                    // Re-mount the player.
                    // If the original seat was taken (e.g. force-mounted by
                    // another player while this one was offline), assignSeat
                    // would overwrite the mapping and orphan the current
                    // occupant. Teleport to the furniture position instead
                    // and drop the tag — keeping either player in a
                    // deterministic seat is better than a double-mount.
                    // Uses ISeatProvider.findPassengerInSeat so future
                    // non-EntityFurniture seat providers also benefit.
                    boolean mounted = false;
                    if (provider.findPassengerInSeat(seatId) != null) {
                        TiedUpMod.LOGGER.warn(
                            "[Network] Seat '{}' on furniture {} is now occupied by another entity; skipping reconnection for {}.",
                            seatId,
                            furnitureUuidStr,
                            player.getName().getString()
                        );
                    } else {
                        mounted = player.startRiding(furniture, true);
                    }

                    if (mounted) {
                        provider.assignSeat(player, seatId);
                        TiedUpMod.LOGGER.info(
                            "[Network] Re-mounted {} in furniture {} seat '{}'",
                            player.getName().getString(),
                            furnitureUuidStr,
                            seatId
                        );
                    } else {
                        TiedUpMod.LOGGER.warn(
                            "[Network] Failed to re-mount {} in furniture {}. Teleporting to position.",
                            player.getName().getString(),
                            furnitureUuidStr
                        );
                        teleportPlayerTo(
                            player,
                            (ServerLevel) furniture.level(),
                            furniture.getX(),
                            furniture.getY(),
                            furniture.getZ()
                        );
                        persistentData.remove("tiedup_locked_furniture");
                    }
                }
            )
        );
    }

    /** Delay before re-mounting on reconnection (in ticks). Allows entity spawn to complete. */
    private static final int FURNITURE_RECONNECT_DELAY = 10;

    /**
     * Teleport a player to a position, handling cross-dimension transfer if needed.
     * Uses the same-dimension {@code teleportTo(x, y, z)} for same-level moves,
     * and TeleportHelper for cross-dimension moves.
     */
    private static void teleportPlayerTo(
        ServerPlayer player,
        ServerLevel targetLevel,
        double x,
        double y,
        double z
    ) {
        if (player.serverLevel() == targetLevel) {
            player.teleportTo(x, y, z);
        } else {
            // Cross-dimension: use the project's TeleportHelper for correct handling
            com.tiedup.remake.util.teleport.Position pos =
                new com.tiedup.remake.util.teleport.Position(
                    x,
                    y,
                    z,
                    targetLevel.dimension()
                );
            com.tiedup.remake.util.teleport.TeleportHelper.teleportEntity(
                player,
                pos
            );
        }
    }

    /**
     * Search for a furniture entity near the given position, matching the expected UUID.
     * Searches a small area around the stored position to account for entity position drift.
     *
     * @param level the server level to search in
     * @param pos the approximate position of the furniture
     * @param expectedUuid the UUID of the furniture entity
     * @return the entity if found, or null
     */
    @Nullable
    private static Entity findFurnitureEntity(
        ServerLevel level,
        BlockPos pos,
        UUID expectedUuid
    ) {
        // Search in a small area around the stored position (furniture shouldn't move,
        // but entity positions can drift slightly due to floating point)
        AABB searchBox = new AABB(pos).inflate(2.0);
        java.util.List<EntityFurniture> entities = level.getEntitiesOfClass(
            EntityFurniture.class,
            searchBox,
            e -> e.isAlive() && !e.isRemoved()
        );

        // First, try to find by UUID (most reliable)
        for (EntityFurniture entity : entities) {
            if (entity.getUUID().equals(expectedUuid)) {
                return entity;
            }
        }

        // Fallback: if UUID doesn't match (entity recreated by chunk reload),
        // find the nearest furniture at approximately the same position
        for (EntityFurniture entity : entities) {
            if (entity.blockPosition().equals(pos)) {
                TiedUpMod.LOGGER.debug(
                    "[Network] Furniture UUID mismatch but position matches at {}. " +
                        "Using entity {} instead of expected {}.",
                    pos,
                    entity.getUUID(),
                    expectedUuid
                );
                return entity;
            }
        }

        return null;
    }
}
