package com.tiedup.remake.mixin;

import com.tiedup.remake.entities.LeashProxyEntity;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.sync.PacketSyncLeashProxy;
import com.tiedup.remake.state.IPlayerLeashAccess;
import net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.decoration.LeashFenceKnotEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;

/**
 * Mixin for ServerPlayer to add leash proxy functionality.
 *
 * This replaces the old EntityInvisibleSlaveTransporter mount-based system
 * with a proxy-based system where:
 * - The player does NOT ride an entity
 * - A LeashProxyEntity follows the player and holds the leash
 * - Traction is applied via push() when the player is too far from the holder
 */
@Mixin(ServerPlayer.class)
public abstract class MixinServerPlayer implements IPlayerLeashAccess {

    @Unique
    private final ServerPlayer tiedup$self = (ServerPlayer) (Object) this;

    /** The proxy entity that follows this player and renders the leash */
    @Unique
    private LeashProxyEntity tiedup$leashProxy;

    /** The entity holding this player's leash (master or fence knot) */
    @Unique
    private Entity tiedup$leashHolder;

    /** Tick counter since last leash attachment (prevents immediate detach) */
    @Unique
    private int tiedup$leashAge;

    /** Previous X position for stuck detection */
    @Unique
    private double tiedup$prevX;

    /** Previous Z position for stuck detection */
    @Unique
    private double tiedup$prevZ;

    /** Ticks spent stuck (not moving towards holder) */
    @Unique
    private int tiedup$leashStuckCounter;

    /** Extra slack on leash - increases pull/max distances (for "pet leads" dogwalk) */
    @Unique
    private double tiedup$leashSlack = 0.0;

    /** Tick counter for periodic leash proxy resync (for late-joining clients) */
    @Unique
    private int tiedup$leashResyncTimer = 0;

    // ==================== Leash Constants ====================

    /** Distance at which pull force starts (4 free blocks before any pull) */
    @Unique
    private static final double LEASH_PULL_START_DISTANCE = 4.0;

    /** Maximum distance before instant teleport (6-block elastic zone) */
    @Unique
    private static final double LEASH_MAX_DISTANCE = 10.0;

    /** Distance at which stuck detection activates (middle of elastic zone) */
    @Unique
    private static final double LEASH_TELEPORT_DISTANCE = 7.0;

    /** Ticks of being stuck before safety teleport (2 seconds) */
    @Unique
    private static final int LEASH_STUCK_THRESHOLD = 40;

    /** Maximum pull force cap */
    @Unique
    private static final double LEASH_MAX_FORCE = 0.14;

    /** Force ramp per block beyond pull start */
    @Unique
    private static final double LEASH_FORCE_RAMP = 0.04;

    /** Blend factor for pull vs momentum (0.6 = 60% pull, 40% momentum) */
    @Unique
    private static final double LEASH_BLEND_FACTOR = 0.6;

    // ==================== IPlayerLeashAccess Implementation ====================

    @Override
    public void tiedup$attachLeash(Entity holder) {
        if (holder == null) return;

        tiedup$leashHolder = holder;

        // Create proxy if not exists
        if (tiedup$leashProxy == null) {
            tiedup$leashProxy = new LeashProxyEntity(tiedup$self);
            tiedup$leashProxy.setPos(
                tiedup$self.getX(),
                tiedup$self.getY(),
                tiedup$self.getZ()
            );
            tiedup$self.level().addFreshEntity(tiedup$leashProxy);
        }

        // Attach leash from proxy to holder
        tiedup$leashProxy.setLeashedTo(tiedup$leashHolder, true);
        tiedup$leashAge = tiedup$self.tickCount;
        tiedup$leashStuckCounter = 0;
        tiedup$leashResyncTimer = 0;

        // Send sync packet to all tracking clients for smooth rendering
        PacketSyncLeashProxy packet = PacketSyncLeashProxy.attach(
            tiedup$self.getUUID(),
            tiedup$leashProxy.getId()
        );
        ModNetwork.sendToAllTrackingAndSelf(packet, tiedup$self);
    }

    @Override
    public void tiedup$detachLeash() {
        tiedup$leashHolder = null;

        if (tiedup$leashProxy != null) {
            if (
                tiedup$leashProxy.isAlive() &&
                !tiedup$leashProxy.proxyIsRemoved()
            ) {
                tiedup$leashProxy.proxyRemove();
            }
            tiedup$leashProxy = null;

            // Send detach packet to all tracking clients
            PacketSyncLeashProxy packet = PacketSyncLeashProxy.detach(
                tiedup$self.getUUID()
            );
            ModNetwork.sendToAllTrackingAndSelf(packet, tiedup$self);
        }
    }

    @Override
    public void tiedup$dropLeash() {
        // Don't drop if player is disconnected or dead (position may be invalid)
        if (tiedup$self.hasDisconnected() || !tiedup$self.isAlive()) {
            return;
        }
        tiedup$self.drop(new ItemStack(Items.LEAD), false, true);
    }

    @Override
    public boolean tiedup$isLeashed() {
        return (
            tiedup$leashHolder != null &&
            tiedup$leashProxy != null &&
            !tiedup$leashProxy.proxyIsRemoved()
        );
    }

    @Override
    public Entity tiedup$getLeashHolder() {
        return tiedup$leashHolder;
    }

    @Override
    public LeashProxyEntity tiedup$getLeashProxy() {
        return tiedup$leashProxy;
    }

    @Override
    public void tiedup$setLeashSlack(double slack) {
        this.tiedup$leashSlack = slack;
    }

    @Override
    public double tiedup$getLeashSlack() {
        return this.tiedup$leashSlack;
    }

    // ==================== Tick Update (called from Forge event) ====================

    /**
     * Update leash state and apply traction if needed.
     * Called from LeashTickHandler via Forge TickEvent.
     */
    @Override
    public void tiedup$tickLeash() {
        // Check if this player is still valid
        if (!tiedup$self.isAlive() || tiedup$self.hasDisconnected()) {
            tiedup$detachLeash();
            // Don't drop leash if we're disconnected (we won't be there to pick it up)
            return;
        }

        // Check if holder is still valid
        if (tiedup$leashHolder != null) {
            boolean holderInvalid =
                !tiedup$leashHolder.isAlive() || tiedup$leashHolder.isRemoved();

            // If holder is a player, also check if they disconnected
            if (
                !holderInvalid &&
                tiedup$leashHolder instanceof ServerPlayer holderPlayer
            ) {
                holderInvalid = holderPlayer.hasDisconnected();
            }

            // If player is being used as vehicle, break leash
            if (!holderInvalid && tiedup$self.isVehicle()) {
                holderInvalid = true;
            }

            if (holderInvalid) {
                tiedup$detachLeash();
                tiedup$dropLeash();
                return;
            }
        }

        // Sync proxy state with actual leash holder
        if (tiedup$leashProxy != null) {
            if (tiedup$leashProxy.proxyIsRemoved()) {
                tiedup$leashProxy = null;
            } else {
                Entity holderActual = tiedup$leashHolder;
                Entity holderFromProxy = tiedup$leashProxy.getLeashHolder();

                // Leash was broken externally (by another player)
                if (holderFromProxy == null && holderActual != null) {
                    tiedup$detachLeash();
                    tiedup$dropLeash();
                    return;
                }
                // Holder changed (shouldn't happen normally)
                else if (holderFromProxy != holderActual) {
                    tiedup$leashHolder = holderFromProxy;
                }
            }
        }

        // Periodic resync for late-joining clients (every 5 seconds)
        if (tiedup$leashProxy != null && ++tiedup$leashResyncTimer >= 100) {
            tiedup$leashResyncTimer = 0;
            PacketSyncLeashProxy packet = PacketSyncLeashProxy.attach(
                tiedup$self.getUUID(),
                tiedup$leashProxy.getId()
            );
            ModNetwork.sendToAllTrackingAndSelf(packet, tiedup$self);
        }

        // Apply traction force
        tiedup$applyLeashPull();
    }

    /**
     * Apply pull force towards the leash holder if player is too far.
     *
     * Uses normalized direction, progressive capped force, velocity blending,
     * and conditional sync to prevent jitter, oscillation, and runaway velocity.
     * Modeled after DamselAIController.tickLeashTraction().
     */
    @Unique
    private void tiedup$applyLeashPull() {
        if (tiedup$leashHolder == null) return;

        // Cross-dimension: detach leash cleanly instead of silently ignoring
        if (tiedup$leashHolder.level() != tiedup$self.level()) {
            tiedup$detachLeash();
            tiedup$dropLeash();
            return;
        }

        float distance = tiedup$self.distanceTo(tiedup$leashHolder);

        // Apply slack to effective distances (for "pet leads" dogwalk)
        double effectivePullStart =
            LEASH_PULL_START_DISTANCE + tiedup$leashSlack;
        double effectiveMaxDistance = LEASH_MAX_DISTANCE + tiedup$leashSlack;
        double effectiveTeleportDist =
            LEASH_TELEPORT_DISTANCE + tiedup$leashSlack;

        // Close enough: no pull needed, reset stuck counter
        if (distance < effectivePullStart) {
            tiedup$leashStuckCounter = 0;
            return;
        }

        // Too far: teleport to holder instead of breaking
        if (distance > effectiveMaxDistance) {
            tiedup$teleportToSafePositionNearHolder();
            tiedup$leashStuckCounter = 0;
            return;
        }

        // Direction to holder
        double dx = tiedup$leashHolder.getX() - tiedup$self.getX();
        double dy = tiedup$leashHolder.getY() - tiedup$self.getY();
        double dz = tiedup$leashHolder.getZ() - tiedup$self.getZ();

        // Normalized horizontal direction (replaces Math.signum bang-bang)
        double horizontalDist = Math.sqrt(dx * dx + dz * dz);
        double dirX = horizontalDist > 0.01 ? dx / horizontalDist : 0.0;
        double dirZ = horizontalDist > 0.01 ? dz / horizontalDist : 0.0;

        // Calculate how much the player moved since last check
        double movedX = tiedup$self.getX() - tiedup$prevX;
        double movedZ = tiedup$self.getZ() - tiedup$prevZ;
        double movedHorizontal = Math.sqrt(movedX * movedX + movedZ * movedZ);

        // Store current position for next stuck check
        tiedup$prevX = tiedup$self.getX();
        tiedup$prevZ = tiedup$self.getZ();

        // Stuck detection - slack-aware threshold
        boolean isStuck =
            distance > effectiveTeleportDist &&
            tiedup$self.getDeltaMovement().lengthSqr() < 0.001 &&
            movedHorizontal < 0.05;

        if (isStuck) {
            tiedup$leashStuckCounter++;

            if (tiedup$leashStuckCounter >= LEASH_STUCK_THRESHOLD) {
                tiedup$teleportToSafePositionNearHolder();
                tiedup$leashStuckCounter = 0;
                return;
            }
        } else {
            tiedup$leashStuckCounter = 0;
        }

        // Progressive capped force: 0.04 per block beyond pull start, max 0.14
        double distanceBeyond = distance - effectivePullStart;
        double forceFactor = Math.min(
            LEASH_MAX_FORCE,
            distanceBeyond * LEASH_FORCE_RAMP
        );

        // Fence knots are static, need 1.3x pull
        if (tiedup$leashHolder instanceof LeashFenceKnotEntity) {
            forceFactor *= 1.3;
        }

        // Velocity blending: 60% pull direction + 40% existing momentum
        net.minecraft.world.phys.Vec3 currentMotion =
            tiedup$self.getDeltaMovement();
        double pullVelX = dirX * forceFactor * 3.0;
        double pullVelZ = dirZ * forceFactor * 3.0;
        double newVelX =
            currentMotion.x * (1.0 - LEASH_BLEND_FACTOR) +
            pullVelX * LEASH_BLEND_FACTOR;
        double newVelZ =
            currentMotion.z * (1.0 - LEASH_BLEND_FACTOR) +
            pullVelZ * LEASH_BLEND_FACTOR;

        // Soft auto-step (replaces 0.42 vanilla jump velocity)
        double newVelY = currentMotion.y;
        if (
            tiedup$self.onGround() &&
            movedHorizontal < 0.1 &&
            distanceBeyond > 0.5
        ) {
            if (dy > 0.3) {
                newVelY += 0.08; // Holder is above: gentle upward boost
            } else {
                newVelY += 0.05; // Normal step-up
            }
        } else if (dy > 0.5 && !tiedup$self.onGround()) {
            newVelY += 0.02; // Gentle aerial drift
        }

        tiedup$self.setDeltaMovement(newVelX, newVelY, newVelZ);

        // Conditional velocity sync: only send packet when force is meaningful
        if (forceFactor > 0.02 && tiedup$self.connection != null) {
            tiedup$self.connection.send(
                new ClientboundSetEntityMotionPacket(tiedup$self)
            );
            // Only suppress impulse flag after we've actually synced to the client
            tiedup$self.hasImpulse = false;
        }
    }

    /**
     * Teleport player to a safe position near the leash holder.
     * Used when player is stuck and can't path to holder.
     */
    @Unique
    private void tiedup$teleportToSafePositionNearHolder() {
        if (tiedup$leashHolder == null) return;

        // Target: 2 blocks away from holder in player's direction
        double dx = tiedup$self.getX() - tiedup$leashHolder.getX();
        double dz = tiedup$self.getZ() - tiedup$leashHolder.getZ();
        double dist = Math.sqrt(dx * dx + dz * dz);

        double offsetX = 0;
        double offsetZ = 0;
        if (dist > 0.1) {
            offsetX = (dx / dist) * 2.0;
            offsetZ = (dz / dist) * 2.0;
        }

        double targetX = tiedup$leashHolder.getX() + offsetX;
        double targetZ = tiedup$leashHolder.getZ() + offsetZ;

        // Find safe Y (ground level)
        double targetY = tiedup$findSafeY(
            targetX,
            tiedup$leashHolder.getY(),
            targetZ
        );

        tiedup$self.teleportTo(targetX, targetY, targetZ);

        // Sync position to client (null check for fake players)
        if (tiedup$self.connection != null) {
            tiedup$self.connection.send(
                new ClientboundSetEntityMotionPacket(tiedup$self)
            );
        }
    }

    /**
     * Find a safe Y coordinate for teleporting.
     *
     * @param x       Target X coordinate
     * @param startY  Starting Y to search from
     * @param z       Target Z coordinate
     * @return Safe Y coordinate on solid ground
     */
    @Unique
    private double tiedup$findSafeY(double x, double startY, double z) {
        net.minecraft.core.BlockPos.MutableBlockPos mutable =
            new net.minecraft.core.BlockPos.MutableBlockPos();

        // Search down first (max 5 blocks)
        for (int y = 0; y > -5; y--) {
            mutable.set((int) x, (int) startY + y, (int) z);
            if (
                tiedup$self
                    .level()
                    .getBlockState(mutable)
                    .isSolidRender(tiedup$self.level(), mutable) &&
                tiedup$self.level().getBlockState(mutable.above()).isAir()
            ) {
                return mutable.getY() + 1;
            }
        }

        // Search up (max 5 blocks)
        for (int y = 1; y < 5; y++) {
            mutable.set((int) x, (int) startY + y, (int) z);
            if (
                tiedup$self
                    .level()
                    .getBlockState(mutable.below())
                    .isSolidRender(tiedup$self.level(), mutable.below()) &&
                tiedup$self.level().getBlockState(mutable).isAir()
            ) {
                return mutable.getY();
            }
        }

        // Fallback: use holder's Y
        return startY;
    }
}
