//// 
import type { Database } from 'bun:sqlite';
import { CommandService } from './services/command';
import { GroupSyncService } from './services/group-sync';
import { ResponseQueue } from './services/response-queue';
import { TaskService } from './tasks/service';
import { WebhookManager } from './services/webhook-manager';
import { normalizeWhatsAppId, isGroupId } from './utils/whatsapp';
import { ensureUserExists, db } from './db';
import { ContactsService } from './services/contacts';
import { Migrator } from './db/migrator';
import { RateLimiter } from './services/rate-limit';
import { RemindersService } from './services/reminders';
import { Metrics } from './services/metrics';
import { MaintenanceService } from './services/maintenance';
import { IdentityService } from './services/identity';
import { AllowedGroups } from './services/allowed-groups';
import { AdminService } from './services/admin';
// Bun is available globally when running under Bun runtime
declare global {
	var Bun: typeof import('bun');
}
export const REQUIRED_ENV = [
	'EVOLUTION_API_URL',
	'EVOLUTION_API_KEY', 
	'EVOLUTION_API_INSTANCE',
	'CHATBOT_PHONE_NUMBER',
	'WEBHOOK_URL'
];
type WebhookPayload = {
	event: string;
	instance: string;
	data: any;
	// Other fields from Evolution API
};
export class WebhookServer {
	static dbInstance: Database = db;
	private static getBaseUrl(request: Request): string {
		const proto = request.headers.get('x-forwarded-proto') || 'http';
		const host = request.headers.get('x-forwarded-host') || request.headers.get('host');
		return `${proto}://${host}`;
	}
	private static getMessageText(message: any): string {
		if (!message || typeof message !== 'object') return '';
		const text =
			message.conversation ||
			message?.extendedTextMessage?.text ||
			message?.imageMessage?.caption ||
			message?.videoMessage?.caption ||
			'';
		return typeof text === 'string' ? text.trim() : '';
	}
	static async handleRequest(request: Request): Promise {
		// Health check endpoint y métricas
		const url = new URL(request.url);
		if (url.pathname.endsWith('/metrics')) {
			if (request.method !== 'GET') {
				return new Response('🚫 Method not allowed', { status: 405 });
			}
			if (!Metrics.enabled()) {
				return new Response('Metrics disabled', { status: 404 });
			}
			// Gauges de allowed_groups por estado (best-effort)
			try {
				const rows = WebhookServer.dbInstance
					.prepare(`SELECT status, COUNT(*) AS c FROM allowed_groups GROUP BY status`)
					.all() as any[];
				let pending = 0, allowed = 0, blocked = 0;
				for (const r of rows) {
					const s = String(r?.status || '');
					const c = Number(r?.c || 0);
					if (s === 'pending') pending = c;
					else if (s === 'allowed') allowed = c;
					else if (s === 'blocked') blocked = c;
				}
				Metrics.set('allowed_groups_total_pending', pending);
				Metrics.set('allowed_groups_total_allowed', allowed);
				Metrics.set('allowed_groups_total_blocked', blocked);
			} catch {}
			// Exponer métrica con el tiempo restante hasta el próximo group sync (o -1 si scheduler inactivo)
			try {
				const secs = GroupSyncService.getSecondsUntilNextGroupSync();
				const val = (secs == null || !Number.isFinite(secs)) ? -1 : secs;
				Metrics.set('group_sync_seconds_until_next', val);
			} catch {}
			const format = (process.env.METRICS_FORMAT || 'prom').toLowerCase() === 'json' ? 'json' : 'prom';
			const body = Metrics.render(format as any);
			return new Response(body, {
				status: 200,
				headers: { 'Content-Type': format === 'json' ? 'application/json' : 'text/plain; version=0.0.4' }
			});
		}
		if (url.pathname.endsWith('/health')) {
			// /health?full=1 devuelve JSON con detalles
			if (url.searchParams.get('full') === '1') {
				try {
					const rowG = WebhookServer.dbInstance.prepare(`SELECT COUNT(*) AS c, MAX(last_verified) AS lv FROM groups WHERE active = 1`).get() as any;
					const rowM = WebhookServer.dbInstance.prepare(`SELECT COUNT(*) AS c FROM group_members WHERE is_active = 1`).get() as any;
					const active_groups = Number(rowG?.c || 0);
					const active_members = Number(rowM?.c || 0);
					const lv = rowG?.lv ? String(rowG.lv) : null;
					let last_sync_at: string | null = lv;
					let snapshot_age_ms: number | null = null;
					if (lv) {
						const iso = lv.includes('T') ? lv : (lv.replace(' ', 'T') + 'Z');
						const ms = Date.parse(iso);
						if (Number.isFinite(ms)) {
							snapshot_age_ms = Date.now() - ms;
						}
					}
					const lastSyncMetric = Metrics.get('last_sync_ok');
					const maxAgeRaw = Number(process.env.MAX_MEMBERS_SNAPSHOT_AGE_MS);
					const maxAgeMs = Number.isFinite(maxAgeRaw) && maxAgeRaw > 0 ? maxAgeRaw : 24 * 60 * 60 * 1000;
					const snapshot_fresh = typeof snapshot_age_ms === 'number' ? (snapshot_age_ms <= maxAgeMs) : false;
					let last_sync_ok: number;
					if (typeof lastSyncMetric === 'number') {
						last_sync_ok = (lastSyncMetric === 1 && snapshot_fresh) ? 1 : 0;
					} else {
						// Si no hay métrica explícita, nos basamos exclusivamente en la frescura de la snapshot
						last_sync_ok = snapshot_fresh ? 1 : 0;
					}
					const payload = { status: 'ok', active_groups, active_members, last_sync_at, snapshot_age_ms, snapshot_fresh, last_sync_ok };
					return new Response(JSON.stringify(payload), {
						status: 200,
						headers: { 'Content-Type': 'application/json' }
					});
				} catch (e) {
					return new Response(JSON.stringify({ status: 'error' }), {
						status: 500,
						headers: { 'Content-Type': 'application/json' }
					});
				}
			}
			return new Response('OK', { status: 200 });
		}
		if (process.env.NODE_ENV !== 'test') {
			console.log('ℹ️ Incoming webhook request:')
		}
		// 1. Method validation
		if (request.method !== 'POST') {
			return new Response('🚫 Method not allowed', { status: 405 });
		}
		// 2. Content-Type validation
		const contentType = request.headers.get('content-type');
		if (!contentType?.includes('application/json')) {
			return new Response('🚫 Invalid content type', { status: 400 });
		}
		try {
			// 3. Parse and validate payload
			const payload = await request.json() as WebhookPayload;
			if (!payload.event || !payload.instance) {
				return new Response('🚫 Invalid payload', { status: 400 });
			}
			// 4. Verify instance matches (skip in test environment unless TEST_VERIFY_INSTANCE is set)
			if ((process.env.NODE_ENV !== 'test' || process.env.TEST_VERIFY_INSTANCE) &&
				payload.instance !== process.env.EVOLUTION_API_INSTANCE) {
				return new Response('🚫 Invalid instance', { status: 403 });
			}
			// 5. Route events
			// console.log('ℹ️ Webhook event received:', {
			// 	event: payload.event,
			// 	instance: payload.instance,
			// 	data: payload.data ? '[...]' : null
			// });
			// Normalize event name to handle different casing/format (e.g., MESSAGES_UPSERT)
			const evt = String(payload.event);
			const evtNorm = evt.toLowerCase().replace(/_/g, '.');
			// Contabilizar evento de webhook por tipo
			try {
				Metrics.inc(`webhook_events_total_${evtNorm.replace(/\./g, '_')}`);
			} catch {}
			switch (evtNorm) {
				case 'messages.upsert':
					if (process.env.NODE_ENV !== 'test') {
						console.log('ℹ️ Handling message upsert:', {
							groupId: payload.data?.key?.remoteJid,
							message: payload.data?.message?.conversation,
							rawEvent: evt
						});
					}
					await WebhookServer.handleMessageUpsert(payload.data);
					break;
				case 'contacts.update':
				case 'chats.update':
					if (process.env.NODE_ENV !== 'test') {
						console.log('ℹ️ Handling contacts/chats update event:', { rawEvent: evt });
					}
					ContactsService.updateFromWebhook(payload.data);
					break;
				case 'groups.upsert':
					if (process.env.NODE_ENV !== 'test') {
						console.log('ℹ️ Handling groups upsert event:', { rawEvent: evt });
					}
					try {
						await GroupSyncService.syncGroups();
						GroupSyncService.refreshActiveGroupsCache();
						await GroupSyncService.syncMembersForActiveGroups();
					} catch (e) {
						console.error('❌ Error handling groups.upsert:', e);
					}
					break;
				// Other events will be added later
			}
			return new Response('OK', { status: 200 });
		} catch (error) {
			console.error('❌ Error processing webhook:', {
				error: error instanceof Error ? error.message : String(error),
				stack: error instanceof Error ? error.stack : undefined,
				time: new Date().toISOString()
			});
			try { Metrics.inc('webhook_errors_total'); } catch {}
			return new Response('Invalid request', { status: 400 });
		}
	}
	static async handleMessageUpsert(data: any) {
		if (!data?.key?.remoteJid || !data.message) {
			if (process.env.NODE_ENV !== 'test') {
				console.log('⚠️ Invalid message format - missing required fields');
				console.log(data);
			}
			return;
		}
		const messageText = WebhookServer.getMessageText(data.message);
		if (!messageText) {
			if (process.env.NODE_ENV !== 'test') {
				console.log('⚠️ Empty or unsupported message content');
			}
			return;
		}
		// Determine sender depending on context (group vs DM) and ignore non-user messages
		const remoteJid = data.key.remoteJid;
		const participant = data.key.participant;
		const fromMe = !!data.key.fromMe;
		// Ignore broadcasts/status
		if (remoteJid === 'status@broadcast' || (typeof remoteJid === 'string' && remoteJid.endsWith('@broadcast'))) {
			if (process.env.NODE_ENV !== 'test') {
				console.log('ℹ️ Ignoring broadcast/status message');
			}
			return;
		}
		// Ignore our own messages
		if (fromMe) {
			if (process.env.NODE_ENV !== 'test') {
				console.log('ℹ️ Ignoring message sent by the bot (fromMe=true)');
			}
			return;
		}
		// Compute sender JID based on chat type (prefer participantAlt when available due to Baileys change)
		const senderRaw = isGroupId(remoteJid)
			? (data.key.participantAlt || participant)
			: remoteJid;
		// Aprender mapping alias→número cuando vienen ambos y difieren (participant vs participantAlt)
		if (isGroupId(remoteJid)) {
			const pAlt = typeof data.key.participantAlt === 'string' ? data.key.participantAlt : null;
			const p = typeof participant === 'string' ? participant : null;
			if (pAlt && p) {
				try {
					const nAlt = normalizeWhatsAppId(pAlt);
					const n = normalizeWhatsAppId(p);
					if (nAlt && n && nAlt !== n) {
						IdentityService.upsertAlias(p, pAlt, 'message.key');
					}
				} catch {}
			}
		}
		// Normalize sender ID for consistency and validation
		const normalizedSenderId = normalizeWhatsAppId(senderRaw);
		if (!normalizedSenderId) {
			if (process.env.NODE_ENV !== 'test') {
				console.debug('⚠️ Invalid sender ID, ignoring message', { remoteJid, participant, fromMe });
			}
			return;
		}
		// Avoid processing messages from the bot number
		if (process.env.CHATBOT_PHONE_NUMBER && normalizedSenderId === process.env.CHATBOT_PHONE_NUMBER) {
			if (process.env.NODE_ENV !== 'test') {
				console.log('ℹ️ Ignoring message from the bot number');
			}
			return;
		}
		// Ensure user exists in database (swallow DB errors to keep webhook 200)
		let userId: string | null = null;
		try {
			userId = ensureUserExists(senderRaw, WebhookServer.dbInstance);
		} catch (e) {
			if (process.env.NODE_ENV !== 'test') {
				console.error('⚠️ Error ensuring user exists, ignoring message:', e);
			}
			return;
		}
		if (!userId) {
			if (process.env.NODE_ENV !== 'test') {
				console.log('⚠️ Failed to ensure user exists, ignoring message');
			}
			return;
		}
		const messageTextTrimmed = messageText.trim();
		const isAdminCmd = messageTextTrimmed.startsWith('/admin');
		// Etapa 2: Descubrimiento seguro de grupos (modo 'discover')
		if (isGroupId(remoteJid)) {
			try { (AllowedGroups as any).dbInstance = WebhookServer.dbInstance; } catch {}
			const gatingMode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
			if (gatingMode === 'discover') {
				try {
					const exists = WebhookServer.dbInstance
						.prepare(`SELECT 1 FROM allowed_groups WHERE group_id = ? LIMIT 1`)
						.get(remoteJid) as any;
					if (!exists) {
						try { await GroupSyncService.ensureGroupLabelAndName(remoteJid); } catch {}
						try { (AllowedGroups as any).dbInstance = WebhookServer.dbInstance; } catch {}
						try { AllowedGroups.upsertPending(remoteJid, (GroupSyncService.activeGroupsCache.get(remoteJid) || null), normalizedSenderId); } catch {}
						try { Metrics.inc('unknown_groups_discovered_total'); } catch {}
						try {
							const notify = String(process.env.NOTIFY_ADMINS_ON_DISCOVERY || 'false').toLowerCase() === 'true';
							if (notify && !isAdminCmd) {
								const admins = AdminService.getAdmins();
								if (admins.length > 0) {
									const info = remoteJid;
									const msg = `🔎 Nuevo grupo detectado: ${info}\nEstado: pending.\nUsa /admin habilitar-aquí desde el grupo o /admin allow-group ${info}.`;
									await ResponseQueue.add(admins.map(a => ({ recipient: a, message: msg })));
								}
							}
						} catch {}
						if (!isAdminCmd) return;
					}
				} catch {
					// Si la tabla no existe por alguna razón, intentar upsert y retornar igualmente
					try { await GroupSyncService.ensureGroupLabelAndName(remoteJid); } catch {}
					try { (AllowedGroups as any).dbInstance = WebhookServer.dbInstance; } catch {}
					try { AllowedGroups.upsertPending(remoteJid, (GroupSyncService.activeGroupsCache.get(remoteJid) || null), normalizedSenderId); } catch {}
					try { Metrics.inc('unknown_groups_discovered_total'); } catch {}
					try {
						const notify = String(process.env.NOTIFY_ADMINS_ON_DISCOVERY || 'false').toLowerCase() === 'true';
						if (notify && !isAdminCmd) {
							const admins = AdminService.getAdmins();
							if (admins.length > 0) {
								const info = remoteJid;
								const msg = `🔎 Nuevo grupo detectado: ${info}\nEstado: pending.\nUsa /admin habilitar-aquí desde el grupo o /admin allow-group ${info}.`;
								await ResponseQueue.add(admins.map(a => ({ recipient: a, message: msg })));
							}
						}
					} catch {}
					if (!isAdminCmd) return;
				}
			}
		}
		// Etapa 3: Gating en modo 'enforce' — ignorar mensajes de grupos no permitidos
		if (isGroupId(remoteJid)) {
			try { (AllowedGroups as any).dbInstance = WebhookServer.dbInstance; } catch {}
			const gatingMode2 = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
			if (gatingMode2 === 'enforce') {
				try {
					const allowed = AllowedGroups.isAllowed(remoteJid);
					if (!allowed && !isAdminCmd) {
						try { Metrics.inc('messages_blocked_group_total'); } catch {}
						return;
					}
				} catch {
					// Si falla el check por cualquier motivo, ser conservadores y permitir
				}
			}
		}
		// Manejo de comandos de administración (/admin) antes de cualquier otra lógica de grupo
		if (messageTextTrimmed.startsWith('/admin')) {
			try { (AdminService as any).dbInstance = WebhookServer.dbInstance; } catch {}
			try { (AllowedGroups as any).dbInstance = WebhookServer.dbInstance; } catch {}
			const adminResponses = await AdminService.handle({
				sender: normalizedSenderId,
				groupId: remoteJid,
				message: messageText
			});
			if (adminResponses.length > 0) {
				await ResponseQueue.add(adminResponses);
			}
			return;
		}
		// Check/ensure group exists (allow DMs always)
		if (isGroupId(data.key.remoteJid) && !GroupSyncService.isGroupActive(data.key.remoteJid)) {
			// En tests, mantener comportamiento anterior: ignorar mensajes de grupos inactivos
			if (process.env.NODE_ENV === 'test') {
				return;
			}
			if (process.env.NODE_ENV !== 'test') {
				console.log('ℹ️ Group not active in cache — ensuring group and triggering quick members sync');
			}
			try {
				GroupSyncService.ensureGroupExists(data.key.remoteJid);
				GroupSyncService.refreshActiveGroupsCache();
				await GroupSyncService.syncMembersForGroup(data.key.remoteJid);
			} catch (e) {
				if (process.env.NODE_ENV !== 'test') {
					console.error('⚠️ Failed to ensure/sync group on-the-fly:', e);
				}
			}
		}
		// Forward to command service only if it's a text-ish message and starts with /t or /tarea
		// messageTextTrimmed computed earlier
		if (messageTextTrimmed.startsWith('/tarea') || messageTextTrimmed.startsWith('/t')) {
			// Rate limiting básico por usuario (desactivado en tests)
			if (process.env.NODE_ENV !== 'test') {
				const allowed = RateLimiter.checkAndConsume(normalizedSenderId);
				if (!allowed) {
					// Notificar como máximo una vez por minuto
					if (RateLimiter.shouldNotify(normalizedSenderId)) {
						await ResponseQueue.add([{
							recipient: normalizedSenderId,
							message: `Has superado el límite de ${((() => { const v = Number(process.env.RATE_LIMIT_PER_MIN); return Number.isFinite(v) && v > 0 ? v : 15; })())} comandos por minuto. Inténtalo de nuevo en un momento.`
						}]);
					}
					return;
				}
			}
			// Extraer menciones desde el mensaje (varios formatos)
			const mentions = data.message?.contextInfo?.mentionedJid
				|| data.message?.extendedTextMessage?.contextInfo?.mentionedJid
				|| data.message?.imageMessage?.contextInfo?.mentionedJid
				|| data.message?.videoMessage?.contextInfo?.mentionedJid
				|| [];
			// Asegurar que CommandService y TaskService usen la misma DB (tests/producción)
			(CommandService as any).dbInstance = WebhookServer.dbInstance;
			(TaskService as any).dbInstance = WebhookServer.dbInstance;
			// Delegar el manejo del comando
			const responses = await CommandService.handle({
				sender: normalizedSenderId,
				groupId: data.key.remoteJid,
				message: messageText,
				mentions
			});
			// Encolar respuestas si las hay
			if (responses.length > 0) {
				await ResponseQueue.add(responses);
			}
		}
	}
	static validateEnv() {
		console.log('ℹ️ Checking environment variables...');
		console.log('EVOLUTION_API_URL:', process.env.EVOLUTION_API_URL ? '***' : 'MISSING');
		console.log('EVOLUTION_API_INSTANCE:', process.env.EVOLUTION_API_INSTANCE || 'MISSING');
		console.log('WEBHOOK_URL:', process.env.WEBHOOK_URL ? `${process.env.WEBHOOK_URL.substring(0, 20)}...` : 'NOT SET');
		console.log('WHATSAPP_COMMUNITY_ID:', process.env.WHATSAPP_COMMUNITY_ID ? '***' : 'NOT SET (se mostrarán comunidades disponibles)');
		const missing = REQUIRED_ENV.filter(v => !process.env[v]);
		if (missing.length) {
			console.error('❌ Missing required environment variables:');
			missing.forEach(v => console.error(`- ${v}`));
			console.error('Add these to your CapRover environment configuration');
			process.exit(1);
		}
		if (process.env.CHATBOT_PHONE_NUMBER &&
			!/^\d+$/.test(process.env.CHATBOT_PHONE_NUMBER)) {
			console.error('❌ CHATBOT_PHONE_NUMBER must contain only digits');
			process.exit(1);
		}
	}
	static async start() {
		this.validateEnv();
		// Run database migrations (up-only) before starting services
		await Migrator.migrateToLatest(this.dbInstance);
		// Etapa 7: seed inicial de grupos permitidos desde ALLOWED_GROUPS (best-effort)
		try { (AllowedGroups as any).dbInstance = this.dbInstance; } catch {}
		try { AllowedGroups.seedFromEnv(); } catch {}
		const PORT = process.env.PORT || '3007';
		console.log('✅ Environment variables validated');
		if (process.env.NODE_ENV !== 'test') {
			try {
				await WebhookManager.registerWebhook();
				// Add small delay to allow webhook to propagate
				await new Promise(resolve => setTimeout(resolve, 1000));
				const isActive = await WebhookManager.verifyWebhook();
				if (!isActive) {
					console.error('❌ Webhook verification failed - retrying in 2 seconds...');
					await new Promise(resolve => setTimeout(resolve, 2000));
					const isActiveRetry = await WebhookManager.verifyWebhook();
					if (!isActiveRetry) {
						console.error('❌ Webhook verification failed after retry');
						process.exit(1);
					}
				}
				// Initialize groups - critical for operation
				await GroupSyncService.checkInitialGroups();
				// Start groups scheduler (periodic sync of groups)
				try {
					GroupSyncService.startGroupsScheduler();
					console.log('✅ Group scheduler started');
				} catch (e) {
					console.error('⚠️ Failed to start Group scheduler:', e);
				}
				// Initial members sync (non-blocking if fails)
				try {
					await GroupSyncService.syncMembersForActiveGroups();
					GroupSyncService.startMembersScheduler();
					console.log('✅ Group members scheduler started');
				} catch (e) {
					console.error('⚠️ Failed to run initial members sync or start scheduler:', e);
				}
				// Start response queue worker (background)
				try {
					await ResponseQueue.process();
					console.log('✅ ResponseQueue worker started');
					// Start cleanup scheduler (daily retention)
					ResponseQueue.startCleanupScheduler();
					console.log('✅ ResponseQueue cleanup scheduler started');
					RemindersService.start();
					console.log('✅ RemindersService started');
				} catch (e) {
					console.error('❌ Failed to start ResponseQueue worker or cleanup scheduler:', e);
				}
				// Mantenimiento (cleanup de miembros inactivos)
				try {
					MaintenanceService.start();
					console.log('✅ MaintenanceService started');
				} catch (e) {
					console.error('⚠️ Failed to start MaintenanceService:', e);
				}
			} catch (error) {
				console.error('❌ Failed to setup webhook:', error instanceof Error ? error.message : error);
				process.exit(1);
			}
		}
		const server = Bun.serve({
			port: parseInt(PORT),
			fetch: (request) => WebhookServer.handleRequest(request)
		});
		console.log(`Server running on port ${PORT}`);
		return server;
	}
}