package com.tiedup.remake.cells;

import com.tiedup.remake.core.TiedUpMod;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.nbt.Tag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import net.minecraft.world.level.saveddata.SavedData;
import org.jetbrains.annotations.Nullable;

/**
 *
 * When a player is imprisoned:
 * 1. Their inventory is saved to NBT
 * 2. Items are transferred to a LOOT chest in the cell
 * 3. Player's inventory is cleared
 * 4. Data is persisted for recovery after server restart
 *
 * Recovery options:
 * - Player finds and opens the chest manually
 * - Player escapes and returns to chest
 * - Admin command /tiedup returnstuff <player>
 */
public class ConfiscatedInventoryRegistry extends SavedData {

    private static final String DATA_NAME =
        TiedUpMod.MOD_ID + "_confiscated_inventories";

    /**
     * Map of prisoner UUID to their confiscated inventory data
     */
    private final Map<UUID, ConfiscatedData> confiscatedInventories =
        new HashMap<>();

    /**
     * Data class for a single confiscated inventory
     */
    public static class ConfiscatedData {

        public final UUID prisonerId;
        public final CompoundTag inventoryNbt;
        public final BlockPos chestPos;
        public final UUID cellId;
        public final long confiscatedTime;

        public ConfiscatedData(
            UUID prisonerId,
            CompoundTag inventoryNbt,
            BlockPos chestPos,
            @Nullable UUID cellId,
            long confiscatedTime
        ) {
            this.prisonerId = prisonerId;
            this.inventoryNbt = inventoryNbt;
            this.chestPos = chestPos;
            this.cellId = cellId;
            this.confiscatedTime = confiscatedTime;
        }

        public CompoundTag save() {
            CompoundTag tag = new CompoundTag();
            tag.putUUID("prisonerId", prisonerId);
            tag.put("inventory", inventoryNbt);
            tag.put("chestPos", NbtUtils.writeBlockPos(chestPos));
            if (cellId != null) {
                tag.putUUID("cellId", cellId);
            }
            tag.putLong("time", confiscatedTime);
            return tag;
        }

        public static ConfiscatedData load(CompoundTag tag) {
            UUID prisonerId = tag.getUUID("prisonerId");
            CompoundTag inventoryNbt = tag.getCompound("inventory");
            BlockPos chestPos = NbtUtils.readBlockPos(
                tag.getCompound("chestPos")
            );
            UUID cellId = tag.contains("cellId") ? tag.getUUID("cellId") : null;
            long time = tag.getLong("time");
            return new ConfiscatedData(
                prisonerId,
                inventoryNbt,
                chestPos,
                cellId,
                time
            );
        }
    }

    public ConfiscatedInventoryRegistry() {}

    // ==================== STATIC ACCESSORS ====================

    /**
     * Get or create the registry for a server level.
     */
    public static ConfiscatedInventoryRegistry get(ServerLevel level) {
        return level
            .getDataStorage()
            .computeIfAbsent(
                ConfiscatedInventoryRegistry::load,
                ConfiscatedInventoryRegistry::new,
                DATA_NAME
            );
    }

    // ==================== CONFISCATION METHODS ====================

    /**
     * Confiscate a player's inventory.
     *
     * @param player The player whose inventory to confiscate
     * @param chestPos The position of the LOOT chest
     * @param cellId The cell ID (optional)
     * @return true if confiscation was successful
     */
    public boolean confiscate(
        ServerPlayer player,
        BlockPos chestPos,
        @Nullable UUID cellId
    ) {
        Inventory inventory = player.getInventory();

        // CRITICAL FIX: Transaction safety - save record BEFORE any state changes
        // This ensures that if the server crashes, we have a backup to restore from

        // Save inventory to NBT (backup in case of issues)
        CompoundTag inventoryNbt = savePlayerInventory(player);

        // Create and persist record FIRST (before modifying game state)
        ConfiscatedData data = new ConfiscatedData(
            player.getUUID(),
            inventoryNbt,
            chestPos,
            cellId,
            System.currentTimeMillis()
        );
        confiscatedInventories.put(player.getUUID(), data);
        setDirty(); // Persist immediately before state changes

        // Now attempt transfer to chest
        boolean transferred = transferToChest(
            player.serverLevel(),
            chestPos,
            inventory
        );

        if (!transferred) {
            // Transfer failed - rollback: remove record and DO NOT clear inventory
            confiscatedInventories.remove(player.getUUID());
            setDirty(); // Persist the rollback

            TiedUpMod.LOGGER.error(
                "[ConfiscatedInventoryRegistry] Failed to transfer items for {} - items NOT confiscated, record rolled back",
                player.getName().getString()
            );
            return false;
        }

        // Transfer succeeded - now safe to clear inventory
        inventory.clearContent();

        TiedUpMod.LOGGER.info(
            "[ConfiscatedInventoryRegistry] Confiscated inventory from {} (chest: {}, cell: {})",
            player.getName().getString(),
            chestPos.toShortString(),
            cellId != null ? cellId.toString().substring(0, 8) : "none"
        );

        // Final persist to save cleared inventory state
        setDirty();
        return true;
    }

    /**
     * Dump a player's inventory to a chest WITHOUT creating a backup record.
     * Used for daily labor returns - items gathered during work are transferred to camp storage.
     * Does NOT clear the player's inventory - caller should clear labor tools separately.
     *
     * @param player The player whose inventory to dump
     * @param chestPos The position of the chest
     * @return true if transfer was successful
     */
    public boolean dumpInventoryToChest(
        ServerPlayer player,
        BlockPos chestPos
    ) {
        Inventory inventory = player.getInventory();

        // Transfer items to chest
        boolean transferred = transferToChest(
            player.serverLevel(),
            chestPos,
            inventory
        );

        if (!transferred) {
            TiedUpMod.LOGGER.warn(
                "[ConfiscatedInventoryRegistry] Failed to dump labor inventory for {} - chest issue",
                player.getName().getString()
            );
            return false;
        }

        // Clear player inventory (items are now in chest)
        inventory.clearContent();

        TiedUpMod.LOGGER.debug(
            "[ConfiscatedInventoryRegistry] Dumped labor inventory from {} to chest at {}",
            player.getName().getString(),
            chestPos.toShortString()
        );

        return true;
    }

    /**
     * Deposits items across multiple LOOT chests with smart rotation.
     * Distributes items evenly to prevent single chest from filling too quickly.
     *
     * @param items List of items to deposit
     * @param chestPositions List of chest positions to use (in priority order)
     * @param level Server level
     * @return Number of items successfully deposited (remainder dropped)
     */
    public int depositItemsInChests(
        List<ItemStack> items,
        List<BlockPos> chestPositions,
        ServerLevel level
    ) {
        if (items.isEmpty() || chestPositions.isEmpty()) {
            return 0;
        }

        int deposited = 0;
        List<ItemStack> overflow = new ArrayList<>();

        // Try to deposit each item
        for (ItemStack stack : items) {
            if (stack.isEmpty()) continue;

            boolean placed = false;

            // Try all chests in order
            for (BlockPos chestPos : chestPositions) {
                BlockEntity be = level.getBlockEntity(chestPos);
                if (!(be instanceof ChestBlockEntity chest)) continue;

                // Try to stack with existing items first
                for (int i = 0; i < chest.getContainerSize(); i++) {
                    ItemStack slot = chest.getItem(i);

                    // Stack with existing
                    if (
                        !slot.isEmpty() &&
                        ItemStack.isSameItemSameTags(slot, stack) &&
                        slot.getCount() < slot.getMaxStackSize()
                    ) {
                        int spaceInSlot =
                            slot.getMaxStackSize() - slot.getCount();
                        int toAdd = Math.min(spaceInSlot, stack.getCount());

                        slot.grow(toAdd);
                        chest.setChanged();
                        stack.shrink(toAdd);
                        deposited += toAdd;

                        if (stack.isEmpty()) {
                            placed = true;
                            break;
                        }
                    }
                }

                if (placed) break;

                // Try to place in empty slot
                if (!stack.isEmpty()) {
                    for (int i = 0; i < chest.getContainerSize(); i++) {
                        if (chest.getItem(i).isEmpty()) {
                            chest.setItem(i, stack.copy());
                            chest.setChanged();
                            deposited += stack.getCount();
                            placed = true;
                            break;
                        }
                    }
                }

                if (placed) break;
            }

            // If not placed, add to overflow
            if (!placed && !stack.isEmpty()) {
                overflow.add(stack);
            }
        }

        // Drop overflow items at first chest location
        if (!overflow.isEmpty() && !chestPositions.isEmpty()) {
            BlockPos dropPos = chestPositions.get(0);
            for (ItemStack stack : overflow) {
                net.minecraft.world.entity.item.ItemEntity itemEntity =
                    new net.minecraft.world.entity.item.ItemEntity(
                        level,
                        dropPos.getX() + 0.5,
                        dropPos.getY() + 1.0,
                        dropPos.getZ() + 0.5,
                        stack
                    );
                level.addFreshEntity(itemEntity);
            }

            TiedUpMod.LOGGER.warn(
                "[ConfiscatedInventoryRegistry] {} items overflowed - dropped at {}",
                overflow.stream().mapToInt(ItemStack::getCount).sum(),
                dropPos.toShortString()
            );
        }

        return deposited;
    }

    /**
     * Save player inventory to NBT.
     */
    private CompoundTag savePlayerInventory(ServerPlayer player) {
        CompoundTag tag = new CompoundTag();
        tag.put("Items", player.getInventory().save(new ListTag()));
        return tag;
    }

    /**
     * Transfer inventory contents to a chest.
     * Handles ALL inventory slots:
     * - Slots 0-35: Main inventory (hotbar 0-8, backpack 9-35)
     * - Slots 36-39: Armor (boots, leggings, chestplate, helmet)
     * - Slot 40: Offhand
     */
    private boolean transferToChest(
        ServerLevel level,
        BlockPos chestPos,
        Inventory inventory
    ) {
        // Find an existing chest near the LOOT marker position
        BlockPos actualChestPos = findExistingChestNear(level, chestPos);
        if (actualChestPos == null) {
            TiedUpMod.LOGGER.warn(
                "[ConfiscatedInventoryRegistry] No existing chest found near {} - structure may be damaged",
                chestPos.toShortString()
            );
            return false;
        }

        BlockEntity be = level.getBlockEntity(actualChestPos);
        if (!(be instanceof Container chest)) {
            TiedUpMod.LOGGER.warn(
                "[ConfiscatedInventoryRegistry] Block at {} is not a container",
                actualChestPos.toShortString()
            );
            return false;
        }

        // Transfer ALL items including armor and offhand
        int mainItems = 0;
        int armorItems = 0;
        int offhandItems = 0;
        int droppedItems = 0;

        // getContainerSize() returns 41: 36 main + 4 armor + 1 offhand
        for (int i = 0; i < inventory.getContainerSize(); i++) {
            ItemStack stack = inventory.getItem(i);
            if (!stack.isEmpty()) {
                ItemStack remaining = addToContainer(chest, stack.copy());
                if (remaining.isEmpty()) {
                    // Track which slot type was transferred
                    if (i < 36) {
                        mainItems++;
                    } else if (i < 40) {
                        armorItems++;
                    } else {
                        offhandItems++;
                    }
                } else {
                    // Couldn't fit all items - drop them on the ground near the chest
                    // This prevents permanent item loss
                    net.minecraft.world.entity.item.ItemEntity itemEntity =
                        new net.minecraft.world.entity.item.ItemEntity(
                            level,
                            actualChestPos.getX() + 0.5,
                            actualChestPos.getY() + 1.0,
                            actualChestPos.getZ() + 0.5,
                            remaining
                        );
                    // Add slight random velocity to spread items
                    itemEntity.setDeltaMovement(
                        (level.random.nextDouble() - 0.5) * 0.2,
                        0.2,
                        (level.random.nextDouble() - 0.5) * 0.2
                    );
                    level.addFreshEntity(itemEntity);
                    droppedItems += remaining.getCount();
                }
            }
        }

        if (droppedItems > 0) {
            TiedUpMod.LOGGER.info(
                "[ConfiscatedInventoryRegistry] Dropped {} overflow items at {}",
                droppedItems,
                actualChestPos.toShortString()
            );
        }

        int totalTransferred = mainItems + armorItems + offhandItems;
        TiedUpMod.LOGGER.info(
            "[ConfiscatedInventoryRegistry] Confiscated {} items (inventory: {}, armor: {}, offhand: {})",
            totalTransferred,
            mainItems,
            armorItems,
            offhandItems
        );

        return true;
    }

    /**
     * Add an item stack to a container, returning any remainder.
     */
    private ItemStack addToContainer(Container container, ItemStack stack) {
        ItemStack remaining = stack.copy();

        for (
            int i = 0;
            i < container.getContainerSize() && !remaining.isEmpty();
            i++
        ) {
            ItemStack slotStack = container.getItem(i);

            if (slotStack.isEmpty()) {
                container.setItem(i, remaining.copy());
                remaining = ItemStack.EMPTY;
            } else if (ItemStack.isSameItemSameTags(slotStack, remaining)) {
                int space = slotStack.getMaxStackSize() - slotStack.getCount();
                int toTransfer = Math.min(space, remaining.getCount());
                if (toTransfer > 0) {
                    slotStack.grow(toTransfer);
                    remaining.shrink(toTransfer);
                }
            }
        }

        container.setChanged();
        return remaining;
    }

    /**
     * Find an existing chest near the given position (typically a LOOT marker).
     * The LOOT marker is placed ABOVE the physical chest in the structure,
     * so we check below first, then at the position, then in a small radius.
     *
     * Does NOT spawn new chests - structures must have chests placed by their templates.
     *
     * @return The position of an existing chest, or null if none found
     */
    @Nullable
    private static BlockPos findExistingChestNear(
        ServerLevel level,
        BlockPos pos
    ) {
        // Check below the marker (chest is usually under the LOOT marker)
        BlockPos below = pos.below();
        if (level.getBlockEntity(below) instanceof ChestBlockEntity) {
            return below;
        }

        // Check at the marker position itself
        if (level.getBlockEntity(pos) instanceof ChestBlockEntity) {
            return pos;
        }

        // Search in a small radius (3 blocks)
        for (int radius = 1; radius <= 3; radius++) {
            for (int dx = -radius; dx <= radius; dx++) {
                for (int dy = -2; dy <= 1; dy++) {
                    for (int dz = -radius; dz <= radius; dz++) {
                        if (dx == 0 && dy == 0 && dz == 0) continue;

                        BlockPos testPos = pos.offset(dx, dy, dz);
                        if (
                            level.getBlockEntity(testPos) instanceof
                                ChestBlockEntity
                        ) {
                            TiedUpMod.LOGGER.debug(
                                "[ConfiscatedInventoryRegistry] Found existing chest at {} (offset from marker {})",
                                testPos.toShortString(),
                                pos.toShortString()
                            );
                            return testPos;
                        }
                    }
                }
            }
        }

        TiedUpMod.LOGGER.warn(
            "[ConfiscatedInventoryRegistry] No existing chest found near marker at {}",
            pos.toShortString()
        );
        return null;
    }

    // ==================== RECOVERY METHODS ====================

    /**
     * Check if a player has confiscated inventory.
     */
    public boolean hasConfiscatedInventory(UUID playerId) {
        return confiscatedInventories.containsKey(playerId);
    }

    /**
     * Get confiscated data for a player.
     */
    @Nullable
    public ConfiscatedData getConfiscatedData(UUID playerId) {
        return confiscatedInventories.get(playerId);
    }

    /**
     * Get the chest position for a player's confiscated inventory.
     */
    @Nullable
    public BlockPos getChestPosition(UUID playerId) {
        ConfiscatedData data = confiscatedInventories.get(playerId);
        return data != null ? data.chestPos : null;
    }

    /**
     * Restore a player's confiscated inventory from the NBT backup.
     * This gives items directly to the player, bypassing the chest.
     *
     * @param player The player to restore inventory to
     * @return true if restoration was successful, false if no confiscated data found
     */
    public boolean restoreInventory(ServerPlayer player) {
        ConfiscatedData data = confiscatedInventories.get(player.getUUID());
        if (data == null) {
            TiedUpMod.LOGGER.debug(
                "[ConfiscatedInventoryRegistry] No confiscated inventory for {}",
                player.getName().getString()
            );
            return false;
        }

        // Load inventory from NBT backup
        if (data.inventoryNbt.contains("Items")) {
            ListTag items = data.inventoryNbt.getList(
                "Items",
                Tag.TAG_COMPOUND
            );
            player.getInventory().load(items);

            TiedUpMod.LOGGER.info(
                "[ConfiscatedInventoryRegistry] Restored {} inventory slots to {}",
                items.size(),
                player.getName().getString()
            );
        }

        // Remove the confiscation record
        confiscatedInventories.remove(player.getUUID());
        setDirty();

        return true;
    }

    /**
     * Remove the confiscation record for a player without restoring items.
     * Used when the player has already retrieved items from the chest manually.
     *
     * @param playerId The player's UUID
     */
    public void clearConfiscationRecord(UUID playerId) {
        if (confiscatedInventories.remove(playerId) != null) {
            TiedUpMod.LOGGER.debug(
                "[ConfiscatedInventoryRegistry] Cleared confiscation record for {}",
                playerId.toString().substring(0, 8)
            );
            setDirty();
        }
    }

    // ==================== SERIALIZATION ====================

    @Override
    public CompoundTag save(CompoundTag tag) {
        ListTag list = new ListTag();
        for (ConfiscatedData data : confiscatedInventories.values()) {
            list.add(data.save());
        }
        tag.put("confiscated", list);
        return tag;
    }

    public static ConfiscatedInventoryRegistry load(CompoundTag tag) {
        ConfiscatedInventoryRegistry registry =
            new ConfiscatedInventoryRegistry();

        if (tag.contains("confiscated")) {
            ListTag list = tag.getList("confiscated", Tag.TAG_COMPOUND);
            for (int i = 0; i < list.size(); i++) {
                ConfiscatedData data = ConfiscatedData.load(
                    list.getCompound(i)
                );
                registry.confiscatedInventories.put(data.prisonerId, data);
            }
        }

        TiedUpMod.LOGGER.info(
            "[ConfiscatedInventoryRegistry] Loaded {} confiscated inventory records",
            registry.confiscatedInventories.size()
        );

        return registry;
    }
}
