package com.tiedup.remake.client.animation;

import com.mojang.logging.LogUtils;
import com.tiedup.remake.v2.furniture.ISeatProvider;
import dev.kosmx.playerAnim.api.layered.IAnimation;
import dev.kosmx.playerAnim.api.layered.KeyframeAnimationPlayer;
import dev.kosmx.playerAnim.api.layered.ModifierLayer;
import dev.kosmx.playerAnim.core.data.KeyframeAnimation;
import dev.kosmx.playerAnim.impl.IAnimatedPlayer;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationFactory;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.slf4j.Logger;

/**
 * Unified animation manager for bondage animations.
 *
 * <p>Handles both players and NPCs (any entity implementing IAnimatedPlayer).
 * Uses PlayerAnimator library for smooth keyframe animations with bendy-lib support.
 *
 * <p>This replaces the previous split system:
 * <ul>
 *   <li>PlayerAnimatorBridge (for players)</li>
 *   <li>DamselAnimationManager (for NPCs)</li>
 * </ul>
 */
@OnlyIn(Dist.CLIENT)
public class BondageAnimationManager {

    private static final Logger LOGGER = LogUtils.getLogger();

    /** Cache of ModifierLayers for NPC entities (players use PlayerAnimationAccess) */
    private static final Map<UUID, ModifierLayer<IAnimation>> npcLayers =
        new ConcurrentHashMap<>();

    /** Cache of context ModifierLayers for NPC entities */
    private static final Map<UUID, ModifierLayer<IAnimation>> npcContextLayers =
        new ConcurrentHashMap<>();

    /** Cache of furniture ModifierLayers for NPC entities */
    private static final Map<
        UUID,
        ModifierLayer<IAnimation>
    > npcFurnitureLayers = new ConcurrentHashMap<>();

    /** Factory ID for PlayerAnimator item layer (players only) */
    private static final ResourceLocation FACTORY_ID =
        ResourceLocation.fromNamespaceAndPath("tiedup", "bondage");

    /** Factory ID for PlayerAnimator context layer (players only) */
    private static final ResourceLocation CONTEXT_FACTORY_ID =
        ResourceLocation.fromNamespaceAndPath("tiedup", "bondage_context");

    /** Factory ID for PlayerAnimator furniture layer (players only) */
    private static final ResourceLocation FURNITURE_FACTORY_ID =
        ResourceLocation.fromNamespaceAndPath("tiedup", "bondage_furniture");

    /** Priority for context animation layer (lower = overridable by item layer) */
    private static final int CONTEXT_LAYER_PRIORITY = 40;
    /** Priority for item animation layer (higher = overrides context layer) */
    private static final int ITEM_LAYER_PRIORITY = 42;
    /**
     * Priority for furniture animation layer (highest = overrides item layer on blocked bones).
     * Non-blocked bones are disabled so items can still animate them via the item layer.
     */
    private static final int FURNITURE_LAYER_PRIORITY = 43;

    /** Number of ticks to wait before removing a stale furniture animation. */
    private static final int FURNITURE_GRACE_TICKS = 3;

    /**
     * Tracks ticks since a player with an active furniture animation stopped riding
     * an ISeatProvider. After {@link #FURNITURE_GRACE_TICKS}, the animation is removed
     * to prevent stuck poses from entity death or network issues.
     *
     * <p>Uses ConcurrentHashMap for safe access from both client tick and render thread.</p>
     */
    private static final Map<UUID, Integer> furnitureGraceTicks =
        new ConcurrentHashMap<>();

    /**
     * Initialize the animation system.
     * Must be called during client setup to register the player animation factory.
     */
    public static void init() {
        LOGGER.info("BondageAnimationManager initializing...");

        // Context layer: lower priority = evaluated first, overridable by item layer.
        // In AnimationStack, layers are sorted ascending by priority and evaluated in order.
        // Higher priority layers override lower ones.
        PlayerAnimationFactory.ANIMATION_DATA_FACTORY.registerFactory(
            CONTEXT_FACTORY_ID,
            CONTEXT_LAYER_PRIORITY,
            player -> new ModifierLayer<>()
        );

        // Item layer: higher priority = evaluated last, overrides context layer
        PlayerAnimationFactory.ANIMATION_DATA_FACTORY.registerFactory(
            FACTORY_ID,
            ITEM_LAYER_PRIORITY,
            player -> new ModifierLayer<>()
        );

        // Furniture layer: highest priority = overrides item layer on blocked bones.
        // Non-blocked bones are disabled via FurnitureAnimationContext so items
        // can still animate free regions (gag, blindfold, etc.).
        PlayerAnimationFactory.ANIMATION_DATA_FACTORY.registerFactory(
            FURNITURE_FACTORY_ID,
            FURNITURE_LAYER_PRIORITY,
            player -> new ModifierLayer<>()
        );

        LOGGER.info(
            "BondageAnimationManager: Factories registered — context (pri {}), item (pri {}), furniture (pri {})",
            CONTEXT_LAYER_PRIORITY,
            ITEM_LAYER_PRIORITY,
            FURNITURE_LAYER_PRIORITY
        );
    }

    // PLAY ANIMATION

    /**
     * Play an animation on any entity (player or NPC).
     *
     * @param entity The entity to animate
     * @param animId Animation ID string (will be prefixed with "tiedup:" namespace)
     * @return true if animation started successfully, false if layer not available
     */
    public static boolean playAnimation(LivingEntity entity, String animId) {
        ResourceLocation location = ResourceLocation.fromNamespaceAndPath(
            "tiedup",
            animId
        );
        return playAnimation(entity, location);
    }

    /**
     * Play an animation on any entity (player or NPC).
     *
     * <p>If the animation layer is not available (e.g., remote player not fully
     * initialized), the animation will be queued for retry via PendingAnimationManager.
     *
     * @param entity The entity to animate
     * @param animId Full ResourceLocation of the animation
     * @return true if animation started successfully, false if layer not available
     */
    public static boolean playAnimation(
        LivingEntity entity,
        ResourceLocation animId
    ) {
        if (entity == null || !entity.level().isClientSide()) {
            return false;
        }

        KeyframeAnimation anim = PlayerAnimationRegistry.getAnimation(animId);
        if (anim == null) {
            // Try fallback: remove _sneak_ suffix if present
            ResourceLocation fallbackId = tryFallbackAnimation(animId);
            if (fallbackId != null) {
                anim = PlayerAnimationRegistry.getAnimation(fallbackId);
                if (anim != null) {
                    LOGGER.debug(
                        "Using fallback animation '{}' for missing '{}'",
                        fallbackId,
                        animId
                    );
                }
            }
            if (anim == null) {
                LOGGER.warn("Animation not found in registry: {}", animId);
                return false;
            }
        }

        ModifierLayer<IAnimation> layer = getOrCreateLayer(entity);
        if (layer != null) {
            // Check if same animation is already playing
            // Use reference comparison (==) instead of equals() because:
            // 1. PlayerAnimationRegistry caches animations by ID
            // 2. Same ID = same cached object reference
            // 3. This avoids issues with KeyframeAnimation.equals() implementation
            IAnimation current = layer.getAnimation();
            if (current instanceof KeyframeAnimationPlayer player) {
                if (player.getData() == anim) {
                    // Same animation already playing, don't reset
                    return true; // Still counts as success
                }
            }
            layer.setAnimation(new KeyframeAnimationPlayer(anim));

            // Remove from pending queue if it was waiting
            PendingAnimationManager.remove(entity.getUUID());

            LOGGER.debug(
                "Playing animation '{}' on entity: {}",
                animId,
                entity.getUUID()
            );
            return true;
        } else {
            // Layer not available - queue for retry if it's a player
            if (entity instanceof AbstractClientPlayer) {
                PendingAnimationManager.queueForRetry(
                    entity.getUUID(),
                    animId.getPath()
                );
                LOGGER.debug(
                    "Animation layer not ready for {}, queued for retry",
                    entity.getName().getString()
                );
            } else {
                LOGGER.warn(
                    "Animation layer is NULL for NPC: {} (type: {})",
                    entity.getName().getString(),
                    entity.getClass().getSimpleName()
                );
            }
            return false;
        }
    }

    /**
     * Play a pre-converted KeyframeAnimation directly on an entity, bypassing the registry.
     * Used by GltfAnimationApplier for GLB-converted poses.
     *
     * @param entity The entity to animate
     * @param anim   The KeyframeAnimation to play
     * @return true if animation started successfully
     */
    public static boolean playDirect(
        LivingEntity entity,
        KeyframeAnimation anim
    ) {
        if (entity == null || anim == null || !entity.level().isClientSide()) {
            return false;
        }

        ModifierLayer<IAnimation> layer = getOrCreateLayer(entity);
        if (layer != null) {
            IAnimation current = layer.getAnimation();
            if (current instanceof KeyframeAnimationPlayer player) {
                if (player.getData() == anim) {
                    return true; // Same animation already playing
                }
            }
            layer.setAnimation(new KeyframeAnimationPlayer(anim));
            PendingAnimationManager.remove(entity.getUUID());
            return true;
        }
        return false;
    }

    // STOP ANIMATION

    /**
     * Stop any currently playing animation on an entity.
     *
     * @param entity The entity
     */
    public static void stopAnimation(LivingEntity entity) {
        if (entity == null || !entity.level().isClientSide()) {
            return;
        }

        ModifierLayer<IAnimation> layer = getLayer(entity);
        if (layer != null) {
            layer.setAnimation(null);
            LOGGER.debug("Stopped animation on entity: {}", entity.getUUID());
        }
    }

    // LAYER MANAGEMENT

    /**
     * Get the ModifierLayer for an entity (without creating).
     */
    private static ModifierLayer<IAnimation> getLayer(LivingEntity entity) {
        // Players: try PlayerAnimationAccess first, then cache
        if (entity instanceof AbstractClientPlayer player) {
            ModifierLayer<IAnimation> factoryLayer = getPlayerLayer(player);
            if (factoryLayer != null) {
                return factoryLayer;
            }
            // Check cache (for remote players using fallback)
            return npcLayers.get(entity.getUUID());
        }

        // NPCs: use cache
        return npcLayers.get(entity.getUUID());
    }

    /**
     * Get or create the ModifierLayer for an entity.
     */
    @SuppressWarnings("unchecked")
    private static ModifierLayer<IAnimation> getOrCreateLayer(
        LivingEntity entity
    ) {
        UUID uuid = entity.getUUID();

        // Players: try factory-based access first, fallback to direct stack access
        if (entity instanceof AbstractClientPlayer player) {
            // Try the registered factory first (works for local player)
            ModifierLayer<IAnimation> factoryLayer = getPlayerLayer(player);
            if (factoryLayer != null) {
                return factoryLayer;
            }

            // Fallback for remote players: use direct stack access like NPCs
            // This handles cases where the factory data isn't available
            if (player instanceof IAnimatedPlayer animated) {
                return npcLayers.computeIfAbsent(uuid, k -> {
                    ModifierLayer<IAnimation> newLayer = new ModifierLayer<>();
                    animated
                        .getAnimationStack()
                        .addAnimLayer(ITEM_LAYER_PRIORITY, newLayer);
                    LOGGER.info(
                        "Created animation layer for remote player via stack: {}",
                        player.getName().getString()
                    );
                    return newLayer;
                });
            }
        }

        // NPCs implementing IAnimatedPlayer: create/cache layer
        if (entity instanceof IAnimatedPlayer animated) {
            return npcLayers.computeIfAbsent(uuid, k -> {
                ModifierLayer<IAnimation> newLayer = new ModifierLayer<>();
                animated
                    .getAnimationStack()
                    .addAnimLayer(ITEM_LAYER_PRIORITY, newLayer);
                LOGGER.debug("Created animation layer for NPC: {}", uuid);
                return newLayer;
            });
        }

        LOGGER.warn(
            "Entity {} does not support animations (not a player or IAnimatedPlayer)",
            uuid
        );
        return null;
    }

    /**
     * Get the animation layer for a player from PlayerAnimationAccess.
     */
    @SuppressWarnings("unchecked")
    private static ModifierLayer<IAnimation> getPlayerLayer(
        AbstractClientPlayer player
    ) {
        try {
            return (ModifierLayer<
                IAnimation
            >) PlayerAnimationAccess.getPlayerAssociatedData(player).get(
                FACTORY_ID
            );
        } catch (Exception e) {
            LOGGER.error(
                "Failed to get animation layer for player: {}",
                player.getName().getString(),
                e
            );
            return null;
        }
    }

    /**
     * Safely get the animation layer for a player.
     * Returns null if the layer is not yet initialized.
     *
     * <p>Public method for PendingAnimationManager to access.
     * Checks both the factory-based layer and the NPC cache fallback.
     *
     * @param player The player
     * @return The animation layer, or null if not available
     */
    @javax.annotation.Nullable
    public static ModifierLayer<IAnimation> getPlayerLayerSafe(
        AbstractClientPlayer player
    ) {
        // Try factory first
        ModifierLayer<IAnimation> factoryLayer = getPlayerLayer(player);
        if (factoryLayer != null) {
            return factoryLayer;
        }

        // Check NPC cache (for remote players using fallback path)
        return npcLayers.get(player.getUUID());
    }

    // CONTEXT LAYER (lower priority, for sit/kneel/sneak)

    /**
     * Get the context animation layer for a player from PlayerAnimationAccess.
     * Returns null if the layer is not yet initialized.
     */
    @SuppressWarnings("unchecked")
    @javax.annotation.Nullable
    private static ModifierLayer<IAnimation> getPlayerContextLayer(
        AbstractClientPlayer player
    ) {
        try {
            return (ModifierLayer<
                IAnimation
            >) PlayerAnimationAccess.getPlayerAssociatedData(player).get(
                CONTEXT_FACTORY_ID
            );
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Get or create the context animation layer for an NPC entity.
     * Uses CONTEXT_LAYER_PRIORITY, below the item layer at ITEM_LAYER_PRIORITY.
     */
    @javax.annotation.Nullable
    private static ModifierLayer<IAnimation> getOrCreateNpcContextLayer(
        LivingEntity entity
    ) {
        if (entity instanceof IAnimatedPlayer animated) {
            return npcContextLayers.computeIfAbsent(entity.getUUID(), k -> {
                ModifierLayer<IAnimation> layer = new ModifierLayer<>();
                animated
                    .getAnimationStack()
                    .addAnimLayer(CONTEXT_LAYER_PRIORITY, layer);
                return layer;
            });
        }
        return null;
    }

    /**
     * Play a context animation on the context layer (lower priority).
     * Context animations (sit, kneel, sneak) can be overridden by item animations
     * on the main layer which has higher priority.
     *
     * @param entity The entity to animate
     * @param anim   The KeyframeAnimation to play on the context layer
     * @return true if animation started successfully
     */
    public static boolean playContext(
        LivingEntity entity,
        KeyframeAnimation anim
    ) {
        if (entity == null || anim == null || !entity.level().isClientSide()) {
            return false;
        }

        ModifierLayer<IAnimation> layer;
        if (entity instanceof AbstractClientPlayer player) {
            layer = getPlayerContextLayer(player);
        } else {
            layer = getOrCreateNpcContextLayer(entity);
        }

        if (layer != null) {
            layer.setAnimation(new KeyframeAnimationPlayer(anim));
            return true;
        }
        return false;
    }

    /**
     * Stop the context layer animation.
     *
     * @param entity The entity whose context animation should stop
     */
    public static void stopContext(LivingEntity entity) {
        if (entity == null || !entity.level().isClientSide()) {
            return;
        }

        ModifierLayer<IAnimation> layer;
        if (entity instanceof AbstractClientPlayer player) {
            layer = getPlayerContextLayer(player);
        } else {
            layer = npcContextLayers.get(entity.getUUID());
        }

        if (layer != null) {
            layer.setAnimation(null);
        }
    }

    // FURNITURE LAYER (highest priority, for seat poses)

    /**
     * Play a furniture animation on the furniture layer (highest priority).
     *
     * <p>The furniture layer sits above the item layer so it controls blocked-region
     * bones. Non-blocked bones should already be disabled in the provided animation
     * (via {@link com.tiedup.remake.v2.furniture.client.FurnitureAnimationContext#create}).
     * This allows bondage items on free regions to still animate via the item layer.</p>
     *
     * @param player    the player to animate
     * @param animation the KeyframeAnimation from FurnitureAnimationContext
     * @return true if animation started successfully
     */
    public static boolean playFurniture(
        Player player,
        KeyframeAnimation animation
    ) {
        if (
            player == null ||
            animation == null ||
            !player.level().isClientSide()
        ) {
            return false;
        }

        ModifierLayer<IAnimation> layer = getFurnitureLayer(player);
        if (layer != null) {
            layer.setAnimation(new KeyframeAnimationPlayer(animation));
            // Reset grace ticks since we just started/refreshed the animation
            furnitureGraceTicks.remove(player.getUUID());
            LOGGER.debug(
                "Playing furniture animation on player: {}",
                player.getName().getString()
            );
            return true;
        }

        LOGGER.warn(
            "Furniture layer not available for player: {}",
            player.getName().getString()
        );
        return false;
    }

    /**
     * Stop the furniture layer animation for a player.
     *
     * @param player the player whose furniture animation should stop
     */
    public static void stopFurniture(Player player) {
        if (player == null || !player.level().isClientSide()) {
            return;
        }

        ModifierLayer<IAnimation> layer = getFurnitureLayer(player);
        if (layer != null) {
            layer.setAnimation(null);
        }
        furnitureGraceTicks.remove(player.getUUID());
        LOGGER.debug(
            "Stopped furniture animation on player: {}",
            player.getName().getString()
        );
    }

    /**
     * Check whether a player currently has an active furniture animation.
     *
     * @param player the player to check
     * @return true if the furniture layer has an active animation
     */
    public static boolean hasFurnitureAnimation(Player player) {
        if (player == null || !player.level().isClientSide()) {
            return false;
        }

        ModifierLayer<IAnimation> layer = getFurnitureLayer(player);
        return layer != null && layer.getAnimation() != null;
    }

    /**
     * Get the furniture ModifierLayer for a player.
     * Uses PlayerAnimationAccess for local/factory-registered players,
     * falls back to NPC cache for remote players.
     */
    @SuppressWarnings("unchecked")
    @javax.annotation.Nullable
    private static ModifierLayer<IAnimation> getFurnitureLayer(Player player) {
        if (player instanceof AbstractClientPlayer clientPlayer) {
            try {
                ModifierLayer<IAnimation> layer = (ModifierLayer<
                    IAnimation
                >) PlayerAnimationAccess.getPlayerAssociatedData(
                    clientPlayer
                ).get(FURNITURE_FACTORY_ID);
                if (layer != null) {
                    return layer;
                }
            } catch (Exception e) {
                // Fall through to NPC cache
            }

            // Fallback for remote players: check NPC furniture cache
            return npcFurnitureLayers.get(player.getUUID());
        }

        // Non-player entities: use NPC cache
        return npcFurnitureLayers.get(player.getUUID());
    }

    /**
     * Safety tick for furniture animations. Call once per client tick per player.
     *
     * <p>If a player has an active furniture animation but is NOT riding an
     * {@link ISeatProvider}, increment a grace counter. After
     * {@link #FURNITURE_GRACE_TICKS} consecutive ticks without a seat, the
     * animation is removed to prevent stuck poses from entity death, network
     * desync, or teleportation.</p>
     *
     * <p>If the player IS riding an ISeatProvider, the counter is reset.</p>
     *
     * @param player the player to check
     */
    public static void tickFurnitureSafety(Player player) {
        if (player == null || !player.level().isClientSide()) {
            return;
        }

        if (!hasFurnitureAnimation(player)) {
            // No furniture animation active, nothing to guard
            furnitureGraceTicks.remove(player.getUUID());
            return;
        }

        UUID uuid = player.getUUID();

        // Check if the player is riding an ISeatProvider
        Entity vehicle = player.getVehicle();
        boolean ridingSeat = vehicle instanceof ISeatProvider;

        if (ridingSeat) {
            // Player is properly seated, reset grace counter
            furnitureGraceTicks.remove(uuid);
        } else {
            // Player has furniture anim but no seat -- increment grace
            int ticks = furnitureGraceTicks.merge(uuid, 1, Integer::sum);
            if (ticks >= FURNITURE_GRACE_TICKS) {
                LOGGER.info(
                    "Removing stale furniture animation for player {} " +
                        "(not riding ISeatProvider for {} ticks)",
                    player.getName().getString(),
                    ticks
                );
                stopFurniture(player);
            }
        }
    }

    // FALLBACK ANIMATION HANDLING

    /**
     * Try to find a fallback animation ID when the requested one doesn't exist.
     *
     * <p>Fallback chain:
     * <ol>
     *   <li>Remove _sneak_ suffix (sneak variants often missing)</li>
     *   <li>For sit_dog/kneel_dog variants, fall back to basic standing DOG</li>
     *   <li>For _arms_ variants, try FULL variant</li>
     * </ol>
     *
     * @param originalId The original animation ID that wasn't found
     * @return A fallback ResourceLocation to try, or null if no fallback
     */
    @javax.annotation.Nullable
    private static ResourceLocation tryFallbackAnimation(
        ResourceLocation originalId
    ) {
        String path = originalId.getPath();
        String namespace = originalId.getNamespace();

        // 1. Remove _sneak_ suffix
        if (path.contains("_sneak_")) {
            String fallback = path.replace("_sneak_", "_");
            return ResourceLocation.fromNamespaceAndPath(namespace, fallback);
        }

        // 2. sit_dog_* / kneel_dog_* -> tied_up_dog_*
        if (path.startsWith("sit_dog_") || path.startsWith("kneel_dog_")) {
            String suffix = path.substring(path.lastIndexOf("_")); // _idle or _struggle
            return ResourceLocation.fromNamespaceAndPath(
                namespace,
                "tied_up_dog" + suffix
            );
        }

        // 3. _arms_ variants -> try FULL variant (remove _arms)
        if (path.contains("_arms_")) {
            String fallback = path.replace("_arms_", "_");
            return ResourceLocation.fromNamespaceAndPath(namespace, fallback);
        }

        // 4. Struggle variants for free/legs -> idle variant
        if (
            (path.startsWith("sit_free_") ||
                path.startsWith("kneel_free_") ||
                path.startsWith("sit_legs_") ||
                path.startsWith("kneel_legs_")) &&
            path.endsWith("_struggle")
        ) {
            String fallback = path.replace("_struggle", "_idle");
            return ResourceLocation.fromNamespaceAndPath(namespace, fallback);
        }

        return null;
    }

    // CLEANUP

    /**
     * Clean up animation layer for an NPC when it's removed.
     *
     * @param entityId UUID of the removed entity
     */
    /** All NPC layer caches, for bulk cleanup operations. */
    private static final Map<UUID, ModifierLayer<IAnimation>>[] ALL_NPC_CACHES =
        new Map[] { npcLayers, npcContextLayers, npcFurnitureLayers };

    public static void cleanup(UUID entityId) {
        for (Map<UUID, ModifierLayer<IAnimation>> cache : ALL_NPC_CACHES) {
            ModifierLayer<IAnimation> layer = cache.remove(entityId);
            if (layer != null) {
                layer.setAnimation(null);
            }
        }
        furnitureGraceTicks.remove(entityId);
        LOGGER.debug("Cleaned up animation layers for entity: {}", entityId);
    }

    /**
     * Clear all NPC animation layers.
     * Should be called on world unload.
     */
    public static void clearAll() {
        for (Map<UUID, ModifierLayer<IAnimation>> cache : ALL_NPC_CACHES) {
            cache.values().forEach(layer -> layer.setAnimation(null));
            cache.clear();
        }
        furnitureGraceTicks.clear();
        LOGGER.info("Cleared all NPC animation layers");
    }
}
