package com.tiedup.remake.client.animation;

import com.mojang.logging.LogUtils;
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.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.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 item-layer ModifierLayers for NPC entities. */
    private static final Map<UUID, ModifierLayer<IAnimation>> npcLayers =
        new ConcurrentHashMap<>();

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

    /** 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;

    /**
     * Initialize the animation system.
     *
     * <p><b>Pipeline NPC-only</b> — depuis Phase 2.7, les joueurs sont tickés par
     * {@code RigAnimationTickHandler} via le renderer RIG patched. Aucune
     * {@link PlayerAnimationFactory} n'est enregistrée pour le joueur et tous
     * les chemins joueur dans cette classe sont de short-circuits logués.</p>
     *
     * <p>Cette classe reste active <b>uniquement pour les NPCs</b>
     * (entités implémentant {@link IAnimatedPlayer} qui ne sont pas un
     * {@link Player}) : {@link #getOrCreateLayer} leur crée un {@link ModifierLayer}
     * via accès direct au stack d'animation
     * ({@code animated.getAnimationStack().addAnimLayer(...)}) — ce path ne dépend
     * d'aucune factory. Consumer principal : {@code NpcAnimationTickHandler}.</p>
     *
     * <p>Conservé comme méthode publique pour ne pas casser les call sites
     * externes. Rework V3 (player anim natives RIG) : voir V3-REW-01 dans
     * {@code docs/plans/rig/V3_REWORK_BACKLOG.md}.</p>
     */
    public static void init() {
        LOGGER.info(
            "BondageAnimationManager: NPC-only pipeline (Phase 2.8 RIG cleanup). " +
                "Players handled by RigAnimationTickHandler; all player call sites no-op."
        );
    }

    // 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.
     *
     * <p><b>Phase 2.8</b> — les appels sur un {@link Player} sont no-op : le pipeline
     * joueur est désormais RIG-native (voir {@link #init} Javadoc). Un WARN est logué
     * une fois par UUID pour signaler les call sites stale qui devraient être purgés
     * lors du rework V3.</p>
     *
     * @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;
        }

        // Phase 2.8 : player path is dead. Log once per UUID and no-op.
        if (entity instanceof Player player) {
            logPlayerCallOnce(player, "playAnimation(" + animId + ")");
            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 (legacy, may still hold NPC entries)
            PendingAnimationManager.remove(entity.getUUID());

            LOGGER.debug(
                "Playing animation '{}' on entity: {}",
                animId,
                entity.getUUID()
            );
            return true;
        } 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;
        }

        // Phase 2.8 : player path is dead.
        if (entity instanceof Player player) {
            logPlayerCallOnce(player, "playDirect");
            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;
        }

        // Phase 2.8 : player path is dead — no layer to clear.
        if (entity instanceof Player) {
            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).
     *
     * <p>Phase 2.8 : returns {@code null} directly for any {@link Player} — the
     * player animation pipeline is RIG-native, this manager only tracks NPCs.</p>
     */
    private static ModifierLayer<IAnimation> getLayer(LivingEntity entity) {
        if (entity instanceof Player) {
            return null;
        }
        return npcLayers.get(entity.getUUID());
    }

    /**
     * Get or create the ModifierLayer for an entity.
     *
     * <p>Phase 2.8 : returns {@code null} directly for any {@link Player} — the
     * player fallback via {@code IAnimatedPlayer.getAnimationStack()} has been
     * retired because it was partially alive (FP vanilla render consumed it,
     * TP RIG override bypassed it), producing a confusing behavior split. All
     * player anim needs are now handled by {@code RigAnimationTickHandler}.</p>
     */
    private static ModifierLayer<IAnimation> getOrCreateLayer(
        LivingEntity entity
    ) {
        // Phase 2.8 : strip player path entirely (no partially-alive fallback).
        if (entity instanceof Player) {
            return null;
        }

        UUID uuid = entity.getUUID();

        // 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;
    }

    /** Per-player-UUID dedup so stale call sites log at most once per session. */
    private static final java.util.Set<UUID> playerCallLogged =
        java.util.concurrent.ConcurrentHashMap.newKeySet();

    /**
     * Log once per player UUID that a stale call site is invoking this manager.
     * Used by the player no-op short-circuits ({@link #playAnimation},
     * {@link #playDirect}) to surface call sites that should be migrated to the
     * RIG pipeline (tracked in V3_REWORK_BACKLOG).
     */
    private static void logPlayerCallOnce(Player player, String op) {
        if (playerCallLogged.add(player.getUUID())) {
            LOGGER.warn(
                "BondageAnimationManager.{} called on player {} — no-op " +
                    "(RIG owns player anims since Phase 2.7). " +
                    "Migrate call site to RigAnimationTickHandler (V3 rework).",
                op,
                player.getName().getString()
            );
        }
    }

    /**
     * Safely get the animation layer for a player.
     *
     * <p>Phase 2.8 : always returns {@code null}. The player pipeline is
     * RIG-native; the {@link PendingAnimationManager} retry loop is no
     * longer fed (player calls to {@link #playAnimation} short-circuit
     * before queueing), so this getter is maintained only to preserve the
     * public signature for external call sites.</p>
     *
     * @param player The player (unused)
     * @return always null in Phase 2.8+
     */
    @javax.annotation.Nullable
    public static ModifierLayer<IAnimation> getPlayerLayerSafe(
        AbstractClientPlayer player
    ) {
        return null;
    }

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

    /**
     * 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;
        }

        // Phase 2.8 : player context layer is dead (sit/kneel/sneak visuals
        // will be re-expressed as RIG StaticAnimations — cf. V3-REW-14).
        if (entity instanceof Player player) {
            logPlayerCallOnce(player, "playContext");
            return false;
        }

        ModifierLayer<IAnimation> 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;
        }

        // Phase 2.8 : player path is dead — no layer to clear.
        if (entity instanceof Player) {
            return;
        }

        ModifierLayer<IAnimation> 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;
        }

        // Phase 2.8 : player furniture seat pose is dead (will be ported to
        // RIG StaticAnimations — cf. V3_REWORK_BACKLOG furniture seat entry).
        logPlayerCallOnce(player, "playFurniture");
        return false;
    }

    /**
     * Stop the furniture layer animation for a player.
     *
     * <p>Phase 2.8 : no-op — the player furniture layer is dead. Kept for
     * signature compatibility with {@code EntityFurniture} cleanup call site.</p>
     *
     * @param player the player whose furniture animation should stop
     */
    public static void stopFurniture(Player player) {
        // Phase 2.8 : dead path. Retained signature for backward-compat.
    }

    /**
     * Check whether a player currently has an active furniture animation.
     *
     * <p>Phase 2.8 : always returns {@code false} — player furniture layer is dead.</p>
     *
     * @param player the player to check
     * @return always false in Phase 2.8+
     */
    public static boolean hasFurnitureAnimation(Player player) {
        return false;
    }

    /**
     * Safety tick for furniture animations.
     *
     * <p>Phase 2.8 : no-op — the player furniture layer is dead, nothing to
     * guard. Kept as an empty stub in case older call sites remain.</p>
     *
     * @param player the player to check (unused)
     */
    public static void tickFurnitureSafety(Player player) {
        // Phase 2.8 : dead path. Retained signature for backward-compat.
    }

    // 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. */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    private static final Map<UUID, ModifierLayer<IAnimation>>[] ALL_NPC_CACHES =
        new Map[] { npcLayers, npcContextLayers };

    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);
            }
        }
        playerCallLogged.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();
        }
        playerCallLogged.clear();
        LOGGER.info("Cleared all NPC animation layers");
    }
}
