'use strict';

const Vec3 = require('vec3');
const conf = require('../../conf');
const { sleep } = require('../../utils');
const Database = require('./database');
const Scanner = require('./scanner');
const ShulkerHandler = require('./shulker-handler');
const StorageWeb = require('./web');
const { applyMapUpdate, renderMapToPNG } = require('./map-renderer');

class Storage {
	static createRouter = StorageWeb.createRouter;
	static webUI = StorageWeb.webUI;

	constructor(args) {
		console.log('Storage: Constructor called');
		this.bot = args.bot;
		this.config = { ...conf.storage, ...args };
		this.isReady = false;
		this.shulkerHandler = new ShulkerHandler();
		this.pendingWithdrawals = new Map(); // playerName → { itemName, count, mode, timeoutId }
		this._craftAvailable = true; // reset when crafting fails, so we don't spam retries
		this._busy = false; // true during organize/withdraw/trade to block interval restock
	}

	init() {
		console.log('Storage: init() called');
		return new Promise((resolve, reject) => {
			this.bot.on('onReady', async () => {
				try {
					console.log(`Storage: Bot ${this.bot.name} is ready, initializing storage system...`);


					const { Movements } = require('mineflayer-pathfinder')

					
/*						console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
					  const mcData = require('minecraft-data')(this.bot.bot.version)
					  const defaultMovements = new Movements(this.bot.bot)

					  // 1. Identify the block we WANT to walk on
					  const targetBlockId = mcData.blocksByName.crafting_table.id

					  // 2. Add an exclusion area rule
					  // This function is called for every block the pathfinder considers stepping on.
					  defaultMovements.exclusionAreasStep.push((block) => {
					    // If the block the bot would stand on is NOT our aisle block, 
					    // make it very "expensive" to move there.
					    if (block.type !== targetBlockId) {
					      return 100 // High cost forces the pathfinder to look for 0-cost blocks (crafting tables)
					    }
					    return 0 // No extra cost for crafting tables
					  })

					  // 3. (Optional) Disable sprinting to prevent sliding off the aisle
					  defaultMovements.allowSprinting = false

					  this.bot.bot.pathfinder.setMovements(defaultMovements)*/
					



					// Initialize database
					if (!Database.db) {
						console.log('Storage: Initializing database...');
						await Database.initialize(this.config.dbPath || './storage/storage.db');
					} else {
						console.log('Storage: Database already initialized');
					}

					// Initialize scanner
					console.log('Storage: Creating Scanner...');
					this.scanner = new Scanner();
					this.shulkerHandler.scanner = this.scanner;

					if (this.config.startupTasks) {
						console.log('Storage: Running startup tasks...');
						await this.config.startupTasks();
					}

					this.isReady = true;
					console.log('Storage: Initialization complete! Ready to use.');

					// Map packet listener for capturing filled map images
					this._mapBuffers = {};
					this._mapListener = (packet) => {
						const mapId = packet.itemDamage;
						if (!packet.columns || packet.columns === 0) return;
						if (!this._mapBuffers[mapId]) this._mapBuffers[mapId] = new Uint8Array(128 * 128);
						applyMapUpdate(this._mapBuffers[mapId], packet);
					};
					this.bot.bot._client.on('map', this._mapListener);

					if (!this.bot.onDemand) {
						// Initial hotbar restock
						try {
							await this.restockHotbar();
						} catch (error) {
							console.error('Storage: Initial hotbar restock failed:', error.message);
						}

						// Start hotbar restock interval
						const restockInterval = this.config.hotbarRestockInterval || 60000;
						this.hotbarInterval = setInterval(async () => {
							try {
								await this.restockHotbar();
							} catch (error) {
								console.error('Storage: Hotbar restock interval failed:', error.message);
							}
						}, restockInterval);

						// Start periodic inventory cleanup
						const cleanupInterval = this.config.inventoryCleanupInterval || 60 * 1000;
						this.cleanupInterval = setInterval(async () => {
							if (!this.isReady || this._busy || this.shulkerHandler.operationInProgress) return;
							try {
								await this.cleanInventory();
							} catch (error) {
								console.error('Storage: Periodic inventory cleanup failed:', error.message);
							}
						}, cleanupInterval);
					}

					resolve();

				} catch (error) {
					console.error('Storage: Error in init:', error);
					reject(error);
				}
			});
		});
	}

	async unload(keepDb = false) {
		console.log('Storage: Unloading...');

		if (this.hotbarInterval) {
			clearInterval(this.hotbarInterval);
			this.hotbarInterval = null;
		}

		if (this.cleanupInterval) {
			clearInterval(this.cleanupInterval);
			this.cleanupInterval = null;
		}

		// Remove map listener
		if (this._mapListener && this.bot?.bot?._client) {
			this.bot.bot._client.removeListener('map', this._mapListener);
			this._mapListener = null;
		}
		this._mapBuffers = {};

		// Clear any pending withdrawal timeouts
		for (const [, pending] of this.pendingWithdrawals) {
			if (pending.timeoutId) clearTimeout(pending.timeoutId);
		}
		this.pendingWithdrawals.clear();

		if (this.scanner) delete this.scanner;

		if (!keepDb && Database.db) {
			console.log('Storage: Closing database...');
			Database.close();
		}

		this.isReady = false;
		console.log('Storage: Unload complete');
	}

	async scanArea(force = false) {
		console.log(`Storage[${this.bot.name}]: Scanning storage area...`);
		const chests = await this.scanner.discoverChests(this.bot, this.config.scanRadius || 30, Database);
		const shulkers = await this.scanner.scanAllChests(this.bot, Database);
		console.log(`Storage[${this.bot.name}]: Complete - ${chests.length} chests, ${shulkers} shulkers`);

		// Capture any map images received passively during scan
		await this.captureMapImages();

		// Actively withdraw and index any filled_maps not yet captured
		await this.indexMapsFromStorage();
	}

	/**
	 * Save any map pixel data received via protocol packets to the database as PNG images.
	 */
	async captureMapImages() {
		const mapIds = Object.keys(this._mapBuffers);
		if (mapIds.length === 0) return;

		let captured = 0;
		for (const mapId of mapIds) {
			const pixels = this._mapBuffers[mapId];
			// Skip maps with no actual data (all zeros)
			if (!pixels.some(p => p !== 0)) continue;

			try {
				const base64png = renderMapToPNG(pixels);
				const pixelJson = JSON.stringify([...pixels]);
				await Database.upsertMap(parseInt(mapId), base64png, pixelJson);
				captured++;
				console.log(`Storage: Captured map #${mapId}`);
			} catch (error) {
				console.error(`Storage: Error capturing map #${mapId}:`, error.message);
			}
		}

		if (captured > 0) {
			console.log(`Storage: Captured ${captured} map image(s)`);
		}
	}

	/**
	 * Actively index filled_map items from shulkers.
	 * Withdraws each map, holds it in hand to trigger map data packets from server,
	 * waits for data, captures the image, then returns the map to storage.
	 */
	async indexMapsFromStorage() {
		const uncaptured = await Database.getUncapturedMaps();
		if (uncaptured.length === 0) {
			console.log('Storage: No filled maps found in storage');
			return;
		}

		// Filter out maps we already have images for
		const existing = await Database.getAllMaps();
		const existingIds = new Set(existing.map(m => m.map_id));

		// Extract map IDs from the filled_map items' NBT data
		// The map ID is in the nbt_data field as parsed JSON
		const mapsToCapture = [];
		for (const item of uncaptured) {
			let mapId = null;
			try {
				if (item.nbt_data) {
					const nbt = JSON.parse(item.nbt_data);
					// map ID can be in nbt.map (pre-1.20.5) or nbt.map_id
					mapId = nbt.map ?? nbt.map_id ?? null;
				}
			} catch (e) { /* ignore parse errors */ }

			if (mapId !== null && !existingIds.has(mapId) && !this._mapBuffers[mapId]) {
				mapsToCapture.push({ ...item, mapId });
				existingIds.add(mapId); // prevent duplicates in same batch
			}
		}

		if (mapsToCapture.length === 0) {
			console.log('Storage: All filled maps already captured');
			return;
		}

		console.log(`Storage: Found ${mapsToCapture.length} uncaptured map(s), indexing...`);

		// Group by shulker to minimize shulker operations
		const byShulker = {};
		for (const map of mapsToCapture) {
			if (!byShulker[map.shulker_id]) byShulker[map.shulker_id] = [];
			byShulker[map.shulker_id].push(map);
		}

		for (const [shulkerId, maps] of Object.entries(byShulker)) {
			const info = maps[0]; // all maps in this group share the same shulker location
			const chestPos = new Vec3(info.pos_x, info.pos_y, info.pos_z);

			console.log(`Storage: Withdrawing ${maps.length} map(s) from shulker #${shulkerId}`);

			try {
				// Withdraw all filled_maps from this shulker
				const { withdrawn } = await this.shulkerHandler.withdrawFromShulker(
					this.bot, chestPos, info.shulker_slot, 'filled_map',
					maps.length * 64, // withdraw all maps
					parseInt(shulkerId), info.chest_id
				);

				if (withdrawn === 0) {
					console.log(`Storage: Could not withdraw maps from shulker #${shulkerId}`);
					continue;
				}

				// Hold each map in hand briefly to trigger map data packets
				const mapItems = this.bot.bot.inventory.items().filter(i => i.name === 'filled_map');
				for (const mapItem of mapItems) {
					try {
						await this.bot.bot.equip(mapItem, 'hand');
						// Wait for server to send map data packets
						await sleep(2000);
					} catch (e) {
						console.log(`Storage: Error holding map: ${e.message}`);
					}
				}

				// Flush received map data to DB
				await this.captureMapImages();

			} catch (error) {
				console.error(`Storage: Error indexing maps from shulker #${shulkerId}:`, error.message);
			}

			// Re-deposit any maps left in inventory (outside try/catch so it always runs)
			const mapCount = this.bot.bot.inventory.items()
				.filter(i => i.name === 'filled_map')
				.reduce((sum, i) => sum + i.count, 0);
			if (mapCount > 0) {
				console.log(`Storage: Re-depositing ${mapCount} filled_map(s) back into storage`);
				try {
					const result = await this.depositItemType('filled_map', mapCount);
					console.log(`Storage: Re-deposited ${result.deposited}/${mapCount} filled_map(s)`);
				} catch (e) {
					console.error(`Storage: Failed to re-deposit maps: ${e.message}`);
				}
			}
		}

		await Database.rebuildItemIndex();
		console.log('Storage: Map indexing complete');
	}

	// ========================================
	// Hotbar Management
	// ========================================

	async restockHotbar() {
		if (!this.isReady) return;
		if (this._busy) {
			console.log('Storage: Skipping hotbar restock — storage operation in progress');
			return;
		}
		if (this.shulkerHandler.operationInProgress) {
			console.log('Storage: Skipping hotbar restock — shulker operation in progress');
			return;
		}

		const hotbarItems = this.config.hotbarItems || [];
		if (hotbarItems.length === 0) return;

		for (const spec of hotbarItems) {
			// Count current inventory
			let currentCount = 0;
			for (const item of this.bot.bot.inventory.items()) {
				if (item.name === spec.name) {
					currentCount += item.count;
				}
			}

			if (currentCount >= spec.min) continue;

			const needed = spec.target - currentCount;
			console.log(`Storage: Hotbar restock — ${spec.name}: have ${currentCount}, need ${needed} more (target ${spec.target})`);

			// Check storage availability
			const available = await Database.getItemTotalCount(spec.name);
			if (available === 0) {
				console.log(`Storage: No ${spec.name} in storage, skipping`);
				continue;
			}

			const toWithdraw = Math.min(needed, available);
			const shulkers = await Database.findShulkersWithItem(spec.name);

			let remaining = toWithdraw;
			let consecutiveFailures = 0;
			for (const shulker of shulkers) {
				if (remaining <= 0) break;
				if (consecutiveFailures >= 2) {
					console.log(`Storage: Too many failures restocking ${spec.name}, giving up`);
					break;
				}

				const chestPos = new Vec3(shulker.pos_x, shulker.pos_y, shulker.pos_z);
				try {
					const { withdrawn, updatedSlotItem } = await this.shulkerHandler.withdrawFromShulker(
						this.bot, chestPos, shulker.slot, spec.name, remaining, shulker.id, shulker.chest_id
					);

					if (withdrawn > 0) {
						remaining -= withdrawn;
						consecutiveFailures = 0;
					} else {
						consecutiveFailures++;
					}
				} catch (error) {
					console.error(`Storage: Hotbar restock error for ${spec.name}:`, error.message);
					consecutiveFailures++;
				}
			}

			console.log(`Storage: Restocked ${toWithdraw - remaining}x ${spec.name}`);
		}

		await Database.rebuildItemIndex();
	}

	// ========================================
	// Deposit Flow
	// ========================================

	async handleTrade(playerName, itemsReceived) {
		console.log(`Storage[${this.bot.name}]: Processing trade from ${playerName}, received ${itemsReceived.length} item types`);
		this._craftAvailable = true; // new items may include craft materials
		this._busy = true;

		try {
			// Log trade
			await Database.logTrade(playerName, 'deposit', itemsReceived);

			// Unpack any traded shulker boxes before depositing
			const hasShulkers = itemsReceived.some(item => item.name.includes('shulker_box'));
			if (hasShulkers) {
				console.log('Storage: Traded items include shulker boxes, unpacking...');
				await this.unpackTradedShulkers();
			}

			// After unpacking, read current bot inventory for depositing
			// Keep hotbar items up to their target, deposit any excess
			const hotbarTargets = {};
			for (const h of (this.config.hotbarItems || [])) {
				hotbarTargets[h.name] = h.target || 0;
			}
			const hotbarSeen = {};
			const grouped = {};
			for (const item of this.bot.bot.inventory.items()) {
				if (item.name.includes('shulker_box')) continue;
				if (item.name in hotbarTargets) {
					const seen = (hotbarSeen[item.name] || 0);
					const keep = Math.max(0, hotbarTargets[item.name] - seen);
					hotbarSeen[item.name] = seen + item.count;
					const excess = item.count - keep;
					if (excess <= 0) continue;
					if (!grouped[item.name]) grouped[item.name] = 0;
					grouped[item.name] += excess;
					continue;
				}
				if (!grouped[item.name]) {
					grouped[item.name] = 0;
				}
				grouped[item.name] += item.count;
			}

			// Deposit each item type
			const results = [];
			for (const [itemName, totalCount] of Object.entries(grouped)) {
				try {
					const result = await this.depositItemType(itemName, totalCount);
					results.push({ itemName, requested: totalCount, deposited: result.deposited });
				} catch (error) {
					console.error(`Storage: Error depositing ${itemName}:`, error);
					results.push({ itemName, requested: totalCount, deposited: 0, error: error.message });
				}
			}

			// Rebuild index after all deposits
			await Database.rebuildItemIndex();

			// Sweep any leftovers that didn't deposit on first pass
			await this.cleanInventory();

			// Whisper summary to player
			const summary = results.map(r => {
				if (r.error) return `${r.itemName}: FAILED (${r.error})`;
				return `${r.itemName}: ${r.deposited}/${r.requested}`;
			}).join(', ');

			// Capture any map images from traded filled maps
			await this.captureMapImages();

			this.bot.whisper(playerName, `Storage complete: ${summary}`);
			return results;
		} finally {
			this._busy = false;
		}
	}

	/**
	 * Process all shulker boxes in bot inventory from a trade.
	 * Mixed shulkers (multiple item types) are unpacked, items deposited, then empty box stored.
	 * Sorted shulkers (single item type) and empty shulkers are stored directly — no unpack needed.
	 * Deposits between each shulker to keep inventory space free.
	 */
	async unpackTradedShulkers() {
		// Phase 1: Store ALL shulkers from inventory into chests.
		// This frees inventory space and registers contents in DB via NBT scan.
		// Sorted/empty shulkers are done after this. Mixed ones need phase 2.
		const shulkersToUnpack = []; // { chestId, chestSlot, chestPos, shulkerId }

		console.log('Storage: Phase 1 — stashing all traded shulkers into chests');
		let stashCount = 0;
		while (stashCount < 50) {
			const shulkerItem = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
			if (!shulkerItem) break;
			stashCount++;

			const contents = this.scanner.extractShulkerContents(this.bot, shulkerItem);
			const itemTypes = new Set(contents.map(c => c.name));
			const isMixed = contents.length > 0 && itemTypes.size > 1;

			const label = !contents.length ? 'empty'
				: isMixed ? `mixed (${itemTypes.size} types)`
				: `sorted (${[...itemTypes][0]})`;
			console.log(`Storage: Stashing shulker ${stashCount}: ${label}`);

			try {
				const info = await this.storeShulker(shulkerItem);
				if (!info) {
					console.error('Storage: No empty chest slot, cannot stash more shulkers');
					break;
				}
				if (isMixed) {
					shulkersToUnpack.push(info);
				}
			} catch (error) {
				console.error(`Storage: Error stashing shulker:`, error.message);
				break;
			}
		}

		console.log(`Storage: Phase 1 complete — stashed ${stashCount} shulkers, ${shulkersToUnpack.length} mixed need unpacking`);

		// Phase 2: Pull each mixed shulker back out, unpack it, deposit items, return empty box to ORIGINAL slot.
		// Bot inventory is now empty (aside from hotbar), so we have room.
		const failedToUnpack = []; // shulkers that failed and should be re-queued

		for (let i = 0; i < shulkersToUnpack.length; i++) {
			const info = shulkersToUnpack[i];
			console.log(`Storage: Phase 2 — unpacking shulker ${i + 1}/${shulkersToUnpack.length} (DB #${info.shulkerId})`);

			// Pre-iteration cleanup: store any leftover shulker boxes from previous iteration
			// This prevents the wrong shulker being picked up by subsequent .find() calls
			let leftoverBoxes = this.bot.bot.inventory.items().filter(item => item.name.includes('shulker_box'));
			for (const leftover of leftoverBoxes) {
				console.log(`Storage: Storing leftover shulker box before next unpack`);
				try { await this.storeShulker(leftover); } catch (e) {
					console.error(`Storage: Error storing leftover shulker:`, e.message);
				}
			}

			// Pre-iteration cleanup: deposit any non-shulker items clogging inventory
			await this._depositNonShulkerInventory();

			// Check inventory space — need at least 3 free slots (shulker + extracted items + buffer)
			const freeSlots = this.bot.bot.inventory.slots.filter((s, idx) => !s && idx >= 9).length;
			if (freeSlots < 3) {
				console.log(`Storage: Inventory too full (${freeSlots} free slots), deferring remaining ${shulkersToUnpack.length - i} shulker(s)`);
				// Re-queue all remaining shulkers for next organize pass
				for (let j = i; j < shulkersToUnpack.length; j++) {
					failedToUnpack.push(shulkersToUnpack[j]);
				}
				break;
			}

			// Re-verify the shulker still exists in the DB and at the expected chest/slot
			// If it was already cleaned up by a previous failure, skip it
			const currentShulker = await Database.getShulkerById(info.shulkerId);
			if (!currentShulker) {
				console.log(`Storage: Shulker #${info.shulkerId} no longer in DB, skipping (may have been recovered)`);
				continue;
			}

			// takeWholeShulker already marks the shulker as in-transit (slot_count=-1) in the DB
			// so we do NOT delete it beforehand — that would leave the DB in a deleted state with no in-transit marker
			let unpackSucceeded = false;
			let shulkerItem = null;
			let extracted = [];
			let inventoryFull = false;

			try {
				// Take the shulker from the chest into inventory
				// This marks slot_count=-1 in DB to indicate in-transit
				await this.shulkerHandler.takeWholeShulker(
					this.bot, info.chestPos, info.chestSlot, info.shulkerId
				);

				// Find the shulker in inventory — use the slot we know it landed in, not .find()
				// The shulker is the only shulker box in inventory after pre-iteration cleanup
				shulkerItem = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
				if (!shulkerItem) {
					console.error('Storage: Shulker not found in inventory after taking from chest');
					// Mark as failed — it may still be at the chest if takeWholeShulker failed silently
					throw new Error('Shulker vanished from inventory after take');
				}

				({ extracted, inventoryFull } = await this.shulkerHandler.unpackShulkerFromInventory(this.bot, shulkerItem));
				console.log(`Storage: Extracted ${extracted.length} item stacks from shulker${inventoryFull ? ' (inventory was full, some items may remain in shulker)' : ''}`);
				unpackSucceeded = true;

				// Deposit all extracted items grouped by type (prevents fragmentation across shulkers)
				await this._depositNonShulkerInventory();

			} catch (error) {
				console.error(`Storage: Error unpacking shulker #${info.shulkerId}:`, error.message);
				// Deposit whatever we managed to extract
				await this._depositNonShulkerInventory();

				// If the shulker is still in inventory (unpack failed partway), store it back
				const leftover = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
				if (leftover) {
					try {
						// Try to return to original slot first
						const result = await this.storeShulker(leftover, {
							chest_id: info.chestId,
							pos_x: info.chestPos.x,
							pos_y: info.chestPos.y,
							pos_z: info.chestPos.z,
							slot: info.chestSlot,
						});
						if (!result) {
							// Original slot occupied, just store anywhere
							await this.storeShulker(leftover);
						}
					} catch (e) {
						console.error(`Storage: Error storing leftover shulker:`, e.message);
						// Best-effort: just store anywhere
						try { await this.storeShulker(leftover); } catch (e2) { /* give up */ }
					}
				}

				// Mark shulker as back in DB (it was marked in-transit by takeWholeShulker)
				// Restore it to the chest (re-insert so it can be found by the chest/slot lookup)
				// The actual physical state is uncertain; this is a best-effort restore
				try {
					await Database.upsertShulker(info.chestId, info.chestSlot, shulkerItem?.name || 'shulker_box', null, null);
					await Database.updateShulkerCounts(info.shulkerId, 0, 0);
				} catch (e) {
					console.error(`Storage: Could not restore shulker record:`, e.message);
				}

				failedToUnpack.push(info);
				continue;
			}

			// Unpack succeeded: re-insert the shulker as empty (or partially-empty if inventory was full)
			// into its ORIGINAL chest slot to preserve physical arrangement
			const emptyBox = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
			if (emptyBox) {
				try {
					// Try to place back in original slot
					const result = await this.storeShulker(emptyBox, {
						chest_id: info.chestId,
						pos_x: info.chestPos.x,
						pos_y: info.chestPos.y,
						pos_z: info.chestPos.z,
						slot: info.chestSlot,
					});
					if (!result) {
						// Original slot occupied (shouldn't happen since we just emptied it)
						await this.storeShulker(emptyBox);
					}
				} catch (e) {
					console.error(`Storage: Error storing shulker box:`, e.message);
					try { await this.storeShulker(emptyBox); } catch (e2) { /* give up */ }
				}
			}

			// If the shulker was only partially unpacked (inventoryFull), re-queue it
			if (inventoryFull) {
				console.log(`Storage: Shulker was only partially unpacked — re-queueing for next organize`);
				// Mark the shulker as "pending" in DB: it still has mixed contents
				// We need to update its item_focus to null so organizeLooseItems will detect it
				try {
					await Database.upsertShulker(info.chestId, info.chestSlot, 'shulker_box', null, null);
					// Rescan to get accurate remaining contents
					const placedItem = emptyBox; // the box still in inventory
					// Note: the box is already stored back via storeShulker above, so we can't rescan it here
					// Instead, mark it as needing rescan on next organize
				} catch (e) {
					console.error(`Storage: Could not mark partial shulker for re-queue:`, e.message);
				}
				failedToUnpack.push(info);
			}
		}

		// Final cleanup: store any shulker boxes still in inventory
		let remaining = this.bot.bot.inventory.items().filter(item => item.name.includes('shulker_box'));
		for (const leftover of remaining) {
			try { await this.storeShulker(leftover); } catch (e) {
				console.error(`Storage: Error storing leftover shulker:`, e.message);
			}
		}

		console.log('Storage: All traded shulkers processed');
	}

	/**
	 * Deposit all non-shulker, non-hotbar items from bot inventory.
	 * Used during trade unpack to free inventory space between shulkers.
	 */
	async _depositNonShulkerInventory() {
		const hotbarNames = new Set((this.config.hotbarItems || []).map(h => h.name));
		const todeposit = {};
		for (const item of this.bot.bot.inventory.items()) {
			if (item.name.includes('shulker_box')) continue;
			if (hotbarNames.has(item.name)) continue;
			if (!todeposit[item.name]) todeposit[item.name] = 0;
			todeposit[item.name] += item.count;
		}

		for (const [itemName, count] of Object.entries(todeposit)) {
			try {
				await this.depositItemType(itemName, count);
			} catch (error) {
				console.error(`Storage: Error depositing ${itemName} during unpack:`, error.message);
			}
		}
	}

	/**
	 * Store a shulker box from bot inventory into an empty chest slot.
	 * Scans NBT to register contents in DB.
	 * @param {object} shulkerItem - The mineflayer item object for the shulker box
	 * @param {object|null} targetSlot - Optional specific slot to store in: { chest_id, pos_x, pos_y, pos_z, slot }
	 * @returns {{ chestId, chestSlot, chestPos, shulkerId, itemFocus }} or false on failure
	 */
	async storeShulker(shulkerItem, targetSlot = null) {
		console.log(`Storage: Storing shulker box (${shulkerItem.name})`);

		// Use provided target slot, or find any empty slot
		const emptySlot = targetSlot || await Database.findEmptyChestSlot();
		if (!emptySlot) {
			console.log('Storage: No empty chest slot for shulker');
			return false;
		}

		const chestPos = new Vec3(emptySlot.pos_x, emptySlot.pos_y, emptySlot.pos_z);
		await this.bot.goTo({ where: chestPos, range: 3 });
		const chestBlock = this.bot.bot.blockAt(chestPos);
		const window = await this.bot.openContainer(chestBlock);
		await sleep(300);

		// Find the specific shulker in inventory portion of the window by matching slot
		let shulkerWindowSlot = null;
		for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
			const item = window.slots[i];
			if (item && item.slot === shulkerItem.slot && item.name.includes('shulker_box')) {
				shulkerWindowSlot = i;
				break;
			}
		}
		// Fallback: find any shulker box if slot match failed
		if (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 this.bot.bot.closeWindow(window);
			console.log('Storage: Shulker not found in window inventory');
			return false;
		}

		await this.bot.bot.moveSlotItem(shulkerWindowSlot, emptySlot.slot);
		await sleep(300);

		// Read NBT from the placed slot and scan actual contents
		const placedItem = window.slots[emptySlot.slot];
		await this.bot.bot.closeWindow(window);
		await sleep(200);

		// Register in DB with actual contents via NBT scan
		let shulkerRecord = null;
		if (placedItem) {
			await this.scanner.scanShulkerFromNBT(this.bot, Database, emptySlot.chest_id, emptySlot.slot, placedItem);
			// Look up the record that was just created/updated
			const chestShulkers = await Database.getShulkersByChest(emptySlot.chest_id);
			shulkerRecord = chestShulkers.find(s => s.slot === emptySlot.slot);
		}

		const info = {
			chestId: emptySlot.chest_id,
			chestSlot: emptySlot.slot,
			chestPos,
			shulkerId: shulkerRecord ? shulkerRecord.id : null,
			itemFocus: shulkerRecord ? shulkerRecord.item_focus : null,
			// Track if this was a forced slot placement (used by Phase 2 to return box to original slot)
			isOriginalSlot: targetSlot !== null,
		};

		console.log(`Storage: Shulker stored at chest ${info.chestId}, slot ${info.chestSlot} (focus: ${info.itemFocus || 'mixed/empty'})`);
		return info;
	}

	/**
	 * Check whether a mineflayer inventory item has special NBT (custom name, lore, custom model data).
	 */
	_isItemSpecial(item) {
		if (!item.nbt) return false;
		const tag = item.nbt?.value?.tag?.value || item.nbt?.value || item.nbt;
		const parsed = this.scanner.parseNBT(tag);
		return Scanner.isSpecialItem(parsed);
	}

	async depositItemType(itemName, count, excludeShulkerId = null) {
		console.log(`Storage: Depositing ${count}x ${itemName}`);

		// Split inventory items into regular and special
		const allInvItems = this.bot.bot.inventory.items().filter(i => i.name === itemName);
		const regularCount = allInvItems.filter(i => !this._isItemSpecial(i)).reduce((s, i) => s + i.count, 0);
		const specialCount = allInvItems.filter(i => this._isItemSpecial(i)).reduce((s, i) => s + i.count, 0);

		let totalDeposited = 0;

		// Deposit regular items first (filter out special ones during shift-click)
		if (regularCount > 0) {
			const toDeposit = Math.min(regularCount, count);
			const result = await this._depositItemBatch(
				itemName, toDeposit, itemName,
				(item) => !this._isItemSpecial(item),
				excludeShulkerId
			);
			totalDeposited += result.deposited;
		}

		// Then deposit special items (filter out regular ones during shift-click)
		if (specialCount > 0 && totalDeposited < count) {
			const remaining = Math.min(specialCount, count - totalDeposited);
			console.log(`Storage: Depositing ${remaining}x ${itemName} (special) separately`);
			const result = await this._depositItemBatch(
				itemName, remaining, itemName + '#special',
				(item) => this._isItemSpecial(item),
				excludeShulkerId
			);
			totalDeposited += result.deposited;
		}

		console.log(`Storage: Deposited ${totalDeposited}/${count} ${itemName}`);
		return { deposited: totalDeposited };
	}

	/**
	 * Internal: deposit items of a given type into shulkers with a specific focus.
	 * @param {string} itemName - The minecraft item name to match
	 * @param {number} count - How many to deposit
	 * @param {string} focus - The shulker item_focus to target (e.g. 'diamond_sword' or 'diamond_sword#special')
	 * @param {Function|null} itemFilter - Filter passed to depositIntoShulker to control which slots get shift-clicked
	 * @param {number|null} excludeShulkerId - Shulker ID to skip
	 */
	async _depositItemBatch(itemName, count, focus, itemFilter, excludeShulkerId = null) {
		let remaining = count;
		let totalDeposited = 0;
		let failedAttempts = 0;

		while (remaining > 0) {
			if (failedAttempts >= 3) {
				console.log(`Storage: ${failedAttempts} consecutive deposit failures for ${itemName} (focus=${focus}), giving up`);
				break;
			}

			// Verify we actually still have matching items in inventory
			const actualInvItems = this.bot.bot.inventory.items().filter(i => i.name === itemName);
			const matchingCount = itemFilter
				? actualInvItems.filter(itemFilter).reduce((sum, i) => sum + i.count, 0)
				: actualInvItems.reduce((sum, i) => sum + i.count, 0);
			if (matchingCount === 0) {
				console.log(`Storage: No more matching ${itemName} in inventory, done`);
				break;
			}
			remaining = Math.min(remaining, matchingCount);

			// Try to find an existing shulker with this focus that has space
			let shulker = await Database.findShulkerWithSpace(focus, excludeShulkerId);
			let wasEmpty = false;
			console.log(`Storage: findShulkerWithSpace('${focus}') → ${shulker ? `shulker ${shulker.id} (slot_count=${shulker.slot_count}, focus=${shulker.item_focus})` : 'none'}`);

			if (!shulker) {
				// No matching shulker, find an empty one
				shulker = await Database.findEmptyShulker(excludeShulkerId);

				if (!shulker) {
					// No empty shulkers - try crafting one (skip if crafting already failed this cycle)
					if (this._craftAvailable) {
						console.log(`Storage: No empty shulkers, attempting to craft one`);
						try {
							await this.craftShulkerBox();
							shulker = await Database.findEmptyShulker();
						} catch (error) {
							console.error('Storage: Failed to craft shulker:', error.message);
							this._craftAvailable = false;
						}
					}

					if (!shulker) {
						console.log(`Storage: Cannot deposit ${remaining}x ${itemName} - no available shulkers`);
						break;
					}
				}

				// Set the item focus for this empty shulker
				await Database.updateShulkerItemFocus(shulker.id, focus);
				wasEmpty = true;
			}

			const chestPos = new Vec3(shulker.pos_x, shulker.pos_y, shulker.pos_z);

			try {
				const { deposited, updatedSlotItem } = await this.shulkerHandler.depositIntoShulker(
					this.bot, chestPos, shulker.slot, itemName, remaining, shulker.id, shulker.chest_id, itemFilter
				);

				totalDeposited += deposited;
				remaining -= deposited;

				if (deposited === 0) {
					failedAttempts++;
					console.log(`Storage: Shulker full or deposit failed (attempt ${failedAttempts}/3), trying next`);
					// Reset item_focus if we just assigned it — don't orphan empty shulkers
					if (wasEmpty) {
						await Database.updateShulkerItemFocus(shulker.id, null);
					}
					continue;
				}
				failedAttempts = 0;
			} catch (error) {
				console.error(`Storage: Error in deposit cycle:`, error);
				if (wasEmpty) {
					await Database.updateShulkerItemFocus(shulker.id, null);
				}
				break;
			}
		}

		return { deposited: totalDeposited };
	}

	// ========================================
	// Withdrawal Flow
	// ========================================

	async handleWithdrawRequest(playerName, itemName, count) {
		console.log(`Storage[${this.bot.name}]: Withdraw request from ${playerName}: ${itemName} x${count}`);
		this._busy = true;

		try {
			// Check total available
			const totalAvailable = await Database.getItemTotalCount(itemName);
			if (totalAvailable === 0) {
				return this.bot.whisper(playerName, `Item not found: ${itemName}`);
			}

			const actualCount = Math.min(count, totalAvailable);
			if (actualCount < count) {
				this.bot.whisper(playerName, `Only ${totalAvailable} ${itemName} available. Withdrawing ${actualCount}.`);
			}

			// Find shulkers containing this item
			const shulkers = await Database.findShulkersWithItem(itemName);
			if (!shulkers || shulkers.length === 0) {
				return this.bot.whisper(playerName, `Cannot find ${itemName} in any shulker`);
			}

			let remaining = actualCount;
			let totalWithdrawn = 0;
			let consecutiveFailures = 0;

			for (const shulker of shulkers) {
				if (remaining <= 0) break;
				if (consecutiveFailures >= 3) {
					console.log(`Storage: Too many withdraw failures for ${itemName}, stopping`);
					break;
				}

				const chestPos = new Vec3(shulker.pos_x, shulker.pos_y, shulker.pos_z);
				try {
					const { withdrawn, updatedSlotItem } = await this.shulkerHandler.withdrawFromShulker(
						this.bot, chestPos, shulker.slot, itemName, remaining, shulker.id, shulker.chest_id
					);

					if (withdrawn > 0) {
						totalWithdrawn += withdrawn;
						remaining -= withdrawn;
						consecutiveFailures = 0;
					} else {
						consecutiveFailures++;
					}
				} catch (error) {
					console.error(`Storage: Error withdrawing from shulker ${shulker.id}:`, error.message);
					consecutiveFailures++;
				}
			}

			// Rebuild index after withdrawals
			await Database.rebuildItemIndex();

			if (totalWithdrawn > 0) {
				// Set up timeout to re-store items if not collected
				const timeoutId = setTimeout(() => {
					this.restoreWithdrawal(playerName);
				}, 5 * 60 * 1000);

				// Store pending withdrawal for trade pickup
				this.pendingWithdrawals.set(playerName, {
					itemName,
					count: totalWithdrawn,
					mode: 'items',
					timeoutId,
				});

				// Log trade
				await Database.logTrade(playerName, 'withdraw', [{ name: itemName, count: totalWithdrawn }]);

				// Initiate trade with the player to hand over items
				await this.initiateTradeWithPlayer(playerName);
			} else {
				this.bot.whisper(playerName, `Failed to withdraw ${itemName}.`);
			}
		} finally {
			this._busy = false;
		}
	}

	async handleWithdrawShulkers(playerName, itemName, shulkerCount) {
		console.log(`Storage[${this.bot.name}]: Shulker withdraw request from ${playerName}: ${shulkerCount} shulkers of ${itemName}`);
		this._busy = true;

		try {
			if (shulkerCount > 12) {
				this.bot.whisper(playerName, `Trade window only has 12 slots. Limiting to 12 shulkers.`);
				shulkerCount = 12;
			}

			// Find shulkers with this item that have items in them
			const shulkers = await Database.findShulkersWithItem(itemName);
			if (!shulkers || shulkers.length === 0) {
				return this.bot.whisper(playerName, `No shulkers containing ${itemName} found`);
			}

			const available = shulkers.length;
			const actualCount = Math.min(shulkerCount, available);
			if (actualCount < shulkerCount) {
				this.bot.whisper(playerName, `Only ${available} shulkers of ${itemName} available. Withdrawing ${actualCount}.`);
			}

			let taken = 0;
			for (let i = 0; i < actualCount; i++) {
				const shulker = shulkers[i];
				const chestPos = new Vec3(shulker.pos_x, shulker.pos_y, shulker.pos_z);

				try {
					await this.shulkerHandler.takeWholeShulker(this.bot, chestPos, shulker.slot, shulker.id);

					// Also fully delete from DB — it's leaving storage permanently
					await Database.deleteShulker(shulker.id);
					taken++;
				} catch (error) {
					console.error(`Storage: Error taking whole shulker ${shulker.id}:`, error.message);
				}
			}

			await Database.rebuildItemIndex();

			if (taken > 0) {
				// Set up timeout to re-store shulkers if not collected
				const timeoutId = setTimeout(() => {
					this.restoreWithdrawal(playerName);
				}, 5 * 60 * 1000);

				this.pendingWithdrawals.set(playerName, {
					itemName,
					count: taken,
					mode: 'shulkers',
					timeoutId,
				});

				await Database.logTrade(playerName, 'withdraw', [{ name: itemName, count: taken, mode: 'shulkers' }]);

				// Initiate trade with the player to hand over shulkers
				await this.initiateTradeWithPlayer(playerName);
			} else {
				this.bot.whisper(playerName, `Failed to withdraw shulkers of ${itemName}.`);
			}
		} finally {
			this._busy = false;
		}
	}

	async restoreWithdrawal(playerName) {
		const pending = this.pendingWithdrawals.get(playerName);
		if (!pending) return;

		console.log(`Storage: Withdrawal timeout for ${playerName} — restoring ${pending.count}x ${pending.itemName} (mode: ${pending.mode || 'items'})`);

		// Clear timeout ref
		if (pending.timeoutId) {
			clearTimeout(pending.timeoutId);
		}
		this.pendingWithdrawals.delete(playerName);

		try {
			if (pending.mode === 'shulkers') {
				// Re-store whole shulker boxes
				const shulkerItems = this.bot.bot.inventory.items().filter(item => item.name.includes('shulker_box'));
				for (const shulkerItem of shulkerItems) {
					await this.storeShulker(shulkerItem);
				}
			} else {
				// Re-deposit individual items
				const grouped = {};
				for (const item of this.bot.bot.inventory.items()) {
					if (item.name === pending.itemName) {
						if (!grouped[item.name]) grouped[item.name] = 0;
						grouped[item.name] += item.count;
					}
				}
				for (const [itemName, count] of Object.entries(grouped)) {
					await this.depositItemType(itemName, count);
				}
			}

			await Database.rebuildItemIndex();
			this.bot.whisper(playerName, `Withdrawal cancelled — ${pending.count}x ${pending.itemName} returned to storage.`);
		} catch (error) {
			console.error(`Storage: Error restoring withdrawal for ${playerName}:`, error.message);
			this.bot.whisper(playerName, `Withdrawal timed out but failed to re-store items. Please contact an admin.`);
		}
	}

	/**
	 * Initiate a /trade with a player and hand over pending withdrawal items.
	 * Sends the trade request, waits for the window, places items, and confirms.
	 * Falls back to the old "use /trade" message if the player is offline or trade fails.
	 */
	async initiateTradeWithPlayer(playerName) {
		const pending = this.pendingWithdrawals.get(playerName);
		if (!pending) return;

		const botSlots = [0, 1, 2, 3, 9, 10, 11, 12, 18, 19, 20, 21];

		// Check if player is online
		const player = this.bot.bot.players[playerName];
		if (!player) {
			this.bot.whisper(playerName, `${pending.count}x ${pending.itemName} ready. Use /trade to collect within 5 minutes.`);
			return;
		}

		try {
			console.log(`Storage: Initiating /trade with ${playerName}`);
			this.bot.whisper(playerName, `${pending.count}x ${pending.itemName} ready — sending trade request...`);
			await this.bot.say(`/trade ${playerName}`);

			// Wait for trade window to open (player must accept)
			const window = await Promise.race([
				this.bot.once('windowOpen'),
				sleep(60000).then(() => null), // 60s timeout
			]);

			if (!window) {
				console.log(`Storage: Trade request to ${playerName} timed out`);
				this.bot.whisper(playerName, `Trade request timed out. Use /trade to collect within 5 minutes.`);
				return;
			}

			// Place items in bot's trade slots
			let placed = 0;
			for (const slotNum of botSlots) {
				if (placed >= 12) break;

				for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
					const item = window.slots[i];
					if (!item) continue;

					if (pending.mode === 'shulkers') {
						if (!item.name.includes('shulker_box')) continue;
					} else {
						if (item.name !== pending.itemName) continue;
					}

					try {
						await this.bot.bot.moveSlotItem(i, slotNum);
						await sleep(200);
						placed++;
						break;
					} catch (error) {
						console.log(`Storage: Could not move item to trade slot ${slotNum}: ${error.message}`);
					}
				}
			}

			console.log(`Storage: Placed ${placed} stack(s) in trade window for ${playerName}`);

			// Poll for customer confirmation (lime_dye at slot 53)
			const timeoutHandle = setTimeout(() => {
				try { this.bot.bot.closeWindow(window); } catch (e) { /* ignore */ }
				this.bot.whisper(playerName, 'Trade timed out.');
			}, 120000);

			const confirmationCheck = setInterval(async () => {
				try {
					const indicator = window.slots[53];
					if (indicator && indicator.name === 'lime_dye') {
						this.bot.bot.moveSlotItem(37, 37);
					}
				} catch (e) { /* window may have closed */ }
			}, 500);

			// Wait for trade to complete
			await this.bot.once('windowClose');
			clearInterval(confirmationCheck);

			if (timeoutHandle._destroyed) return;
			clearTimeout(timeoutHandle);

			// Withdrawal complete — clear pending
			if (pending.timeoutId) clearTimeout(pending.timeoutId);
			this.pendingWithdrawals.delete(playerName);
			this.bot.whisper(playerName, `Withdrawal complete! Enjoy your ${pending.itemName}.`);

		} catch (error) {
			console.error(`Storage: Error initiating trade with ${playerName}:`, error.message);
			this.bot.whisper(playerName, `Trade failed. Use /trade to collect within 5 minutes.`);
		}
	}

	// ========================================
	// Crafting
	// ========================================

	async craftShulkerBox() {
		console.log('Storage: Attempting to craft shulker box');

		// Check bot inventory first for materials
		let shellsInInv = 0;
		let chestsInInv = 0;
		for (const item of this.bot.bot.inventory.items()) {
			if (item.name === 'shulker_shell') shellsInInv += item.count;
			if (item.name === 'chest') chestsInInv += item.count;
		}

		// Take enough shells for the recipe AND to restock to hotbar target (saves repeat trips)
		const shellHotbar = (this.config.hotbarItems || []).find(h => h.name === 'shulker_shell');
		const shellTarget = shellHotbar ? shellHotbar.target : 2;
		let shellsNeeded = Math.max(0, Math.max(2, shellTarget) - shellsInInv);
		let chestsNeeded = Math.max(0, 1 - chestsInInv);

		console.log(`Storage: Craft materials — inventory: ${shellsInInv} shells, ${chestsInInv} chests. Need from storage: ${shellsNeeded} shells (target ${shellTarget}), ${chestsNeeded} chests`);

		// Only check/withdraw from storage if inventory doesn't have enough
		if (shellsNeeded > 0) {
			const shellCount = await Database.getItemTotalCount('shulker_shell');
			const minForRecipe = Math.max(0, 2 - shellsInInv);
			if (shellCount < minForRecipe) {
				throw new Error(`Not enough shulker shells (have ${shellsInInv} inv + ${shellCount} storage, need 2)`);
			}
			// Take up to target, but at least enough for the recipe
			shellsNeeded = Math.min(shellsNeeded, shellCount);

			const shellShulkers = await Database.findShulkersWithItem('shulker_shell');
			// Sort by distance so the bot tries nearby chests first
			const botPos = this.bot.bot.entity.position;
			shellShulkers.sort((a, b) => {
				const da = botPos.distanceTo(new Vec3(a.pos_x, a.pos_y, a.pos_z));
				const db = botPos.distanceTo(new Vec3(b.pos_x, b.pos_y, b.pos_z));
				return da - db;
			});
			let shellsObtained = 0;
			for (const shulker of shellShulkers) {
				if (shellsObtained >= shellsNeeded) break;
				const chestPos = new Vec3(shulker.pos_x, shulker.pos_y, shulker.pos_z);
				const { withdrawn } = await this.shulkerHandler.withdrawFromShulker(
					this.bot, chestPos, shulker.slot, 'shulker_shell', shellsNeeded - shellsObtained,
					shulker.id, shulker.chest_id
				);
				shellsObtained += withdrawn;
			}

			if (shellsObtained < minForRecipe) {
				throw new Error(`Failed to gather shells: got ${shellsObtained}/${minForRecipe} from storage`);
			}
		}

		if (chestsNeeded > 0) {
			const chestCount = await Database.getItemTotalCount('chest');
			if (chestCount < chestsNeeded) {
				throw new Error(`Not enough chests (have ${chestsInInv} inv + ${chestCount} storage, need 1)`);
			}

			const chestShulkers = await Database.findShulkersWithItem('chest');
			chestShulkers.sort((a, b) => {
				const da = botPos.distanceTo(new Vec3(a.pos_x, a.pos_y, a.pos_z));
				const db = botPos.distanceTo(new Vec3(b.pos_x, b.pos_y, b.pos_z));
				return da - db;
			});
			let chestsObtained = 0;
			for (const shulker of chestShulkers) {
				if (chestsObtained >= chestsNeeded) break;
				const chestPos = new Vec3(shulker.pos_x, shulker.pos_y, shulker.pos_z);
				const { withdrawn } = await this.shulkerHandler.withdrawFromShulker(
					this.bot, chestPos, shulker.slot, 'chest', chestsNeeded - chestsObtained,
					shulker.id, shulker.chest_id
				);
				chestsObtained += withdrawn;
			}

			if (chestsObtained < chestsNeeded) {
				throw new Error(`Failed to gather chests: got ${chestsObtained}/${chestsNeeded} from storage`);
			}
		}

		// Find or navigate to crafting table
		const craftingTablePos = this.config.craftingTablePos;
		let craftingTable;

		if (craftingTablePos) {
			craftingTable = this.bot.bot.blockAt(new Vec3(craftingTablePos.x, craftingTablePos.y, craftingTablePos.z));
		} else {
			craftingTable = this.bot.bot.findBlock({
				matching: this.bot.mcData.blocksByName.crafting_table?.id,
				maxDistance: 32,
			});
		}

		if (!craftingTable) {
			throw new Error('No crafting table found nearby');
		}

		await this.bot.goTo({ where: craftingTable.position, range: 3 });

		// Craft shulker box manually (bot.craft() broken on 1.21+)
		const shulkerBoxRecipes = this.bot.bot.recipesAll(
			this.bot.mcData.itemsByName.shulker_box.id,
			null,
			craftingTable
		);

		if (!shulkerBoxRecipes || shulkerBoxRecipes.length === 0) {
			throw new Error('No recipe found for shulker box');
		}

		const recipe = shulkerBoxRecipes[0];
		const window = await this.bot.openCraftingTable(craftingTable);
		const inventory = window.slots.slice(window.inventoryStart, window.inventoryEnd);

		// Group grid slots by ingredient type (handles single stack needing multiple grid slots)
		const ingredientsByType = {};
		for (let row = 0; row < recipe.inShape.length; row++) {
			for (let col = 0; col < recipe.inShape[row].length; col++) {
				const shape = recipe.inShape[row][col];
				if (shape.id === -1) continue;
				const gridSlot = row * 3 + col + 1;
				if (!ingredientsByType[shape.id]) ingredientsByType[shape.id] = [];
				ingredientsByType[shape.id].push(gridSlot);
			}
		}

		// For each ingredient type: pick up stack, right-click into each grid slot, put stack back
		for (const [typeId, gridSlots] of Object.entries(ingredientsByType)) {
			const invIdx = inventory.findIndex(el => el && el.type === parseInt(typeId));
			if (invIdx === -1) {
				await window.close();
				throw new Error(`Missing ingredient type ${typeId} in inventory`);
			}
			const actualSlot = window.inventoryStart + invIdx;

			// Left-click to pick up the stack
			await this.bot.bot.clickWindow(actualSlot, 0, 0);
			await sleep(100);

			// Right-click on each grid slot to place one item
			for (const gridSlot of gridSlots) {
				await this.bot.bot.clickWindow(gridSlot, 1, 0);
				await sleep(100);
			}

			// Left-click to put remaining stack back
			await this.bot.bot.clickWindow(actualSlot, 0, 0);
			await sleep(100);
		}

		await sleep(500);

		// Take crafted shulker box from result slot
		if (window.slots[0]) {
			let outputSlot = null;
			for (let j = window.inventoryStart; j < window.inventoryEnd; j++) {
				if (!window.slots[j]) { outputSlot = j; break; }
			}
			if (outputSlot === null) outputSlot = window.inventoryStart;
			await this.bot.bot.clickWindow(0, 0, 0);
			await sleep(100);
			await this.bot.bot.clickWindow(outputSlot, 0, 0);
			await sleep(100);
		}

		await window.close();
		await sleep(500);

		// Find an empty chest slot to store the new shulker
		const emptySlot = await Database.findEmptyChestSlot();
		if (emptySlot) {
			const chestPos = new Vec3(emptySlot.pos_x, emptySlot.pos_y, emptySlot.pos_z);
			await this.bot.goTo({ where: chestPos, range: 3 });
			const chestBlock = this.bot.bot.blockAt(chestPos);
			const storeWindow = await this.bot.openContainer(chestBlock);
			await sleep(300);

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

			if (shulkerWindowSlot !== null) {
				await this.bot.bot.moveSlotItem(shulkerWindowSlot, emptySlot.slot);
				await sleep(300);
			}

			await this.bot.bot.closeWindow(storeWindow);
			await sleep(200);

			// Register in DB as empty shulker
			await Database.upsertShulker(emptySlot.chest_id, emptySlot.slot, 'shulker_box', null, null);
			const craftedShulkers = await Database.getShulkersByChest(emptySlot.chest_id);
			const newShulker = craftedShulkers.find(s => s.slot === emptySlot.slot);
			if (newShulker) {
				await Database.updateShulkerCounts(newShulker.id, 0, 0);
			}
		}

		this._craftAvailable = true; // crafting succeeded, reset flag
		console.log('Storage: Successfully crafted shulker box');
	}

	// ========================================
	// Inventory Cleanup
	// ========================================

	/**
	 * Deposit any non-shulker, non-hotbar items from bot inventory into storage.
	 * Called periodically and as pre/post-flight during organize.
	 */
	async cleanInventory() {
		const hotbarTargets = {};
		for (const h of (this.config.hotbarItems || [])) {
			hotbarTargets[h.name] = h.target || 0;
		}
		const hotbarSeen = {};
		const grouped = {};
		for (const item of this.bot.bot.inventory.items()) {
			if (item.name.includes('shulker_box')) continue;
			if (item.name in hotbarTargets) {
				const seen = (hotbarSeen[item.name] || 0);
				const keep = Math.max(0, hotbarTargets[item.name] - seen);
				hotbarSeen[item.name] = seen + item.count;
				const excess = item.count - keep;
				if (excess <= 0) continue;
				if (!grouped[item.name]) grouped[item.name] = 0;
				grouped[item.name] += excess;
				continue;
			}
			if (!grouped[item.name]) grouped[item.name] = 0;
			grouped[item.name] += item.count;
		}

		const itemTypes = Object.entries(grouped);
		if (itemTypes.length === 0) return 0;

		let cleaned = 0;
		for (const [itemName, count] of itemTypes) {
			try {
				console.log(`Storage: Cleaning inventory — depositing ${count}x ${itemName}`);
				await this.depositItemType(itemName, count);
				cleaned++;
			} catch (error) {
				console.log(`Storage: Inventory cleanup failed for ${itemName}: ${error.message}`);
			}
		}

		if (cleaned > 0) {
			await Database.rebuildItemIndex();
			console.log(`Storage: Inventory cleanup deposited ${cleaned} item type(s)`);
		}

		return cleaned;
	}

	// ========================================
	// Organize
	// ========================================

	async organizeLooseItems() {
		console.log('Storage: Organizing loose items into shulkers...');
		this._craftAvailable = true; // fresh organize, allow craft retry
		this._busy = true;
		let organized = 0;

		try {
			// Pre-flight: deposit any stray items in bot inventory
			await this.cleanInventory();

			// Also detect and unpack any mixed shulkers that ended up in chests
			// (e.g., from failed Phase 2 unpacks or shulkers that were manually placed)
			await this.organizeMixedShulkers();

			const chests = await Database.getChestsWithLooseItems();
			console.log(`Storage: ${chests.length} chest(s) have loose items in DB`);

			for (const chest of chests) {
				// Check inventory has room before processing next chest (need 3+ free: items + shulker + buffer)
				const freeSlots = this.bot.bot.inventory.slots.filter((s, i) => !s && i >= 9).length;
				if (freeSlots < 3) {
					console.log(`Storage: Inventory too full (${freeSlots} free slots), stopping organize loop`);
					break;
				}

				const chestPos = new Vec3(chest.pos_x, chest.pos_y, chest.pos_z);

				try {
					// Process one item type at a time from this chest
					const failedItems = new Set();
					while (true) {
						await this.bot.goTo({ where: chestPos, range: 3 });
						const chestBlock = this.bot.bot.blockAt(chestPos);
						const window = await this.bot.openContainer(chestBlock);
						await sleep(300);

						// Find the first non-shulker item in the chest (skip already-failed types)
						const chestSlotCount = window.inventoryStart;
						let firstLooseSlot = null;
						for (let i = 0; i < chestSlotCount; i++) {
							const slot = window.slots[i];
							if (slot && !slot.name.includes('shulker_box') && !failedItems.has(slot.name)) {
								firstLooseSlot = i;
								break;
							}
						}

						if (firstLooseSlot === null) {
							// No loose items left in this chest
							await Database.clearLooseItems(chest.id);
							await this.bot.bot.closeWindow(window);
							break;
						}

						const itemName = window.slots[firstLooseSlot].name;
						console.log(`Storage: Picking up ${itemName} from chest at ${chestPos}`);

						// Count empty inventory slots
						let emptyInvSlots = 0;
						for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
							if (!window.slots[i]) emptyInvSlots++;
						}

						// Pick up all stacks of THIS item type only
						const pickedUpSlots = [];
						let totalCount = 0;

						for (let i = 0; i < chestSlotCount; i++) {
							const srcItem = window.slots[i];
							if (!srcItem || srcItem.name !== itemName) continue;

							// Find stackable slot first (doesn't consume an empty slot)
							let targetSlot = null;
							for (let j = window.inventoryStart; j < window.inventoryEnd; j++) {
								const invItem = window.slots[j];
								if (invItem && invItem.name === itemName && invItem.count < invItem.stackSize) {
									targetSlot = j;
									break;
								}
							}

							// Then find empty slot, but keep at least 1 free for shulker
							if (targetSlot === null) {
								if (emptyInvSlots <= 1) {
									console.log('Storage: Reserving last inventory slot for shulker operations');
									break;
								}
								for (let j = window.inventoryStart; j < window.inventoryEnd; j++) {
									if (!window.slots[j]) {
										targetSlot = j;
										break;
									}
								}
								emptyInvSlots--;
							}

							if (targetSlot === null) {
								console.log('Storage: Bot inventory full');
								break;
							}

							try {
								await this.bot.bot.moveSlotItem(i, targetSlot);
								await sleep(200);
								// Measure actual items removed from chest slot (handles partial stacking)
								const afterItem = window.slots[i];
								const moved = srcItem.count - (afterItem ? afterItem.count : 0);
								pickedUpSlots.push(i);
								totalCount += moved;
							} catch (error) {
								console.log(`Storage: Could not pick up slot ${i}: ${error.message}`);
							}
						}

						await this.bot.bot.closeWindow(window);

						// Sync DB: remove picked-up items individually
						for (const slot of pickedUpSlots) {
							await Database.deleteLooseItem(chest.id, slot);
						}

						if (totalCount === 0) {
							console.log(`Storage: Could not pick up any ${itemName}, skipping chest`);
							break;
						}

						// Deposit into shulker(s)
						try {
							const result = await this.depositItemType(itemName, totalCount);
							organized++;

							// If deposit was partial, return remaining items to the chest
							if (result.deposited < totalCount) {
								console.log(`Storage: Partial deposit for ${itemName} (${result.deposited}/${totalCount}), returning remainder to chest`);
								await this._returnItemsToChest(chestPos, itemName);
							}
						} catch (error) {
							console.log(`Storage: Could not deposit ${itemName}: ${error.message}`);
							failedItems.add(itemName);
							// Try to return items to the chest instead of leaving them in inventory
							try {
								await this._returnItemsToChest(chestPos, itemName);
							} catch (returnError) {
								console.log(`Storage: Could not return ${itemName} to chest: ${returnError.message}`);
								break; // Can't recover, stop this chest
							}
						}

						// Check if inventory is nearly full before processing next item type
						const innerFreeSlots = this.bot.bot.inventory.slots.filter((s, i) => !s && i >= 9).length;
						if (innerFreeSlots < 3) {
							console.log(`Storage: Inventory nearly full (${innerFreeSlots} free), stopping this chest`);
							break;
						}

						await sleep(250);
						// Loop back to re-open chest for next item type
					}

				} catch (error) {
					console.log(`Storage: Error organizing chest at ${chestPos}: ${error.message}`);
				}

				// Check if inventory is too full to continue organizing other chests
				const outerFreeSlots = this.bot.bot.inventory.slots.filter((s, i) => !s && i >= 9).length;
				if (outerFreeSlots < 3) {
					console.log(`Storage: Inventory too full (${outerFreeSlots} free slots), stopping organize`);
					break;
				}

				await sleep(250);
			}

			// Consolidate partially filled shulkers of the same item type
			try {
				await this.consolidateShulkers();
			} catch (error) {
				console.error('Storage: Consolidation error:', error.message);
			}

			// Post-flight: deposit any items still in inventory (may succeed now that organize freed shulker space)
			await this.cleanInventory();

			await Database.rebuildItemIndex();
			console.log(`Storage: Organized ${organized} item types into shulkers`);
			return organized;
		} finally {
			this._busy = false;
		}
	}

	/**
	 * Find and unpack mixed/unsorted shulkers during the organize pass.
	 * This is the recovery path for shulkers that failed Phase 2 unpacking
	 * or were otherwise left with mixed contents.
	 */
	async organizeMixedShulkers() {
		console.log('Storage: Checking for mixed shulkers to unpack...');

		// Find shulkers with null item_focus (mixed/unsorted) that have items in them
		// These are candidates for unpacking
		const mixedShulkers = await Database.db.all(`
			SELECT s.*, c.pos_x, c.pos_y, c.pos_z, c.chest_type
			FROM shulkers s
			INNER JOIN chests c ON c.id = s.chest_id
			WHERE s.item_focus IS NULL AND s.total_items > 0 AND s.slot_count >= 0
			ORDER BY s.id ASC
		`);

		if (mixedShulkers.length === 0) {
			return;
		}

		console.log(`Storage: Found ${mixedShulkers.length} mixed shulker(s) to unpack`);

		for (const shulker of mixedShulkers) {
			// Check inventory space
			const freeSlots = this.bot.bot.inventory.slots.filter((s, i) => !s && i >= 9).length;
			if (freeSlots < 3) {
				console.log(`Storage: Inventory too full (${freeSlots} free slots), stopping mixed shulker organize`);
				break;
			}

			const chestPos = new Vec3(shulker.pos_x, shulker.pos_y, shulker.pos_z);
			console.log(`Storage: Unpacking mixed shulker #${shulker.id} from chest at ${chestPos}`);

			try {
				// Clean up any leftover shulkers first
				const leftoverBoxes = this.bot.bot.inventory.items().filter(item => item.name.includes('shulker_box'));
				for (const leftover of leftoverBoxes) {
					try { await this.storeShulker(leftover); } catch (e) { /* ignore */ }
				}
				await this._depositNonShulkerInventory();

				// takeWholeShulker marks slot_count=-1 in DB (in-transit)
				await this.shulkerHandler.takeWholeShulker(this.bot, chestPos, shulker.slot, shulker.id);

				const shulkerItem = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
				if (!shulkerItem) {
					console.error('Storage: Shulker vanished during organizeMixedShulkers');
					// Upsert it back so it's not orphaned
					await Database.upsertShulker(shulker.chest_id, shulker.slot, 'shulker_box', null, null);
					await Database.updateShulkerCounts(shulker.id, shulker.slot_count, shulker.total_items);
					continue;
				}

				const { extracted, inventoryFull } = await this.shulkerHandler.unpackShulkerFromInventory(this.bot, shulkerItem);
				console.log(`Storage: Mixed shulker unpacked — ${extracted.length} stacks extracted`);

				// Deposit all extracted items
				await this._depositNonShulkerInventory();

				// Return empty (or partially-empty) box to its original slot
				const emptyBox = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
				if (emptyBox) {
					try {
						const result = await this.storeShulker(emptyBox, {
							chest_id: shulker.chest_id,
							pos_x: shulker.pos_x,
							pos_y: shulker.pos_y,
							pos_z: shulker.pos_z,
							slot: shulker.slot,
						});
						if (!result) {
							await this.storeShulker(emptyBox);
						}
					} catch (e) {
						console.error(`Storage: Error storing mixed shulker box:`, e.message);
						try { await this.storeShulker(emptyBox); } catch (e2) { /* give up */ }
					}
				}
			} catch (error) {
				console.error(`Storage: Error unpacking mixed shulker #${shulker.id}:`, error.message);
				// Upsert it back so it can be retried
				try {
					await Database.upsertShulker(shulker.chest_id, shulker.slot, 'shulker_box', null, null);
					await Database.updateShulkerCounts(shulker.id, shulker.slot_count, shulker.total_items);
				} catch (e2) { /* ignore */ }
			}
		}
	}


	/**
	 * Consolidate partially filled shulkers of the same item type.
	 * Withdraws from least-full shulker and deposits into the most-full one.
	 */
	async consolidateShulkers() {
		console.log('Storage: Consolidating partially filled shulkers...');
		let consolidated = 0;

		const candidates = await Database.findConsolidatableItems();
		console.log(`Storage: ${candidates.length} item type(s) have multiple non-full shulkers`);

		for (const candidate of candidates) {
			const itemName = candidate.item_focus;

			// Re-fetch shulkers each iteration (DB may have changed from previous consolidation)
			const shulkers = await Database.getShulkersByItemFocus(itemName);
			// Need at least 2 non-full shulkers to consolidate
			const nonFull = shulkers.filter(s => s.slot_count < 27);
			if (nonFull.length < 2) continue;

			// Withdraw from the least-full shulker (first in list, sorted ASC)
			const source = nonFull[0];
			console.log(`Storage: Consolidating ${itemName} — withdrawing from shulker ${source.id} (${source.slot_count} slots used)`);

			const chestPos = new Vec3(source.pos_x, source.pos_y, source.pos_z);

			try {
				const { withdrawn } = await this.shulkerHandler.withdrawFromShulker(
					this.bot, chestPos, source.slot, itemName, source.total_items,
					source.id, source.chest_id
				);

				if (withdrawn === 0) {
					console.log(`Storage: Could not withdraw from shulker ${source.id}, skipping`);
					continue;
				}

				console.log(`Storage: Withdrew ${withdrawn}x ${itemName}, depositing into fuller shulker(s)`);

				// Deposit will fill the most-full matching shulker first (exclude source to prevent depositing back)
				const result = await this.depositItemType(itemName, withdrawn, source.id);
				consolidated++;

				// If source shulker is now empty, clear its item_focus
				const updatedSource = await Database.getShulkerById(source.id);
				if (updatedSource && updatedSource.total_items === 0) {
					await Database.updateShulkerItemFocus(source.id, null);
					console.log(`Storage: Shulker ${source.id} is now empty, cleared item_focus`);
				}

			} catch (error) {
				console.error(`Storage: Error consolidating ${itemName}:`, error.message);
			}

			// Check inventory space before continuing
			const freeSlots = this.bot.bot.inventory.slots.filter((s, i) => !s && i >= 9).length;
			if (freeSlots < 3) {
				console.log('Storage: Inventory too full, stopping consolidation');
				break;
			}

			await sleep(250);
		}

		if (consolidated > 0) {
			await Database.rebuildItemIndex();
		}
		console.log(`Storage: Consolidated ${consolidated} item type(s)`);
		return consolidated;
	}

	/**
	 * Return items of a given type from bot inventory back into a chest.
	 * Used by organize when deposit is partial to prevent inventory buildup.
	 */
	async _returnItemsToChest(chestPos, itemName) {
		const itemsInInv = this.bot.bot.inventory.items().filter(i => i.name === itemName);
		if (itemsInInv.length === 0) return;

		console.log(`Storage: Returning ${itemsInInv.length} stack(s) of ${itemName} to chest at ${chestPos}`);
		await this.bot.goTo({ where: chestPos, range: 3 });
		const chestBlock = this.bot.bot.blockAt(chestPos);
		const window = await this.bot.openContainer(chestBlock);
		await sleep(300);

		for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
			const item = window.slots[i];
			if (!item || item.name !== itemName) continue;

			// Find empty chest slot
			let targetSlot = null;
			for (let j = 0; j < window.inventoryStart; j++) {
				if (!window.slots[j]) {
					targetSlot = j;
					break;
				}
			}
			if (targetSlot === null) {
				console.log('Storage: Chest full, cannot return all items');
				break;
			}

			await this.bot.bot.moveSlotItem(i, targetSlot);
			await sleep(200);
		}

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

	// ========================================
	// Special Item Withdrawal
	// ========================================

	async handleWithdrawSpecialItem(playerName, shulkerItemId) {
		console.log(`Storage[${this.bot.name}]: Special item withdraw request from ${playerName}: shulker_item #${shulkerItemId}`);
		this._busy = true;

		try {
			// Look up the specific shulker_items row
			const itemRow = await Database.getShulkerItemById(shulkerItemId);
			if (!itemRow) {
				return this.bot.whisper(playerName, `Special item #${shulkerItemId} not found.`);
			}

			const shulker = await Database.getShulkerById(itemRow.shulker_id);
			if (!shulker) {
				return this.bot.whisper(playerName, `Shulker for item #${shulkerItemId} not found.`);
			}

			const chest = await Database.getChestById(shulker.chest_id);
			if (!chest) {
				return this.bot.whisper(playerName, `Chest for shulker #${shulker.id} not found.`);
			}

			const chestPos = new Vec3(chest.pos_x, chest.pos_y, chest.pos_z);

			try {
				const { withdrawn } = await this.shulkerHandler.withdrawFromShulkerSlot(
					this.bot, chestPos, shulker.slot, itemRow.slot, itemRow.count, shulker.id, shulker.chest_id
				);

				if (withdrawn > 0) {
					await Database.rebuildItemIndex();

					const timeoutId = setTimeout(() => {
						this.restoreWithdrawal(playerName);
					}, 5 * 60 * 1000);

					this.pendingWithdrawals.set(playerName, {
						itemName: itemRow.item_name,
						count: withdrawn,
						mode: 'items',
						timeoutId,
					});

					await Database.logTrade(playerName, 'withdraw', [{ name: itemRow.item_name, count: withdrawn, special: true }]);

					// Initiate trade with the player to hand over special item
					await this.initiateTradeWithPlayer(playerName);
				} else {
					this.bot.whisper(playerName, `Failed to withdraw special item.`);
				}
			} catch (error) {
				console.error(`Storage: Error withdrawing special item:`, error.message);
				this.bot.whisper(playerName, `Withdraw failed: ${error.message}`);
			}
		} finally {
			this._busy = false;
		}
	}

	// ========================================
	// Status & Search
	// ========================================

	async getStatus(playerName) {
		const stats = await Database.getStats();
		this.bot.whisper(playerName, `Storage: ${stats.totalItems} items in ${stats.totalShulkers} shulkers (${stats.totalChests} chests)`);
	}

	async findItem(itemName) {
		return await Database.searchItems(itemName);
	}

	async checkPermission(name, requiredRole) {
		return await Database.checkPermission(name, requiredRole);
	}

	// ========================================
	// Command Handler
	// ========================================

	async handleCommand(from, command, ...args) {
		console.log(`Storage: Command '${command}' from ${from} with args:`, args);
		this._currentCommand = command;

		try { switch (command) {
			case 'scan':
				this.bot.whisper(from, 'Starting storage scan...');
				try {
					await this.scanArea(true);
					const stats = await Database.getStats();
					this.bot.whisper(from, `Scan complete! ${stats.totalChests} chests, ${stats.totalShulkers} shulkers, ${stats.totalItems} items`);
				} catch (error) {
					console.error('Storage: Scan error:', error);
					this.bot.whisper(from, `Scan failed: ${error.message}`);
				}
				break;

			case 'status':
				await this.getStatus(from);
				break;

			case 'withdraw': {
				const [itemName, count] = args;
				const parsedCount = parseInt(count) || 1;
				await this.handleWithdrawRequest(from, itemName, parsedCount);
				break;
			}

			case 'withdraw-shulkers': {
				const [itemName, shulkerCount] = args;
				await this.handleWithdrawShulkers(from, itemName, shulkerCount);
				break;
			}

			case 'find': {
				const [searchTerm] = args;
				const items = await this.findItem(searchTerm);
				if (items.length === 0) {
					this.bot.whisper(from, `No items found matching '${searchTerm}'`);
				} else {
					const results = items.slice(0, 5).map(i => `${i.item_name}: ${i.total_count}`);
					this.bot.whisper(from, `Found: ${results.join(', ')}`);
				}
				break;
			}

			case 'chests': {
				const chests = await Database.getChests();
				this.bot.whisper(from, `Tracking ${chests.length} chests`);
				break;
			}

			case 'organize':
				this.bot.whisper(from, 'Starting organize...');
				try {
					const count = await this.organizeLooseItems();
					this.bot.whisper(from, `Organize complete! Sorted ${count} item stacks.`);
				} catch (error) {
					console.error('Storage: Organize error:', error);
					this.bot.whisper(from, `Organize failed: ${error.message}`);
				}
				break;

			case 'consolidate':
				this.bot.whisper(from, 'Starting consolidation...');
				try {
					this._busy = true;
					const count = await this.consolidateShulkers();
					this.bot.whisper(from, `Consolidation complete! Merged ${count} item type(s).`);
				} catch (error) {
					console.error('Storage: Consolidate error:', error);
					this.bot.whisper(from, `Consolidation failed: ${error.message}`);
				} finally {
					this._busy = false;
				}
				break;

			case 'addplayer': {
				const [playerName, role] = args;
				try {
					await Database.addPlayer(playerName, role || 'team');
					this.bot.whisper(from, `Added ${playerName} as ${role || 'team'}`);
				} catch (error) {
					this.bot.whisper(from, `Failed to add player: ${error.message}`);
				}
				break;
			}

			case 'removeplayer': {
				const [removePlayer] = args;
				try {
					await Database.removePlayer(removePlayer);
					this.bot.whisper(from, `Removed ${removePlayer}`);
				} catch (error) {
					this.bot.whisper(from, `Failed to remove player: ${error.message}`);
				}
				break;
			}

			case 'players': {
				const players = await Database.getAllPlayers();
				const playerList = players.map(p => `${p.player_name}(${p.role})`).join(', ');
				this.bot.whisper(from, `Players: ${playerList}`);
				break;
			}

			default:
				this.bot.whisper(from, `Unknown command: ${command}`);
		}
		} finally {
			this._currentCommand = null;
		}
	}
}

module.exports = Storage;
