package com.tiedup.remake.client.animation.tick;

import com.tiedup.remake.client.animation.BondageAnimationManager;
import com.tiedup.remake.client.animation.context.AnimationContext;
import com.tiedup.remake.client.animation.context.AnimationContextResolver;
import com.tiedup.remake.client.animation.context.RegionBoneMapper;
import com.tiedup.remake.client.animation.util.AnimationIdBuilder;
import com.tiedup.remake.client.gltf.GltfAnimationApplier;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.entities.ai.master.MasterState;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.client.Minecraft;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;

/**
 * Tick handler for NPC (AbstractTiedUpNpc) bondage animations.
 *
 * <p>Same pattern as AnimationTickHandler for players, but for loaded
 * AbstractTiedUpNpc instances. Tracks last animation ID per NPC UUID and
 * triggers BondageAnimationManager.playAnimation() on state changes.
 *
 * <p>Extracted from DamselModel.setupAnim() to decouple animation
 * triggering from model rendering.
 */
@OnlyIn(Dist.CLIENT)
@Mod.EventBusSubscriber(
    modid = "tiedup",
    value = Dist.CLIENT,
    bus = Mod.EventBusSubscriber.Bus.FORGE
)
public class NpcAnimationTickHandler {

    /** Track last animation ID per NPC to avoid redundant updates */
    private static final Map<UUID, String> lastNpcAnimId =
        new ConcurrentHashMap<>();

    /**
     * Client tick: update animations for all loaded AbstractTiedUpNpc instances.
     */
    @SubscribeEvent
    public static void onClientTick(TickEvent.ClientTickEvent event) {
        if (event.phase != TickEvent.Phase.END) {
            return;
        }

        Minecraft mc = Minecraft.getInstance();
        if (mc.level == null || mc.isPaused()) {
            return;
        }

        for (Entity entity : mc.level.entitiesForRendering()) {
            if (
                entity instanceof AbstractTiedUpNpc damsel &&
                entity.isAlive() &&
                !entity.isRemoved()
            ) {
                updateNpcAnimation(damsel);
            }
        }
    }

    /**
     * Update animation for a single NPC.
     *
     * <p>Dual-layer V2 path: if the highest-priority equipped V2 item has a GLB model,
     * uses {@link GltfAnimationApplier#applyV2Animation} which plays a context layer
     * (base posture) and an item layer (GLB-driven bones). Sitting and kneeling are
     * handled by the context resolver, so the V2 path now covers all postures.
     *
     * <p>V1 fallback: if no V2 GLB model is found, falls back to JSON-based
     * PlayerAnimator animations via {@link BondageAnimationManager}.
     */
    private static void updateNpcAnimation(AbstractTiedUpNpc entity) {
        boolean inPose =
            entity.isTiedUp() || entity.isSitting() || entity.isKneeling();

        UUID uuid = entity.getUUID();

        if (inPose) {
            // Resolve V2 equipment map
            IV2BondageEquipment equipment = V2EquipmentHelper.getEquipment(
                entity
            );
            Map<BodyRegionV2, net.minecraft.world.item.ItemStack> equipped =
                equipment != null ? equipment.getAllEquipped() : Map.of();
            RegionBoneMapper.GlbModelResult glbResult =
                RegionBoneMapper.resolveWinningItem(equipped);

            if (glbResult != null) {
                // V2 path: dual-layer animation with per-item bone ownership
                RegionBoneMapper.BoneOwnership ownership =
                    RegionBoneMapper.computePerItemParts(
                        equipped,
                        glbResult.winningItem()
                    );
                AnimationContext context = AnimationContextResolver.resolveNpc(
                    entity
                );
                GltfAnimationApplier.applyV2Animation(
                    entity,
                    glbResult.modelLoc(),
                    glbResult.animSource(),
                    context,
                    ownership
                );
                lastNpcAnimId.remove(uuid);
            } else {
                // V1 fallback: JSON-based PlayerAnimator animations
                if (GltfAnimationApplier.hasActiveState(entity)) {
                    GltfAnimationApplier.clearV2Animation(entity);
                }

                String animId = buildNpcAnimationId(entity);
                String lastId = lastNpcAnimId.get(uuid);
                if (!animId.equals(lastId)) {
                    BondageAnimationManager.playAnimation(entity, animId);
                    lastNpcAnimId.put(uuid, animId);
                }
            }
        } else {
            if (
                lastNpcAnimId.containsKey(uuid) ||
                GltfAnimationApplier.hasActiveState(entity)
            ) {
                if (GltfAnimationApplier.hasActiveState(entity)) {
                    GltfAnimationApplier.clearV2Animation(entity);
                } else {
                    BondageAnimationManager.stopAnimation(entity);
                }
                lastNpcAnimId.remove(uuid);
            }
        }
    }

    /**
     * Build animation ID for an NPC from its current state (V1 path).
     */
    private static String buildNpcAnimationId(AbstractTiedUpNpc entity) {
        // Determine position prefix for SIT/KNEEL poses
        String positionPrefix = null;
        if (entity.isSitting()) {
            positionPrefix = "sit";
        } else if (entity.isKneeling()) {
            positionPrefix = "kneel";
        }

        net.minecraft.world.item.ItemStack bind = entity.getEquipment(
            BodyRegionV2.ARMS
        );
        PoseType poseType = PoseType.STANDARD;
        boolean hasBind = false;

        if (bind.getItem() instanceof ItemBind itemBind) {
            poseType = itemBind.getPoseType();
            hasBind = true;
        }

        // Derive bound state from V2 regions (AbstractTiedUpNpc implements IV2EquipmentHolder)
        boolean armsBound = V2EquipmentHelper.isRegionOccupied(
            entity,
            BodyRegionV2.ARMS
        );
        boolean legsBound = V2EquipmentHelper.isRegionOccupied(
            entity,
            BodyRegionV2.LEGS
        );

        // V1 fallback: if no V2 regions set but NPC has a bind, derive from ItemBind NBT
        if (!armsBound && !legsBound && bind.getItem() instanceof ItemBind) {
            armsBound = ItemBind.hasArmsBound(bind);
            legsBound = ItemBind.hasLegsBound(bind);
        }

        boolean isStruggling = entity.isStruggling();
        boolean isSneaking = entity.isCrouching();
        boolean isMoving =
            entity.getDeltaMovement().horizontalDistanceSqr() > 1e-6;

        String animId = AnimationIdBuilder.build(
            poseType,
            armsBound,
            legsBound,
            positionPrefix,
            isStruggling,
            hasBind,
            isSneaking,
            isMoving
        );

        // Master NPC sitting on human chair: use dedicated sitting animation
        if (
            entity instanceof EntityMaster masterEntity &&
            masterEntity.getMasterState() == MasterState.HUMAN_CHAIR &&
            masterEntity.isSitting()
        ) {
            animId = "master_chair_sit_idle";
        }

        return animId;
    }

    /**
     * Clear all NPC animation state.
     * Called on world unload to prevent memory leaks.
     */
    public static void clearAll() {
        lastNpcAnimId.clear();
    }
}
