'use strict';

const Vec3 = require('vec3');
const { sleep } = require('../../utils');

class Scanner {
	constructor() {
		this.chestBlockType = null;
	}

	async discoverChests(bot, radius, database) {
		if (!this.chestBlockType) {
			this.chestBlockType = bot.mcData.blocksByName.chest?.id;
			if (!this.chestBlockType) {
				throw new Error('Chest block not found in minecraft-data');
			}
		}

		this._scanRadius = radius;
		console.log(`Scanner: Discovering chests within ${radius} blocks...`);
		const chestPositions = bot.bot.findBlocks({
			matching: this.chestBlockType,
			maxDistance: radius,
			count: Infinity,
		});

		console.log(`Scanner: Found ${chestPositions.length} chest block(s)`);

		const discoveredChests = [];
		const processed = new Set();

		for (const pos of chestPositions) {
			const key = `${pos.x},${pos.y},${pos.z}`;
			if (processed.has(key)) continue;
			processed.add(key);

			const chestInfo = this.detectChestType(bot, pos);

			// Skip the second half of double chests
			if (chestInfo.type === 'skip') {
				continue;
			}

			const rowColumn = this.assignRowColumn(pos);
			const category = this.columnToCategory(rowColumn.column);

			await database.upsertChest(
				pos.x, pos.y, pos.z,
				chestInfo.type,
				rowColumn.row,
				rowColumn.column,
				category
			);

			discoveredChests.push({
				x: pos.x, y: pos.y, z: pos.z,
				type: chestInfo.type,
				...rowColumn,
				category
			});
		}

		// Remove DB records for chest positions no longer discovered
		// (e.g., the old canonical half of a double chest that switched sides)
		if (discoveredChests.length > 0) {
			await database.deleteOrphanChests(discoveredChests);
		}
		console.log(`Scanner: Discovered/updated ${discoveredChests.length} chest(s)`);
		return discoveredChests;
	}

	detectChestType(bot, position) {
		const block = bot.bot.blockAt(position);
		if (!block) return { type: 'single' };

		// Use block state properties (Minecraft 1.13+ has type: single/left/right)
		const props = typeof block.getProperties === 'function' ? block.getProperties() : null;
		if (props && props.type) {
			if (props.type === 'single') return { type: 'single' };
			// Register the 'left' half as canonical, skip 'right'
			if (props.type === 'left') return { type: 'double' };
			return { type: 'skip' }; // 'right' half
		}

		// Fallback: adjacency check for older versions
		const directions = [
			new Vec3(1, 0, 0),
			new Vec3(-1, 0, 0),
			new Vec3(0, 0, 1),
			new Vec3(0, 0, -1)
		];

		for (const dir of directions) {
			const adjacentPos = position.offset(dir.x, dir.y, dir.z);
			const adjacentBlock = bot.bot.blockAt(adjacentPos);

			if (adjacentBlock && adjacentBlock.name === 'chest') {
				if (dir.x === -1 || dir.z === -1) {
					return { type: 'double' };
				}
				return { type: 'skip' };
			}
		}

		return { type: 'single' };
	}

	assignRowColumn(position) {
		const row = Math.floor(position.y / 4) + 1;
		const column = position.x;
		return { row, column };
	}

	columnToCategory(column) {
		if (column <= 1) return 'minerals';
		if (column === 2) return 'food';
		if (column === 3) return 'tools';
		if (column === 4) return 'armor';
		if (column === 5) return 'blocks';
		if (column === 6) return 'redstone';
		return 'misc';
	}

	async scanChest(bot, database, chestPosition) {
		console.log(`Scanner: Scanning chest at ${chestPosition.x},${chestPosition.y},${chestPosition.z}`);

		try {
			// Ensure bot is close enough to interact
			const distance = bot.bot.entity.position.distanceTo(chestPosition);
			if (distance > 4) {
				await bot.goTo({ where: chestPosition, range: 3 });
			}

			const chestBlock = bot.bot.blockAt(chestPosition);
			if (!chestBlock || !chestBlock.name.includes('chest')) {
				console.log(`Scanner: Not a chest at ${chestPosition.x},${chestPosition.y},${chestPosition.z}`);
				return 0;
			}

			// Get chest from database
			const chest = await database.getChestByPosition(chestPosition.x, chestPosition.y, chestPosition.z);
			if (!chest) {
				console.log(`Scanner: Chest not in database`);
				return 0;
			}

			const window = await bot.openContainer(chestBlock);
			const slots = window.slots;
			let shulkerCount = 0;

			// Only scan chest inventory slots (not player inventory)
			const chestSlotCount = window.inventoryStart || 27;
			console.log(`Scanner: Chest has ${chestSlotCount} slots`);

			// Correct DB chest_type if it doesn't match the actual window size
			const actualType = chestSlotCount > 27 ? 'double' : 'single';
			if (chest.chest_type !== actualType) {
				console.log(`Scanner: Correcting chest type: DB says '${chest.chest_type}', actual is '${actualType}'`);
				await database.upsertChest(
					chestPosition.x, chestPosition.y, chestPosition.z,
					actualType, chest.row, chest.column, chest.category
				);
			}

			// Clear previous loose item records before re-scanning
			await database.clearLooseItems(chest.id);

			const looseItems = [];
			for (let i = 0; i < chestSlotCount; i++) {
				const slot = slots[i];
				if (!slot) continue;

				if (slot.name.includes('shulker_box')) {
					console.log(`Scanner: Found shulker at slot ${i}: ${slot.name}`);
					await this.scanShulkerFromNBT(bot, database, chest.id, i, slot);
					shulkerCount++;
				} else {
					looseItems.push({ slot: i, name: slot.name, id: slot.type, count: slot.count });
				}
			}

			if (looseItems.length > 0) {
				await database.batchUpsertLooseItems(chest.id, looseItems);
			}

			await bot.bot.closeWindow(window);
			console.log(`Scanner: Found ${shulkerCount} shulkers in chest`);
			return shulkerCount;

		} catch (error) {
			console.error(`Scanner: Error scanning chest at ${chestPosition.x},${chestPosition.y},${chestPosition.z}:`, error);
			return 0;
		}
	}

	async scanAllChests(bot, database) {
		const chests = await database.getChests();
		console.log(`Scanner: Scanning all ${chests.length} tracked chests`);

		let totalShulkers = 0;
		let scannedCount = 0;
		let skippedCount = 0;

		// Track scanned positions so we don't re-scan or re-queue
		const scannedPositions = new Set();

		// Visit chests in nearest-neighbor order to minimize travel
		const remaining = chests.map(c => ({ ...c, pos: new Vec3(c.pos_x, c.pos_y, c.pos_z) }));
		for (const c of remaining) {
			scannedPositions.add(`${c.pos.x},${c.pos.y},${c.pos.z}`);
		}

		while (remaining.length > 0) {
			const botPos = bot.bot.entity.position;

			// Find the closest unscanned chest
			let closestIdx = 0;
			let closestDist = botPos.distanceTo(remaining[0].pos);
			for (let i = 1; i < remaining.length; i++) {
				const dist = botPos.distanceTo(remaining[i].pos);
				if (dist < closestDist) {
					closestDist = dist;
					closestIdx = i;
				}
			}

			const chest = remaining.splice(closestIdx, 1)[0];

			if (closestDist > 4.5) {
				console.log(`Scanner: Walking to chest at ${chest.pos} (distance: ${closestDist.toFixed(1)})`);
				try {
					const reached = await bot.goTo({
						where: chest.pos,
						range: 3,
					});
					if (reached === false) {
						console.log(`Scanner: Could not reach chest at ${chest.pos}: no path`);
						skippedCount++;
						continue;
					}
				} catch (error) {
					console.log(`Scanner: Could not reach chest at ${chest.pos}: ${error.message}`);
					skippedCount++;
					continue;
				}
			}

			// Wait for anti-ESP to reveal nearby blocks after arriving
			await sleep(250);

			// Discover any new chests now visible from this position (every 5th stop or first)
			if (scannedCount % 5 === 0) {
				const newChests = await this.discoverChests(bot, this._scanRadius || 30, database);
				for (const nc of newChests) {
					const key = `${nc.x},${nc.y},${nc.z}`;
					if (!scannedPositions.has(key)) {
						scannedPositions.add(key);
						remaining.push({ ...nc, pos: new Vec3(nc.x, nc.y, nc.z) });
						console.log(`Scanner: Discovered new chest at ${nc.x},${nc.y},${nc.z} while walking`);
					}
				}
			}

			const shulkerCount = await this.scanChest(bot, database, chest.pos);
			totalShulkers += shulkerCount;
			scannedCount++;

			if (scannedCount % 10 === 0) {
				console.log(`Scanner: Progress - ${scannedCount}/${scannedCount + remaining.length} chests scanned, ${totalShulkers} shulkers found`);
			}

		}

		await database.rebuildItemIndex();
		console.log(`Scanner: Scanned ${scannedCount} chests, skipped ${skippedCount}, found ${totalShulkers} shulkers`);
		return totalShulkers;
	}

	// Read shulker contents from NBT data (no physical interaction needed)
	async scanShulkerFromNBT(bot, database, chestId, chestSlot, shulkerItem) {
		console.log(`Scanner: Reading shulker NBT at slot ${chestSlot}`);

		try {
			// Create/update shulker record and get its ID in one call
			const shulkerRecord = await database.upsertAndGetShulker(
				chestId,
				chestSlot,
				shulkerItem.name,
				null // category will be set based on contents
			);
			if (!shulkerRecord) {
				console.error(`Scanner: No shulker record found for chest ${chestId} slot ${chestSlot}`);
				return [];
			}
			const shulkerId = shulkerRecord.id;

			await database.clearShulkerItems(shulkerId);

			// Extract items from shulker NBT
			const items = this.extractShulkerContents(bot, shulkerItem);

			let totalItems = 0;
			const itemTypes = new Set();

			await database.batchUpsertShulkerItems(shulkerId, items);

			for (const item of items) {
				totalItems += item.count;
				itemTypes.add(item.name);
			}

			// Update shulker stats
			let itemFocus = itemTypes.size === 1 ? Array.from(itemTypes)[0] : null;
			const usedSlots = items.length;

			// If any item in the shulker is special, append #special to the focus
			if (itemFocus) {
				const hasSpecial = items.some(item => Scanner.isSpecialItem(item.nbt));
				if (hasSpecial) {
					itemFocus = itemFocus + '#special';
				}
			}

			await database.updateShulkerCounts(shulkerId, usedSlots, totalItems);

			await database.updateShulkerItemFocus(shulkerId, itemFocus);

			console.log(`Scanner: Shulker has ${items.length} slot(s) used, ${totalItems} total items`);
			return items;

		} catch (error) {
			console.error(`Scanner: Error reading shulker NBT:`, error);
			return [];
		}
	}

	// Extract items from shulker box NBT data
	extractShulkerContents(bot, shulkerItem) {
		const items = [];

		if (!shulkerItem.nbt) {
			console.log('Scanner: Shulker has no NBT data (empty)');
			return items;
		}

		try {
			// Navigate the NBT structure to find Items array
			// Structure is: nbt.value.BlockEntityTag.value.Items.value.value (array)
			let nbtItems = null;
			const nbt = shulkerItem.nbt;

			// Try multiple paths to find the items array
			const paths = [
				() => nbt.value?.BlockEntityTag?.value?.Items?.value?.value,  // Full nested path
				() => nbt.value?.BlockEntityTag?.value?.Items?.value,          // One less nesting
				() => nbt.BlockEntityTag?.Items?.value?.value,                 // Without top value
				() => nbt.BlockEntityTag?.Items?.value,                        // Simpler
				() => nbt.BlockEntityTag?.Items,                               // Direct
				() => nbt.value?.Items?.value?.value,                          // No BlockEntityTag
				() => nbt.Items?.value?.value,                                 // Even simpler
				() => nbt.Items,                                               // Direct Items
			];

			for (const pathFn of paths) {
				const result = pathFn();
				if (Array.isArray(result)) {
					nbtItems = result;
					break;
				}
			}

			if (!nbtItems || !Array.isArray(nbtItems)) {
				console.log('Scanner: No items array found in shulker (may be empty)');
				return items;
			}

			console.log(`Scanner: Found ${nbtItems.length} items in shulker NBT`);

			for (const nbtItem of nbtItems) {
				// Extract slot, id, count from NBT item
				const slot = nbtItem.Slot?.value ?? nbtItem.Slot ?? 0;
				const id = nbtItem.id?.value ?? nbtItem.id ?? 'unknown';
				const count = nbtItem.Count?.value ?? nbtItem.Count ?? 1;

				// Clean up the id (remove minecraft: prefix)
				const cleanId = String(id).replace('minecraft:', '');

				if (count <= 0 || cleanId === 'air') continue;

				// tag may be a prismarine-nbt compound or a plain object
				const tag = nbtItem.tag?.value ?? nbtItem.tag ?? null;
				items.push({
					slot: slot,
					name: cleanId,
					id: bot.mcData.itemsByName[cleanId]?.id || 0,
					count: count,
					nbt: tag ? this.parseNBT(tag) : null
				});
			}
		} catch (error) {
			console.error('Scanner: Error parsing shulker NBT:', error);
			console.log('Scanner: Raw NBT:', JSON.stringify(shulkerItem.nbt).substring(0, 500));
		}

		return items;
	}

	// Recursively unwrap prismarine-nbt {type, value} structures into plain objects
	simplifyNBT(nbt) {
		if (nbt === null || nbt === undefined) return nbt;
		if (typeof nbt !== 'object') return nbt;

		// prismarine-nbt compound/value wrapper
		if (nbt.type !== undefined && nbt.value !== undefined) {
			return this.simplifyNBT(nbt.value);
		}

		if (Array.isArray(nbt)) {
			return nbt.map(v => this.simplifyNBT(v));
		}

		const out = {};
		for (const key of Object.keys(nbt)) {
			out[key] = this.simplifyNBT(nbt[key]);
		}
		return out;
	}

	parseNBT(nbt) {
		if (!nbt) return null;
		if (typeof nbt === 'string') {
			try {
				nbt = JSON.parse(nbt);
			} catch (e) {
				return null;
			}
		}

		// Unwrap prismarine-nbt wrappers so we can access keys directly
		nbt = this.simplifyNBT(nbt);

		const result = {};

		if (nbt.Enchantments) {
			let enchList = nbt.Enchantments;
			if (Array.isArray(enchList)) {
				result.enchantments = enchList.map(e => ({
					id: e.id,
					level: e.lvl
				}));
			}
		}

		if (nbt.Damage) {
			result.damage = nbt.Damage;
		}

		if (nbt.display?.Name) {
			const name = nbt.display.Name;
			if (typeof name === 'string') {
				try { result.displayName = JSON.parse(name).text || name; } catch (e) { result.displayName = name; }
			} else {
				result.displayName = name?.text || String(name);
			}
		}

		if (nbt.display?.Lore) {
			let lore = nbt.display.Lore;
			if (!Array.isArray(lore)) lore = [lore];
			result.lore = lore.map(l => {
				if (typeof l === 'string') {
					try { return JSON.parse(l).text || l; } catch (e) { return l; }
				}
				return l?.text || String(l);
			});
		}

		if (nbt.CustomModelData) {
			result.customModelData = nbt.CustomModelData;
		}

		if (nbt.RepairCost) {
			result.repairCost = nbt.RepairCost;
		}

		return Object.keys(result).length > 0 ? result : null;
	}

	/**
	 * Check if parsed NBT data indicates a "special" item — one with a custom
	 * display name, lore, or custom model data that should be stored separately.
	 */
	static isSpecialItem(nbtData) {
		if (!nbtData) return false;
		if (typeof nbtData === 'string') {
			try { nbtData = JSON.parse(nbtData); } catch (e) { return false; }
		}
		return !!(nbtData.displayName || nbtData.lore || nbtData.customModelData);
	}
}

module.exports = Scanner;