'use strict';

const conf = require('../../conf');
const Database = require('../storage/database');

// In-memory cache: key -> { value, type, category }
let _cache = null;
// Bot settings cache: botName -> { key -> { value, type } }
let _botCache = null;

// Registry of all known settings with their types, categories, labels, defaults
const SETTINGS_REGISTRY = [
	// ---- MC / Server ----
	{ key: 'mc.host',              type: 'string',  category: 'server', label: 'Server Host',       description: 'Minecraft server address' },

	// ---- AI category ----
	{ key: 'ai.provider',          type: 'string',  category: 'ai', label: 'AI Provider',         description: 'LLM provider (ollama or gemini)' },
	{ key: 'ai.model',             type: 'string',  category: 'ai', label: 'Model',               description: 'Model name to use' },
	{ key: 'ai.baseUrl',           type: 'string',  category: 'ai', label: 'Ollama Base URL',     description: 'Ollama server URL (only used if provider is ollama)' },
	{ key: 'ai.key',               type: 'secret',  category: 'ai', label: 'API Key',             description: 'Gemini API key (only used if provider is gemini)' },
	{ key: 'ai.temperature',       type: 'number',  category: 'ai', label: 'Temperature',         description: 'LLM temperature (0-2)' },
	{ key: 'ai.topP',              type: 'number',  category: 'ai', label: 'Top P',               description: 'Nucleus sampling parameter' },
	{ key: 'ai.topK',              type: 'number',  category: 'ai', label: 'Top K',               description: 'Top-K sampling parameter' },
	{ key: 'ai.interval',          type: 'number',  category: 'ai', label: 'Poll Interval',       description: 'Seconds between AI poll cycles' },
	{ key: 'ai.timeout',           type: 'number',  category: 'ai', label: 'Request Timeout',     description: 'LLM request timeout in ms' },
	{ key: 'ai.promptName',        type: 'string',  category: 'ai', label: 'Active Prompt',       description: 'Active prompt personality name' },
	{ key: 'ai.enableNativeTools', type: 'boolean', category: 'ai', label: 'Native Tools',        description: 'Enable native function calling' },
	{ key: 'ai.faceBot',           type: 'string',  category: 'ai', label: 'Face Bot',            description: 'Bot that runs the AI coordinator' },
	{ key: 'ai.storageBot',        type: 'string',  category: 'ai', label: 'Storage Bot',         description: 'Bot that handles storage operations' },
	{ key: 'ai.prompCustom',       type: 'string',  category: 'ai', label: 'Custom Prompt Text',  description: 'Injected text when prompt name is "custom"' },
	{ key: 'ai.prompts',           type: 'json',    category: 'ai', label: 'Prompt Templates',    description: 'All prompt templates: {"name": "template...", ...}' },

	// ---- Storage category ----
	{ key: 'storage.dbPath',              type: 'string',  category: 'storage', label: 'Database Path',           description: 'Path to SQLite database file' },
	{ key: 'storage.scanRadius',          type: 'number',  category: 'storage', label: 'Scan Radius',             description: 'Block radius for chest scanning' },
	{ key: 'storage.homePos',             type: 'json',    category: 'storage', label: 'Home Position',           description: 'Bot home position {x, y, z} or null' },
	{ key: 'storage.craftingTablePos',    type: 'json',    category: 'storage', label: 'Crafting Table Pos',      description: 'Crafting table position {x, y, z} or null' },
	{ key: 'storage.inboxShulkerName',    type: 'string',  category: 'storage', label: 'Inbox Shulker Name',      description: 'Name tag for inbox shulker boxes' },
	{ key: 'storage.outboxShulkerName',   type: 'string',  category: 'storage', label: 'Outbox Shulker Name',     description: 'Name tag for outbox shulker boxes' },
	{ key: 'storage.newShulkersName',     type: 'string',  category: 'storage', label: 'New Shulkers Name',       description: 'Name tag for empty/new shulker boxes' },
	{ key: 'storage.hotbarItems',         type: 'json',    category: 'storage', label: 'Hotbar Items',            description: 'Array of items to keep in hotbar' },
	{ key: 'storage.hotbarRestockInterval', type: 'number',category: 'storage', label: 'Restock Interval',        description: 'ms between hotbar restock checks' },
	{ key: 'storage.categories',          type: 'json',    category: 'storage', label: 'Item Categories',         description: 'Item name classification lists' },
	{ key: 'storage.defaultPlayers',      type: 'json',    category: 'storage', label: 'Default Players',          description: 'Default player role assignments' },
	{ key: 'storage.webPort',             type: 'number',  category: 'storage', label: 'Web UI Port',             description: 'Port for the web dashboard' },
	{ key: 'storage.webHost',             type: 'string',  category: 'storage', label: 'Web UI Host',             description: 'Bind address for the web dashboard' },

	// ---- Farm supply ----
	{ key: 'farmSupply.enabled',         type: 'boolean', category: 'farm', label: 'Farm Supply Enabled',  description: 'Enable farm supply plugin' },
	{ key: 'farmSupply.storageBotName',  type: 'string',  category: 'farm', label: 'Storage Bot Name',     description: 'Name of bot handling storage trades' },

	// ---- Web auth (OIDC / SSO) ----
	{ key: 'auth.enabled',               type: 'boolean', category: 'auth', label: 'Auth Enabled',          description: 'Require SSO login for the web dashboard' },
	{ key: 'auth.authorizationEndpoint', type: 'string',  category: 'auth', label: 'Authorize Endpoint',    description: 'SSO OAuth authorize URL' },
	{ key: 'auth.tokenEndpoint',         type: 'string',  category: 'auth', label: 'Token Endpoint',        description: 'SSO OAuth token URL' },
	{ key: 'auth.userinfoEndpoint',      type: 'string',  category: 'auth', label: 'Userinfo Endpoint',     description: 'SSO OIDC userinfo URL' },
	{ key: 'auth.clientId',              type: 'string',  category: 'auth', label: 'Client ID',             description: 'OAuth client ID registered on the SSO' },
	{ key: 'auth.clientSecret',          type: 'secret',  category: 'auth', label: 'Client Secret',         description: 'OAuth client secret' },
	{ key: 'auth.redirectUri',           type: 'string',  category: 'auth', label: 'Redirect URI',          description: 'Absolute callback URL — must match the SSO client registration' },
	{ key: 'auth.scopes',                type: 'json',    category: 'auth', label: 'Scopes',                description: 'OAuth scopes to request' },
	{ key: 'auth.usernameClaim',         type: 'string',  category: 'auth', label: 'Username Claim',        description: 'Userinfo claim used as the username' },
	{ key: 'auth.groupsClaim',           type: 'string',  category: 'auth', label: 'Groups Claim',          description: 'Userinfo claim carrying group membership' },
	{ key: 'auth.allowedUsers',          type: 'json',    category: 'auth', label: 'Allowed Users',         description: 'Usernames allowed to log in (empty = any SSO user)' },
	{ key: 'auth.allowedGroups',         type: 'json',    category: 'auth', label: 'Allowed Groups',        description: 'SSO groups allowed to log in (empty = any SSO user)' },
	{ key: 'auth.tokenTTL',              type: 'number',  category: 'auth', label: 'Session TTL (s)',       description: 'Seconds a login session stays valid' },

	// ---- Invites ----
	{ key: 'invite.seedSites',   type: 'json',    category: 'invites', label: 'Invite Sites',        description: 'Array of invite site configurations' },

	// ---- Plugins ----
	{ key: 'plugings',           type: 'json',    category: 'general', label: 'Default Plugins',    description: 'Default plugin configurations for all bots' },

	// ---- Player list ----
	{ key: 'playerListDir',      type: 'string',  category: 'general', label: 'Player List Dir',    description: 'Directory for player list output' },
];

// Registry of per-bot settings
const BOT_SETTINGS_REGISTRY = [
	{ key: 'username',      type: 'string',  label: 'Username/Email',   description: 'Microsoft/Mojang auth email' },
	{ key: 'password',      type: 'secret',  label: 'Password',         description: 'Account password' },
	{ key: 'auth',          type: 'string',  label: 'Auth Method',      description: 'microsoft or mojang' },
	{ key: 'autoConnect',   type: 'boolean', label: 'Auto Connect',     description: 'Connect on startup' },
	{ key: 'autoReConnect', type: 'boolean', label: 'Auto Reconnect',   description: 'Reconnect after disconnect' },
	{ key: 'onDemand',      type: 'boolean', label: 'On Demand',        description: 'Only connect when needed, auto-disconnect when idle' },
	{ key: 'idleTimeout',   type: 'number',  label: 'Idle Timeout',     description: 'ms before on-demand bot disconnects' },
	{ key: 'commands',      type: 'json',    label: 'Commands',         description: 'Array of command module names to load' },
	{ key: 'plugins',       type: 'json',    label: 'Plugins',          description: 'Plugin configurations: {"PluginName": {...options...}}' },
	{ key: 'hasAi',         type: 'boolean', label: 'Has AI',           description: 'Load the AI plugin for this bot' },
];

// Build a map from config key path to config file value
function _getConfigDefault(key) {
	const parts = key.split('.');
	let node = conf;
	for (const p of parts) {
		if (node == null) return undefined;
		node = node[p];
	}
	return node;
}

function _serializeDefault(value, type) {
	if (value === undefined || value === null) return '';
	switch (type) {
	case 'number': return String(value);
	case 'boolean': return String(value);
	case 'json': return JSON.stringify(value);
	case 'secret': return String(value);
	default: return String(value);
	}
}

// Serialize a native value to the string stored in the DB/cache.
function _serialize(value, type) {
	switch (type) {
	case 'number': {
		const n = Number(value);
		if (isNaN(n)) throw new Error(`Expected number, got: ${value}`);
		return String(n);
	}
	case 'boolean': return (value === true || value === 'true' || value === '1') ? 'true' : 'false';
	case 'json': return JSON.stringify(typeof value === 'string' ? JSON.parse(value) : value);
	case 'secret': return String(value);
	default: return String(value);
	}
}

// Extract serialized defaults from a bot config object.
function _botConfigToDefaults(botConfig) {
	return {
		username:      { value: botConfig.username || '',                              type: 'string' },
		password:      { value: botConfig.password || '',                              type: 'secret' },
		auth:          { value: botConfig.auth || 'microsoft',                         type: 'string' },
		autoConnect:   { value: String(botConfig.autoConnect ?? true),                 type: 'boolean' },
		autoReConnect: { value: String(botConfig.autoReConnect ?? true),               type: 'boolean' },
		onDemand:      { value: String(botConfig.onDemand || false),                   type: 'boolean' },
		idleTimeout:   { value: String(botConfig.idleTimeout || 30000),                type: 'number' },
		commands:      { value: JSON.stringify(botConfig.commands || ['default']),      type: 'json' },
		plugins:       { value: JSON.stringify(botConfig.plugins || {}),               type: 'json' },
		hasAi:         { value: String(botConfig.hasAi || false),                      type: 'boolean' },
	};
}

function _coerceForConsumer(value, type) {
	if (value === undefined || value === null || value === '') {
		switch (type) {
		case 'number': return 0;
		case 'boolean': return false;
		case 'json': return null;
		case 'secret': return '';
		default: return '';
		}
	}
	switch (type) {
	case 'number': {
		const n = Number(value);
		return isNaN(n) ? 0 : n;
	}
	case 'boolean': return value === 'true' || value === true;
	case 'json': {
		try { return JSON.parse(value); }
		catch (e) { return null; }
	}
	case 'secret': return String(value);
	default: return String(value);
	}
}

function _getTypeForKey(key) {
	const entry = SETTINGS_REGISTRY.find(e => e.key === key);
	return entry ? entry.type : 'string';
}

/**
 * Populate the in-memory cache from DB (with config file fallback).
 * Must be called after Database is initialized.
 */
async function initialize() {
	if (_cache) return;

	// Build cache from registry defaults first
	_cache = {};
	for (const entry of SETTINGS_REGISTRY) {
		const configDefault = _getConfigDefault(entry.key);
		_cache[entry.key] = {
			value: _serializeDefault(configDefault, entry.type),
			type: entry.type,
			category: entry.category,
		};
	}

	// Overlay DB values if DB is available
	if (Database && Database.db) {
		try {
			const rows = await Database.getAllSettings();
			for (const row of rows) {
				if (_cache[row.key]) {
					_cache[row.key].value = row.value;
					_cache[row.key].fromDb = true;
				}
			}
		} catch (err) {
			console.warn('SettingsManager: failed to load from DB, using config defaults:', err.message);
		}
	}

	// Initialize bot settings cache
	await _initBotCache();

	console.log(`SettingsManager: initialized with ${Object.keys(_cache).length} global settings`);
}

async function _initBotCache() {
	_botCache = {};

	// Load from config first as defaults
	const bots = conf.mc?.bots || {};
	for (const [botName, botConfig] of Object.entries(bots)) {
		_botCache[botName] = _botConfigToDefaults(botConfig);
	}

	// Overlay DB values
	if (Database && Database.db) {
		for (const botName of Object.keys(_botCache)) {
			try {
				const rows = await Database.getAllBotSettings(botName);
				for (const row of rows) {
					if (_botCache[botName] && _botCache[botName][row.key] !== undefined) {
						_botCache[botName][row.key].value = row.value;
						_botCache[botName][row.key].fromDb = true;
					}
				}
			} catch (e) { /* bot settings not in DB yet */ }
		}
	}
}

/** Synchronous get — reads from cache. Returns coerced native type. */
function get(key) {
	if (!_cache) {
		const raw = _getConfigDefault(key);
		return _coerceForConsumer(raw, _getTypeForKey(key));
	}
	const entry = _cache[key];
	if (!entry) return undefined;
	return _coerceForConsumer(entry.value, entry.type);
}

/** Synchronous getAll — returns { [key]: nativeValue } */
function getAll() {
	const result = {};
	if (!_cache) return result;
	for (const [key, entry] of Object.entries(_cache)) {
		result[key] = _coerceForConsumer(entry.value, entry.type);
	}
	return result;
}

/** Synchronous getAllByCategory */
function getAllByCategory(category) {
	const result = {};
	if (!_cache) return result;
	for (const [key, entry] of Object.entries(_cache)) {
		if (entry.category === category) {
			result[key] = _coerceForConsumer(entry.value, entry.type);
		}
	}
	return result;
}

/** Async set — writes to DB and updates cache */
async function set(key, value) {
	if (!_cache) throw new Error('SettingsManager not initialized');
	const entry = _cache[key];
	if (!entry) throw new Error(`Unknown setting: ${key}`);

	const serialized = _serialize(value, entry.type);

	if (Database && Database.db) {
		await Database.setSetting(key, serialized);
	}

	entry.value = serialized;
	entry.fromDb = true;

	// Notify AiManager of the change (if it's an ai.* setting)
	try {
		const { getInstance } = require('../ai/manager');
		const manager = getInstance();
		if (manager && key.startsWith('ai.')) {
			manager.onSettingChanged(key, _coerceForConsumer(serialized, entry.type));
		}
	} catch (e) { /* ignore */ }

	return _coerceForConsumer(serialized, entry.type);
}

/**
 * Return all settings under a key prefix as a flat object with short keys.
 * e.g. getSection('storage') → { dbPath: './storage/storage.db', scanRadius: 30, ... }
 * Falls back to conf when cache is not yet initialized.
 * Skips keys whose cached value is empty (unset), preserving caller's defaults.
 */
function getSection(prefix) {
	const strip = prefix + '.';
	if (!_cache) {
		// Not yet initialized — read directly from conf
		const parts = prefix.split('.');
		let node = conf;
		for (const p of parts) { if (node == null) return {}; node = node[p]; }
		return (typeof node === 'object' && node !== null && !Array.isArray(node)) ? { ...node } : {};
	}
	const result = {};
	for (const [key, entry] of Object.entries(_cache)) {
		if (!key.startsWith(strip)) continue;
		if (entry.value === '') continue; // unset — let caller's default win
		result[key.slice(strip.length)] = _coerceForConsumer(entry.value, entry.type);
	}
	return result;
}

/** Reload cache from DB */
async function reload() {
	_cache = null;
	_botCache = null;
	await initialize();
}

function getRegistry() {
	return SETTINGS_REGISTRY;
}

// ========================================
// Bot Settings
// ========================================

function getBotSettingsRegistry() {
	return BOT_SETTINGS_REGISTRY;
}

/** Get all bot names known to the system */
function getBotNames() {
	if (!_botCache) return Object.keys(conf.mc?.bots || {});
	return Object.keys(_botCache);
}

/** Get all settings for a specific bot. Returns { key: nativeValue, ... } with metadata. */
function getBotSettings(botName) {
	const result = { _meta: { name: botName } };
	if (!_botCache || !_botCache[botName]) {
		const botConfig = conf.mc?.bots?.[botName];
		if (!botConfig) return null;
		const defaults = _botConfigToDefaults(botConfig);
		for (const [key, entry] of Object.entries(defaults)) {
			result[key] = _coerceForConsumer(entry.value, entry.type);
		}
		return result;
	}

	for (const [key, cacheEntry] of Object.entries(_botCache[botName])) {
		result[key] = _coerceForConsumer(cacheEntry.value, cacheEntry.type);
	}
	return result;
}

/** Set a single bot setting. Writes to DB and updates cache. */
async function setBotSetting(botName, key, value) {
	if (!_botCache) throw new Error('SettingsManager not initialized');

	// Ensure bot exists in cache
	if (!_botCache[botName]) {
		const botConfig = conf.mc?.bots?.[botName];
		if (!botConfig) throw new Error(`Unknown bot: ${botName}`);
		_botCache[botName] = _botConfigToDefaults(botConfig);
	}

	const cacheEntry = _botCache[botName][key];
	if (!cacheEntry) throw new Error(`Unknown bot setting: ${key}`);

	const serialized = _serialize(value, cacheEntry.type);

	if (Database && Database.db) {
		await Database.setBotSetting(botName, key, serialized, cacheEntry.type);
	}

	cacheEntry.value = serialized;
	cacheEntry.fromDb = true;

	// Apply to live bot instance if connected
	try {
		const { CJbot } = require('../../model/minecraft');
		const bot = CJbot.bots[botName];
		if (bot) {
			const nativeVal = _coerceForConsumer(serialized, cacheEntry.type);
			switch (key) {
			case 'autoConnect': bot.autoConnect = nativeVal; break;
			case 'autoReConnect': bot.autoReConnect = nativeVal; break;
			case 'onDemand': bot.onDemand = nativeVal; break;
			case 'idleTimeout': bot._idleTimeout = nativeVal; break;
			case 'plugins': bot.pluginsWanted = nativeVal || {}; break;
			}
		}
	} catch (e) { /* ignore */ }

	return _coerceForConsumer(serialized, cacheEntry.type);
}

module.exports = {
	initialize, get, getAll, getAllByCategory, getSection, set, reload, getRegistry,
	getBotNames, getBotSettings, setBotSetting, getBotSettingsRegistry,
};
