'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;

	// AI tool documentation - commands available to AI
	static commands = [
		{
			name: 'status',
			description: 'Get storage system status - chest count, shulker count, item count',
			parameters: [],
			examples: [
				"Get overall statistics about the storage system",
				"Show how many chests, shulkers, and items are tracked"
			],
			category: 'information'
		},
		{
			name: 'scan',
			description: 'Scan the area for chests and update database',
			parameters: [],
			examples: [
				"Scan the surrounding area to discover new chests",
				"Update the storage database with newly found containers"
			],
			category: 'discovery'
		},
		{
			name: 'find',
			description: 'Find an item in storage - returns location and quantity',
			parameters: [
				{ name: 'itemName', type: 'string', required: true, description: 'Item name to search for (e.g., "diamond", "iron_ingot")' }
			],
			examples: [
				"find diamond",
				"find iron_ingot"
			],
			category: 'search'
		},
		{
			name: 'withdraw',
			description: 'Withdraw items from storage',
			parameters: [
				{ name: 'itemName', type: 'string', required: true, description: 'Item name to withdraw' },
				{ name: 'count', type: 'number', required: false, description: 'Number of items (default: 1)' }
			],
			examples: [
				"withdraw diamond 5",
				"withdraw cooked_beef"
			],
			category: 'retrieval'
		},
		{
			name: 'withdraw-shulkers',
			description: 'Withdraw full shulker boxes of a specific item',
			parameters: [
				{ name: 'itemName', type: 'string', required: true, description: 'Item name stored in shulkers' },
				{ name: 'shulkerCount', type: 'number', required: false, description: 'Number of shulkers to withdraw (default: 1)' }
			],
			examples: [
				"withdraw-shulkers diamond 2",
				"withdraw-shulkers cooked_beef"
			],
			category: 'retrieval'
		},
		{
			name: 'chests',
			description: 'List all chests in storage with their contents summary',
			parameters: [],
			examples: [
				"List all tracked chests in the storage system"
			],
			category: 'retrieval'
		},
		{
			name: 'organize',
			description: 'Organize loose items from bot inventory into storage',
			parameters: [],
			examples: [
				"Organize items in the bot's inventory into storage shulkers"
			],
			category: 'organization'
		},
		{
			name: 'consolidate',
			description: 'Consolidate partial stacks of the same item type',
			parameters: [],
			examples: [
				"Merge partially filled shulkers of the same item type"
			],
			category: 'organization'
		},
		{
			name: 'addplayer',
			description: 'Add a player to storage access control',
			parameters: [
				{ name: 'name', type: 'string', required: true, description: 'Player name to add' },
				{ name: 'role', type: 'string', required: true, description: 'Role for the player (e.g., "team", "admin")' }
			],
			examples: [
				"addplayer Steve team",
				"addplayer Alex admin"
			],
			category: 'administration'
		},
		{
			name: 'removeplayer',
			description: 'Remove a player from storage access control',
			parameters: [
				{ name: 'name', type: 'string', required: true, description: 'Player name to remove' }
			],
			examples: [
				"removeplayer Steve"
			],
			category: 'administration'
		},
		{
			name: 'players',
			description: 'List all players with storage access',
			parameters: [],
			examples: [
				"Show all players who have access to storage"
			],
			category: 'information'
		},
	];

	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
		this._operationLock = false;
			console.trace('Storage: lock RELEASED'); // prevents concurrent storage operations
		this._operationQueue = []; // queue for pending operations
	}

	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...`);


					// 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) {
						// 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) return;
							try {
								await this.cleanInventory();
							} catch (error) {
								console.error('Storage: Periodic inventory cleanup failed:', error.message);
							}
						}, cleanupInterval);
					}

					resolve();

					// Run initial hotbar restock AFTER resolve so it doesn't block
					// subsequent plugins (Ai, Navigation, etc.) from loading
					if (!this.bot.onDemand) {
						try {
							await this.restockHotbar();
						} catch (error) {
							console.error('Storage: Initial hotbar restock failed:', error.message);
						}
					}

				} 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) {
		if (!this.scanner) {
			throw new Error('Storage scanner not initialized');
		}
		if (!Database.db) {
			throw new Error('Database not initialized');
		}

		console.log(`Storage[${this.bot.name}]: Scanning storage area...`);

		// Acquire operation lock to prevent concurrent storage operations
		try {
			await this._acquireOperationLock();
		} catch (error) {
			console.log(`Storage: Operation lock timeout for scan operation`);
			throw new Error('Storage busy, scan operation timeout');
		}

		try {
			const chests = await this.scanner.discoverChests(this.bot, this.config.scanRadius || 30, Database);
			// Register as interruptible task
			this.bot.registerTask('Storage', 'scan', async () => {});
			const shulkers = await this.scanner.scanAllChests(
				this.bot, Database,
				() => this.bot.wasInterrupted()
			);
			console.log(`Storage[${this.bot.name}]: Complete - ${chests.length} chests, ${shulkers} shulkers`);

			// Capture map images and index maps if not interrupted
			if (!this.bot.wasInterrupted()) {
				await this.captureMapImages();
				await this.indexMapsFromStorage();
			}
		} finally {
			this.bot.clearTask();
			this._releaseOperationLock();
		}
	}

	/**
	 * 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) return;

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

		for (const spec of hotbarItems) {
			if (this._busy) {
				console.log('Storage: Hotbar restock interrupted — storage operation in progress');
				return;
			}

			// 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;
			}

			// Acquire lock per-item so trades can interleave
			try {
				await this._acquireOperationLock(10000);
			} catch (error) {
				console.log(`Storage: Operation lock timeout for hotbar restock`);
				return;
			}

			try {
				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}`);
			} finally {
				this._releaseOperationLock();
			}
		}

		await Database.rebuildItemIndex();
	}

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

		async handleTrade(playerName, itemsReceived) {
			console.log(`Storage[${this.bot.name}]: Processing trade from ${playerName}, received ${itemsReceived.length} item types`);

			// Caller holds the operation lock; we just process
			this._craftAvailable = true;

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

			// Quick-stash all shulker boxes to nearest chest(s)
			let shulkersStashed = 0;
			const hasShulkers = itemsReceived.some(item => item.name.includes('shulker_box'));
			if (hasShulkers) {
				console.log('Storage: Traded items include shulker boxes, quick-stashing...');
				shulkersStashed = await this.quickStashAllShulkers();
			}

			// Deposit any non-shulker items currently in inventory
			const cleanedBefore = await this.cleanInventory();

			await Database.rebuildItemIndex();
			await this.captureMapImages();

			const parts = [];
			if (shulkersStashed > 0) parts.push(`${shulkersStashed} shulker(s)`);
			if (cleanedBefore > 0) parts.push(`${cleanedBefore} loose item type(s)`);
			const msg = parts.length > 0
				? `Received ${parts.join(' + ')}. Organizing into storage now.`
				: `Trade processed.`;

			this.bot.whisper(playerName, msg);

			return { shulkersStashed, looseItemTypes: cleanedBefore, needsOrganize: shulkersStashed > 0 || cleanedBefore > 0 };
		}

	/**
	 * Batch-stash all shulker boxes from bot inventory into the nearest chest(s).
	 * Opens each chest once, moves all shulkers that fit, NBT-scans them in one pass.
	 * Does NOT synchronously unpack mixed shulkers — that's deferred to organize.
	 * Returns the count of shulkers stashed.
	 */
	async quickStashAllShulkers() {
		const shulkerItems = this.bot.bot.inventory.items().filter(item => item.name.includes('shulker_box'));
		if (shulkerItems.length === 0) return 0;

		const botPos = this.bot.bot.entity.position;
		const neededSlots = shulkerItems.length;

		const candidates = await Database.findNearestChestsWithSpace(
			neededSlots, botPos.x, botPos.y, botPos.z
		);

		if (candidates.length === 0) {
			console.log('Storage: No chests with empty slots available');
			return 0;
		}

		let stashed = 0;
		let remaining = neededSlots;

		for (const chest of candidates) {
			if (remaining <= 0) break;

			const chestPos = new Vec3(chest.pos_x, chest.pos_y, chest.pos_z);
			const batchSize = Math.min(remaining, chest.empty_slots);

			console.log(`Storage: Stashing ${batchSize} shulker(s) into chest at ${chestPos} (${chest.empty_slots} free slots)`);

			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);

			// Resolve which chest slots are actually empty right now
			const existingShulkers = await Database.getShulkersByChest(chest.id);
			const usedSlots = new Set(existingShulkers.map(s => s.slot));
			const maxSlots = window.inventoryStart; // mineflayer returns the chest boundary
			const emptySlots = [];
			for (let s = 0; s < maxSlots; s++) {
				if (!usedSlots.has(s) && !window.slots[s]) {
					emptySlots.push(s);
					if (emptySlots.length >= batchSize) break;
				}
			}

			if (emptySlots.length === 0) {
				await this.bot.bot.closeWindow(window);
				continue; // next chest
			}

			const actualBatch = Math.min(batchSize, emptySlots.length);
			const stashSlots = []; // track (chestSlot, windowSlot) for NBT scan

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

				const chestSlot = emptySlots[i];
				try {
					await this.bot.bot.moveSlotItem(srcSlot, chestSlot);
					await sleep(150);
					stashSlots.push({ chestSlot, windowSlot: srcSlot });
					stashed++;
				} catch (e) {
					console.error(`Storage: Error moving shulker to slot ${chestSlot}:`, e.message);
				}
			}

			// NBT-scan every placed shulker before closing (data is in window slots)
			for (const { chestSlot } of stashSlots) {
				const placed = window.slots[chestSlot];
				if (placed) {
					await this.scanner.scanShulkerFromNBT(this.bot, Database, chest.id, chestSlot, placed);
				}
			}

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

			remaining -= actualBatch;
			console.log(`Storage: Stashed ${actualBatch} shulker(s) into this chest (${stashed} total so far)`);
		}

		console.log(`Storage: Quick stash complete — ${stashed}/${neededSlots} shulker(s) stored`);
		return stashed;
	}

	/**
	 * 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 _acquireOperationLock(timeout = 30000) {
		const startTime = Date.now();
		while (this._operationLock) {
			if (Date.now() - startTime > timeout) {
				console.trace('Storage: lock ACQUIRE TIMEOUT'); throw new Error('Storage operation timeout');
			}
			await sleep(100);
		}
		this._operationLock = true;
			console.trace('Storage: lock ACQUIRED');
	}

	_releaseOperationLock() {
		this._operationLock = false;
	}

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

		// Interrupt any active task then acquire operation lock
		await this.bot.interruptTask(playerName);
		try {
			await this._acquireOperationLock();
		} catch (error) {
			console.log(`Storage: Operation lock timeout for ${playerName}: ${itemName} x${count}`);
			return this.bot.whisper(playerName, `Storage busy, please try again in a moment.`);
		}

		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
				console.log(`Storage: Items withdrawn for ${playerName}, initiating trade`);
				await this.initiateTradeWithPlayer(playerName);
			} else {
				this.bot.whisper(playerName, `Failed to withdraw ${itemName}.`);
			}
		} finally {
			this._busy = false;
			this._releaseOperationLock();
		}
	}

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

		// Acquire operation lock to prevent concurrent storage operations
		try {
			await this._acquireOperationLock();
		} catch (error) {
			console.log(`Storage: Operation lock timeout for ${playerName}: ${shulkerCount} shulkers of ${itemName}`);
			return this.bot.whisper(playerName, `Storage busy, please try again in a moment.`);
		}

		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
				console.log(`Storage: Shulkers withdrawn for ${playerName}, initiating trade`);
				await this.initiateTradeWithPlayer(playerName);
			} else {
				this.bot.whisper(playerName, `Failed to withdraw shulkers of ${itemName}.`);
			}
		} finally {
			this._busy = false;
			this._releaseOperationLock();
		}
	}

	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.`);
		}
	}

	/**
	 * Place withdrawn items into an already-open trade window.
	 * Called by AI trade tools for the feedback loop — the AI manages
	 * the window lifecycle; this method just handles item placement.
	 * @returns {number} Number of stacks placed
	 */
	async placeWithdrawnItemsInTrade(window, playerName) {
		const pending = this.pendingWithdrawals.get(playerName);
		if (!pending) return 0;

		const botSlots = [0, 1, 2, 3, 9, 10, 11, 12, 18, 19, 20, 21];
		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}`);
		return placed;
	}

	// ========================================
	// 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...');

		// Acquire operation lock to prevent concurrent storage operations
		try {
			await this._acquireOperationLock();
		} catch (error) {
			console.log(`Storage: Operation lock timeout for organize operation`);
			return 0;
		}

		this._craftAvailable = true; // fresh organize, allow craft retry
		this._busy = true;
		this.bot.registerTask('Storage', 'organize', async () => {});
		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) {
				if (this.bot.wasInterrupted()) {
						console.log('Storage: Organize interrupted');
						break;
					}
					// 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.bot.clearTask();
			this._busy = false;
			this._releaseOperationLock();
		}
	}

	/**
	 * 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) {
			if (this.bot.wasInterrupted()) { console.log('Storage: Consolidation interrupted'); break; }
			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
					console.log(`Storage: Special item withdrawn for ${playerName}, initiating trade`);
					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);

		if (!this.isReady || !this.scanner) {
			const msg = 'Storage system is not ready. Please wait for initialization to complete.';
			if (from === 'ai') return msg;
			this.bot.whisper(from, msg);
			return msg;
		}

		this._currentCommand = command;

		try { switch (command) {
			case 'scan':
					// Reject if another operation is already in progress
					if (this._operationLock) {
						const msg = 'Scan already in progress — wait for it to complete.';
						if (from === 'ai') return msg;
						this.bot.whisper(from, msg);
						return msg;
					}
					if (from === 'ai') {
						try {
							await this.scanArea(true);
							const stats = await Database.getStats();
							return `Scan complete! ${stats.totalChests} chests, ${stats.totalShulkers} shulkers, ${stats.totalItems} items`;
						} catch (error) {
							console.error('Storage: Scan error:', error);
							return `Scan failed: ${error.message}`;
						}
					}
					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':
				if (from === 'ai') {
					// Return result directly to AI instead of whispering
					const stats = await Database.getStats();
					return `Storage: ${stats.totalItems} items in ${stats.totalShulkers} shulkers (${stats.totalChests} chests)`;
				} else {
					await this.getStatus(from);
				}
				break;

			case 'withdraw': {
				const [itemName, count, playerName] = args;
				const actualPlayer = playerName || (from !== 'ai' ? from : null);
				if (!actualPlayer) {
					return 'Cannot withdraw items — unable to determine which player requested them.';
				}
				const parsedCount = parseInt(count) || 1;
				await this.handleWithdrawRequest(actualPlayer, itemName, parsedCount);
				break;
			}

			case 'withdraw-shulkers': {
				const [itemName, shulkerCount, playerName] = args;
				const actualPlayer = playerName || (from !== 'ai' ? from : null);
				if (!actualPlayer) {
					return 'Cannot withdraw shulkers - no player specified. Ask the player to use the /trade command instead.';
				}
				await this.handleWithdrawShulkers(actualPlayer, itemName, shulkerCount);
				break;
			}

			case 'find': {
				const [searchTerm] = args;
				const items = await this.findItem(searchTerm);
				if (items.length === 0) {
					if (from === 'ai') {
						// Return result directly to AI instead of whispering
						return `No items found matching '${searchTerm}'`;
					} else {
						this.bot.whisper(from, `No items found matching '${searchTerm}'`);
					}
				} else {
					const results = items.slice(0, 5).map(i => `${i.item_name}: ${i.total_count}`);
					const responseText = `Found: ${results.join(', ')}`;
					if (from === 'ai') {
						// Return result directly to AI instead of whispering
						return responseText;
					} else {
						this.bot.whisper(from, responseText);
					}
				}
				break;
			}

			case 'chests': {
				const chests = await Database.getChests();
				if (from === 'ai') {
					// Return result directly to AI instead of whispering
					return `Tracking ${chests.length} chests`;
				} else {
					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': {
				if (from === 'ai') return 'Cannot manage storage access via AI.'; 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': {
				if (from === 'ai') return 'Cannot manage storage access via AI.'; 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(', ');
				if (from === 'ai') {
						return `Players: ${playerList}`;
					} else {
						this.bot.whisper(from, `Players: ${playerList}`);
					}
				break;
			}

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

module.exports = Storage;
