package com.tiedup.remake.v2.bondage.movement;

import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.sync.PacketSyncMovementStyle;
import com.tiedup.remake.state.PlayerBindState;
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 net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.EntityDimensions;
import net.minecraft.world.entity.Pose;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.living.LivingEvent;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * Server-side manager for movement style mechanics.
 *
 * <p>Hooks into two events:
 * <ul>
 *   <li>{@code PlayerTickEvent(Phase.END)} at HIGH priority -- resolves style,
 *       manages lifecycle transitions, dispatches per-style tick logic. Runs after
 *       vanilla {@code travel()} so velocity modifications apply correctly.</li>
 *   <li>{@code LivingJumpEvent} -- suppresses jump for styles with jump disabled.
 *       {@code LivingJumpEvent} is NOT cancelable; jump is neutralized by subtracting
 *       the jump impulse from Y velocity.</li>
 * </ul>
 *
 * <p>Per-player state lives on {@link PlayerBindState} to piggyback on existing
 * lifecycle cleanup hooks (death, logout, dimension change).</p>
 *
 * @see MovementStyleResolver for resolution logic
 * @see MovementStyle for style definitions
 */
@Mod.EventBusSubscriber(
    modid = TiedUpMod.MOD_ID,
    bus = Mod.EventBusSubscriber.Bus.FORGE
)
public class MovementStyleManager {

    private static final Logger LOGGER = LogManager.getLogger("MovementStyles");

    // --- V1 legacy modifier UUID (H6 cleanup) ---
    // Source of truth: RestraintEffectUtils.BIND_SPEED_MODIFIER_UUID (same value).
    // RestraintEffectUtils used this UUID with ADDITION operation and addPermanentModifier().
    // Players upgrading from V1 may still have this modifier saved in their NBT.
    // Removed on tick to prevent double stacking with V2 MULTIPLY_BASE modifiers.
    private static final UUID V1_BIND_SPEED_MODIFIER_UUID = UUID.fromString(
        "7f3c7c8e-9d4e-4c7a-8e5f-1a2b3c4d5e6f"
    );

    // --- Unique UUIDs for AttributeModifiers (one per style to allow clean removal) ---
    private static final UUID WADDLE_SPEED_UUID = UUID.fromString(
        "d7a1c001-0000-0000-0000-000000000001"
    );
    private static final UUID SHUFFLE_SPEED_UUID = UUID.fromString(
        "d7a1c001-0000-0000-0000-000000000002"
    );
    private static final UUID HOP_SPEED_UUID = UUID.fromString(
        "d7a1c001-0000-0000-0000-000000000003"
    );
    private static final UUID CRAWL_SPEED_UUID = UUID.fromString(
        "d7a1c001-0000-0000-0000-000000000004"
    );

    // --- Hop tuning constants ---
    private static final double HOP_Y_IMPULSE = 0.28;
    private static final double HOP_FORWARD_IMPULSE = 0.18;
    private static final int HOP_COOLDOWN_TICKS = 10;
    private static final int HOP_STARTUP_DELAY_TICKS = 4;

    // --- Movement detection threshold (squared distance) ---
    private static final double MOVEMENT_THRESHOLD_SQ = 0.001;

    // --- Number of consecutive non-moving ticks before hop startup resets ---
    private static final int HOP_STARTUP_RESET_TICKS = 2;

    // ==================== Tick Event ====================

    /**
     * Per-tick movement style processing. Runs at HIGH priority at Phase.END
     * so it executes before {@code BondageItemRestrictionHandler} (default priority).
     *
     * <p>Tick flow:
     * <ol>
     *   <li>Skip conditions: passenger, dead, struggling</li>
     *   <li>Pending pose restore (crawl deactivated but can't stand yet)</li>
     *   <li>Resolve current style from equipped items</li>
     *   <li>Compare with active style, handle transitions</li>
     *   <li>Dispatch to style-specific tick (unless on ladder)</li>
     *   <li>Update last position for next tick's movement detection</li>
     * </ol>
     */
    @SubscribeEvent(priority = EventPriority.HIGH)
    public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
        if (event.side.isClient() || event.phase != TickEvent.Phase.END) {
            return;
        }
        if (!(event.player instanceof ServerPlayer player)) {
            return;
        }

        PlayerBindState state = PlayerBindState.getInstance(player);
        if (state == null) {
            return;
        }

        // --- Skip conditions ---
        // Update last position even when suspended to prevent false movement
        // detection on resume (e.g., teleport while riding)
        if (
            player.isPassenger() ||
            player.isDeadOrDying() ||
            state.isStruggling()
        ) {
            state.getMovement().setLastX(player.getX());
            state.getMovement().setLastY(player.getY());
            state.getMovement().setLastZ(player.getZ());
            return;
        }

        // --- Pending pose restore (crawl deactivated but can't stand) ---
        if (state.getMovement().isPendingPoseRestore()) {
            tryRestoreStandingPose(player, state);
        }

        // --- H6: Remove stale V1 permanent modifier if present ---
        // Players upgrading from V1 may have a permanent ADDITION modifier saved in NBT.
        // This one-time cleanup prevents double stacking with the V2 MULTIPLY_BASE modifier.
        cleanupV1Modifier(player);

        // --- Resolve current style from equipped items ---
        IV2BondageEquipment equipment = V2EquipmentHelper.getEquipment(player);
        Map<BodyRegionV2, ItemStack> equipped =
            equipment != null ? equipment.getAllEquipped() : Map.of();
        ResolvedMovement resolved = MovementStyleResolver.resolve(equipped);

        // --- Compare with current active style ---
        MovementStyle newStyle = resolved.style();
        MovementStyle oldStyle = state.getMovement().getActiveMovementStyle();

        if (newStyle != oldStyle) {
            // Style changed: deactivate old, activate new
            if (oldStyle != null) {
                onDeactivate(player, state, oldStyle);
            }
            if (newStyle != null) {
                state.getMovement().setResolvedMovementSpeed(resolved.speedMultiplier());
                state.getMovement().setResolvedJumpDisabled(resolved.jumpDisabled());
                onActivate(player, state, newStyle);
            } else {
                state.getMovement().setResolvedMovementSpeed(1.0f);
                state.getMovement().setResolvedJumpDisabled(false);
            }
            state.getMovement().setActiveMovementStyle(newStyle);

            // Sync to all tracking clients (animation + crawl pose)
            ModNetwork.sendToAllTrackingAndSelf(
                new PacketSyncMovementStyle(player.getUUID(), newStyle),
                player
            );
        }

        // --- Per-style tick ---
        if (state.getMovement().getActiveMovementStyle() != null) {
            // Ladder suspension: skip style tick when on ladder
            // (ladder movement is controlled by BondageItemRestrictionHandler)
            if (player.onClimbable()) {
                state.getMovement().setLastX(player.getX());
                state.getMovement().setLastY(player.getY());
                state.getMovement().setLastZ(player.getZ());
                return;
            }

            tickStyle(player, state);
        }

        // Update last position for next tick's movement detection
        state.getMovement().setLastX(player.getX());
        state.getMovement().setLastY(player.getY());
        state.getMovement().setLastZ(player.getZ());
    }

    // ==================== Jump Suppression ====================

    /**
     * Suppress jumps for styles with jump disabled.
     *
     * <p>{@code LivingJumpEvent} is NOT cancelable. Standard approach: subtract
     * the known jump impulse from Y velocity, preserving knockback and other
     * sources of Y motion.</p>
     *
     * <p>A {@link ClientboundSetEntityMotionPacket} is sent to minimize the
     * client-side 1-frame bounce artifact.</p>
     */
    @SubscribeEvent
    public static void onLivingJump(LivingEvent.LivingJumpEvent event) {
        if (!(event.getEntity() instanceof ServerPlayer player)) {
            return;
        }

        PlayerBindState state = PlayerBindState.getInstance(player);
        if (state == null || !state.getMovement().isResolvedJumpDisabled()) {
            return;
        }

        // Subtract vanilla jump impulse, preserving other Y velocity (knockback, etc.)
        // Vanilla: jumpPower = 0.42 + (amplifier + 1) * 0.1 = 0.42 * factor
        double jumpVelocity = 0.42 * getJumpBoostFactor(player);
        Vec3 motion = player.getDeltaMovement();
        player.setDeltaMovement(motion.x, motion.y - jumpVelocity, motion.z);

        // Sync to client to minimize visual bounce artifact
        player.connection.send(new ClientboundSetEntityMotionPacket(player));
    }

    /**
     * Calculate the Jump Boost potion factor.
     * Vanilla adds {@code (amplifier + 1) * 0.1} to the base 0.42 jump height.
     * We express this as a multiplicative factor on 0.42 for clean subtraction.
     *
     * @return 1.0 with no Jump Boost, higher with Jump Boost active
     */
    private static double getJumpBoostFactor(Player player) {
        var jumpBoost = player.getEffect(MobEffects.JUMP);
        if (jumpBoost != null) {
            return 1.0 + ((jumpBoost.getAmplifier() + 1) * 0.1) / 0.42;
        }
        return 1.0;
    }

    // ==================== Lifecycle ====================

    private static void onActivate(
        ServerPlayer player,
        PlayerBindState state,
        MovementStyle style
    ) {
        switch (style) {
            case WADDLE -> activateWaddle(player, state);
            case SHUFFLE -> activateShuffle(player, state);
            case HOP -> activateHop(player, state);
            case CRAWL -> activateCrawl(player, state);
        }
    }

    private static void onDeactivate(
        ServerPlayer player,
        PlayerBindState state,
        MovementStyle style
    ) {
        switch (style) {
            case WADDLE -> deactivateWaddle(player, state);
            case SHUFFLE -> deactivateShuffle(player, state);
            case HOP -> deactivateHop(player, state);
            case CRAWL -> deactivateCrawl(player, state);
        }
    }

    private static void tickStyle(ServerPlayer player, PlayerBindState state) {
        switch (state.getMovement().getActiveMovementStyle()) {
            case WADDLE -> tickWaddle(player, state);
            case SHUFFLE -> tickShuffle(player, state);
            case HOP -> tickHop(player, state);
            case CRAWL -> tickCrawl(player, state);
        }
    }

    // ==================== Waddle ====================

    private static void activateWaddle(
        ServerPlayer player,
        PlayerBindState state
    ) {
        applySpeedModifier(
            player,
            WADDLE_SPEED_UUID,
            "tiedup.waddle_speed",
            state.getMovement().getResolvedMovementSpeed()
        );
    }

    private static void deactivateWaddle(
        ServerPlayer player,
        PlayerBindState state
    ) {
        removeSpeedModifier(player, WADDLE_SPEED_UUID);
    }

    private static void tickWaddle(ServerPlayer player, PlayerBindState state) {
        // Waddle is animation-only on the server. No velocity manipulation.
        // The visual zigzag is handled by the context animation on the client.
    }

    // ==================== Shuffle ====================

    private static void activateShuffle(
        ServerPlayer player,
        PlayerBindState state
    ) {
        applySpeedModifier(
            player,
            SHUFFLE_SPEED_UUID,
            "tiedup.shuffle_speed",
            state.getMovement().getResolvedMovementSpeed()
        );
    }

    private static void deactivateShuffle(
        ServerPlayer player,
        PlayerBindState state
    ) {
        removeSpeedModifier(player, SHUFFLE_SPEED_UUID);
    }

    private static void tickShuffle(
        ServerPlayer player,
        PlayerBindState state
    ) {
        // Shuffle: speed reduction via attribute is sufficient. No per-tick work.
    }

    // ==================== Hop ====================

    private static void activateHop(
        ServerPlayer player,
        PlayerBindState state
    ) {
        // Apply base speed reduction (~15% base speed between hops)
        applySpeedModifier(
            player,
            HOP_SPEED_UUID,
            "tiedup.hop_speed",
            state.getMovement().getResolvedMovementSpeed()
        );
        state.getMovement().setHopCooldown(0);
        state.getMovement().setHopStartupPending(true);
        state.getMovement().setHopStartupTicks(HOP_STARTUP_DELAY_TICKS);
    }

    private static void deactivateHop(
        ServerPlayer player,
        PlayerBindState state
    ) {
        removeSpeedModifier(player, HOP_SPEED_UUID);
        state.getMovement().setHopCooldown(0);
        state.getMovement().setHopStartupPending(false);
        state.getMovement().setHopStartupTicks(0);
        state.getMovement().setHopNotMovingTicks(0);
    }

    /**
     * Hop tick logic:
     * <ul>
     *   <li>Detect movement via position delta (not player.zza/xxa)</li>
     *   <li>If moving + on ground + cooldown expired: execute hop (with startup delay on first hop)</li>
     *   <li>If not moving for >= 2 ticks: reset startup pending</li>
     *   <li>Decrement cooldown each tick</li>
     * </ul>
     */
    private static void tickHop(ServerPlayer player, PlayerBindState state) {
        var mov = state.getMovement();
        boolean isMoving =
            player.distanceToSqr(mov.getLastX(), mov.getLastY(), mov.getLastZ()) >
            MOVEMENT_THRESHOLD_SQ;

        // Decrement cooldown
        if (mov.getHopCooldown() > 0) {
            mov.setHopCooldown(mov.getHopCooldown() - 1);
        }

        if (isMoving && player.onGround() && mov.getHopCooldown() <= 0) {
            if (mov.isHopStartupPending()) {
                // Startup delay: decrement and wait (latched: completes even if
                // player briefly releases input during these 4 ticks)
                mov.setHopStartupTicks(mov.getHopStartupTicks() - 1);
                if (mov.getHopStartupTicks() <= 0) {
                    // Startup complete: execute first hop
                    mov.setHopStartupPending(false);
                    executeHop(player, state);
                }
            } else {
                // Normal hop
                executeHop(player, state);
            }
            mov.setHopNotMovingTicks(0);
        } else if (!isMoving) {
            mov.setHopNotMovingTicks(mov.getHopNotMovingTicks() + 1);
            // Reset startup if not moving for >= 2 consecutive ticks
            if (
                mov.getHopNotMovingTicks() >= HOP_STARTUP_RESET_TICKS &&
                !mov.isHopStartupPending()
            ) {
                mov.setHopStartupPending(true);
                mov.setHopStartupTicks(HOP_STARTUP_DELAY_TICKS);
            }
        } else {
            // Moving but not on ground or cooldown active — reset not-moving counter
            mov.setHopNotMovingTicks(0);
        }
    }

    /**
     * Execute a single hop: apply Y impulse + forward impulse along look direction.
     * Sends {@link ClientboundSetEntityMotionPacket} to sync velocity to client.
     */
    private static void executeHop(ServerPlayer player, PlayerBindState state) {
        Vec3 look = player.getLookAngle();
        // Project look onto horizontal plane and normalize (safe: zero vec normalizes to zero)
        Vec3 forward = new Vec3(look.x, 0, look.z).normalize();
        Vec3 currentMotion = player.getDeltaMovement();

        player.setDeltaMovement(
            currentMotion.x + forward.x * HOP_FORWARD_IMPULSE,
            HOP_Y_IMPULSE,
            currentMotion.z + forward.z * HOP_FORWARD_IMPULSE
        );

        state.getMovement().setHopCooldown(HOP_COOLDOWN_TICKS);

        // Sync velocity to client to prevent rubber-banding
        player.connection.send(new ClientboundSetEntityMotionPacket(player));
    }

    // ==================== Crawl ====================

    private static void activateCrawl(
        ServerPlayer player,
        PlayerBindState state
    ) {
        applySpeedModifier(
            player,
            CRAWL_SPEED_UUID,
            "tiedup.crawl_speed",
            state.getMovement().getResolvedMovementSpeed()
        );
        player.setForcedPose(Pose.SWIMMING);
        player.refreshDimensions();
        state.getMovement().setPendingPoseRestore(false);
    }

    private static void deactivateCrawl(
        ServerPlayer player,
        PlayerBindState state
    ) {
        removeSpeedModifier(player, CRAWL_SPEED_UUID);

        // Space check: can the player stand up?
        EntityDimensions standDims = player.getDimensions(Pose.STANDING);
        AABB standBox = standDims.makeBoundingBox(player.position());
        boolean canStand = player.level().noCollision(player, standBox);

        if (canStand) {
            player.setForcedPose(null);
            player.refreshDimensions();
        } else {
            // Can't stand yet -- flag for periodic retry in tick flow (step 2)
            state.getMovement().setPendingPoseRestore(true);
        }
    }

    private static void tickCrawl(ServerPlayer player, PlayerBindState state) {
        // Guard re-assertion: only re-apply if something cleared the forced pose
        // (avoids unnecessary per-tick SynchedEntityData dirty-marking)
        if (player.getForcedPose() != Pose.SWIMMING) {
            player.setForcedPose(Pose.SWIMMING);
            player.refreshDimensions();
        }
    }

    // ==================== Pending Pose Restore ====================

    /**
     * Try to restore standing pose after crawl deactivation.
     * Called every tick regardless of active style (step 2 in tick flow).
     * Retries until space is available for the player to stand.
     */
    private static void tryRestoreStandingPose(
        ServerPlayer player,
        PlayerBindState state
    ) {
        EntityDimensions standDims = player.getDimensions(Pose.STANDING);
        AABB standBox = standDims.makeBoundingBox(player.position());
        boolean canStand = player.level().noCollision(player, standBox);

        if (canStand) {
            player.setForcedPose(null);
            player.refreshDimensions();
            state.getMovement().setPendingPoseRestore(false);
            LOGGER.debug(
                "Restored standing pose for {} (pending pose restore cleared)",
                player.getName().getString()
            );
        }
    }

    // ==================== V1 Legacy Cleanup (H6) ====================

    /**
     * Remove the legacy V1 {@code RestraintEffectUtils} speed modifier if present.
     *
     * <p>V1 used {@code addPermanentModifier()} with UUID {@code 7f3c7c8e-...} and
     * {@link AttributeModifier.Operation#ADDITION}. Because permanent modifiers are
     * serialized to player NBT, players upgrading mid-session or loading old saves
     * may still carry this modifier. Removing it here ensures only the V2
     * {@code MULTIPLY_BASE} modifier is active.</p>
     *
     * <p>This is a no-op if the modifier is not present (cheap UUID lookup).</p>
     */
    private static void cleanupV1Modifier(ServerPlayer player) {
        AttributeInstance attr = player.getAttribute(Attributes.MOVEMENT_SPEED);
        if (
            attr != null &&
            attr.getModifier(V1_BIND_SPEED_MODIFIER_UUID) != null
        ) {
            attr.removeModifier(V1_BIND_SPEED_MODIFIER_UUID);
            LOGGER.info(
                "Removed stale V1 speed modifier from player {}",
                player.getName().getString()
            );
        }
    }

    // ==================== Attribute Modifier Helpers ====================

    /**
     * Apply a transient {@code MULTIPLY_BASE} speed modifier.
     * Always removes any existing modifier with the same UUID first, because
     * {@code addTransientModifier} throws {@link IllegalArgumentException}
     * if a modifier with the same UUID already exists.
     *
     * <p>{@code MULTIPLY_BASE} means the modifier value is added to 1.0 and
     * multiplied with the base value. A multiplier of 0.4 requires a modifier
     * value of -0.6: {@code base * (1 + (-0.6)) = base * 0.4}.</p>
     *
     * @param player     the target player
     * @param uuid       unique modifier UUID per style
     * @param name       modifier name (for debugging in F3 screen)
     * @param multiplier the desired speed fraction (0.0-1.0)
     */
    private static void applySpeedModifier(
        ServerPlayer player,
        UUID uuid,
        String name,
        float multiplier
    ) {
        AttributeInstance attr = player.getAttribute(Attributes.MOVEMENT_SPEED);
        if (attr == null) return;

        // Remove existing modifier first (no-op if not present)
        attr.removeModifier(uuid);

        // MULTIPLY_BASE: value of -(1 - multiplier) reduces base speed to multiplier fraction
        double value = -(1.0 - multiplier);
        attr.addTransientModifier(
            new AttributeModifier(
                uuid,
                name,
                value,
                AttributeModifier.Operation.MULTIPLY_BASE
            )
        );
    }

    /**
     * Remove a speed modifier by UUID. Safe to call even if no modifier
     * with this UUID is present.
     */
    private static void removeSpeedModifier(ServerPlayer player, UUID uuid) {
        AttributeInstance attr = player.getAttribute(Attributes.MOVEMENT_SPEED);
        if (attr == null) return;
        attr.removeModifier(uuid);
    }
}
