package com.tiedup.remake.entities.ai.personality;

import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.v2.bondage.BindModeHelper;
import com.tiedup.remake.personality.NpcCommand;
import com.tiedup.remake.personality.ToolMode;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.EnumSet;
import java.util.List;
import javax.annotation.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.monster.Monster;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;

/**
 * AI Goal: NPC follows their master when FOLLOW command is active.
 * Tool-based behaviors are activated based on the item in NPC's main hand.
 *
 * <p><b>Tool Modes:</b></p>
 * <ul>
 *   <li>PASSIVE: No tool - just follow</li>
 *   <li>ATTACK: Sword - attack hostile mobs near master</li>
 *   <li>MINING: Pickaxe - mine stone/ores near master</li>
 *   <li>WOODCUTTING: Axe - chop logs near master</li>
 *   <li>CAPTURE: Bondage bind item - capture untied damsels</li>
 * </ul>
 *
 * <p>Training tier affects damage, speed, and cooldown.
 * Mood affects behavior frequency.</p>
 */
public class NpcFollowCommandGoal extends Goal {

    private final EntityDamsel npc;
    private final double speedModifier;

    /** Distance at which to teleport */
    private static final double TELEPORT_DISTANCE = 20.0;

    /** Tool behavior search radius */
    private static final double BEHAVIOR_RADIUS = 12.0;

    /** Block break radius */
    private static final double BLOCK_RADIUS = 6.0;

    /** Attack range */
    private static final double ATTACK_RANGE = 2.0;

    /** Block interact range */
    private static final double BLOCK_RANGE = 2.5;

    @Nullable
    private LivingEntity master;

    private int pathRecalcTimer;
    private int xpTimer;
    private int stuckTimer;

    // Tool behavior state
    private ToolMode currentToolMode = ToolMode.PASSIVE;
    private int behaviorCooldown;

    @Nullable
    private LivingEntity combatTarget;

    @Nullable
    private BlockPos targetBlock;

    private int breakProgress;

    /** Cooldown for block search when no target found (prevents expensive searches every tick) */
    private int noTargetCooldown;

    @Nullable
    private EntityDamsel captureTarget;

    /**
     * Get the current follow distance configuration.
     */
    private NpcCommand.FollowDistance getFollowDistance() {
        return npc.getPersonalityState().getFollowDistance();
    }

    public NpcFollowCommandGoal(EntityDamsel npc, double speedModifier) {
        this.npc = npc;
        this.speedModifier = speedModifier;
        this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
    }

    @Override
    public boolean canUse() {
        // Must have FOLLOW command active
        if (npc.getActiveCommand() != NpcCommand.FOLLOW) {
            return false;
        }

        // Must have collar
        if (!npc.hasCollar()) {
            return false;
        }

        // Find the commanding player
        this.master = NpcGoalHelper.findCommandingPlayer(npc);
        return this.master != null;
    }

    @Override
    public boolean canContinueToUse() {
        // Stop if command changed
        if (npc.getActiveCommand() != NpcCommand.FOLLOW) {
            return false;
        }

        // Stop if no collar
        if (!npc.hasCollar()) {
            return false;
        }

        // Stop if master is gone
        if (this.master == null || !this.master.isAlive()) {
            return false;
        }

        // Stop if master is in different dimension
        if (this.master.level() != this.npc.level()) {
            return false;
        }

        return true;
    }

    @Override
    public void start() {
        this.pathRecalcTimer = 0;
        this.xpTimer = 0;
        this.stuckTimer = 0;
        this.behaviorCooldown = 0;
        this.noTargetCooldown = 0;
        this.combatTarget = null;
        this.targetBlock = null;
        this.captureTarget = null;
        this.breakProgress = 0;
    }

    @Override
    public void stop() {
        this.master = null;
        this.combatTarget = null;
        this.targetBlock = null;
        this.captureTarget = null;
        this.npc.getNavigation().stop();
    }

    @Override
    public void tick() {
        if (this.master == null) return;

        NpcCommand.FollowDistance followDist = getFollowDistance();
        double distance = this.npc.distanceToSqr(this.master);
        double distanceFlat = Math.sqrt(distance);

        // Detect tool mode
        ItemStack mainHand = npc.getMainHandItem();
        this.currentToolMode = ToolMode.fromItem(mainHand);

        // Look at master (unless engaged in combat)
        if (this.combatTarget == null || !this.combatTarget.isAlive()) {
            this.npc.getLookControl().setLookAt(
                this.master,
                10.0F,
                (float) this.npc.getMaxHeadXRot()
            );
        }

        // Handle teleport if too far
        if (distanceFlat > TELEPORT_DISTANCE) {
            if (!tryTeleportNear()) {
                // Can't teleport, try pathing anyway
                this.stuckTimer++;
                if (this.stuckTimer > 100) {
                    // Give up after 5 seconds of being stuck
                    this.npc.cancelCommand();
                }
            }
            return;
        }

        // Reset stuck timer if within range
        this.stuckTimer = 0;

        // Move towards master if far enough
        if (distanceFlat > followDist.startDistance) {
            if (--this.pathRecalcTimer <= 0) {
                this.pathRecalcTimer = followDist.pathRecalcInterval;

                // Calculate target position
                double targetX, targetZ;
                if (followDist.followBehind) {
                    // HEEL mode: follow behind the master
                    double yaw = Math.toRadians(this.master.getYRot());
                    double behindDist = followDist.minDistance + 0.5;
                    targetX = this.master.getX() + Math.sin(yaw) * behindDist;
                    targetZ = this.master.getZ() - Math.cos(yaw) * behindDist;
                    this.npc.getNavigation().moveTo(
                        targetX,
                        this.master.getY(),
                        targetZ,
                        this.speedModifier * 1.2 // Slightly faster to keep up
                    );
                } else {
                    // Normal follow: move towards master
                    this.npc.getNavigation().moveTo(
                        this.master,
                        this.speedModifier
                    );
                }
            }
        } else if (distanceFlat < followDist.minDistance) {
            // Too close, stop moving
            this.npc.getNavigation().stop();
        }

        // Tool-based behavior (only when not too far from master)
        if (
            distanceFlat <= BEHAVIOR_RADIUS &&
            this.currentToolMode != ToolMode.PASSIVE
        ) {
            tickToolBehavior();
        }

        // Award XP periodically for successful following
        if (++this.xpTimer >= followDist.xpInterval) {
            this.xpTimer = 0;
            if (this.master instanceof Player player) {
            }
        }
    }

    /**
     * Tick tool-based behavior based on current tool mode.
     */
    private void tickToolBehavior() {
        if (--this.behaviorCooldown > 0) {
            return;
        }

        switch (this.currentToolMode) {
            case ATTACK -> tickCombatBehavior();
            case MINING -> tickMiningBehavior();
            case WOODCUTTING -> tickWoodcuttingBehavior();
            case CAPTURE -> tickCaptureBehavior();
            default -> {
            }
        }
    }

    /**
     * Combat behavior: Find and attack hostile mobs near master.
     */
    private void tickCombatBehavior() {
        // Continue attacking current target
        if (this.combatTarget != null && this.combatTarget.isAlive()) {
            double dist = npc.distanceTo(combatTarget);

            // Look at target
            npc.getLookControl().setLookAt(combatTarget, 30.0F, 30.0F);

            if (dist <= ATTACK_RANGE) {
                // Attack with base damage
                float baseDamage = 3.0f;

                // Apply damage directly (bypasses ATTACK_DAMAGE attribute)
                combatTarget.hurt(
                    npc.damageSources().mobAttack(npc),
                    baseDamage
                );
                npc.swing(InteractionHand.MAIN_HAND);

                // Cooldown based on mood
                this.behaviorCooldown = 20;

                // Award XP for hit
                if (this.master instanceof Player player) {
                }
            } else {
                // Move towards target
                npc.getNavigation().moveTo(combatTarget, speedModifier * 1.3);
            }
            return;
        }

        // Find new target
        this.combatTarget = findHostileMob();
        // Cooldown whether target found or not - prevents expensive search every tick
        this.behaviorCooldown = (this.combatTarget != null) ? 10 : 40;
    }

    /**
     * Mining behavior: Find and mine stone/ores near master.
     */
    private void tickMiningBehavior() {
        // Continue mining current block
        if (this.targetBlock != null) {
            BlockState state = npc.level().getBlockState(targetBlock);
            if (!isMineable(state)) {
                this.targetBlock = null;
                this.breakProgress = 0;
                return;
            }

            double dist = Math.sqrt(npc.blockPosition().distSqr(targetBlock));
            if (dist <= BLOCK_RANGE) {
                // Look at block
                npc
                    .getLookControl()
                    .setLookAt(
                        targetBlock.getX() + 0.5,
                        targetBlock.getY() + 0.5,
                        targetBlock.getZ() + 0.5
                    );

                // Mine progress
                this.breakProgress += 5;

                // Block broken at 100
                if (this.breakProgress >= 100) {
                    Level level = npc.level();
                    if (level instanceof ServerLevel serverLevel) {
                        // Break block with drops
                        Block.dropResources(
                            state,
                            level,
                            targetBlock,
                            null,
                            npc,
                            npc.getMainHandItem()
                        );
                        level.destroyBlock(targetBlock, false);
                        level.playSound(
                            null,
                            targetBlock,
                            SoundEvents.STONE_BREAK,
                            SoundSource.BLOCKS,
                            1.0f,
                            1.0f
                        );

                        // Award XP
                        if (this.master instanceof Player player) {
                        }
                    }
                    this.targetBlock = null;
                    this.breakProgress = 0;

                    // Cooldown
                    this.behaviorCooldown = 30;
                }
            } else {
                // Move towards block
                npc
                    .getNavigation()
                    .moveTo(
                        targetBlock.getX() + 0.5,
                        targetBlock.getY(),
                        targetBlock.getZ() + 0.5,
                        speedModifier
                    );
            }
            return;
        }

        // Cooldown for expensive block search when no target found
        if (this.noTargetCooldown > 0) {
            this.noTargetCooldown--;
            return;
        }

        // Find new block to mine
        this.targetBlock = findMineableBlock();
        this.breakProgress = 0;

        // If no block found, wait before searching again (2 seconds)
        if (this.targetBlock == null) {
            this.noTargetCooldown = 40;
        }
    }

    /**
     * Woodcutting behavior: Find and chop logs near master.
     */
    private void tickWoodcuttingBehavior() {
        // Continue chopping current block
        if (this.targetBlock != null) {
            BlockState state = npc.level().getBlockState(targetBlock);
            if (!isChoppable(state)) {
                this.targetBlock = null;
                this.breakProgress = 0;
                return;
            }

            double dist = Math.sqrt(npc.blockPosition().distSqr(targetBlock));
            if (dist <= BLOCK_RANGE) {
                // Look at block
                npc
                    .getLookControl()
                    .setLookAt(
                        targetBlock.getX() + 0.5,
                        targetBlock.getY() + 0.5,
                        targetBlock.getZ() + 0.5
                    );

                // Chop progress
                this.breakProgress += 6;

                // Block broken at 100
                if (this.breakProgress >= 100) {
                    Level level = npc.level();
                    if (level instanceof ServerLevel serverLevel) {
                        // Break block with drops
                        Block.dropResources(
                            state,
                            level,
                            targetBlock,
                            null,
                            npc,
                            npc.getMainHandItem()
                        );
                        level.destroyBlock(targetBlock, false);
                        level.playSound(
                            null,
                            targetBlock,
                            SoundEvents.WOOD_BREAK,
                            SoundSource.BLOCKS,
                            1.0f,
                            1.0f
                        );

                        // Award XP
                        if (this.master instanceof Player player) {
                        }
                    }
                    this.targetBlock = null;
                    this.breakProgress = 0;

                    // Cooldown
                    this.behaviorCooldown = 25;
                }
            } else {
                // Move towards block
                npc
                    .getNavigation()
                    .moveTo(
                        targetBlock.getX() + 0.5,
                        targetBlock.getY(),
                        targetBlock.getZ() + 0.5,
                        speedModifier
                    );
            }
            return;
        }

        // Cooldown for expensive block search when no target found
        if (this.noTargetCooldown > 0) {
            this.noTargetCooldown--;
            return;
        }

        // Find new block to chop
        this.targetBlock = findChoppableBlock();
        this.breakProgress = 0;

        // If no block found, wait before searching again (2 seconds)
        if (this.targetBlock == null) {
            this.noTargetCooldown = 40;
        }
    }

    /**
     * Capture behavior: Find and capture untied damsels.
     */
    private void tickCaptureBehavior() {
        // Continue capturing current target
        if (this.captureTarget != null && this.captureTarget.isAlive()) {
            // Skip if already tied
            if (this.captureTarget.isTiedUp()) {
                this.captureTarget = null;
                return;
            }

            double dist = npc.distanceTo(captureTarget);

            // Look at target
            npc.getLookControl().setLookAt(captureTarget, 30.0F, 30.0F);

            if (dist <= ATTACK_RANGE) {
                // Try to capture using bind item
                ItemStack bindItem = npc.getMainHandItem();
                if (BindModeHelper.isBindItem(bindItem)) {
                    // Apply bind to target
                    captureTarget.equip(BodyRegionV2.ARMS, bindItem.copy());

                    // Consume one bind item (or reduce durability)
                    bindItem.shrink(1);
                    npc.setMainHandItem(bindItem);

                    // Award XP
                    if (this.master instanceof Player player) {
                    }

                    // Cooldown
                    this.behaviorCooldown = 60;
                }
                this.captureTarget = null;
            } else {
                // Move towards target
                npc.getNavigation().moveTo(captureTarget, speedModifier * 1.2);
            }
            return;
        }

        // Find new target
        this.captureTarget = findCapturableDamsel();
        // Cooldown whether target found or not - prevents expensive search every tick
        this.behaviorCooldown = (this.captureTarget != null) ? 10 : 40;
    }

    // --- Target finding methods ---

    @Nullable
    private Monster findHostileMob() {
        if (this.master == null) return null;

        AABB searchArea = new AABB(master.blockPosition()).inflate(
            BEHAVIOR_RADIUS
        );
        List<Monster> monsters = npc
            .level()
            .getEntitiesOfClass(Monster.class, searchArea);

        Monster nearest = null;
        double nearestDist = Double.MAX_VALUE;

        for (Monster monster : monsters) {
            double dist = npc.distanceToSqr(monster);
            if (dist < nearestDist) {
                nearest = monster;
                nearestDist = dist;
            }
        }

        return nearest;
    }

    @Nullable
    private BlockPos findMineableBlock() {
        if (this.master == null) return null;

        BlockPos masterPos = master.blockPosition();
        Level level = npc.level();
        BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();

        BlockPos nearest = null;
        double nearestDist = Double.MAX_VALUE;

        int radius = (int) BLOCK_RADIUS;
        for (int x = -radius; x <= radius; x++) {
            for (int z = -radius; z <= radius; z++) {
                for (int y = -2; y <= 2; y++) {
                    mutable.set(
                        masterPos.getX() + x,
                        masterPos.getY() + y,
                        masterPos.getZ() + z
                    );
                    BlockState state = level.getBlockState(mutable);

                    if (isMineable(state)) {
                        double dist = npc.blockPosition().distSqr(mutable);
                        if (dist < nearestDist) {
                            nearest = mutable.immutable();
                            nearestDist = dist;
                        }
                    }
                }
            }
        }

        return nearest;
    }

    @Nullable
    private BlockPos findChoppableBlock() {
        if (this.master == null) return null;

        BlockPos masterPos = master.blockPosition();
        Level level = npc.level();
        BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();

        BlockPos nearest = null;
        double nearestDist = Double.MAX_VALUE;

        int radius = (int) BLOCK_RADIUS;
        for (int x = -radius; x <= radius; x++) {
            for (int z = -radius; z <= radius; z++) {
                for (int y = -2; y <= 4; y++) {
                    // Trees are taller
                    mutable.set(
                        masterPos.getX() + x,
                        masterPos.getY() + y,
                        masterPos.getZ() + z
                    );
                    BlockState state = level.getBlockState(mutable);

                    if (isChoppable(state)) {
                        double dist = npc.blockPosition().distSqr(mutable);
                        if (dist < nearestDist) {
                            nearest = mutable.immutable();
                            nearestDist = dist;
                        }
                    }
                }
            }
        }

        return nearest;
    }

    @Nullable
    private EntityDamsel findCapturableDamsel() {
        if (this.master == null) return null;

        AABB searchArea = new AABB(master.blockPosition()).inflate(
            BEHAVIOR_RADIUS
        );
        List<EntityDamsel> damsels = npc
            .level()
            .getEntitiesOfClass(EntityDamsel.class, searchArea);

        EntityDamsel nearest = null;
        double nearestDist = Double.MAX_VALUE;

        for (EntityDamsel damsel : damsels) {
            // Skip self
            if (damsel == this.npc) continue;
            // Skip already tied
            if (damsel.isTiedUp()) continue;
            // Skip collared (owned)
            if (damsel.hasCollar()) continue;

            double dist = npc.distanceToSqr(damsel);
            if (dist < nearestDist) {
                nearest = damsel;
                nearestDist = dist;
            }
        }

        return nearest;
    }

    private boolean isMineable(BlockState state) {
        return state.is(BlockTags.MINEABLE_WITH_PICKAXE);
    }

    private boolean isChoppable(BlockState state) {
        return state.is(BlockTags.LOGS);
    }

    /**
     * Try to teleport near the master.
     */
    private boolean tryTeleportNear() {
        if (this.master == null) return false;

        // Don't teleport if NPC is tied up (can't move freely)
        if (this.npc.isTiedUp()) {
            return false;
        }

        // Find a safe spot near master
        double x =
            this.master.getX() + (this.npc.getRandom().nextDouble() - 0.5) * 4;
        double y = this.master.getY();
        double z =
            this.master.getZ() + (this.npc.getRandom().nextDouble() - 0.5) * 4;

        // Check if spot is safe
        if (
            this.npc.level().noCollision(
                this.npc,
                this.npc.getBoundingBox().move(
                    x - this.npc.getX(),
                    y - this.npc.getY(),
                    z - this.npc.getZ()
                )
            )
        ) {
            this.npc.teleportTo(x, y, z);
            this.npc.getNavigation().stop();
            return true;
        }

        return false;
    }

    @Override
    public boolean requiresUpdateEveryTick() {
        return true;
    }
}
