'use strict';

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

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

class FarmSupply {
	constructor(args) {
		this.bot = args.bot;
		const settings = require('./settings/manager');
		this.config = {
			enabled: settings.get('farmSupply.enabled'),
			storageBotName: settings.get('farmSupply.storageBotName'),
		};
		this._storageBotKey = this.config.storageBotName;
		this.isAction = true;
		this._onTimeListen = null;
		this._onReadyListen = null;
		this._resupplying = false;
	}

	get storageBotName() {
		const storageBot = this.bot.constructor.bots[this._storageBotKey];
		if (storageBot && storageBot.bot && storageBot.bot.entity) {
			return storageBot.bot.entity.username;
		}
		return this._storageBotKey;
	}

	async init() {
		if (!this._storageBotKey) {
			console.log('FarmSupply: No storageBotName configured, plugin disabled');
			return false;
		}

		console.log(`FarmSupply: Initialized for bot ${this.bot.name}, storage bot: ${this._storageBotKey}`);

		this._onReadyListen = this.bot.on('onReady', async () => {
			await sleep(3000);
			let lastTimeOfDay = this.bot.bot.time.timeOfDay;

			this._onTimeListen = this.bot.bot.on('time', async () => {
				const currentTime = this.bot.bot.time.timeOfDay;
				if (lastTimeOfDay < 12000 && currentTime >= 12000 && !this._resupplying) {
					lastTimeOfDay = currentTime;
					console.log('FarmSupply: Sunset detected, starting resupply');
					try {
						await this.resupply();
					} catch (error) {
						console.error('FarmSupply: Sunset resupply error:', error);
					}
				} else {
					lastTimeOfDay = currentTime;
				}
			});
		});

		return true;
	}

	unload() {
		if (this._onReadyListen) this._onReadyListen();
		if (this._onTimeListen) this._onTimeListen = null;
		return true;
	}

	// ========================================
	// Pause / Resume farm plugins
	// ========================================

	pauseFarmPlugins() {
		const paused = [];
		for (const [name, plugin] of Object.entries(this.bot.plunginsLoaded)) {
			if (name === 'FarmSupply' || name === 'AutoEat') continue;
			if (plugin.isAction) {
				console.log(`FarmSupply: Pausing plugin ${name}`);
				try {
					plugin.unload();
				} catch (error) {
					console.error(`FarmSupply: Error pausing ${name}:`, error);
				}
				paused.push(name);
				delete this.bot.plunginsLoaded[name];
			}
		}
		return paused;
	}

	async resumeFarmPlugins(paused) {
		for (const name of paused) {
			console.log(`FarmSupply: Resuming plugin ${name}`);
			try {
				await this.bot.pluginLoad(name);
			} catch (error) {
				console.error(`FarmSupply: Error resuming ${name}:`, error);
			}
		}
	}

	// ========================================
	// Wait after trade teleport — let server tp us back and chunks load
	// ========================================

	async settleAfterTrade() {
		console.log('FarmSupply: Settling after trade teleport...');
		await sleep(3000);
		// Wait for pathfinder to be idle
		while (this.bot.bot.pathfinder.isMoving()) {
			this.bot.bot.clearControlStates();
			await sleep(500);
		}
		this.bot.bot.clearControlStates();
		await sleep(1000);
	}

	// ========================================
	// Main resupply flow
	// ========================================

	async resupply() {
		if (this._resupplying) {
			console.log('FarmSupply: Already resupplying, skipping');
			return;
		}

		this._resupplying = true;
		console.log('FarmSupply: Starting resupply...');
		const paused = this.pauseFarmPlugins();

		try {
			await this.emptyFilledBoxes();
			await this.fillEmptyShulkers();
		} catch (error) {
			console.error('FarmSupply: Resupply error:', error);
		} finally {
			// Always resume the farm and clear the flag — a stuck trade must
			// not leave the farm paused until restart
			try {
				await this.resumeFarmPlugins(paused);
			} catch (resumeError) {
				console.error('FarmSupply: Error resuming farm plugins:', resumeError);
			}
			this._resupplying = false;
			console.log('FarmSupply: Resupply complete, farm resumed.');
		}
	}

	// ========================================
	// Storage bot access
	// ========================================

	/**
	 * Bring the on-demand storage bot online (with plugins loaded) and return
	 * its Storage plugin instance. Returns null if unavailable.
	 */
	async _ensureStorageOnline() {
		const storageBot = this.bot.constructor.bots[this._storageBotKey];
		if (!storageBot) {
			console.log(`FarmSupply: Storage bot '${this._storageBotKey}' not configured`);
			return null;
		}

		if (!storageBot.isReady) {
			console.log('FarmSupply: Bringing storage bot online...');
			try {
				await Promise.race([
					storageBot.ensureConnected(async () => {}),
					sleep(60000).then(() => { throw new Error('Storage bot connect timeout'); }),
				]);
			} catch (error) {
				console.error(`FarmSupply: Could not bring storage bot online: ${error.message}`);
				return null;
			}
		}

		return storageBot.plunginsLoaded['Storage'] || null;
	}

	// ========================================
	// Empty "filled boxes" chest
	// ========================================

	async emptyFilledBoxes() {
		let filledChest;
		try {
			filledChest = this.bot.findBlockBySign('filled boxes');
		} catch (error) {
			console.log('FarmSupply: No "filled boxes" chest found, skipping');
			return;
		}
		if (!filledChest) {
			console.log('FarmSupply: No "filled boxes" chest found, skipping');
			return;
		}

		console.log('FarmSupply: Processing filled boxes chest');

		// Boxes left in inventory by a previously failed deposit go first,
		// before taking more from the chest
		const leftover = this.bot.bot.inventory.items().filter(i => i.name.includes('shulker_box'));
		if (leftover.length > 0) {
			console.log(`FarmSupply: ${leftover.length} leftover box(es) in inventory, depositing those first`);
			// Player inventory slot 9+k maps to window.inventoryStart+k
			await this.tradeDeposit(leftover.slice(0, 12).map(i => i.slot - 9));
			await this.waitForStorageBotIdle();
			await this.settleAfterTrade();
		}

		let hasMore = true;
		while (hasMore) {
			await this.bot.goToMust({ where: filledChest.position, range: 2 });
			let window = await this.bot.openContainer(this.bot.findChestBySign('filled boxes'));
			await sleep(300);

			// Track slots RELATIVE to inventoryStart — the trade window has a
			// different inventoryStart than this chest window, so absolute slot
			// numbers from here would point at the wrong items there
			const takenSlots = [];
			for (let chestSlot = 0; chestSlot < window.inventoryStart; chestSlot++) {
				if (takenSlots.length >= 12) break;
				const item = window.slots[chestSlot];
				if (!item || !item.name.includes('shulker_box')) continue;

				const destSlot = window.inventoryStart + takenSlots.length;
				if (window.slots[destSlot]) {
					console.log(`FarmSupply: Inventory slot ${destSlot} occupied, finding free slot`);
					// Find an actually empty slot
					let found = false;
					for (let freeSlot = window.inventoryStart; freeSlot < window.inventoryEnd; freeSlot++) {
						if (!window.slots[freeSlot]) {
							await this.bot.bot.moveSlotItem(chestSlot, freeSlot);
							await sleep(200);
							takenSlots.push(freeSlot - window.inventoryStart);
							found = true;
							break;
						}
					}
					if (!found) {
						console.log('FarmSupply: No empty inventory slots, stopping');
						break;
					}
				} else {
					await this.bot.bot.moveSlotItem(chestSlot, destSlot);
					await sleep(200);
					takenSlots.push(destSlot - window.inventoryStart);
				}
			}

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

			if (takenSlots.length === 0) {
				console.log('FarmSupply: No more filled shulker boxes to deposit');
				hasMore = false;
				break;
			}

			console.log(`FarmSupply: Took ${takenSlots.length} shulker boxes, initiating trade deposit`);
			await this.tradeDeposit(takenSlots);
			await this.waitForStorageBotIdle();
			await this.settleAfterTrade();
		}
	}

	// ========================================
	// Fill "empty shulkers" chest
	// ========================================

	async fillEmptyShulkers() {
		let emptyChest;
		try {
			emptyChest = this.bot.findBlockBySign('empty shulkers');
		} catch (error) {
			console.log('FarmSupply: No "empty shulkers" chest found, skipping');
			return;
		}
		if (!emptyChest) {
			console.log('FarmSupply: No "empty shulkers" chest found, skipping');
			return;
		}

		console.log('FarmSupply: Processing empty shulkers chest');

		await this.bot.goToMust({ where: emptyChest.position, range: 2 });
		let window = await this.bot.openContainer(this.bot.findChestBySign('empty shulkers'));
		await sleep(300);

		let emptySlots = 0;
		for (let chestSlot = 0; chestSlot < window.inventoryStart; chestSlot++) {
			if (!window.slots[chestSlot]) emptySlots++;
		}

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

		if (emptySlots === 0) {
			console.log('FarmSupply: Empty shulkers chest is full, skipping');
			return;
		}

		console.log(`FarmSupply: Need ${emptySlots} shulker boxes`);

		// Check current inventory for materials
		let shellsInInv = 0;
		let chestsInInv = 0;
		let shulkerBoxesInInv = 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;
			if (item.name.includes('shulker_box')) shulkerBoxesInInv += item.count;
		}

		const canCraft = Math.min(Math.floor(shellsInInv / 2), chestsInInv);
		const totalAvailable = shulkerBoxesInInv + canCraft;
		const needed = Math.min(emptySlots, 27);

		if (totalAvailable < needed) {
			const toCraft = needed - shulkerBoxesInInv;
			const shellsNeeded = Math.max(0, (toCraft * 2) - shellsInInv);
			const chestsNeeded = Math.max(0, toCraft - chestsInInv);

			if (shellsNeeded > 0) {
				console.log(`FarmSupply: Withdrawing ${shellsNeeded} shulker_shell from storage`);
				await this.tradeWithdraw('shulker_shell', shellsNeeded);
				await this.waitForStorageBotIdle();
				await this.settleAfterTrade();
			}

			if (chestsNeeded > 0) {
				console.log(`FarmSupply: Withdrawing ${chestsNeeded} chest from storage`);
				await this.tradeWithdraw('chest', chestsNeeded);
				await this.waitForStorageBotIdle();
				await this.settleAfterTrade();
			}
		}

		// Recount after possible withdrawal
		shellsInInv = 0;
		chestsInInv = 0;
		shulkerBoxesInInv = 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;
			if (item.name.includes('shulker_box')) shulkerBoxesInInv += item.count;
		}

		// Craft shulker boxes
		const craftCount = Math.min(Math.floor(shellsInInv / 2), chestsInInv, needed - shulkerBoxesInInv);
		if (craftCount > 0) {
			console.log(`FarmSupply: Crafting ${craftCount} shulker boxes`);
			for (let i = 0; i < craftCount; i++) {
				try {
					await this.craftShulkerBox();
					await sleep(300);
				} catch (error) {
					console.error('FarmSupply: Crafting error:', error);
					break;
				}
			}
		}

		// Re-find chest and dump shulker boxes
		emptyChest = this.bot.findChestBySign('empty shulkers');
		await this.bot.goToMust({ where: emptyChest.position, range: 2 });
		window = await this.bot.openContainer(emptyChest);
		await sleep(300);

		let deposited = 0;
		for (let invSlot = window.inventoryStart; invSlot < window.inventoryEnd; invSlot++) {
			const item = window.slots[invSlot];
			if (item && item.name.includes('shulker_box')) {
				let targetSlot = null;
				for (let emptyChestSlot = 0; emptyChestSlot < window.inventoryStart; emptyChestSlot++) {
					if (!window.slots[emptyChestSlot]) {
						targetSlot = emptyChestSlot;
						break;
					}
				}
				if (targetSlot === null) break;

				try {
					await this.bot.bot.moveSlotItem(invSlot, targetSlot);
					await sleep(200);
					deposited++;
				} catch (error) {
					console.error('FarmSupply: Error depositing shulker box:', error);
				}
			}
		}

		await this.bot.bot.closeWindow(window);
		console.log(`FarmSupply: Deposited ${deposited} shulker boxes into empty shulkers chest`);
	}

	// ========================================
	// Trade: Withdraw items from storage bot
	// ========================================

	async tradeWithdraw(itemName, count) {
		const storageName = this.storageBotName;
		console.log(`FarmSupply: Requesting ${count}x ${itemName} from ${storageName}`);

		// Both bots run in this process — call the storage plugin directly
		// instead of whispering a chat command (which raced against the storage
		// bot's command lock and required the bot to already be online)
		const storage = await this._ensureStorageOnline();
		if (!storage) throw new Error(`Storage bot unavailable for ${itemName} withdraw`);

		// Listen for the "ready — sending trade request" whisper the storage
		// bot sends right before initiating /trade with us
		let cleanupWhisper = () => {};
		const whisperPromise = new Promise((resolve, reject) => {
			const timeout = setTimeout(() => {
				cleanupWhisper();
				reject(new Error(`Timeout waiting for ${storageName} to prepare ${itemName}`));
			}, 180000);
			const onWhisper = (from, message) => {
				if (from !== storageName) return;
				if (message.includes('ready — sending trade')) {
					cleanupWhisper();
					resolve();
				} else if (message.includes('Item not found') || message.includes('Failed to withdraw')
					|| message.includes('Cannot find') || message.includes('Storage busy')) {
					cleanupWhisper();
					reject(new Error(`Storage bot: ${message}`));
				}
			};
			cleanupWhisper = () => {
				this.bot.bot.removeListener('whisper', onWhisper);
				clearTimeout(timeout);
			};
			this.bot.bot.on('whisper', onWhisper);
		});

		// Fire the withdraw — it pulls items from shulkers (can take minutes),
		// then trades with us. Don't await yet: we have to accept its trade
		// for it to complete.
		const myUsername = this.bot.bot.entity.username;
		const requestPromise = storage.handleWithdrawRequest(myUsername, itemName, count)
			.catch(error => console.error(`FarmSupply: Storage withdraw error: ${error.message}`));

		try {
			await whisperPromise;
		} catch (error) {
			await requestPromise;
			throw error;
		}

		// Poll /trade accept until the trade window opens.
		// Set up windowOpen listener BEFORE chat to avoid missing the event.
		let window = null;
		const tradeStart = Date.now();
		while (!window && (Date.now() - tradeStart) < 45000) {
			const winPromise = this.bot.once('windowOpen');
			await this.bot.bot.chat('/trade accept');
			window = await Promise.race([winPromise, sleep(2000).then(() => null)]);
		}
		if (!window) throw new Error(`Trade window did not open with ${storageName}`);

		// Click 1: lock our items — single left-click (moveSlotItem's
		// pickup+putdown pair desyncs on the cancelled GUI slot and trips
		// anti-cheat)
		await sleep(500);
		try { await this.bot.bot.clickWindow(37, 0, 0); } catch (e) { /* ignore */ }
		console.log('FarmSupply: Trade click 1 — items locked');

		// Wait for storage bot to lock (slot 53 turns lime_dye), bounded —
		// an unbounded poll here used to hang resupply forever on a dead trade
		const locked = await this._waitForTradeLock(window, 120000);
		if (!locked) {
			try { this.bot.bot.closeWindow(window); } catch (e) { /* ignore */ }
			await requestPromise;
			throw new Error(`${storageName} never confirmed the ${itemName} trade`);
		}

		// Click 2: final confirmation
		if (this.bot.bot.currentWindow === window) {
			try { await this.bot.bot.clickWindow(37, 0, 0); } catch (e) { /* ignore */ }
			console.log('FarmSupply: Trade click 2 — final confirm');
		}

		// Wait for trade to complete
		await Promise.race([
			this.bot.once('windowClose'),
			sleep(120000),
		]);

		await requestPromise;
		await sleep(500);
		console.log(`FarmSupply: Withdraw trade complete for ${itemName}`);
	}

	/**
	 * Poll the trade window for the other party's lock indicator (slot 53
	 * turning lime_dye). Resolves true when locked, false on timeout or if
	 * the window closes.
	 */
	async _waitForTradeLock(window, timeoutMs) {
		const start = Date.now();
		while (Date.now() - start < timeoutMs) {
			try {
				const indicator = window.slots[53];
				if (indicator && indicator.name === 'lime_dye') {
					console.log('FarmSupply: Storage bot has locked');
					return true;
				}
			} catch (e) {
				return false; // window closed
			}
			if (this.bot.bot.currentWindow !== window) return false;
			await sleep(500);
		}
		return false;
	}

	// ========================================
	// Trade: Deposit items to storage bot
	// ========================================

	async tradeDeposit(slotsToTrade) {
		console.log(`FarmSupply: Depositing ${slotsToTrade.length} shulker boxes to ${this.storageBotName}`);

		// Storage bot is on-demand — it must be online to accept the trade
		const storage = await this._ensureStorageOnline();
		if (!storage) throw new Error('Storage bot unavailable for deposit');

		const windowPromise = this.bot.once('windowOpen');
		await this.bot.say(`/trade ${this.storageBotName}`);
		let window = await Promise.race([
			windowPromise,
			sleep(45000).then(() => null),
		]);
		if (!window) throw new Error(`Trade window with ${this.storageBotName} never opened`);

		// slotsToTrade holds inventory-relative indices — offset them into
		// this window's inventory section
		let placed = 0;
		for (const tradeSlot of botSlots) {
			if (placed >= slotsToTrade.length) break;
			try {
				await this.bot.bot.moveSlotItem(window.inventoryStart + slotsToTrade[placed], tradeSlot);
				await sleep(200);
				placed++;
			} catch (error) {
				console.error('FarmSupply: Error placing item in trade:', error);
			}
		}

		console.log(`FarmSupply: Placed ${placed} shulker boxes in trade window`);

		// Click 1: lock items (green wool — first confirmation, single click)
		try {
			await this.bot.bot.clickWindow(37, 0, 0);
			console.log('FarmSupply: Trade click 1 — items locked');
		} catch (error) {
			console.error('FarmSupply: Error on trade click 1:', error);
		}

		// Wait for storage bot to lock (slot 53 turns lime_dye from grey_dye)
		const locked = await this._waitForTradeLock(window, 120000);
		if (!locked) {
			try { this.bot.bot.closeWindow(window); } catch (e) { /* ignore */ }
			throw new Error('Storage bot never confirmed the deposit trade');
		}

		// Click 2: final confirmation (green wool — second click, both locked)
		try {
			if (this.bot.bot.currentWindow === window) {
				await this.bot.bot.clickWindow(37, 0, 0);
				console.log('FarmSupply: Trade click 2 — final confirm');
			}
		} catch (error) {
			console.error('FarmSupply: Error on trade click 2:', error);
		}

		try {
			await Promise.race([
				this.bot.once('windowClose'),
				sleep(30000),
			]);
		} catch (error) {
			console.error('FarmSupply: Trade deposit timeout or error:', error);
		}

		await sleep(500);
		console.log(`FarmSupply: Deposit trade complete`);
	}

	// ========================================
	// Wait for storage bot to finish processing
	// ========================================

	async waitForStorageBotIdle() {
		const storageBot = this.bot.constructor.bots[this._storageBotKey];
		if (!storageBot) return;

		const storage = storageBot.plunginsLoaded['Storage'];
		if (!storage) return;

		console.log('FarmSupply: Waiting for storage bot to finish processing...');
		const maxWait = 300000;
		const start = Date.now();
		const isBusy = () => storage._busy || storage._operationLock;

		while (Date.now() - start < maxWait) {
			if (!isBusy()) {
				// There's a short gap between the trade handler releasing the
				// lock and the post-trade organize grabbing it — require the
				// bot to stay idle across a re-check before trusting it
				await sleep(3000);
				if (!isBusy()) break;
			}
			await sleep(2000);
		}

		if (isBusy()) {
			console.log('FarmSupply: Storage bot still busy after timeout, continuing anyway');
		} else {
			console.log('FarmSupply: Storage bot idle');
		}
		await sleep(1000);
	}

	// ========================================
	// Craft a shulker box
	// ========================================

	async craftShulkerBox() {
		let craftingTable;
		try {
			const signBlock = this.bot.findBlockBySign('crafting table');
			if (signBlock) {
				craftingTable = this.bot.bot.findBlock({
					point: signBlock.position,
					matching: this.bot.mcData.blocksByName.crafting_table.id,
					maxDistance: 4,
				});
			}
		} catch (error) {
			// No sign, search nearby
		}

		if (!craftingTable) {
			craftingTable = this.bot.bot.findBlock({
				matching: this.bot.mcData.blocksByName.crafting_table.id,
				maxDistance: 64,
			});
		}

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

		await this.bot.goToMust({ where: craftingTable.position, range: 2 });

		const recipe = this.bot.bot.recipesAll(
			this.bot.mcData.itemsByName.shulker_box.id,
			null,
			craftingTable
		)[0];

		if (!recipe) {
			throw new Error('FarmSupply: No recipe found for shulker box');
		}

		const window = await this.bot.openCraftingTable(craftingTable);
		const inventory = window.slots.slice(window.inventoryStart, window.inventoryEnd);

		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 (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(`FarmSupply: Missing ingredient type ${typeId} in inventory`);
			}
			const actualSlot = window.inventoryStart + invIdx;

			await this.bot.bot.clickWindow(actualSlot, 0, 0);
			await sleep(100);

			for (const gridSlot of gridSlots) {
				await this.bot.bot.clickWindow(gridSlot, 1, 0);
				await sleep(100);
			}

			await this.bot.bot.clickWindow(actualSlot, 0, 0);
			await sleep(100);
		}

		await sleep(500);

		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(300);
		console.log('FarmSupply: Crafted a shulker box');
	}
}

FarmSupply.getStatus = function(instance) {
	return {
		active: true,
		storageBotName: instance._storageBotKey,
		resupplying: instance._resupplying,
	};
};

module.exports = FarmSupply;
