'use strict';

const Vec3 = require('vec3');
const { sleep } = require('../../utils');
const { goals: { GoalNear } } = require('mineflayer-pathfinder');
const Database = require('./database');

class ShulkerHandler {
	constructor() {
		this.operationInProgress = false;
		this.scanner = null; // set by Storage after init
	}

	/**
	 * Find an air block near the bot with a solid block below it.
	 * Used as a spot to place a shulker box on the ground.
	 * Prefers 2-block offsets so the bot isn't standing on the placement block.
	 */
	findPlacementSpot(bot, excludePositions = []) {
		const botPos = bot.bot.entity.position.floored();
		const excludeSet = new Set(excludePositions.map(p => `${p.x},${p.y},${p.z}`));
		const offsets = [
			// 2-block offsets preferred — bot has room to stand and place
			new Vec3(2, 0, 0),
			new Vec3(-2, 0, 0),
			new Vec3(0, 0, 2),
			new Vec3(0, 0, -2),
			new Vec3(2, 0, 1),
			new Vec3(-2, 0, 1),
			new Vec3(2, 0, -1),
			new Vec3(-2, 0, -1),
			new Vec3(1, 0, 2),
			new Vec3(-1, 0, 2),
			new Vec3(1, 0, -2),
			new Vec3(-1, 0, -2),
			// Fallback: 1-block offsets (tight spaces)
			new Vec3(1, 0, 0),
			new Vec3(-1, 0, 0),
			new Vec3(0, 0, 1),
			new Vec3(0, 0, -1),
			new Vec3(1, 0, 1),
			new Vec3(-1, 0, 1),
			new Vec3(1, 0, -1),
			new Vec3(-1, 0, -1),
			// Y-1 for tight storage areas
			new Vec3(2, -1, 0),
			new Vec3(-2, -1, 0),
			new Vec3(0, -1, 2),
			new Vec3(0, -1, -2),
			new Vec3(1, -1, 0),
			new Vec3(-1, -1, 0),
			new Vec3(0, -1, 1),
			new Vec3(0, -1, -1),
		];

		for (const offset of offsets) {
			const checkPos = botPos.offset(offset.x, offset.y, offset.z);
			if (excludeSet.has(`${checkPos.x},${checkPos.y},${checkPos.z}`)) continue;
			const blockAtPos = bot.bot.blockAt(checkPos);
			const blockBelow = bot.bot.blockAt(checkPos.offset(0, -1, 0));

			if (blockAtPos && blockAtPos.name === 'air' && blockBelow && blockBelow.boundingBox === 'block') {
				return {
					position: checkPos,
					placeOn: blockBelow,
					faceVec: new Vec3(0, 1, 0),
				};
			}
		}

		throw new Error('No suitable placement spot found near bot');
	}

	/**
	 * Break a placed shulker and wait for it to be collected into inventory.
	 * Retries dig if block survives, then walks to drop position if item not picked up.
	 * Returns true if shulker is in inventory, false otherwise.
	 */
	async digAndCollectShulker(bot, placedPos) {
		console.log(`ShulkerHandler: Breaking shulker at ${placedPos}`);

		// Ensure all movement is fully stopped before digging
		bot.bot.setControlState('forward', false);
		bot.bot.setControlState('back', false);
		bot.bot.setControlState('left', false);
		bot.bot.setControlState('right', false);
		bot.bot.setControlState('jump', false);
		bot.bot.setControlState('sprint', false);
		bot.bot.setControlState('sneak', false);

		// Stop pathfinder movement too
		try { bot.bot.pathfinder.stop(); } catch (e) { /* ignore */ }

		// Retry dig up to 3 times — shulker closing animation can block the first attempt
		for (let digAttempt = 0; digAttempt < 3; digAttempt++) {
			const block = bot.bot.blockAt(placedPos);
			if (!block || !block.name.includes('shulker_box')) {
				break;
			}

			console.log(`ShulkerHandler: Digging shulker at ${placedPos} (attempt ${digAttempt + 1})`);
			try {
				// Look at block center before equipping tool
				await bot.bot.lookAt(placedPos.offset(0.5, 0.5, 0.5), true);

				// Equip best tool for the block (anti-cheat expects tool-appropriate timing)
				const pickaxe = bot.bot.inventory.items().find(i => i.name.includes('pickaxe'));
				if (pickaxe) {
					await bot.bot.equip(pickaxe, 'hand');
				} else {
					try { await bot.bot.unequip('hand'); } catch (e) { /* empty hand is fine */ }
				}


				// Anti-cheat cooldown before dig
				await bot.bot.waitForTicks(20);

				// Use 'raycast' so mineflayer sends the correct block face in dig packets
				await bot.bot.dig(block, 'raycast');

				// Anti-cheat cooldown after dig
				await bot.bot.waitForTicks(25);

			} catch (e) {
				console.log(`ShulkerHandler: Dig error: ${e.message}`);
			}

			// Check if block is actually gone
			const afterDig = bot.bot.blockAt(placedPos);
			if (!afterDig || !afterDig.name.includes('shulker_box')) {
				console.log('ShulkerHandler: Shulker block broken successfully');
				break;
			}

			// Wait between retries
			console.log('ShulkerHandler: Block still present after dig, waiting before retry...');
			await bot.bot.waitForTicks(40);
		}

		// Verify block is gone
		const finalCheck = bot.bot.blockAt(placedPos);
		if (finalCheck && finalCheck.name.includes('shulker_box')) {
			console.error('ShulkerHandler: Could not break shulker block after 3 attempts');
			return false;
		}

		// Wait a few ticks for the item entity to spawn
		await bot.bot.waitForTicks(3);

		// Attempt 1: Jump onto the drop position to collect it (like a human player)
		console.log(`ShulkerHandler: Jumping onto ${placedPos} to collect drop`);
		try {
			await bot.bot.lookAt(placedPos.offset(0.5, 0, 0.5), true);
			await bot.bot.waitForTicks(2);
			bot.bot.setControlState('jump', true);
			bot.bot.setControlState('forward', true);
			await sleep(600);
			bot.bot.setControlState('forward', false);
			bot.bot.setControlState('jump', false);
			await sleep(300);
		} catch (e) {
			console.log(`ShulkerHandler: Jump to collect failed: ${e.message}`);
		}

		// Quick check if already picked up
		if (bot.bot.inventory.items().find(item => item.name.includes('shulker_box'))) {
			return true;
		}

		// Attempt 2: Pathfind directly to the drop position
		try {
			await bot.goTo({ where: placedPos, range: 0 });
		} catch (e) {
			try { await bot.goTo({ where: placedPos, range: 1 }); } catch (e2) { /* ignore */ }
		}

		await bot.bot.waitForTicks(5);
		if (bot.bot.inventory.items().find(item => item.name.includes('shulker_box'))) {
			return true;
		}

		// Attempt 3: Search for the dropped item entity nearby and walk to it
		// Item may have bounced away from the block position
		let droppedEntity = null;
		for (const entity of Object.values(bot.bot.entities)) {
			if (!entity.name || entity.name !== 'item') continue;
			const dist = entity.position.distanceTo(placedPos);
			if (dist < 5 && (!droppedEntity || dist < droppedEntity.position.distanceTo(placedPos))) {
				droppedEntity = entity;
			}
		}

		if (droppedEntity) {
			console.log(`ShulkerHandler: Found item entity at ${droppedEntity.position}, walking to it`);
			try {
				await bot.goTo({ where: droppedEntity.position, range: 0 });
			} catch (e) {
				try { await bot.goTo({ where: droppedEntity.position, range: 1 }); } catch (e2) { /* ignore */ }
			}
			await bot.bot.waitForTicks(5);
			if (bot.bot.inventory.items().find(item => item.name.includes('shulker_box'))) {
				return true;
			}
		}

		// Final poll — wait up to 5 seconds in case of lag
		for (let i = 0; i < 10; i++) {
			await sleep(500);
			if (bot.bot.inventory.items().find(item => item.name.includes('shulker_box'))) {
				return true;
			}
		}

		console.error('ShulkerHandler: Failed to collect shulker after all attempts');
		return !!bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
	}

	/**
	 * Return a shulker from bot inventory back to its chest slot.
	 * If placedPos is given, dig the placed shulker first.
	 * @throws Error if the shulker cannot be returned (caller should handle the failure).
	 */
	async returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId) {
		// If shulker is placed on the ground, break it first
		if (placedPos) {
			try {
				await bot.bot.closeWindow(bot.bot.currentWindow);
			} catch (e) { /* ignore */ }
			await sleep(300);

			await this.digAndCollectShulker(bot, placedPos);
		}

		// Find shulker in bot inventory — throw if not found so caller knows recovery failed
		const shulkerInInv = bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
		if (!shulkerInInv) {
			throw new Error('Recovery failed — no shulker in inventory');
		}

		// Go to chest and open it
		await bot.goTo({ where: chestPos, range: 3 });
		const chestBlock = bot.bot.blockAt(chestPos);
		if (!chestBlock || !chestBlock.name.includes('chest')) {
			throw new Error(`Recovery failed — no chest at ${chestPos}`);
		}
		const window = await bot.openContainer(chestBlock);
		await sleep(300);

		// Find shulker in inventory portion of the window
		let shulkerWindowSlot = null;
		for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
			const item = window.slots[i];
			if (item && item.name.includes('shulker_box')) {
				shulkerWindowSlot = i;
				break;
			}
		}

		if (shulkerWindowSlot === null) {
			await bot.bot.closeWindow(window);
			throw new Error('Recovery failed — shulker not found in window inventory');
		}

		// Try original slot first; if occupied, find any empty chest slot
		let targetSlot = chestSlot;
		if (window.slots[chestSlot]) {
			targetSlot = null;
			for (let i = 0; i < window.inventoryStart; i++) {
				if (!window.slots[i]) {
					targetSlot = i;
					break;
				}
			}
		}

		if (targetSlot === null) {
			await bot.bot.closeWindow(window);
			throw new Error('Recovery failed — no empty chest slot');
		}

		await bot.bot.moveSlotItem(shulkerWindowSlot, targetSlot);
		await sleep(300);

		// Read the NBT and sync DB immediately
		const updatedSlotItem = window.slots[targetSlot];
		await bot.bot.closeWindow(window);
		await sleep(200);

		if (updatedSlotItem && chestId && this.scanner) {
			try {
				await this.scanner.scanShulkerFromNBT(bot, Database, chestId, targetSlot, updatedSlotItem);
				await Database.rebuildItemIndex();
				console.log(`ShulkerHandler: Recovery DB synced — shulker at chest slot ${targetSlot}`);
			} catch (e) {
				console.error(`ShulkerHandler: Recovery DB sync failed: ${e.message}`);
			}
		}

		// Verify the shulker is actually back in the chest
		await bot.goTo({ where: chestPos, range: 3 });
		const verifyBlock = bot.bot.blockAt(chestPos);
		if (!verifyBlock || !verifyBlock.name.includes('chest')) {
			throw new Error('Recovery verification failed — chest not found');
		}
		const verifyWindow = await bot.openContainer(verifyBlock);
		await sleep(200);
		const verifyItem = verifyWindow.slots[targetSlot];
		await bot.bot.closeWindow(verifyWindow);
		await sleep(200);

		if (!verifyItem || !verifyItem.name.includes('shulker_box')) {
			throw new Error(`Recovery verification failed — shulker not found at chest slot ${targetSlot} after return`);
		}

		console.log(`ShulkerHandler: Recovery succeeded — shulker returned to chest slot ${targetSlot}`);
		return true;
	}

	/**
	 * Take a shulker from a chest at a given slot into bot inventory.
	 * Immediately marks shulker as removed in DB (slot_count = -1) so DB stays in sync.
	 * Returns the inventory slot index where the shulker ended up.
	 */
	async takeShulkerFromChest(bot, chestPos, chestSlot, shulkerId) {
		console.log(`ShulkerHandler: Taking shulker from chest at ${chestPos}, slot ${chestSlot}`);

		await bot.goTo({ where: chestPos, range: 3 });
		const chestBlock = bot.bot.blockAt(chestPos);
		if (!chestBlock || !chestBlock.name.includes('chest')) {
			throw new Error(`No chest at ${chestPos} (found: ${chestBlock?.name || 'null'})`);
		}
		const window = await bot.openContainer(chestBlock);
		await sleep(300);

		const slotItem = window.slots[chestSlot];
		if (!slotItem || !slotItem.name.includes('shulker_box')) {
			await bot.bot.closeWindow(window);
			throw new Error(`No shulker at chest slot ${chestSlot}`);
		}

		// Find an empty inventory slot to avoid swapping existing items into the chest
		let targetInvSlot = null;
		for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
			if (!window.slots[i]) {
				targetInvSlot = i;
				break;
			}
		}
		if (targetInvSlot === null) {
			await bot.bot.closeWindow(window);
			throw new Error('No empty inventory slot — refusing to swap items into chest');
		}
		await bot.bot.moveSlotItem(chestSlot, targetInvSlot);
		await sleep(300);

		// Immediately mark shulker as out of chest in DB
		if (shulkerId) {
			try {
				await Database.clearShulkerItems(shulkerId);
				await Database.updateShulkerCounts(shulkerId, -1, 0);
				console.log(`ShulkerHandler: DB marked shulker ${shulkerId} as in-transit`);
			} catch (e) {
				console.error(`ShulkerHandler: DB sync on take failed: ${e.message}`);
			}
		}

		// Find which inventory slot the shulker ended up in
		let shulkerInvSlot = null;
		for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
			const item = window.slots[i];
			if (item && item.name.includes('shulker_box')) {
				shulkerInvSlot = i;
				break;
			}
		}

		await bot.bot.closeWindow(window);
		await sleep(200);

		if (shulkerInvSlot === null) {
			throw new Error('Failed to move shulker to inventory');
		}

		// Convert window slot to bot inventory slot
		const botInvSlot = shulkerInvSlot - window.inventoryStart;
		console.log(`ShulkerHandler: Shulker now in inventory window slot ${shulkerInvSlot}`);
		return shulkerInvSlot;
	}

	/**
	 * Take a whole shulker from a chest and leave it in bot inventory (don't unpack).
	 * Used for shulker-mode withdrawals where the player gets the entire shulker box.
	 * Returns the inventory slot index where the shulker ended up.
	 */
	async takeWholeShulker(bot, chestPos, chestSlot, shulkerId) {
		console.log(`ShulkerHandler: Taking whole shulker from chest at ${chestPos}, slot ${chestSlot}`);

		if (this.operationInProgress) {
			throw new Error('Another shulker operation is in progress');
		}
		this.operationInProgress = true;

		try {
			const invSlot = await this.takeShulkerFromChest(bot, chestPos, chestSlot, shulkerId);
			console.log(`ShulkerHandler: Whole shulker taken, now in inventory slot ${invSlot}`);
			return invSlot;
		} finally {
			this.operationInProgress = false;
		}
	}

	/**
	 * Place a shulker from inventory onto the ground and open it.
	 * Retries once on failure.
	 * Returns { window, placedPos }.
	 */
	async placeAndOpenShulker(bot, inventorySlot) {
		console.log(`ShulkerHandler: Placing shulker from slot ${inventorySlot}`);
		const triedPositions = [];

		for (let attempt = 0; attempt < 5; attempt++) {
			try {
				const spot = this.findPlacementSpot(bot, triedPositions);
				triedPositions.push(spot.position);

				// Ensure bot is at least 1.5 blocks away from placement spot so it's not standing on it
				const botPos = bot.bot.entity.position;
				const placeDist = botPos.distanceTo(spot.position.offset(0.5, 0, 0.5));
				if (placeDist < 1.5) {
					console.log(`ShulkerHandler: Too close to placement spot (${placeDist.toFixed(1)} blocks), stepping back`);
					const dx = botPos.x - spot.position.x;
					const dz = botPos.z - spot.position.z;
					const dist = Math.sqrt(dx * dx + dz * dz) || 1;
					const retreatX = botPos.x + (dx / dist) * 1.5;
					const retreatZ = botPos.z + (dz / dist) * 1.5;
					try {
	
						await bot.bot.pathfinder.goto(new GoalNear(retreatX, botPos.y, retreatZ, 0.5));
						await sleep(300);
					} catch (e) {
						console.log(`ShulkerHandler: Retreat pathfind failed: ${e.message}`);
					}
				}

				// Find the shulker item in bot inventory
				const shulkerItem = bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
				if (!shulkerItem) {
					throw new Error('No shulker box found in inventory');
				}

				await bot.bot.equip(shulkerItem, 'hand');
				await bot.bot.waitForTicks(5);

				// Verify we're actually holding the shulker
				const heldItem = bot.bot.heldItem;
				if (!heldItem || !heldItem.name.includes('shulker_box')) {
					console.log(`ShulkerHandler: Not holding shulker after equip (holding: ${heldItem?.name || 'nothing'}), retrying equip`);
					await bot.bot.equip(shulkerItem, 'hand');
					await bot.bot.waitForTicks(5);
					const heldRetry = bot.bot.heldItem;
					if (!heldRetry || !heldRetry.name.includes('shulker_box')) {
						throw new Error(`Cannot equip shulker (holding: ${heldRetry?.name || 'nothing'})`);
					}
				}

				// Look at the top face center of the block we're placing on
				const lookTarget = spot.placeOn.position.offset(0.5, 1, 0.5);
				await bot.bot.lookAt(lookTarget, true);
				await bot.bot.waitForTicks(3);

				// SAFETY: verify held item is a shulker RIGHT before placing — never place anything else
				const prePlaceItem = bot.bot.heldItem;
				if (!prePlaceItem || !prePlaceItem.name.includes('shulker_box')) {
					throw new Error(`ABORT: held item changed before place (holding: ${prePlaceItem?.name || 'nothing'})`);
				}

				await bot.bot.placeBlock(spot.placeOn, spot.faceVec);
				await bot.bot.waitForTicks(10);

				// Verify shulker was placed
				const placedBlock = bot.bot.blockAt(spot.position);
				if (!placedBlock || !placedBlock.name.includes('shulker_box')) {
					// Something wrong was placed — break it immediately
					console.error(`ShulkerHandler: WRONG BLOCK placed at ${spot.position} (${placedBlock?.name}), breaking it`);
					try { await bot.bot.dig(bot.bot.blockAt(spot.position)); } catch (e) { /* best effort */ }
					throw new Error(`Failed to place shulker at ${spot.position} (found: ${placedBlock?.name || 'null'})`);
				}

				const window = await bot.openContainer(placedBlock);
				await bot.bot.waitForTicks(5);

				console.log(`ShulkerHandler: Shulker placed and opened at ${spot.position}`);
				return { window, placedPos: spot.position };
			} catch (error) {
				console.log(`ShulkerHandler: Place attempt ${attempt + 1} failed (${error.message})`);
				if (attempt < 4) {
					// Move the bot a few blocks so findPlacementSpot finds new spots
					console.log('ShulkerHandler: Moving to find a better placement spot...');
					try {
	
						const pos = bot.bot.entity.position;
						// Walk 3 blocks in a different direction each attempt
						const angle = (attempt * Math.PI / 2) + Math.PI / 4;
						const moveX = pos.x + Math.cos(angle) * 3;
						const moveZ = pos.z + Math.sin(angle) * 3;
						await bot.bot.pathfinder.goto(new GoalNear(moveX, pos.y, moveZ, 1));
					} catch (e) {
						console.log(`ShulkerHandler: Move failed: ${e.message}`);
					}
					await sleep(500);
					continue;
				}
				throw error;
			}
		}
	}

	/**
	 * Close a placed shulker, break it, collect it, then put it back in the chest.
	 * Immediately syncs DB via scanShulkerFromNBT after return.
	 * Returns the updated slot item.
	 */
	async closeBreakReturn(bot, shulkerWindow, placedPos, chestPos, chestSlot, chestId) {
		console.log(`ShulkerHandler: Closing shulker at ${placedPos}, returning to chest at ${chestPos} slot ${chestSlot}`);

		// Close the shulker window — wait for closing animation to fully finish
		await bot.bot.closeWindow(shulkerWindow);
		await bot.bot.waitForTicks(30);

		// Break the shulker block and collect it
		const collected = await this.digAndCollectShulker(bot, placedPos);
		if (!collected) {
			throw new Error('Shulker not found in inventory after breaking');
		}

		// Navigate back to chest and put shulker back
		await bot.goTo({ where: chestPos, range: 3 });
		const chestBlock = bot.bot.blockAt(chestPos);
		const window = await bot.openContainer(chestBlock);
		await sleep(300);

		// Find the shulker in inventory portion of the window
		let shulkerWindowSlot = null;
		for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
			const item = window.slots[i];
			if (item && item.name.includes('shulker_box')) {
				shulkerWindowSlot = i;
				break;
			}
		}

		if (shulkerWindowSlot === null) {
			await bot.bot.closeWindow(window);
			throw new Error('Shulker not found in inventory window after breaking');
		}

		// Move shulker back to its original chest slot
		await bot.bot.moveSlotItem(shulkerWindowSlot, chestSlot);
		await sleep(300);

		// Read the updated NBT from the chest slot
		const updatedSlotItem = window.slots[chestSlot];
		await bot.bot.closeWindow(window);
		await sleep(200);

		// Immediately sync DB with the returned shulker's contents
		if (updatedSlotItem && chestId && this.scanner) {
			try {
				await this.scanner.scanShulkerFromNBT(bot, Database, chestId, chestSlot, updatedSlotItem);
				await Database.rebuildItemIndex();
				console.log(`ShulkerHandler: DB synced after returning shulker to chest slot ${chestSlot}`);
			} catch (e) {
				console.error(`ShulkerHandler: DB sync on return failed: ${e.message}`);
			}
		}

		console.log(`ShulkerHandler: Shulker returned to chest slot ${chestSlot}`);
		return updatedSlotItem;
	}

	/**
	 * Full deposit cycle: take shulker from chest, place it, deposit items, break, return.
	 * Returns { deposited, updatedSlotItem }.
	 * On failure, attempts recovery and returns { deposited: 0, updatedSlotItem: null }.
	 */
	/**
	 * @param {Function|null} itemFilter - Optional filter: (windowSlotItem) => boolean.
	 *   When provided, only shift-click items where the filter returns true.
	 *   Used to separate special (named/lore) items from regular ones.
	 */
	async depositIntoShulker(bot, chestPos, chestSlot, itemName, count, shulkerId, chestId, itemFilter = null) {
		console.log(`ShulkerHandler: Depositing ${count}x ${itemName} into shulker at chest ${chestPos} slot ${chestSlot}`);

		if (this.operationInProgress) {
			throw new Error('Another shulker operation is in progress');
		}
		this.operationInProgress = true;

		let placedPos = null;

		try {
			// Step 1: Take shulker from chest (DB immediately marks it in-transit)
			const invSlot = await this.takeShulkerFromChest(bot, chestPos, chestSlot, shulkerId);

			// Step 2: Place shulker on ground and open it
			let shulkerWindow;
			try {
				const result = await this.placeAndOpenShulker(bot, invSlot);
				shulkerWindow = result.window;
				placedPos = result.placedPos;
			} catch (placeError) {
				console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message);
				let recovered = false;
				try {
					await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId);
					recovered = true;
				} catch (recoverError) {
					console.error('ShulkerHandler: Recovery failed:', recoverError.message);
				}
				if (!recovered) {
					// Recovery failed — shulker state is uncertain, propagate error upward
					// so the caller knows not to retry this same shulker
					throw recoverError;
				}
				return { deposited: 0, updatedSlotItem: null };
			}

			// Step 3: Wait for shulker window slots to populate, then verify contents
			const shulkerSlotCount = shulkerWindow.inventoryStart;
			for (let wait = 0; wait < 30; wait++) {
				if (shulkerWindow.slots.slice(0, shulkerSlotCount).some(s => s !== null)) break;
				await bot.bot.waitForTicks(2);
			}

			for (let s = 0; s < shulkerSlotCount; s++) {
				const existing = shulkerWindow.slots[s];
				if (existing && existing.name !== itemName) {
					console.error(`ShulkerHandler: Shulker contains ${existing.name} but expected ${itemName} — aborting deposit to prevent mixing`);
					try {
						const updatedSlotItem = await this.closeBreakReturn(bot, shulkerWindow, placedPos, chestPos, chestSlot, chestId);
						return { deposited: 0, updatedSlotItem };
					} catch (returnError) {
						await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
						return { deposited: 0, updatedSlotItem: null };
					}
				}
			}

			// Step 4: Move items from bot inventory into the shulker via shift-click
			let deposited = 0;

			for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd && deposited < count; i++) {
				const invItem = shulkerWindow.slots[i];
				if (!invItem || invItem.name !== itemName) continue;

				// If a filter is provided, skip items that don't match
				if (itemFilter && !itemFilter(invItem)) continue;

				// Check if shulker has any space (stackable or empty slot)
				const hasSpace = (() => {
					for (let s = 0; s < shulkerSlotCount; s++) {
						const slot = shulkerWindow.slots[s];
						if (!slot) return true;
						if (slot.name === itemName && slot.count < slot.stackSize) return true;
					}
					return false;
				})();
				if (!hasSpace) {
					console.log('ShulkerHandler: Shulker is full');
					break;
				}

				const beforeCount = invItem.count;
				try {
					// Shift-click handles stacking optimally — no cursor issues
					await bot.bot.clickWindow(i, 0, 1);
					await sleep(200);

					// Measure what actually left this slot
					const afterItem = shulkerWindow.slots[i];
					const afterCount = afterItem ? afterItem.count : 0;
					const moved = beforeCount - afterCount;
					deposited += moved;
					console.log(`ShulkerHandler: Shift-clicked slot ${i}: moved ${moved}/${beforeCount} ${itemName}`);
				} catch (error) {
					console.error(`ShulkerHandler: Error shift-clicking slot ${i}:`, error);
				}
			}

			// Step 5: Close, break, return to chest (DB synced inside closeBreakReturn)
			try {
				const updatedSlotItem = await this.closeBreakReturn(bot, shulkerWindow, placedPos, chestPos, chestSlot, chestId);
				console.log(`ShulkerHandler: Deposited ${deposited}/${count} ${itemName}`);
				return { deposited, updatedSlotItem };
			} catch (returnError) {
				console.error('ShulkerHandler: Failed to close/break/return, recovering:', returnError.message);
				let recovered = false;
				try {
					await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
					recovered = true;
				} catch (recoverError) {
					console.error('ShulkerHandler: Recovery failed:', recoverError.message);
				}
				if (!recovered) throw returnError;
				return { deposited, updatedSlotItem: null };
			}

		} catch (error) {
			console.error('ShulkerHandler: Deposit failed, attempting recovery:', error.message);
			let recovered = false;
			try {
				await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
				recovered = true;
			} catch (recoverError) {
				console.error('ShulkerHandler: Recovery failed:', recoverError.message);
			}
			if (!recovered) throw error;
			return { deposited: 0, updatedSlotItem: null };
		} finally {
			this.operationInProgress = false;
		}
	}

	/**
	 * Unpack a shulker box already in bot inventory.
	 * Places it on the ground, pulls ALL items out, breaks the empty shulker.
	 * Returns { extracted: [{ name, count }], inventoryFull: bool }.
	 */
	async unpackShulkerFromInventory(bot, shulkerItem) {
		console.log(`ShulkerHandler: Unpacking shulker ${shulkerItem.name} from inventory`);

		if (this.operationInProgress) {
			throw new Error('Another shulker operation is in progress');
		}
		this.operationInProgress = true;

		try {
			// Step 1: Find placement spot
			const spot = this.findPlacementSpot(bot);

			// Ensure bot is far enough from placement spot
			const botPos = bot.bot.entity.position;
			const placeDist = botPos.distanceTo(spot.position.offset(0.5, 0, 0.5));
			if (placeDist < 1.5) {
				console.log(`ShulkerHandler: Too close to placement spot (${placeDist.toFixed(1)} blocks), stepping back`);
				const dx = botPos.x - spot.position.x;
				const dz = botPos.z - spot.position.z;
				const dist = Math.sqrt(dx * dx + dz * dz) || 1;
				const retreatX = botPos.x + (dx / dist) * 1.5;
				const retreatZ = botPos.z + (dz / dist) * 1.5;
				try {

					await bot.bot.pathfinder.goto(new GoalNear(retreatX, botPos.y, retreatZ, 0.5));
					await sleep(300);
				} catch (e) {
					console.log(`ShulkerHandler: Retreat pathfind failed: ${e.message}`);
				}
			}

			// Step 2: Equip and place the shulker
			await bot.bot.equip(shulkerItem, 'hand');
			await bot.bot.waitForTicks(5);

			// Verify we're actually holding the shulker
			const heldItem = bot.bot.heldItem;
			if (!heldItem || !heldItem.name.includes('shulker_box')) {
				throw new Error(`Cannot equip shulker for unpack (holding: ${heldItem?.name || 'nothing'})`);
			}

			// Look at the top face center of placement block
			await bot.bot.lookAt(spot.placeOn.position.offset(0.5, 1, 0.5), true);
			await bot.bot.waitForTicks(3);

			// SAFETY: verify held item is a shulker RIGHT before placing — never place anything else
			const prePlaceItem = bot.bot.heldItem;
			if (!prePlaceItem || !prePlaceItem.name.includes('shulker_box')) {
				throw new Error(`ABORT: held item changed before place (holding: ${prePlaceItem?.name || 'nothing'})`);
			}

			await bot.bot.placeBlock(spot.placeOn, spot.faceVec);
			await bot.bot.waitForTicks(10);

			// Verify shulker was placed
			const placedBlock = bot.bot.blockAt(spot.position);
			if (!placedBlock || !placedBlock.name.includes('shulker_box')) {
				// Something wrong was placed — break it immediately
				console.error(`ShulkerHandler: WRONG BLOCK placed at ${spot.position} (${placedBlock?.name}), breaking it`);
				try { await bot.bot.dig(bot.bot.blockAt(spot.position)); } catch (e) { /* best effort */ }
				throw new Error(`Failed to place shulker at ${spot.position}`);
			}

			// Step 3: Open the shulker
			const shulkerWindow = await bot.openContainer(placedBlock);
			await bot.bot.waitForTicks(5);

			// Step 4: Move ALL items from shulker into bot inventory
			const extracted = [];
			let inventoryFull = false;
			const shulkerSlotCount = shulkerWindow.inventoryStart;

			for (let s = 0; s < shulkerSlotCount; s++) {
				const shulkerSlotItem = shulkerWindow.slots[s];
				if (!shulkerSlotItem) continue;

				// Find a target slot in bot inventory (stack first, then empty)
				let targetSlot = null;

				for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) {
					const invItem = shulkerWindow.slots[i];
					if (invItem && invItem.name === shulkerSlotItem.name && invItem.count < invItem.stackSize) {
						targetSlot = i;
						break;
					}
				}

				if (targetSlot === null) {
					for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) {
						if (!shulkerWindow.slots[i]) {
							targetSlot = i;
							break;
						}
					}
				}

				if (targetSlot === null) {
					console.log('ShulkerHandler: Bot inventory full during unpack');
					inventoryFull = true;
					break;
				}

				try {
					const itemName = shulkerSlotItem.name;
					const itemCount = shulkerSlotItem.count;
					await bot.bot.moveSlotItem(s, targetSlot);
					await sleep(200);
					extracted.push({ name: itemName, count: itemCount });
				} catch (error) {
					console.error(`ShulkerHandler: Error moving item from shulker slot ${s}:`, error);
				}
			}

			// Step 5: Close the shulker window — wait for closing animation
			await bot.bot.closeWindow(shulkerWindow);
			await bot.bot.waitForTicks(30);

			// Step 6: Break the shulker block and pick it up
			const collected = await this.digAndCollectShulker(bot, spot.position);
			if (!collected) {
				console.error('ShulkerHandler: Shulker not found in inventory after breaking');
			}

			console.log(`ShulkerHandler: Unpacked ${extracted.length} item stacks from shulker`);
			return { extracted, inventoryFull };

		} finally {
			this.operationInProgress = false;
		}
	}

	/**
	 * Full withdrawal cycle: take shulker from chest, place it, take items out, break, return.
	 * Returns { withdrawn, updatedSlotItem }.
	 * On failure, attempts recovery and returns { withdrawn: 0, updatedSlotItem: null }.
	 */
	async withdrawFromShulker(bot, chestPos, chestSlot, itemName, count, shulkerId, chestId) {
		console.log(`ShulkerHandler: Withdrawing ${count}x ${itemName} from shulker at chest ${chestPos} slot ${chestSlot}`);

		if (this.operationInProgress) {
			throw new Error('Another shulker operation is in progress');
		}
		this.operationInProgress = true;

		let placedPos = null;

		try {
			// Step 1: Take shulker from chest (DB immediately marks it in-transit)
			const invSlot = await this.takeShulkerFromChest(bot, chestPos, chestSlot, shulkerId);

			// Step 2: Place shulker on ground and open it
			let shulkerWindow;
			try {
				const result = await this.placeAndOpenShulker(bot, invSlot);
				shulkerWindow = result.window;
				placedPos = result.placedPos;
			} catch (placeError) {
				console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message);
				let recovered = false;
				try {
					await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId);
					recovered = true;
				} catch (recoverError) {
					console.error('ShulkerHandler: Recovery failed:', recoverError.message);
				}
				if (!recovered) throw placeError;
				return { withdrawn: 0, updatedSlotItem: null };
			}

			// Step 3: Wait for shulker window slots to populate (server sends data after open)
			const shulkerSlotCount = shulkerWindow.inventoryStart;
			for (let wait = 0; wait < 30; wait++) {
				if (shulkerWindow.slots.slice(0, shulkerSlotCount).some(s => s !== null)) break;
				await bot.bot.waitForTicks(2);
			}

			// Move items from shulker into bot inventory
			let withdrawn = 0;
			const remaining = () => count - withdrawn;

			for (let s = 0; s < shulkerSlotCount && remaining() > 0; s++) {
				const shulkerItem = shulkerWindow.slots[s];
				if (!shulkerItem || shulkerItem.name !== itemName) continue;

				// Skip special/named items — only withdraw via special item handler
				if (shulkerItem.nbt?.value?.display) continue;

				// Check if inventory has space
				const hasInvSpace = (() => {
					for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) {
						const slot = shulkerWindow.slots[i];
						if (!slot) return true;
						if (slot.name === itemName && slot.count < slot.stackSize) return true;
					}
					return false;
				})();
				if (!hasInvSpace) {
					console.log('ShulkerHandler: Bot inventory is full');
					break;
				}

				const beforeCount = shulkerItem.count;
				try {
					if (remaining() >= beforeCount) {
						// Need the whole stack or more — shift-click is optimal
						await bot.bot.clickWindow(s, 0, 1);
						await sleep(200);
					} else {
						// Need fewer than the full stack — pick up, right-click exact amount, return rest
						// Find an empty inventory slot to place items into
						let emptyInvSlot = null;
						for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) {
							if (!shulkerWindow.slots[i]) { emptyInvSlot = i; break; }
						}
						if (emptyInvSlot === null) {
							console.log('ShulkerHandler: No empty slot for partial withdraw');
							break;
						}

						// Left-click to pick up full stack onto cursor
						await bot.bot.clickWindow(s, 0, 0);
						await sleep(150);

						// Right-click on empty inventory slot N times to place exactly N items
						for (let n = 0; n < remaining(); n++) {
							await bot.bot.clickWindow(emptyInvSlot, 1, 0);
							await sleep(100);
						}

						// Left-click back on shulker slot to return the remainder from cursor
						await bot.bot.clickWindow(s, 0, 0);
						await sleep(150);
					}

					// Measure what actually left this slot
					const afterItem = shulkerWindow.slots[s];
					const afterCount = afterItem ? afterItem.count : 0;
					const moved = beforeCount - afterCount;
					withdrawn += moved;
					console.log(`ShulkerHandler: Withdrew ${moved}/${beforeCount} ${itemName} from shulker slot ${s}`);
				} catch (error) {
					console.error(`ShulkerHandler: Error withdrawing from shulker slot ${s}:`, error);
				}
			}

			// Step 4: Close, break, return to chest (DB synced inside closeBreakReturn)
			try {
				const updatedSlotItem = await this.closeBreakReturn(bot, shulkerWindow, placedPos, chestPos, chestSlot, chestId);
				console.log(`ShulkerHandler: Withdrew ${withdrawn}/${count} ${itemName}`);
				return { withdrawn, updatedSlotItem };
			} catch (returnError) {
				console.error('ShulkerHandler: Failed to close/break/return, recovering:', returnError.message);
				let recovered = false;
				try {
					await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
					recovered = true;
				} catch (recoverError) {
					console.error('ShulkerHandler: Recovery failed:', recoverError.message);
				}
				if (!recovered) throw returnError;
				return { withdrawn: 0, updatedSlotItem: null };
			}

		} catch (error) {
			console.error('ShulkerHandler: Withdraw failed, attempting recovery:', error.message);
			let recovered = false;
			try {
				await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
				recovered = true;
			} catch (recoverError) {
				console.error('ShulkerHandler: Recovery failed:', recoverError.message);
			}
			if (!recovered) throw error;
			return { withdrawn: 0, updatedSlotItem: null };
		} finally {
			this.operationInProgress = false;
		}
	}
	/**
	 * Withdraw a specific item from a specific shulker slot (for special/named items).
	 * Same lifecycle as withdrawFromShulker but targets an exact slot instead of matching by item name.
	 * Returns { withdrawn, updatedSlotItem }.
	 */
	async withdrawFromShulkerSlot(bot, chestPos, chestSlot, shulkerSlot, count, shulkerId, chestId) {
		console.log(`ShulkerHandler: Withdrawing from shulker slot ${shulkerSlot} at chest ${chestPos} slot ${chestSlot}`);

		if (this.operationInProgress) {
			throw new Error('Another shulker operation is in progress');
		}
		this.operationInProgress = true;

		let placedPos = null;

		try {
			// Step 1: Take shulker from chest
			const invSlot = await this.takeShulkerFromChest(bot, chestPos, chestSlot, shulkerId);

			// Step 2: Place shulker on ground and open it
			let shulkerWindow;
			try {
				const result = await this.placeAndOpenShulker(bot, invSlot);
				shulkerWindow = result.window;
				placedPos = result.placedPos;
			} catch (placeError) {
				console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message);
				let recovered = false;
				try {
					await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId);
					recovered = true;
				} catch (recoverError) {
					console.error('ShulkerHandler: Recovery failed:', recoverError.message);
				}
				if (!recovered) throw placeError;
				return { withdrawn: 0, updatedSlotItem: null };
			}

			// Step 3: Wait for slots to populate
			const shulkerSlotCount = shulkerWindow.inventoryStart;
			for (let wait = 0; wait < 30; wait++) {
				if (shulkerWindow.slots.slice(0, shulkerSlotCount).some(s => s !== null)) break;
				await bot.bot.waitForTicks(2);
			}

			// Step 4: Take the item at the specific slot
			let withdrawn = 0;
			const targetItem = shulkerWindow.slots[shulkerSlot];
			if (targetItem) {
				const beforeCount = targetItem.count;
				const toTake = Math.min(count, beforeCount);

				// Check if inventory has space
				const hasInvSpace = (() => {
					for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) {
						const slot = shulkerWindow.slots[i];
						if (!slot) return true;
						if (slot.name === targetItem.name && slot.count < slot.stackSize) return true;
					}
					return false;
				})();

				if (hasInvSpace) {
					try {
						if (toTake >= beforeCount) {
							// Take the whole stack
							await bot.bot.clickWindow(shulkerSlot, 0, 1);
							await sleep(200);
						} else {
							// Partial: pick up, right-click exact amount, return rest
							let emptyInvSlot = null;
							for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) {
								if (!shulkerWindow.slots[i]) { emptyInvSlot = i; break; }
							}
							if (emptyInvSlot !== null) {
								await bot.bot.clickWindow(shulkerSlot, 0, 0);
								await sleep(150);
								for (let n = 0; n < toTake; n++) {
									await bot.bot.clickWindow(emptyInvSlot, 1, 0);
									await sleep(100);
								}
								await bot.bot.clickWindow(shulkerSlot, 0, 0);
								await sleep(150);
							}
						}

						const afterItem = shulkerWindow.slots[shulkerSlot];
						const afterCount = afterItem ? afterItem.count : 0;
						withdrawn = beforeCount - afterCount;
						console.log(`ShulkerHandler: Withdrew ${withdrawn} from shulker slot ${shulkerSlot}`);
					} catch (error) {
						console.error(`ShulkerHandler: Error withdrawing from slot ${shulkerSlot}:`, error);
					}
				} else {
					console.log('ShulkerHandler: Bot inventory is full');
				}
			} else {
				console.log(`ShulkerHandler: No item at shulker slot ${shulkerSlot}`);
			}

			// Step 5: Close, break, return to chest
			try {
				const updatedSlotItem = await this.closeBreakReturn(bot, shulkerWindow, placedPos, chestPos, chestSlot, chestId);
				console.log(`ShulkerHandler: Slot withdraw complete: ${withdrawn} items`);
				return { withdrawn, updatedSlotItem };
			} catch (returnError) {
				console.error('ShulkerHandler: Failed to close/break/return, recovering:', returnError.message);
				await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
				return { withdrawn: 0, updatedSlotItem: null };
			}

		} catch (error) {
			console.error('ShulkerHandler: Slot withdraw failed, attempting recovery:', error.message);
			await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
			return { withdrawn: 0, updatedSlotItem: null };
		} finally {
			this.operationInProgress = false;
		}
	}
}

module.exports = ShulkerHandler;
