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.PoseType;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
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<>();

    /**
     * NPCs currently in a posed state (tied / sitting / kneeling). Values hold
     * the live entity reference so the per-tick fast path doesn't need to
     * resolve UUID → Entity (the client level doesn't expose a UUID index).
     * Populated by {@link #fullSweep}; cleared by {@link #updateNpcAnimation}
     * on pose exit, and by {@link #remove} on {@code EntityLeaveLevelEvent}.
     */
    private static final Map<UUID, AbstractTiedUpNpc> ACTIVE_NPCS =
        new ConcurrentHashMap<>();

    /**
     * Tick-count gate for the periodic full-entity sweep. A low-frequency
     * O(N) fallback catches NPCs that entered the posed state via paths the
     * fast path hasn't seen yet (e.g. just-spawned, just-loaded-into-chunk,
     * state flipped by a packet we didn't mirror into ACTIVE_NPCS). 20 ticks
     * ≈ 1 second — latency is invisible in practice.
     */
    private static int sweepCounter = 0;
    private static final int FULL_SWEEP_INTERVAL_TICKS = 20;

    /**
     * Client tick: update animations for posed NPCs.
     *
     * <p>Fast path (19 of every 20 ticks): iterate only {@link #ACTIVE_NPCS}
     * — typically 1–5 entries — so the cost is O(|active|) instead of
     * O(|all client entities|). Full sweep (every 20th tick): re-scan
     * {@code entitiesForRendering()} to discover NPCs that entered the pose
     * via an untracked path.</p>
     */
    @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;
        }

        sweepCounter++;
        if (sweepCounter >= FULL_SWEEP_INTERVAL_TICKS) {
            sweepCounter = 0;
            fullSweep(mc);
        } else {
            fastTick();
        }
    }

    /**
     * Fast path: iterate only tracked posed NPCs. Entities that have died or
     * been removed from the level are dropped from the set here so stale
     * references don't linger between full sweeps.
     */
    private static void fastTick() {
        // ConcurrentHashMap.values() iterator is weakly consistent, so
        // concurrent remove() during iteration (from updateNpcAnimation) is
        // explicitly supported by the JDK contract.
        for (AbstractTiedUpNpc npc : ACTIVE_NPCS.values()) {
            if (!npc.isAlive() || npc.isRemoved()) {
                ACTIVE_NPCS.remove(npc.getUUID());
                continue;
            }
            updateNpcAnimation(npc);
        }
    }

    /**
     * Fallback sweep: O(N) over all rendered entities. Adds newly-posed NPCs
     * to {@link #ACTIVE_NPCS} via {@link #updateNpcAnimation}; also runs the
     * same logic for already-tracked NPCs, which is idempotent.
     */
    private static void fullSweep(Minecraft mc) {
        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>Legacy fallback: if no GLB model is found, falls back to JSON-based
     * PlayerAnimator animations via {@link BondageAnimationManager}.
     *
     * <p><b>For future contributors</b>: this method is the sole writer of the
     * {@link #ACTIVE_NPCS} fast-path set. Any new code that flips an NPC into a
     * posed state (new packet handler, new AI transition, etc.) should call
     * this method directly — otherwise the NPC will not be animated until the
     * next 1 Hz full sweep picks it up (~1 s visible latency). If the worst-
     * case latency matters for your use case, call
     * {@link #updateNpcAnimation(AbstractTiedUpNpc)} yourself to register the
     * NPC immediately.</p>
     */
    private static void updateNpcAnimation(AbstractTiedUpNpc entity) {
        boolean inPose =
            entity.isTiedUp() || entity.isSitting() || entity.isKneeling();

        UUID uuid = entity.getUUID();

        // Track/untrack in ACTIVE_NPCS so the fast-tick path sees state
        // transitions as soon as they're observed here. Idempotent put/remove
        // — no double-tracking and no missed removal even if two code paths
        // race to the same update.
        if (inPose) {
            ACTIVE_NPCS.put(uuid, entity);
        } else {
            ACTIVE_NPCS.remove(uuid);
        }

        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 {
                // Legacy 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 (legacy JSON 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 = PoseTypeHelper.getPoseType(bind);
        boolean hasBind = BindModeHelper.isBindItem(bind);

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

        // Legacy fallback: if no V2 regions set but NPC has a bind, derive from bind mode NBT
        if (!armsBound && !legsBound && BindModeHelper.isBindItem(bind)) {
            armsBound = BindModeHelper.hasArmsBound(bind);
            legsBound = BindModeHelper.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();
        ACTIVE_NPCS.clear();
    }

    /**
     * Remove an individual NPC's animation state.
     * Called from {@link com.tiedup.remake.client.events.EntityCleanupHandler}
     * on {@code EntityLeaveLevelEvent} so the per-UUID map doesn't accumulate
     * stale entries from dead/unloaded NPCs between world unloads.
     */
    public static void remove(java.util.UUID uuid) {
        lastNpcAnimId.remove(uuid);
        ACTIVE_NPCS.remove(uuid);
    }
}
