'use strict';

const conf = require('../conf');
const {sleep} = require('../utils');
const { ProviderFactory } = require('./ai/providers');
const memoryDB = require('./ai/memory-db');


class Ai{
	constructor(args){
		this.bot = args.bot;
		this.promptName = args.promptName;
		this.prompCustom = args.prompCustom || '';
		// interval takes precedence over intervalLength (both are valid config names)
		this.intervalLength = args.interval || args.intervalLength || 30;
		this.intervalStop;
		this.messageListener;
		this.provider = null;
		this.memoryDB = memoryDB;
		this._allTools = [];
		this._lastSentMessages = []; // prevent duplicate chat spam (LRU queue)
		this._active = false;              // AI is initialized and listening
		this._consecutiveFailures = 0;     // track API failures for backoff
		this._backoffUntil = 0;            // suppress calls until this timestamp
		this._messages = null;             // messages array reference (set in init)
		this._polling = false;             // prevent concurrent poll cycles
		this._pollTimer = null;            // interval ref for polling

		// Trade feedback loop
		this._tradeWindow = null;
		this._tradeWindowState = null;
		this._expectingTradeWindow = false; // set by trade.js before auto-accept

		// Bot-specific AI config (overrides global config)
		// When loaded via config, args contains provider, model, baseUrl, etc. directly
		// When loaded via /ai command, only promptName/prompCustom are passed
		const { bot, promptName, prompCustom, intervalLength, interval, ...configProps } = args;
		this.botConfig = args.botConfig || configProps || {};
	}

	// Get merged config: bot-specific settings override global settings
	__getConfig() {
		return {
			...conf.ai,  // Global defaults
			...this.botConfig,  // Bot-specific overrides
		};
	}

	async init(){
		// If bot is already ready, complete setup immediately and await it.
		// Otherwise, queue for when the bot becomes ready.
		if (this.bot.isReady) {
			await this._completeSetup();
		} else {
			this.bot.on('onReady', () => this._completeSetup());
		}
	}

	async _completeSetup() {
		try {
			await this.start();
			this._messages = [];
			this._active = true;

			this.messageListener = this.bot.on('message', (message, type)=>{
				if(type === 'game_info') return;
				const msgText = message.toString();
				if(msgText.startsWith('<')){
					const firstBracket = msgText.split('>')[0];
					// Extract username from <[lvl] username> or <username> format
					const userMatch = firstBracket.match(/^<\[.*?\]\s*(\w+)>$|^<(\w+)>$/);
					if(userMatch){
						const speakerName = userMatch[1] || userMatch[2];
						if(speakerName === this.bot.bot.entity.username){
							console.log('message blocked from message array')
							return;
						}
					}
				}
				console.log(`Message ${type}: ${message.toString()}`)
				// Add timestamp to message for time awareness
				const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
				this._messages.push({
					type: 'message',
					text: message.toString(),
					timestamp: timestamp,
					timeAgo: this.getTimeAgo(timestamp)
				});
				// Ensure poll timer is running
				this._ensurePolling();
			});

			// Monitor trade windows (even auto-accepted ones) for feedback loop
			this.bot.bot.on('windowOpen', (window) => {
				// Only fires when trade.js signals an expected trade — avoids
				// false positives from scanner opening chests (also 54+ slots).
				if (!this._tradeWindow && this._expectingTradeWindow && window.slots && window.slots.length >= 54) {
					this._expectingTradeWindow = false;
					console.log('AI: Trade window detected, setting up feedback loop');
					this._setupTradeWindow(window, 'auto-accepted');
					this._ensurePolling();
				}
			});

			console.log(`${this.bot.name} AI ready — waiting for chat activity`);

		} catch(error) {
			console.error(`${this.bot.name} AI setup failed:`, error);
			throw error;
		}
	}

	async unload(){
		if(this._pollTimer){
			clearInterval(this._pollTimer);
			this._pollTimer = null;
		}
		if(this.messageListener){
			this.messageListener();
		}
		if(this.provider){
			await this.provider.close();
		}
		this._active = false;
		return true;
	}

	// ---- Simple interval polling ----

	_ensurePolling() {
		if (this._pollTimer) return;
		const intervalMs = (this.intervalLength || 5) * 1000;
		this._pollTimer = setInterval(() => this._pollCycle(), intervalMs);
	}

	// ---- Main polling cycle ----

	async _pollCycle() {
		// Respect backoff after errors
		if (Date.now() < this._backoffUntil) return;
		// Prevent concurrent cycles
		if (this._polling) return;
		this._polling = true;

		try {

			// Snapshot messages so new arrivals during processing aren't lost
			const currentMessages = [...this._messages];
			// Reset for the next accumulation window
			this._messages = [];

			// Skip API call if there's no real data
			const hasRealData = currentMessages.some(m => typeof m === 'object' && m.text);
			const hasTradeWindow = !!this._tradeWindowState;
			if (!hasRealData && !hasTradeWindow) return;

			let result;
			try{
				const currentTime = new Date();
				const requestData = {
					messages: currentMessages,
					currentTime: currentTime.toISOString().replace('T', ' ').substring(0, 19),
					timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
					tradeWindow: this._tradeWindowState,
				};
				result = await this.chat(JSON.stringify(requestData));
			}catch(error){
				console.log('error AI API', error);
				// Exponential backoff on failure — don't hammer an overloaded server
				this._consecutiveFailures++;
				const backoffMs = Math.min(1000 * Math.pow(2, this._consecutiveFailures), 30000);
				this._backoffUntil = Date.now() + backoffMs;
				console.log(`AI backoff: ${backoffMs}ms (failure #${this._consecutiveFailures}), until ${new Date(this._backoffUntil).toISOString()}`);
				return;
			}

			// Success — reset failure tracking
			this._consecutiveFailures = 0;
			this._backoffUntil = 0;

			try{
				// Determine the requesting player from chat context
				const requestingPlayer = this.getLastSpeaker(currentMessages);

				// Check for tool calls first
				const toolCalls = this.getToolCalls(result);
				if (toolCalls && toolCalls.length > 0) {
					// Deduplicate tool calls
					const seen = new Set();
					const uniqueCalls = toolCalls.filter(tc => {
						const key = `${tc.name || tc.function?.name}:${JSON.stringify(tc.args || tc.arguments || {})}`;
						if (seen.has(key)) { console.log(`Deduplicating duplicate tool call: ${key}`); return false; }
						seen.add(key);
						return true;
					});
					console.log(`Tool calls from AI: ${toolCalls.length} raw, ${uniqueCalls.length} after dedup — ${uniqueCalls.map(c => c.name || c.function?.name).join(', ')}`);
					// Execute tool calls and get results
					const toolResults = [];
					for (const toolCall of uniqueCalls) {
						try {
							const toolResult = await this._executeTool(
								toolCall.name || toolCall.function?.name,
								toolCall.args || toolCall.arguments || {},
								requestingPlayer
							);
							toolResults.push({
								name: toolCall.name || toolCall.function?.name,
								result: toolResult,
								success: true
							});
						} catch (execError) {
							console.error('Tool execution error:', execError);
							toolResults.push({
								name: toolCall.name || toolCall.function?.name,
								error: execError.message,
								success: false
							});
						}
					}

					// Send tool results back to AI for natural language response
					if (toolResults.length > 0) {
						const toolResultMessage = JSON.stringify({
							toolResults: toolResults,
							tradeWindow: this._tradeWindowState,
							instruction: 'Tool results above. Respond with ONE brief message (max 150 chars) in the JSON array format. Be short and direct - do not narrate what happened.'
						});

						try {
							const followupResult = await this.chat(toolResultMessage);
							const responseText = this.provider.getResponse(followupResult);
							await this.processResponse(responseText);
						} catch (followupError) {
							console.error('Error generating followup response:', followupError);
						}
					}
					return;
				}

				// No tool calls, process as normal chat response
				const responseText = this.provider.getResponse(result);
				await this.processResponse(responseText);

			}catch(error){
				console.log('Error in AI message loop', error, result);
				try {
					if(result && this.provider.getResponse(result)){
						console.log(this.provider.getResponse(result))
					}
				} catch(e) {
					// Ignore
				}
			}
		} finally {
			this._polling = false;
		}
	}

	async start(history){
		const config = this.__getConfig();
		console.log(`${this.bot.name} AI config:`, {
			provider: config.provider,
			model: config.model,
			promptName: this.promptName,
			baseUrl: config.baseUrl,
			maxOutputTokens: config.maxOutputTokens,
			interval: config.interval,
		});

		// Let the AI control trade decisions (disable auto-accept in minecraft.js)
		this.bot._aiControlsTrade = true;

		// Initialize memory database
		await this.memoryDB.initialize('./storage/ai-memory.db', this.bot.name);
		console.log(`${this.bot.name} AI memory database initialized`);

		// Get memory context for prompt (directives + general memories)
		const memoryContext = await this.memoryDB.getMemoryContext();

		// Get player-specific memories for currently online players
		const onlinePlayers = Object.values(this.bot.getPlayers()).map(player => player.username);
		const playerMemoryContext = await this.memoryDB.getPlayerMemoriesForPrompt(onlinePlayers);

		// Combine memory contexts
		const fullMemoryContext = [memoryContext, playerMemoryContext].filter(Boolean).join('\n\n');

		// Get current time info
		const timeInfo = this.getCurrentTimeInfo();

		// Build consolidated tool registry
		this._buildAllTools();
		const toolsDocs = this._getToolsDocumentation();

		const prompt = conf.ai.prompts[this.promptName](
			this.bot.bot.entity.username,
			config.interval,
			Object.values(this.bot.getPlayers()).map(player=>`<[${player.lvl}] ${player.username}>`).join('\n'),
			toolsDocs,
			fullMemoryContext,
			timeInfo,
			this.prompCustom,
		);

		// Create the provider instance with merged config and prompt
		this.provider = ProviderFactory.create({
			...config,
			prompt: prompt,
		});

		if (this.provider.supportsTools && this.provider.supportsTools()) {
			this.provider.setTools(this._getToolsSchema());
			console.log(`${this.bot.name} AI tools configured: ${this._allTools.length} tools available`);
		}

		await this.provider.start(history);
		console.log(`${this.bot.name} AI ${config.provider} provider started (model: ${config.model})`);
	}

	async chat(message, retryCount=0){
		console.log(`chat ${this.bot.name}`, retryCount)
		try{
			let result = await this.provider.chat(message);
			return result
		}catch(error){
			console.log('AI chat error', error)
			throw error;
		}
	}

	/**
	 * Extract tool calls from provider response
	 */
	getToolCalls(result) {
		// Gemini: functionCalls() method
		if (result.response && typeof result.response.functionCalls === 'function') {
			const calls = result.response.functionCalls();
			if (calls && calls.length > 0) {
				return calls.map(call => ({
					name: call.name,
					function: { name: call.name },
					args: call.args
				}));
			}
		}

		// Ollama: tool_calls in result or message data
		if (result.tool_calls && result.tool_calls.length > 0) {
			return result.tool_calls.map(tc => ({
				name: tc.name || tc.function?.name,
				function: { name: tc.function?.name || tc.name },
				args: tc.args || tc.arguments || tc.function?.arguments || {}
			}));
		}

		// Check for tool call in response text (fallback for models without native tool support)
		const responseText = result.response ? result.response.text() : null;
		if (responseText) {
			try {
				const parsed = JSON.parse(responseText);
				if (parsed.tool_call || parsed.toolCall) {
					const toolCall = parsed.tool_call || parsed.toolCall;
					return [{
						name: toolCall.name,
						function: { name: toolCall.name },
						args: toolCall.args || toolCall.arguments || toolCall.parameters || {}
					}];
				}
				// Check for array of tool calls
				if (Array.isArray(parsed.tool_calls)) {
					return parsed.tool_calls.map(tc => ({
						name: tc.name || tc.function?.name,
						function: { name: tc.function?.name || tc.name },
						args: tc.args || tc.arguments || tc.function?.arguments || {}
					}));
				}
			} catch (e) {
				// Not JSON, no tool calls
			}
		}

		return null;
	}

	// ========================================
	// Consolidated Tool Registry
	// ========================================

	_buildAllTools() {
		this._allTools = [];

		// Plugin tools — discovered from loaded plugin.commands[]
		for (const [pluginName, plugin] of Object.entries(this.bot.plunginsLoaded)) {
			const commands = plugin.commands || [];
			for (const cmd of commands) {
				this._allTools.push({
					name: `${pluginName}_${cmd.name}`,
					description: cmd.description,
					parameters: cmd.parameters || [],
					category: cmd.category || 'plugin',
					execute: async (params) => {
						const args = (cmd.parameters || []).map(p => params[p.name]);
						return plugin.handleCommand('ai', cmd.name, ...args);
					}
				});
			}
		}

		// Memory tools
		this._allTools.push(
			{
				name: 'remember_player', category: 'memory',
				description: 'Store a memory about a player (trust level, role, preferences)',
				parameters: [
					{ name: 'playerName', type: 'string', required: true, description: 'Player name' },
					{ name: 'key', type: 'string', required: true, description: 'Memory key (trust_level, role, etc.)' },
					{ name: 'value', type: 'string', required: true, description: 'Value to store' }
				],
				execute: (p) => this.memoryDB.setPlayerMemory(p.playerName, p.key, p.value)
					.then(() => `Stored ${p.key}=${p.value} for ${p.playerName}`)
			},
			{
				name: 'recall_player', category: 'memory',
				description: 'Retrieve all stored memories about a player',
				parameters: [
					{ name: 'playerName', type: 'string', required: true, description: 'Player name' }
				],
				execute: async (p) => {
					const m = await this.memoryDB.getAllPlayerMemories(p.playerName);
					const keys = Object.keys(m);
					return keys.length ? `Memories for ${p.playerName}: ${JSON.stringify(m)}` : `No memories for ${p.playerName}`;
				}
			},
			{
				name: 'list_known_players', category: 'memory',
				description: 'List all players the bot has memories about',
				parameters: [],
				execute: async () => {
					const players = await this.memoryDB.getAllKnownPlayers();
					return players.length ? `Known players: ${players.join(', ')}` : 'No known players yet';
				}
			},
			{
				name: 'set_directive', category: 'memory',
				description: 'Set a bot self-directive (current_goal, mood, focus)',
				parameters: [
					{ name: 'key', type: 'string', required: true, description: 'Directive key' },
					{ name: 'value', type: 'string', required: true, description: 'Directive value' }
				],
				execute: (p) => this.memoryDB.setDirective(p.key, p.value)
					.then(() => `Directive set: ${p.key}=${p.value}`)
			},
			{
				name: 'get_directive', category: 'memory',
				description: 'Retrieve a specific bot directive',
				parameters: [
					{ name: 'key', type: 'string', required: true, description: 'Directive key' }
				],
				execute: async (p) => {
					const d = await this.memoryDB.getDirective(p.key);
					return d !== null ? `${p.key}=${d}` : `No directive for '${p.key}'`;
				}
			},
			{
				name: 'list_directives', category: 'memory',
				description: 'List all active bot directives',
				parameters: [],
				execute: async () => {
					const all = await this.memoryDB.getAllDirectives();
					const keys = Object.keys(all);
					return keys.length ? `Directives: ${JSON.stringify(all)}` : 'No active directives';
				}
			}
		);

		// Storage lookup tools (direct DB access, no lock needed)
		this._allTools.push(
			{
				name: 'lookup_item', category: 'storage',
				description: 'Check how many of an item we have in storage. Use this when asked "how much X do we have" or "do we have X".',
				parameters: [
					{ name: 'itemName', type: 'string', required: true, description: 'Item name or part of name' }
				],
				execute: async (p) => {
					const Database = require('./storage/database');
					const items = await Database.searchItems(p.itemName);
					if (items.length === 0) return `No items found matching '${p.itemName}'`;
					return items.slice(0, 8).map(i => `${i.item_name}: ${i.total_count}`).join(', ');
				}
			},
			{
				name: 'list_items', category: 'storage',
				description: 'List top stocked items in storage. Use to see what items are available.',
				parameters: [
					{ name: 'limit', type: 'number', required: false, description: 'Max results (default 10)' }
				],
				execute: async (p) => {
					const Database = require('./storage/database');
					const items = await Database.searchItems(null);
					const limit = p.limit || 10;
					return items.slice(0, limit).map(i => `${i.item_name}: ${i.total_count}`).join(', ');
				}
			}
		);

		// Trade tools
		this._allTools.push(
			{
				name: 'trade_initiate', category: 'trade',
				description: 'Start a trade with a player. Returns trade window state.',
				parameters: [
					{ name: 'playerName', type: 'string', required: true, description: 'Player to trade with' }
				],
				execute: (p, from) => this._tradeInitiate(p.playerName || from)
			},
			{
				name: 'trade_accept', category: 'trade',
				description: 'Accept an incoming trade request. Returns trade window state.',
				parameters: [],
				execute: () => this._tradeAccept()
			},
			{
				name: 'trade_decline', category: 'trade',
				description: 'Cancel/close the active trade window.',
				parameters: [],
				execute: () => this._tradeDecline()
			},
			{
				name: 'trade_confirm', category: 'trade',
				description: 'Confirm the trade on the bot side (ready check).',
				parameters: [],
				execute: () => this._tradeConfirm()
			},
			{
				name: 'trade_status', category: 'trade',
				description: 'Get current trade window state (items on each side).',
				parameters: [],
				execute: () => this._tradeWindowState || 'No active trade window'
			}
		);
	}

	_getToolsSchema() {
		return this._allTools.map(t => ({
			name: t.name,
			description: t.description,
			parameters: {
				type: 'object',
				properties: (t.parameters || []).reduce((acc, p) => {
					acc[p.name] = { type: p.type || 'string', description: p.description || '' };
					return acc;
				}, {}),
				required: t.parameters.filter(p => p.required).map(p => p.name)
			}
		}));
	}

	_getToolsDocumentation() {
		if (this._allTools.length === 0) return '';

		// Group by category
		const byCategory = {};
		for (const t of this._allTools) {
			const cat = t.category || 'other';
			if (!byCategory[cat]) byCategory[cat] = [];
			byCategory[cat].push(t);
		}

		let doc = '';
		for (const [cat, tools] of Object.entries(byCategory)) {
			doc += `## ${cat}\n`;
			for (const t of tools) {
				doc += `- **${t.name}**: ${t.description}`;
				if (t.parameters && t.parameters.length > 0) {
					doc += ` (params: ${t.parameters.map(p => p.name).join(', ')})`;
				}
				doc += '\n';
			}
			doc += '\n';
		}
		return doc;
	}

	async _executeTool(toolName, params = {}, from = 'ai') {
		const tool = this._allTools.find(t => t.name === toolName);
		if (!tool) throw new Error(`Unknown tool: ${toolName}`);
		console.log(`AI tool: ${toolName}`, params);
		return tool.execute(params, from);
	}

	// ---- Trade tool implementations ----

	async _tradeInitiate(playerName) {
		if (this._tradeWindow) {
			return `Trade window already open. Current state: ${JSON.stringify(this._tradeWindowState)}`;
		}

		const player = this.bot.bot.players[playerName];
		if (!player) {
			return `Player "${playerName}" is not online or not in range.`;
		}

		console.log(`AI: Initiating /trade with ${playerName}`);
		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),
		]);

		if (!window) {
			return `Trade request to ${playerName} timed out (60s). They may not have accepted.`;
		}

		this._setupTradeWindow(window, playerName);

		// Place any withdrawn items into the trade window
		const storage = this.bot.plunginsLoaded['Storage'];
		if (storage && typeof storage.placeWithdrawnItemsInTrade === 'function') {
			await storage.placeWithdrawnItemsInTrade(window, playerName);
			// Re-capture state after placing items
			this._tradeWindowState = this._captureTradeState(window);
		}

		return `Trade opened with ${playerName}. State: ${JSON.stringify(this._tradeWindowState)}`;
	}

	async _tradeAccept() {
		if (this._tradeWindow) {
			return `Trade window already open. Current state: ${JSON.stringify(this._tradeWindowState)}`;
		}

		console.log('AI: Accepting incoming trade');
		// Use bot.chat directly to bypass sayAiSafe command filtering
		this.bot.bot.chat('/trade accept');

		const window = await Promise.race([
			this.bot.once('windowOpen'),
			sleep(30000).then(() => null),
		]);

		if (!window) {
			return 'Trade accept timed out (30s). The trade request may have expired.';
		}

		this._setupTradeWindow(window, 'incoming');
		return `Trade accepted. State: ${JSON.stringify(this._tradeWindowState)}`;
	}

	async _tradeDecline() {
		if (!this._tradeWindow) {
			return 'No active trade to decline.';
		}

		console.log('AI: Declining/cancelling trade');
		try {
			this.bot.bot.closeWindow(this._tradeWindow);
		} catch (e) {
			// Window may already be closed
		}
		this._tradeWindow = null;
		this._tradeWindowState = null;
		this._expectingTradeWindow = false;
		return 'Trade declined/window closed.';
	}

	async _tradeConfirm() {
		if (!this._tradeWindow) {
			return 'No active trade to confirm. Use trade_accept or trade_initiate first.';
		}

		console.log('AI: Confirming trade');
		this.bot.bot.moveSlotItem(37, 37);
		// Update state after confirmation
		this._tradeWindowState = this._captureTradeState(this._tradeWindow);
		return `Trade confirmed on bot side. State: ${JSON.stringify(this._tradeWindowState)}`;
	}

	// ---- Trade window management ----

	_setupTradeWindow(window, playerName) {
		this._tradeWindow = window;
		this._tradeWindowState = this._captureTradeState(window);

		// Monitor customer-side slots for real-time feedback
		const customerSlots = [5, 6, 7, 8, 14, 15, 16, 17, 23, 24, 25, 26];
		for (const slot of customerSlots) {
			window.on(`updateSlot:${slot}`, () => {
				if (this._tradeWindow === window) {
					this._tradeWindowState = this._captureTradeState(window);
					this._ensurePolling();
				}
			});
		}

		// Monitor confirmation indicator slots
		window.on('updateSlot:53', () => {
			if (this._tradeWindow === window) {
				this._tradeWindowState = this._captureTradeState(window);
				this._ensurePolling();
			}
		});
		window.on('updateSlot:37', () => {
			if (this._tradeWindow === window) {
				this._tradeWindowState = this._captureTradeState(window);
				this._ensurePolling();
			}
		});

		// Cleanup on window close
		const onClose = () => {
			if (this._tradeWindow === window) {
				this._tradeWindow = null;
				this._tradeWindowState = null;
			}
		};
		this.bot.bot.once('windowClose', onClose);
	}

	_captureTradeState(window) {
		const botSlots = [0, 1, 2, 3, 9, 10, 11, 12, 18, 19, 20, 21];
		const customerSlots = [5, 6, 7, 8, 14, 15, 16, 17, 23, 24, 25, 26];

		const readSlots = (slots) => slots
			.map(s => window.slots[s])
			.filter(Boolean)
			.map(item => ({ name: item.name, count: item.count }));

		return {
			botItems: readSlots(botSlots),
			customerItems: readSlots(customerSlots),
			customerConfirmed: !!(window.slots[53] && window.slots[53].name === 'lime_dye'),
			botConfirmed: !!(window.slots[37] && window.slots[37].name === 'lime_dye'),
		};
	}

	/**
	 * Process AI response and send to chat.
	 * Strips markdown code fences, deduplicates messages, and skips noise.
	 */
	async processResponse(responseText) {
		if (!responseText) return;

		// Strip markdown code fences that some models wrap JSON in
		const cleaned = this._stripMarkdownFences(responseText);

		// Try to parse as JSON array [{text, delay}]
		try {
			const parsed = JSON.parse(cleaned);
			if (Array.isArray(parsed)) {
				for (let message of parsed) {
					const msgText = (message.text || '').trim();
					console.log('toSay', message.delay, msgText);

					// Skip empty, underscore-only, or noise responses
					if (!msgText || msgText === '_' || msgText.match(/^[-_]+$/)) continue;

					// Deduplicate — don't send the same message twice
					const dedupeKey = msgText.toLowerCase();
					if (this._lastSentMessages.includes(dedupeKey)) {
						console.log('Skipping duplicate message:', msgText);
						continue;
					}
					if (this._lastSentMessages.length >= 50) this._lastSentMessages.shift();
					this._lastSentMessages.push(dedupeKey);

					await this.bot.sayAiSafe(msgText);
				}
				return;
			}
		} catch (jsonError) {
			// Not valid JSON array — fall through to plain text
		}

		// Plain text fallback
		const text = cleaned.trim();
		if (!text || text === '_' || text === '___' || text.match(/^[-_]+$/)) return;

		// Don't send raw JSON or code blocks
		if (text.startsWith('{') || text.startsWith('```')) {
			console.log('Skipping raw JSON/code block response:', text.substring(0, 80));
			return;
		}

		const dedupeKey = text.toLowerCase();
		if (this._lastSentMessages.includes(dedupeKey)) {
			console.log('Skipping duplicate plain-text message:', text);
			return;
		}
		if (this._lastSentMessages.length >= 50) this._lastSentMessages.shift();
		this._lastSentMessages.push(dedupeKey);

		await this.bot.sayAiSafe(text);
	}

	/**
	 * Strip markdown code fences from AI response text.
	 */
	_stripMarkdownFences(text) {
		if (!text || typeof text !== 'string') return text;
		let cleaned = text.trim();
		cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, '');
		cleaned = cleaned.replace(/\n?```\s*$/, '');
		return cleaned.trim();
	}

	/**
	 * Extract the last player who addressed this bot from recent messages
	 * @param {Array} messages - Array of message objects {type, text, ...}
	 * @returns {string} Player name or 'ai' if no player found
	 */
	getLastSpeaker(messages) {
		if (!messages || !Array.isArray(messages)) return 'ai';
		// Search in reverse to find the most recent player message
		for (let i = messages.length - 1; i >= 0; i--) {
			const msg = messages[i];
			if (!msg || msg.type !== 'message') continue;
			const text = msg.text || '';
			// Parse format: <[lvl] playername> message text
			const match = text.match(/^<\[.*?\]\s+(\w+)>/);
			if (match && match[1] && match[1] !== this.bot.bot.entity.username) {
				return match[1];
			}
		}
		return 'ai';
	}

	getTimeAgo(timestamp) {
		const now = new Date();
		const past = new Date(timestamp);
		const diffMs = now - past;
		const diffSecs = Math.floor(diffMs / 1000);
		const diffMins = Math.floor(diffSecs / 60);
		const diffHours = Math.floor(diffMins / 60);
		const diffDays = Math.floor(diffHours / 24);

		if (diffSecs < 60) return 'just now';
		if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
		if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
		if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
		return past.toLocaleDateString();
	}

	/**
	 * Get current time info for prompt injection
	 */
	getCurrentTimeInfo() {
		const now = new Date();
		return {
			iso: now.toISOString().replace('T', ' ').substring(0, 19),
			timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
			human: now.toLocaleString('en-US', {
				weekday: 'long',
				year: 'numeric',
				month: 'long',
				day: 'numeric',
				hour: 'numeric',
				minute: '2-digit',
				hour12: true
			})
		};
	}
}



const AiWeb = require('./ai/web');
Ai.createRouter = AiWeb.createRouter;
Ai.webUI = AiWeb.webUI;

module.exports = Ai;
