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

import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.personality.JobExperience;
import com.tiedup.remake.personality.JobPersonalityModifiers;
import com.tiedup.remake.personality.NpcCommand;
import com.tiedup.remake.personality.PersonalityState;
import java.util.List;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.core.NonNullList;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.Container;
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.ChestBlock;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import org.jetbrains.annotations.Nullable;

/**
 * Utility methods shared by NPC command goals.
 *
 * Personality System: Shared goal utilities
 */
public class NpcGoalHelper {

    /**
     * Find the player who gave the current command to this NPC.
     * Uses O(1) ServerLevel.getPlayerByUUID instead of O(N) iteration.
     *
     * @param npc The NPC entity
     * @return The commanding player, or null if not found
     */
    @Nullable
    public static Player findCommandingPlayer(EntityDamsel npc) {
        var state = npc.getPersonalityState();
        if (state == null) return null;

        UUID commanderUUID = state.getCommandingPlayer();
        if (commanderUUID == null) return null;

        // O(1) lookup via ServerLevel
        if (npc.level() instanceof ServerLevel serverLevel) {
            return serverLevel.getPlayerByUUID(commanderUUID);
        }
        return null;
    }

    /**
     * Award XP to the NPC's commanding player.
     * Consolidates duplicated XP awarding logic from multiple command goals.
     *
     * @param npc    The NPC entity
     * @param amount The XP amount to award
     * @return true if XP was awarded, false if no commander found
     */
    public static boolean awardXPToMaster(EntityDamsel npc, int amount) {
        Player commander = findCommandingPlayer(npc);
        if (commander != null) {
            return true;
        }
        return false;
    }

    /**
     * Get combined job efficiency for an NPC performing a command.
     * Combines: rest efficiency × personality modifier × experience speed.
     *
     * @param npc     The NPC entity
     * @param command The job command being performed
     * @return Combined efficiency multiplier (typically 0.3 to 2.0)
     */
    public static float getJobEfficiency(EntityDamsel npc, NpcCommand command) {
        PersonalityState state = npc.getPersonalityState();
        if (state == null) return 1.0f;

        float restEfficiency = state.getNeeds().getEfficiencyModifier();
        float personalityMod = JobPersonalityModifiers.getEfficiencyModifier(
            state.getPersonality(),
            command
        );
        float experienceSpeed = state
            .getJobExperience()
            .getSpeedMultiplier(command);

        return restEfficiency * personalityMod * experienceSpeed;
    }

    /**
     * Increment job experience for an NPC on a specific command.
     * Should be called once per work cycle (~1200 ticks).
     *
     * @param npc     The NPC entity
     * @param command The job command being performed
     */
    public static void incrementJobExperience(
        EntityDamsel npc,
        NpcCommand command
    ) {
        PersonalityState state = npc.getPersonalityState();
        if (state == null) return;
        state.getJobExperience().addExperience(command);
    }

    /**
     * Get the yield multiplier for an NPC performing a job command.
     * Based on experience level - higher experience = better yields.
     *
     * @param npc     The NPC entity
     * @param command The job command being performed
     * @return Yield multiplier (1.0 to 1.3)
     */
    public static float getJobYieldMultiplier(
        EntityDamsel npc,
        NpcCommand command
    ) {
        PersonalityState state = npc.getPersonalityState();
        if (state == null) return 1.0f;
        return state.getJobExperience().getYieldMultiplier(command);
    }

    // ==================== Cell Navigation Utilities ====================

    /**
     * Teleport NPC to a position (centered on block).
     */
    public static void teleportTo(EntityDamsel npc, BlockPos target) {
        npc.teleportTo(target.getX() + 0.5, target.getY(), target.getZ() + 0.5);
        npc.getNavigation().stop();
    }

    /**
     * Check if NPC is near a position.
     * @param distSq Squared distance threshold
     */
    public static boolean isNearPos(
        EntityDamsel npc,
        BlockPos pos,
        double distSq
    ) {
        return (
            pos != null &&
            npc.distanceToSqr(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5) <
            distSq
        );
    }

    /**
     * Teleport NPC outside cell if currently inside.
     * Used by job goals when starting work - NPC needs to leave the cell first.
     * @return true if teleported
     */
    public static boolean ensureOutsideCell(EntityDamsel npc) {
        PersonalityState state = npc.getPersonalityState();
        if (state == null) return false;
        BlockPos delivery = state.getCellDeliveryPoint();
        BlockPos home = state.getHomePos();
        if (
            delivery != null &&
            home != null &&
            !delivery.equals(home) &&
            isNearPos(npc, home, 9.0)
        ) {
            // < 3 blocks
            teleportTo(npc, delivery);
            return true;
        }
        return false;
    }

    /**
     * Get navigation target for going home (deliveryPoint if cell, otherwise homePos).
     * When a cell has a deliveryPoint different from homePos, NPC should navigate
     * to the deliveryPoint (exterior) then teleport inside.
     */
    public static BlockPos getHomeNavigationTarget(EntityDamsel npc) {
        PersonalityState state = npc.getPersonalityState();
        if (state == null) return npc.blockPosition();
        BlockPos delivery = state.getCellDeliveryPoint();
        BlockPos home = state.getHomePos();
        if (delivery != null && home != null && !delivery.equals(home)) {
            return delivery;
        }
        return home != null ? home : npc.blockPosition();
    }

    // ==================== Chest / Inventory Utilities ====================

    /**
     * Get the container for a chest at the given position.
     * Supports double chests via {@link ChestBlock#getContainer}.
     *
     * @param level The world level
     * @param pos   The chest block position
     * @return The container, or null if no chest at that position
     */
    @Nullable
    public static Container getChestContainer(Level level, BlockPos pos) {
        if (level.getBlockEntity(pos) instanceof ChestBlockEntity chest) {
            return ChestBlock.getContainer(
                (ChestBlock) chest.getBlockState().getBlock(),
                chest.getBlockState(),
                level,
                pos,
                false
            );
        }
        return null;
    }

    /**
     * Insert an ItemStack into a container, stacking with existing items first,
     * then filling empty slots. Calls {@code container.setChanged()} after any
     * modification so changes persist to disk.
     *
     * @param container The target container
     * @param stack     The stack to insert (count is mutated in-place)
     * @return The remaining ItemStack that could not be inserted (or {@link ItemStack#EMPTY})
     */
    public static ItemStack insertIntoContainer(
        Container container,
        ItemStack stack
    ) {
        boolean modified = false;

        // Try to stack with existing items first
        for (
            int i = 0;
            i < container.getContainerSize() && !stack.isEmpty();
            i++
        ) {
            ItemStack slotStack = container.getItem(i);
            if (
                !slotStack.isEmpty() &&
                ItemStack.isSameItemSameTags(slotStack, stack)
            ) {
                int space = slotStack.getMaxStackSize() - slotStack.getCount();
                int toTransfer = Math.min(space, stack.getCount());
                if (toTransfer > 0) {
                    slotStack.grow(toTransfer);
                    stack.shrink(toTransfer);
                    modified = true;
                }
            }
        }

        // Then try empty slots
        for (
            int i = 0;
            i < container.getContainerSize() && !stack.isEmpty();
            i++
        ) {
            if (container.getItem(i).isEmpty()) {
                container.setItem(i, stack.copy());
                stack.setCount(0);
                modified = true;
            }
        }

        if (modified) {
            container.setChanged();
        }

        return stack.isEmpty() ? ItemStack.EMPTY : stack;
    }

    /**
     * Store a list of items into a chest, dropping any overflow via the NPC.
     * Plays the chest-close sound on completion.
     *
     * @param npc      The NPC (used for overflow drops)
     * @param level    The world level
     * @param chestPos The chest block position
     * @param items    The items to store (list is NOT cleared by this method)
     */
    public static void storeItemsInChest(
        EntityDamsel npc,
        Level level,
        BlockPos chestPos,
        List<ItemStack> items
    ) {
        Container container = getChestContainer(level, chestPos);
        if (container != null) {
            for (ItemStack item : items) {
                ItemStack remaining = insertIntoContainer(container, item);
                if (!remaining.isEmpty()) {
                    npc.spawnAtLocation(remaining);
                }
            }
        } else {
            // No chest — drop everything
            for (ItemStack item : items) {
                if (!item.isEmpty()) {
                    npc.spawnAtLocation(item);
                }
            }
        }

        level.playSound(
            null,
            chestPos,
            SoundEvents.CHEST_CLOSE,
            SoundSource.BLOCKS,
            0.5f,
            1.0f
        );
    }

    /**
     * Insert an ItemStack into the NPC's internal inventory (NonNullList).
     * Stacks with existing matching items first, then fills empty slots.
     * Drops any overflow via {@code npc.spawnAtLocation()}.
     *
     * @param npc   The NPC entity
     * @param stack The stack to insert
     * @return true if at least some items were added to inventory
     */
    public static boolean addToNpcInventory(EntityDamsel npc, ItemStack stack) {
        NonNullList<ItemStack> inventory = npc.getNpcInventory();
        int originalCount = stack.getCount();

        // Try to stack with existing items first
        for (int i = 0; i < inventory.size() && !stack.isEmpty(); i++) {
            ItemStack slotStack = inventory.get(i);
            if (
                !slotStack.isEmpty() &&
                ItemStack.isSameItemSameTags(slotStack, stack)
            ) {
                int space = slotStack.getMaxStackSize() - slotStack.getCount();
                int toTransfer = Math.min(space, stack.getCount());
                if (toTransfer > 0) {
                    slotStack.grow(toTransfer);
                    stack.shrink(toTransfer);
                }
            }
        }

        // Then try empty slots
        for (int i = 0; i < inventory.size() && !stack.isEmpty(); i++) {
            if (inventory.get(i).isEmpty()) {
                inventory.set(i, stack.copy());
                stack.setCount(0);
            }
        }

        // Drop overflow
        if (!stack.isEmpty()) {
            npc.spawnAtLocation(stack);
        }

        return stack.getCount() < originalCount;
    }
}
