//// <reference types="bun-types" />
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<Response> {
		// 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 {
						const res = await GroupSyncService.syncGroups();
						GroupSyncService.refreshActiveGroupsCache();
						const changed = GroupSyncService.getLastChangedActive();
						if (changed.length > 0) {
							await GroupSyncService.syncMembersForGroups(changed);
						} else {
							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 (process.env.NODE_ENV !== 'test') {
						console.log('[A0] message.key participants', {
							participant: p,
							participantAlt: pAlt,
							normalized_participant: n,
							normalized_participantAlt: nAlt,
							alias_upsert: !!(nAlt && n && nAlt !== n)
						});
					}
					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');

		// A4: Primer DM "activar" — alta/confirmación idempotente (solo en DM)
		if (!isGroupId(remoteJid) && messageTextTrimmed === 'activar') {
			const base = (process.env.WEB_BASE_URL || '').trim();
			const msg = base
				? "Listo, ya puedes reclamar/ser responsable y acceder a la web. Para acceder a la web, envía '/t web' y abre el enlace."
				: "Listo, ya puedes reclamar/ser responsable.";
			try {
				await ResponseQueue.add([{ recipient: normalizedSenderId, message: msg }]);
			} catch {}
			return;
		}

		// 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 (no immediate members sync)');
			}
			try {
				GroupSyncService.ensureGroupExists(data.key.remoteJid);
				try { GroupSyncService.upsertMemberSeen(data.key.remoteJid, normalizedSenderId); } catch {}
			} catch (e) {
				if (process.env.NODE_ENV !== 'test') {
					console.error('⚠️ Failed to ensure 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 messageId = typeof data?.key?.id === 'string' ? data.key.id : null;
			const participantForKey = typeof data?.key?.participantAlt === 'string'
				? data.key.participantAlt
				: (typeof data?.key?.participant === 'string' ? data.key.participant : null);
			const outcome = await CommandService.handleWithOutcome({
				sender: normalizedSenderId,
				groupId: data.key.remoteJid,
				message: messageText,
				mentions,
				messageId: messageId || undefined,
				participant: participantForKey || undefined,
				fromMe: !!data?.key?.fromMe
			});
			const responses = outcome.responses;

			// Encolar respuestas si las hay
			if (responses.length > 0) {
				await ResponseQueue.add(responses);
			}

			// Reaccionar al mensaje del comando con outcome explícito
			try {
				const reactionsEnabled = String(process.env.REACTIONS_ENABLED || 'false').toLowerCase();
				const enabled = ['true','1','yes','on'].includes(reactionsEnabled);
				if (!enabled) return;

				if (!messageId) return;

				const scope = String(process.env.REACTIONS_SCOPE || 'groups').toLowerCase();
				const isGroup = isGroupId(data.key.remoteJid);
				if (scope !== 'all' && !isGroup) return;

				// Respetar gating 'enforce'
				try { (AllowedGroups as any).dbInstance = WebhookServer.dbInstance; } catch {}
				const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
				if (mode === 'enforce' && isGroup) {
					try {
						if (!AllowedGroups.isAllowed(data.key.remoteJid)) {
							return;
						}
					} catch {}
				}

				const emoji = outcome.ok ? '🤖' : '⚠️';
				const participant = typeof data?.key?.participantAlt === 'string'
					? data.key.participantAlt
					: (typeof data?.key?.participant === 'string' ? data.key.participant : undefined);
				await ResponseQueue.enqueueReaction(data.key.remoteJid, messageId, emoji, { participant, fromMe: !!data?.key?.fromMe });
			} catch (e) {
				// No romper el flujo por errores de reacción
				if (process.env.NODE_ENV !== 'test') {
					console.warn('⚠️ Reaction enqueue failed:', e);
				}
			}
		}
	}

	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');
		// A0: pre-crear contadores para que aparezcan en /metrics
		try {
			Metrics.inc('onboarding_prompts_sent_total', 0);
			Metrics.inc('onboarding_prompts_skipped_total', 0);
			Metrics.inc('onboarding_assign_failures_total', 0);

			// Precalentar métricas de reacciones por emoji
			for (const emoji of ['robot', 'warn', 'check', 'other']) {
				Metrics.inc('reactions_enqueued_total', 0, { emoji });
				Metrics.inc('reactions_sent_total', 0, { emoji });
				Metrics.inc('reactions_failed_total', 0, { emoji });
			}
		} catch {}

		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');
					// Ejecutar reconciliación de alias una vez al arranque (one-shot)
					try {
						await MaintenanceService.reconcileAliasUsersOnce();
						console.log('✅ MaintenanceService: reconciliación de alias ejecutada (one-shot)');
					} catch (e2) {
						console.error('⚠️ Failed to run alias reconciliation one-shot:', e2);
					}
				} 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;
	}
}
