import { GoogleGenerativeAI, HarmCategory, HarmBlockThreshold } from "@google/generative-ai";
import { GoogleGenAI, PersonGeneration } from "@google/genai";
import logger from "../lib/logger";
import { Collection } from "discord.js";
import { prisma } from "../database/prisma";
import { getDatabases, APPWRITE_DATABASE_ID, APPWRITE_COLLECTION_AI_CONVERSATIONS_ID, isAIConversationsConfigured } from "../api/appwrite";
import { ensureAIConversationsSchema } from "../api/aiConversationsSchema";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const sdk: any = require('node-appwrite');

// Tipos mejorados para mejor type safety
interface ConversationContext {
    messages: Array<{
        role: 'user' | 'assistant';
        content: string;
        timestamp: number;
        tokens: number;
        messageId?: string; // ID del mensaje de Discord
        referencedMessageId?: string; // ID del mensaje al que responde
    }>;
    totalTokens: number;
    imageRequests: number;
    lastActivity: number;
    userId: string;
    guildId?: string;
    channelId?: string;
    conversationId?: string; // ID único de la conversación
}

interface AIRequest {
    userId: string;
    guildId?: string;
    channelId?: string;
    prompt: string;
    priority: 'low' | 'normal' | 'high';
    timestamp: number;
    resolve: (value: string) => void;
    reject: (error: Error) => void;
    aiRolePrompt?: string;
    meta?: string;
    messageId?: string;
    referencedMessageId?: string;
}

interface AppwriteConversation {
    $id?: string;
    userId: string;
    guildId?: string | null;
    channelId?: string | null;
    conversationId: string;
    messagesJson?: string; // JSON serializado del historial
    lastActivity: string; // ISO
    createdAt: string; // ISO
}

// Utility function para manejar errores de forma type-safe
function getErrorMessage(error: unknown): string {
    if (error instanceof Error) {
        return error.message;
    }
    if (typeof error === 'string') {
        return error;
    }
    if (error && typeof error === 'object' && 'message' in error) {
        return String((error as any).message);
    }
    return 'Error desconocido';
}

// Type guard para verificar si es un Error
function isError(error: unknown): error is Error {
    return error instanceof Error;
}

// Type guard para verificar errores de API específicos
function isAPIError(error: unknown): error is { message: string; code?: string } {
    return (
        error !== null &&
        typeof error === 'object' &&
        'message' in error &&
        typeof (error as any).message === 'string'
    );
}

const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));

function isServiceUnavailableError(error: unknown): boolean {
    if (!error) {
        return false;
    }

    const message = getErrorMessage(error).toLowerCase();

    if (
        message.includes('503') ||
        message.includes('service unavailable') ||
        message.includes('model is overloaded') ||
        message.includes('model estuvo sobrecargado') ||
        message.includes('overloaded') ||
        message.includes('temporarily unavailable')
    ) {
        return true;
    }

    const status = (error as any)?.status ?? (error as any)?.statusCode ?? (error as any)?.code;
    if (typeof status === 'number' && status === 503) {
        return true;
    }
    if (typeof status === 'string' && status.includes('503')) {
        return true;
    }

    if (isAPIError(error)) {
        const apiMessage = error.message.toLowerCase();
        return (
            apiMessage.includes('503') ||
            apiMessage.includes('service unavailable') ||
            apiMessage.includes('overloaded')
        );
    }

    return false;
}

export class AIService {
    private genAI: GoogleGenerativeAI;
    private genAIv2: any;
    // Cache del modelo de imágenes detectado
    private imageModelName?: string | null;
    private conversations = new Collection<string, ConversationContext>();
    private requestQueue: AIRequest[] = [];
    private processing = false;
    private userCooldowns = new Collection<string, number>();
    private rateLimitTracker = new Collection<string, { count: number; resetTime: number }>();
    // Cache de configuración por guild
    private guildPromptCache = new Collection<string, { prompt: string | null; fetchedAt: number }>();

    // Configuración mejorada y escalable
    private readonly config = {
        maxInputTokens: 1048576,      // 1M tokens Gemini 2.5 Flash (entrada)
        maxOutputTokens: 65536,       // 65,536 salida (según aclaración del usuario para preview 09-2025)
        tokenResetThreshold: 0.80,    // Más conservador
        maxConversationAge: 30 * 60 * 1000, // 30 minutos
        maxMessageHistory: 8,         // Reducido para mejor memoria
        cooldownMs: 3000,            // 3 segundos entre requests
        maxImageRequests: 3,         // Reducido para evitar spam
        requestTimeout: 30000,       // 30 segundos timeout
        maxConcurrentRequests: 3,    // Máximo 3 requests simultáneos
        rateLimitWindow: 60000,      // 1 minuto
        rateLimitMax: 20,           // 20 requests por minuto por usuario
        cleanupInterval: 5 * 60 * 1000, // Limpiar cada 5 minutos
        guildConfigTTL: 5 * 60 * 1000, // 5 minutos de cache para prompts de guild
    } as const;

    constructor() {
        const apiKey = process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY;
        if (!apiKey) {
            throw new Error('Falta la clave de Google AI. Define GOOGLE_AI_API_KEY o GEMINI_API_KEY en las variables de entorno.');
        }

        this.genAI = new GoogleGenerativeAI(apiKey);

        try {
            this.genAIv2 = new GoogleGenAI({ apiKey });
            logger.info('GoogleGenAI v2 inicializado correctamente para generación de imágenes');
        } catch (e) {
            logger.warn(`GoogleGenAI v2 no pudo inicializarse: ${getErrorMessage(e)}`);
            this.genAIv2 = null;
        }

        // Permitir override de modelo por variable de entorno
        const envImageModel = process.env.GENAI_IMAGE_MODEL;
        if (envImageModel && envImageModel.trim()) {
            this.imageModelName = envImageModel.trim();
            logger.info({ model: this.imageModelName }, 'Modelo de imágenes fijado por GENAI_IMAGE_MODEL');
        }

        this.startQueueProcessor();
        this.startCleanupService();
        this.detectImageModel();
    }

    /**
     * Auto-detectar modelo de imagen disponible
     */
    private async detectImageModel(): Promise<string | null> {
        if (!this.genAIv2) {
            logger.warn('GoogleGenAI v2 no disponible; sin soporte para imágenes');
            return null;
        }

        // Lista de candidatos de modelos de imagen ordenados por preferencia (Imagen 4.0 primero, con retrocompatibilidad)
        const candidates = [
            'models/imagen-4.0-generate-001',
            'imagen-4.0-generate-001',
            'models/imagen-3.0-fast',
            'imagen-3.0-fast',
            'models/imagen-3.0',
            'imagen-3.0',
            'models/gemini-2.5-flash-image',
            'gemini-2.5-flash-image',
        ];

        // Intentar listar modelos primero
        try {
            const listed: any = await (this.genAIv2 as any).models?.listModels?.();
            if (listed?.models && Array.isArray(listed.models)) {
                const models: string[] = listed.models
                    .map((m: any) => m?.name || m?.model || m?.id || m?.displayName)
                    .filter(Boolean);

                logger.debug({ availableModels: models }, 'Modelos disponibles detectados');

                // Buscar modelos de imagen disponibles
                const imageModels = models.filter((id: string) =>
                    /imagen|image|generate|vision/i.test(id) &&
                    !/text|chat|embed|code/i.test(id)
                );

                if (imageModels.length > 0) {
                    // Priorizar según orden de candidatos
                    for (const candidate of candidates) {
                        const candidateBase = candidate.replace(/^models\//, '');
                        const found = imageModels.find(m =>
                            m === candidate ||
                            m === candidateBase ||
                            m.includes(candidateBase)
                        );
                        if (found) {
                            this.imageModelName = found;
                            logger.info({ model: found, source: 'listModels' }, 'Modelo de imágenes detectado automáticamente');
                            return found;
                        }
                    }

                    // Si no coincide con candidatos conocidos, usar el primero disponible
                    this.imageModelName = imageModels[0];
                    logger.info({ model: imageModels[0], source: 'listModels-fallback' }, 'Modelo de imágenes detectado (fallback)');
                    return imageModels[0];
                }
            }
        } catch (e) {
            logger.debug({ err: getErrorMessage(e) }, 'listModels no disponible');
        }

        // Fallback: probar modelos uno por uno
        for (const candidate of candidates) {
            try {
                await (this.genAIv2 as any).models.generateImages({
                    model: candidate,
                    prompt: 'test',
                    config: {
                        numberOfImages: 1,
                        outputMimeType: 'image/jpeg',
                        aspectRatio: '1:1',
                        imageSize: '1K',
                    }
                });

                // Si no lanza error, el modelo existe
                this.imageModelName = candidate;
                logger.info({ model: candidate, source: 'direct-test' }, 'Modelo de imágenes detectado por prueba directa');
                return candidate;
            } catch (e: any) {
                const msg = getErrorMessage(e);
                if (msg.includes('not found') || msg.includes('404')) {
                    continue; // Modelo no disponible, probar siguiente
                }
                // Otros errores pueden indicar que el modelo existe pero falló por otra razón
                logger.debug({ candidate, err: msg }, 'Modelo podría existir pero falló la prueba');
            }
        }

        // No se encontró ningún modelo de imagen
        this.imageModelName = null;
        logger.warn('No se detectó ningún modelo de imagen disponible');
        return null;
    }

    /**
     * Obtener prompt de rol de IA por guild con caché
     */
    public async getGuildAiPrompt(guildId: string): Promise<string | null> {
        try {
            const cached = this.guildPromptCache.get(guildId);
            const now = Date.now();
            if (cached && (now - cached.fetchedAt) < this.config.guildConfigTTL) {
                return cached.prompt;
            }
            // @ts-ignore
            const guild = await prisma.guild.findUnique({ where: { id: guildId }, select: { aiRolePrompt: true } });
            //@ts-ignore
            const prompt = guild?.aiRolePrompt ?? null;
            this.guildPromptCache.set(guildId, { prompt, fetchedAt: now });
            return prompt;
        } catch (e) {
            logger.warn(`No se pudo cargar aiRolePrompt para guild ${guildId}: ${getErrorMessage(e)}`);
            return null;
        }
    }

    /**
     * Invalidar cache de configuración de un guild (llamar tras guardar cambios)
     */
    public invalidateGuildConfig(guildId: string): void {
        this.guildPromptCache.delete(guildId);
    }

    /**
     * Procesa una request de IA de forma asíncrona y controlada
     */
    async processAIRequest(
        userId: string, 
        prompt: string, 
        guildId?: string,
        priority: 'low' | 'normal' | 'high' = 'normal',
        options?: { aiRolePrompt?: string; meta?: string }
    ): Promise<string> {
        // Validaciones exhaustivas
        if (!prompt?.trim()) {
            throw new Error('El prompt no puede estar vacío');
        }

        if (prompt.length > 4000) {
            throw new Error('El prompt excede el límite de 4000 caracteres');
        }

        // Rate limiting por usuario
        if (!this.checkRateLimit(userId)) {
            throw new Error('Has excedido el límite de requests. Espera un momento.');
        }

        // Cooldown entre requests
        const lastRequest = this.userCooldowns.get(userId) || 0;
        const timeSinceLastRequest = Date.now() - lastRequest;
        
        if (timeSinceLastRequest < this.config.cooldownMs) {
            const waitTime = Math.ceil((this.config.cooldownMs - timeSinceLastRequest) / 1000);
            throw new Error(`Debes esperar ${waitTime} segundos antes de hacer otra consulta`);
        }

        // Agregar a la queue con Promise
        return new Promise((resolve, reject) => {
            const request: AIRequest = {
                userId,
                guildId,
                prompt: prompt.trim(),
                priority,
                timestamp: Date.now(),
                resolve,
                reject,
                aiRolePrompt: options?.aiRolePrompt,
                meta: options?.meta,
            };

            // Insertar según prioridad
            if (priority === 'high') {
                this.requestQueue.unshift(request);
            } else {
                this.requestQueue.push(request);
            }

            // Timeout automático
            setTimeout(() => {
                const index = this.requestQueue.findIndex(r => r === request);
                if (index !== -1) {
                    this.requestQueue.splice(index, 1);
                    reject(new Error('Request timeout: La solicitud tardó demasiado tiempo'));
                }
            }, this.config.requestTimeout);

            this.userCooldowns.set(userId, Date.now());
        });
    }

    /**
     * Procesador de queue mejorado con control de concurrencia
     */
    private async startQueueProcessor(): Promise<void> {
        setInterval(async () => {
            if (this.processing || this.requestQueue.length === 0) return;
            
            this.processing = true;
            
            try {
                // Procesar hasta 3 requests simultáneamente
                const batch = this.requestQueue.splice(0, this.config.maxConcurrentRequests);
                
                await Promise.allSettled(
                    batch.map(request => this.processRequest(request))
                );
            } catch (error) {
                // Usar nuestro helper para manejar el error de forma type-safe
                const errorMessage = getErrorMessage(error);
                logger.error(`Error en el procesador de queue: ${errorMessage}`);

                // Si necesitamos más detalles del error, podemos usar type guards
                if (isError(error) && error.stack) {
                    logger.error(`Stack trace: ${error.stack}`);
                }
            } finally {
                this.processing = false;
            }
        }, 1000); // Revisar cada segundo
    }

    /**
     * Resetear conversación
     */
    private resetConversation(userId: string, guildId?: string): void {
        const key = `${userId}-${guildId || 'dm'}`;
        this.conversations.delete(key);
    }

    /**
     * Servicio de limpieza automática
     */
    private startCleanupService(): void {
        setInterval(() => {
            const now = Date.now();
            const toDelete: string[] = [];

            this.conversations.forEach((context, key) => {
                if (now - context.lastActivity > this.config.maxConversationAge) {
                    toDelete.push(key);
                }
            });

            toDelete.forEach(key => this.conversations.delete(key));

            if (toDelete.length > 0) {
                logger.info(`Limpieza automática: ${toDelete.length} conversaciones expiradas eliminadas`);
            }
        }, this.config.cleanupInterval);
    }

    /**
     * Parser mejorado de errores de API - Type-safe sin ts-ignore
     */
    private parseAPIError(error: unknown): string {
        // Extraer mensaje de forma type-safe
        const message = getErrorMessage(error).toLowerCase();

        // Verificar si es un error de API estructurado
        if (isAPIError(error)) {
            const apiMessage = error.message.toLowerCase();

            if (apiMessage.includes('api key') || apiMessage.includes('authentication')) {
                return 'Error de autenticación con la API de IA';
            }
            if (apiMessage.includes('quota') || apiMessage.includes('exceeded')) {
                return 'Se ha alcanzado el límite de uso de la API. Intenta más tarde';
            }
            if (apiMessage.includes('service unavailable') || apiMessage.includes('overloaded') || apiMessage.includes('503')) {
                return 'El servicio de IA está saturado. Intenta de nuevo en unos segundos';
            }
            if (apiMessage.includes('safety') || apiMessage.includes('blocked')) {
                return 'Tu mensaje fue bloqueado por las políticas de seguridad';
            }
            if (apiMessage.includes('timeout') || apiMessage.includes('deadline')) {
                return 'La solicitud tardó demasiado tiempo. Intenta de nuevo';
            }
            if (apiMessage.includes('model not found')) {
                return 'El modelo de IA no está disponible en este momento';
            }
            if (apiMessage.includes('token') || apiMessage.includes('length')) {
                return 'El mensaje excede los límites permitidos';
            }
        }

        // Manejo genérico para otros tipos de errores
        if (message.includes('api key') || message.includes('authentication')) {
            return 'Error de autenticación con la API de IA';
        }
        if (message.includes('quota') || message.includes('exceeded')) {
            return 'Se ha alcanzado el límite de uso de la API. Intenta más tarde';
        }
        if (message.includes('service unavailable') || message.includes('overloaded') || message.includes('503')) {
            return 'El servicio de IA está saturado. Intenta de nuevo en unos segundos';
        }
        if (message.includes('safety') || message.includes('blocked')) {
            return 'Tu mensaje fue bloqueado por las políticas de seguridad';
        }
        if (message.includes('timeout') || message.includes('deadline')) {
            return 'La solicitud tardó demasiado tiempo. Intenta de nuevo';
        }
        if (message.includes('model not found')) {
            return 'El modelo de IA no está disponible en este momento';
        }
        if (message.includes('token') || message.includes('length')) {
            return 'El mensaje excede los límites permitidos';
        }

        return 'Error temporal del servicio de IA. Intenta de nuevo';
    }

    private async generateContentWithRetries(model: any, content: any, options?: {
        maxAttempts?: number;
        baseDelayMs?: number;
        maxDelayMs?: number;
    }): Promise<any> {
        const {
            maxAttempts = 3,
            baseDelayMs = 1200,
            maxDelayMs = 10_000
        } = options ?? {};

        let lastError: unknown;

        for (let attempt = 0; attempt < maxAttempts; attempt++) {
            try {
                return await model.generateContent(content);
            } catch (error) {
                lastError = error;
                const isRetryable = isServiceUnavailableError(error);
                const isLastAttempt = attempt === maxAttempts - 1;

                if (!isRetryable || isLastAttempt) {
                    throw error;
                }

                const backoff = Math.min(maxDelayMs, Math.floor(baseDelayMs * Math.pow(2, attempt)));
                const jitter = Math.floor(Math.random() * Math.max(200, Math.floor(baseDelayMs / 2)));
                const waitMs = backoff + jitter;

                logger.warn(
                    { attempt: attempt + 1, waitMs },
                    `Gemini respondió 503 (overloaded). Reintentando en ${waitMs}ms (intento ${attempt + 2}/${maxAttempts})`
                );

                await sleep(waitMs);
            }
        }

        throw lastError ?? new Error('Error desconocido al generar contenido con Gemini');
    }

    /**
     * Procesa una request de IA con soporte para conversaciones y memoria persistente
     */
    async processAIRequestWithMemory(
        userId: string,
        prompt: string,
        guildId?: string,
        channelId?: string,
        messageId?: string,
        referencedMessageId?: string,
        client?: any,
        priority: 'low' | 'normal' | 'high' = 'normal',
        options?: { aiRolePrompt?: string; meta?: string; attachments?: any[] }
    ): Promise<string> {
        // Validaciones exhaustivas
        if (!prompt?.trim()) {
            throw new Error('El prompt no puede estar vacío');
        }

        if (prompt.length > 4000) {
            throw new Error('El prompt excede el límite de 4000 caracteres');
        }

        // Rate limiting por usuario
        if (!this.checkRateLimit(userId)) {
            throw new Error('Has excedido el límite de requests. Espera un momento.');
        }

        // Cooldown entre requests
        const lastRequest = this.userCooldowns.get(userId) || 0;
        const timeSinceLastRequest = Date.now() - lastRequest;

        if (timeSinceLastRequest < this.config.cooldownMs) {
            const waitTime = Math.ceil((this.config.cooldownMs - timeSinceLastRequest) / 1000);
            throw new Error(`Debes esperar ${waitTime} segundos antes de hacer otra consulta`);
        }

        // Agregar a la queue con Promise
        return new Promise((resolve, reject) => {
            const request: AIRequest & { client?: any; attachments?: any[] } = {
                userId,
                guildId,
                channelId,
                prompt: prompt.trim(),
                priority,
                timestamp: Date.now(),
                resolve,
                reject,
                aiRolePrompt: options?.aiRolePrompt,
                meta: options?.meta,
                messageId,
                referencedMessageId,
                client,
                attachments: options?.attachments
            };

            // Insertar según prioridad
            if (priority === 'high') {
                this.requestQueue.unshift(request);
            } else {
                this.requestQueue.push(request);
            }

            // Timeout automático
            setTimeout(() => {
                const index = this.requestQueue.findIndex(r => r === request);
                if (index !== -1) {
                    this.requestQueue.splice(index, 1);
                    reject(new Error('Request timeout: La solicitud tardó demasiado tiempo'));
                }
            }, this.config.requestTimeout);

            this.userCooldowns.set(userId, Date.now());
        });
    }

    /**
     * Lista modelos de imagen visibles por la clave (si el SDK lo permite)
     */
    public async listImageModels(): Promise<string[]> {
        if (!this.genAIv2 || !(this.genAIv2 as any).models?.listModels) return [];
        try {
            const listed: any = await (this.genAIv2 as any).models.listModels();
            const models: string[] = Array.isArray(listed?.models)
                ? listed.models.map((m: any) => m?.name || m?.model || m?.id).filter(Boolean)
                : [];
            // Filtrar a modelos de imagen de forma heurística
            return models.filter((id) => /imagen|image/i.test(id));
        } catch {
            return [];
        }
    }

    // Override manual del modelo de imágenes (útil para runtime)
    public setImageModel(model: string | null | undefined): void {
        this.imageModelName = model ?? null;
        if (this.imageModelName) {
            logger.info({ model: this.imageModelName }, 'Modelo de imágenes fijado manualmente');
        } else {
            logger.info('Modelo de imágenes reseteado; se volverá a detectar automáticamente');
        }
    }

    /**
     * Detectar si hay imágenes adjuntas en el mensaje para análisis
     */
    public hasImageAttachments(attachments?: any[]): boolean {
        if (!attachments || attachments.length === 0) return false;

        const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
        const imageMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'];

        return attachments.some(attachment => {
            const hasImageExtension = imageExtensions.some(ext =>
                attachment.name?.toLowerCase().endsWith(ext)
            );
            const hasImageMimeType = imageMimeTypes.includes(attachment.contentType?.toLowerCase());

            return hasImageExtension || hasImageMimeType;
        });
    }

    /**
     * Procesar imágenes adjuntas para análisis con Gemini Vision
     */
    private async processImageAttachments(attachments: any[]): Promise<any[]> {
        const imageAttachments: Array<{ inlineData: { data: string; mimeType: string } }> = [];

        for (const attachment of attachments) {
            if (this.hasImageAttachments([attachment])) {
                try {
                    // Descargar la imagen
                    const response = await fetch(attachment.url);
                    if (!response.ok) {
                        logger.warn(`Error descargando imagen: ${response.statusText}`);
                        continue;
                    }

                    const arrayBuffer = await response.arrayBuffer();
                    const base64Data = Buffer.from(arrayBuffer).toString('base64');

                    // Determinar el tipo MIME
                    let mimeType = attachment.contentType || 'image/png';
                    if (!mimeType.startsWith('image/')) {
                        // Inferir del nombre del archivo
                        const ext = attachment.name?.toLowerCase().split('.').pop();
                        switch (ext) {
                            case 'jpg':
                            case 'jpeg':
                                mimeType = 'image/jpeg';
                                break;
                            case 'png':
                                mimeType = 'image/png';
                                break;
                            case 'gif':
                                mimeType = 'image/gif';
                                break;
                            case 'webp':
                                mimeType = 'image/webp';
                                break;
                            default:
                                mimeType = 'image/png';
                        }
                    }

                    imageAttachments.push({
                        inlineData: {
                            data: base64Data,
                            mimeType: mimeType
                        }
                    });

                    logger.info(`Imagen procesada: ${attachment.name} (${mimeType})`);

                } catch (error) {
                    logger.error(`Error procesando imagen ${attachment.name}: ${getErrorMessage(error)}`);
                }
            }
        }

        return imageAttachments;
    }

    /**
     * Procesa una request individual con manejo completo de errores y memoria persistente
     */
    private async processRequest(request: AIRequest): Promise<void> {
        try {
            const { userId, prompt, guildId, channelId, messageId, referencedMessageId } = request;
            const context = await this.getOrCreateContextWithMemory(userId, guildId, channelId);

            // Obtener imágenes adjuntas si existen
            const messageAttachments = (request as any).attachments || [];
            const hasImages = this.hasImageAttachments(messageAttachments);
            const isImageRequest = this.detectImageRequest(prompt);

            if (isImageRequest && context.imageRequests >= this.config.maxImageRequests) {
                const error = new Error(`Has alcanzado el límite de ${this.config.maxImageRequests} solicitudes de imagen. La conversación se ha reiniciado.`);
                request.reject(error);
                return;
            }

            // Verificar límites de tokens
            const estimatedTokens = this.estimateTokens(prompt);
            if (context.totalTokens + estimatedTokens > this.config.maxInputTokens * this.config.tokenResetThreshold) {
                this.resetConversation(userId, guildId);
                logger.info(`Conversación reseteada para usuario ${userId} por límite de tokens`);
            }

            // Obtener prompt del sistema (desde opciones o DB)
            let effectiveAiRolePrompt = request.aiRolePrompt;
            if (effectiveAiRolePrompt === undefined && guildId) {
                effectiveAiRolePrompt = (await this.getGuildAiPrompt(guildId)) ?? undefined;
            }

            // Obtener jerarquía de roles si está en un servidor
            let roleHierarchy = '';
            if (guildId) {
                const client = (request as any).client;
                if (client) {
                    roleHierarchy = await this.getGuildRoleHierarchy(guildId, client);
                }
            }

            // Construir metadatos mejorados
            const enhancedMeta = (request.meta || '') + roleHierarchy;

            // Construir prompt del sistema optimizado
            let systemPrompt = this.buildSystemPrompt(
                prompt,
                context,
                isImageRequest,
                effectiveAiRolePrompt,
                enhancedMeta
            );

            // Procesar imágenes si las hay
            let imageAttachments: any[] = [];
            if (hasImages) {
                imageAttachments = await this.processImageAttachments(messageAttachments);
                if (imageAttachments.length > 0) {
                    systemPrompt = `${systemPrompt}\n\n## Imágenes adjuntas:\nPor favor, analiza las imágenes proporcionadas y responde de acuerdo al contexto.`;
                }
            }

            // Usar gemini-2.5-flash-preview-09-2025 que puede leer imágenes y responder con texto
            const model = this.genAI.getGenerativeModel({
                model: "gemini-2.5-flash",
                generationConfig: {
                    maxOutputTokens: Math.min(this.config.maxOutputTokens, Math.max(1024, estimatedTokens * 0.5)),
                    temperature: 0.7,
                    topP: 0.85,
                    topK: 40,
                },
                safetySettings: [
                    {
                        category: HarmCategory.HARM_CATEGORY_HARASSMENT,
                        threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE
                    },
                    {
                        category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
                        threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE
                    }
                ]
            });

            // Construir el contenido para la API
            let content: any;
            if (hasImages && imageAttachments.length > 0) {
                // Para multimodal (texto + imágenes)
                content = [
                    { text: systemPrompt },
                    ...imageAttachments
                ];
                logger.info(`Procesando ${imageAttachments.length} imagen(es) con Gemini Vision`);
            } else {
                // Solo texto
                content = systemPrompt;
            }

            const result = await this.generateContentWithRetries(model, content);
            const response = await result.response;
            const aiResponse = response.text()?.trim();

            if (!aiResponse) {
                const error = new Error('La IA no generó una respuesta válida');
                request.reject(error);
                return;
            }

            // Actualizar contexto con memoria persistente
            await this.updateContextWithMemory(
                context,
                prompt,
                aiResponse,
                estimatedTokens,
                isImageRequest || hasImages,
                messageId,
                referencedMessageId
            );

            request.resolve(aiResponse);

        } catch (error) {
            // Manejo type-safe de errores sin ts-ignore
            const errorMessage = this.parseAPIError(error);
            const logMessage = getErrorMessage(error);
            logger.error(`Error procesando AI request para ${request.userId}: ${logMessage}`);

            // Log adicional si es un Error con stack trace
            if (isError(error) && error.stack) {
                logger.error(`Stack trace completo: ${error.stack}`);
            }

            request.reject(new Error(errorMessage));
        }
    }

    /**
     * Construcción optimizada del prompt del sistema
     */
    private buildSystemPrompt(
        userPrompt: string,
        context: ConversationContext,
        isImageRequest: boolean,
        aiRolePrompt?: string,
        meta?: string
    ): string {
        const recentMessages = context.messages
            .slice(-4)
            .map(msg => `${msg.role === 'user' ? 'Usuario' : 'Asistente'}: ${msg.content}`)
            .join('\n');

        const roleBlock = aiRolePrompt && aiRolePrompt.trim() ? `\n## Rol del sistema (servidor):\n${aiRolePrompt.trim().slice(0, 1200)}\n` : '';
        const metaBlock = meta && meta.trim() ? `\n## Contexto del mensaje:\n${meta.trim().slice(0, 800)}\n` : '';

        return `Eres una hermana mayor kawaii y cariñosa que habla por Discord. Responde de manera natural, útil y concisa.${roleBlock}${metaBlock}

## Reglas Discord:
- USA **markdown de Discord**: **negrita**, *cursiva*, \`código\`, \`\`\`bloques\`\`\`
- NUNCA uses LaTeX ($$)
- Máximo 2-3 emojis por respuesta
- Prefiere emojis Unicode estándar (🙂, 🎯, etc.) cuando no haya más contexto
- Si se te proporciona una lista de "Emojis personalizados disponibles", puedes usarlos escribiendo :nombre: exactamente como aparece; NO inventes nombres
- Respuestas concisas y claras

${isImageRequest ? `
## Limitación:
- No puedes generar imágenes
- Ofrece ayuda alternativa (descripciones, recursos, etc.)
` : ''}

## Contexto reciente:
${recentMessages || 'Sin historial previo'}

## Consulta actual:
${userPrompt}

Responde de forma directa y útil:`;
    }

    /**
     * Sistema de rate limiting mejorado
     */
    private checkRateLimit(userId: string): boolean {
        const now = Date.now();
        const userLimit = this.rateLimitTracker.get(userId);

        if (!userLimit || now > userLimit.resetTime) {
            this.rateLimitTracker.set(userId, {
                count: 1,
                resetTime: now + this.config.rateLimitWindow
            });
            return true;
        }

        if (userLimit.count >= this.config.rateLimitMax) {
            return false;
        }

        userLimit.count++;
        return true;
    }

    /**
     * Estimación de tokens más precisa
     */
    private estimateTokens(text: string): number {
        // Aproximación mejorada basada en la tokenización real
        const words = text.split(/\s+/).length;
        const chars = text.length;

        // Fórmula híbrida más precisa
        return Math.ceil((words * 1.3) + (chars * 0.25));
    }

    /**
     * Detección mejorada de requests de imagen
     */
    private detectImageRequest(prompt: string): boolean {
        const imageKeywords = [
            'imagen', 'image', 'dibujo', 'draw', 'dibujar',
            'generar imagen', 'create image', 'picture', 'foto',
            'ilustración', 'arte', 'pintura', 'sketch'
        ];

        const lowerPrompt = prompt.toLowerCase();
        return imageKeywords.some(keyword => lowerPrompt.includes(keyword));
    }

    /**
     * Obtener o crear contexto de conversación con carga desde Appwrite
     */
    private async getOrCreateContextWithMemory(userId: string, guildId?: string, channelId?: string): Promise<ConversationContext> {
        const key = `${userId}-${guildId || 'dm'}`;
        let context = this.conversations.get(key);

        if (!context) {
            // Intentar cargar desde Appwrite
            const loadedContext = await this.loadConversationFromAppwrite(userId, guildId, channelId);

            if (loadedContext) {
                context = loadedContext;
            } else {
                // Crear nuevo contexto si no existe en Appwrite
                context = {
                    messages: [],
                    totalTokens: 0,
                    imageRequests: 0,
                    lastActivity: Date.now(),
                    userId,
                    guildId,
                    channelId
                };
            }

            this.conversations.set(key, context);
        }

        context.lastActivity = Date.now();
        return context;
    }

    /**
     * Actualizar contexto de forma eficiente con guardado en Appwrite
     */
    private async updateContextWithMemory(
        context: ConversationContext,
        userPrompt: string,
        aiResponse: string,
        inputTokens: number,
        isImageRequest: boolean,
        messageId?: string,
        referencedMessageId?: string
    ): Promise<void> {
        const outputTokens = this.estimateTokens(aiResponse);
        const now = Date.now();

        // Agregar mensajes con IDs de Discord
        context.messages.push(
            {
                role: 'user',
                content: userPrompt,
                timestamp: now,
                tokens: inputTokens,
                messageId,
                referencedMessageId
            },
            {
                role: 'assistant',
                content: aiResponse,
                timestamp: now,
                tokens: outputTokens
            }
        );

        // Mantener solo los mensajes más recientes
        if (context.messages.length > this.config.maxMessageHistory) {
            const removed = context.messages.splice(0, context.messages.length - this.config.maxMessageHistory);
            const removedTokens = removed.reduce((sum, msg) => sum + msg.tokens, 0);
            context.totalTokens -= removedTokens;
        }

        context.totalTokens += inputTokens + outputTokens;
        context.lastActivity = now;

        if (isImageRequest) {
            context.imageRequests++;
        }

        // Guardar en Appwrite de forma asíncrona
        this.saveConversationToAppwrite(context).catch(error => {
            logger.warn(`Error guardando conversación: ${getErrorMessage(error)}`);
        });
    }


    /**
     * Limpiar cache pero mantener memoria persistente
     */
    public clearCache(): void {
        this.conversations.clear();
        this.userCooldowns.clear();
        this.rateLimitTracker.clear();
        this.guildPromptCache.clear();
        logger.info('Cache de AI limpiado, memoria persistente mantenida');
    }

    /**
     * Reset completo pero mantener memoria persistente
     */
    public fullReset(): void {
        this.clearCache();
        this.requestQueue.length = 0;
        logger.info('AI completamente reseteada, memoria persistente mantenida');
    }

    /**
     * Obtener estadísticas del servicio
     */
    getStats(): {
        activeConversations: number;
        queueLength: number;
        totalRequests: number;
        averageResponseTime: number;
    } {
        return {
            activeConversations: this.conversations.size,
            queueLength: this.requestQueue.length,
            totalRequests: this.userCooldowns.size,
            averageResponseTime: 0
        };
    }

    /**
     * Guardar conversación en Appwrite para memoria persistente
     */
    private async saveConversationToAppwrite(context: ConversationContext): Promise<void> {
        if (!isAIConversationsConfigured()) {
            return; // Si no está configurado, no guardamos
        }

        try {
            await ensureAIConversationsSchema();
            const databases = getDatabases();
            if (!databases) return;

            // Asegurar conversationId válido y corto para Appwrite
            let conversationId = context.conversationId;
            if (!conversationId) {
                const userIdShort = context.userId.slice(-8);
                const guildIdShort = context.guildId ? context.guildId.slice(-8) : 'dm';
                const timestamp = Date.now().toString(36);
                conversationId = `ai_${userIdShort}_${guildIdShort}_${timestamp}`.slice(0, 36);
                context.conversationId = conversationId;
            }

            // Serializar mensajes a JSON
            const messagesPayload = context.messages.map(m => ({
                role: m.role,
                content: m.content,
                timestamp: m.timestamp,
                messageId: m.messageId,
                referencedMessageId: m.referencedMessageId,
            }));
            const messagesJson = JSON.stringify(messagesPayload);

            const data: AppwriteConversation = {
                userId: context.userId,
                guildId: context.guildId ?? null,
                channelId: context.channelId ?? null,
                conversationId,
                messagesJson,
                lastActivity: new Date(context.lastActivity).toISOString(),
                createdAt: new Date().toISOString(),
            };

            // Upsert por ID estable
            try {
                await databases.updateDocument(
                    APPWRITE_DATABASE_ID,
                    APPWRITE_COLLECTION_AI_CONVERSATIONS_ID,
                    conversationId,
                    data
                );
            } catch (updateError) {
                await databases.createDocument(
                    APPWRITE_DATABASE_ID,
                    APPWRITE_COLLECTION_AI_CONVERSATIONS_ID,
                    conversationId,
                    data
                );
            }

            logger.debug(`Conversación guardada en Appwrite: ${conversationId}`);
        } catch (error) {
            logger.warn(`Error guardando conversación en Appwrite: ${getErrorMessage(error)}`);
        }
    }

    /**
     * Cargar conversación desde Appwrite
     */
    private async loadConversationFromAppwrite(userId: string, guildId?: string, channelId?: string): Promise<ConversationContext | null> {
        if (!isAIConversationsConfigured()) {
            return null;
        }

        try {
            await ensureAIConversationsSchema();
            const databases = getDatabases();
            if (!databases) return null;

            const queries: any[] = [sdk.Query.equal('userId', userId)];
            if (guildId) queries.push(sdk.Query.equal('guildId', guildId));
            if (channelId) queries.push(sdk.Query.equal('channelId', channelId));
            queries.push(sdk.Query.orderDesc('lastActivity'));
            queries.push(sdk.Query.limit(1));

            const response = await databases.listDocuments(
                APPWRITE_DATABASE_ID,
                APPWRITE_COLLECTION_AI_CONVERSATIONS_ID,
                queries
            ) as unknown as { documents: AppwriteConversation[] };

            const docs = (response?.documents || []) as AppwriteConversation[];
            if (!docs.length) return null;

            const latest = docs[0];
            const messagesArray: any[] = (() => {
                try { return latest.messagesJson ? JSON.parse(latest.messagesJson) : []; } catch { return []; }
            })();

            const context: ConversationContext = {
                messages: messagesArray.map((msg: any) => ({
                    role: msg.role === 'assistant' ? 'assistant' : 'user',
                    content: String(msg.content || ''),
                    timestamp: Number(msg.timestamp || Date.now()),
                    tokens: this.estimateTokens(String(msg.content || '')),
                    messageId: msg.messageId,
                    referencedMessageId: msg.referencedMessageId,
                })),
                totalTokens: messagesArray.reduce((sum: number, m: any) => sum + this.estimateTokens(String(m.content || '')), 0),
                imageRequests: 0,
                lastActivity: Date.parse(latest.lastActivity || new Date().toISOString()) || Date.now(),
                userId: latest.userId,
                guildId: latest.guildId || undefined,
                channelId: latest.channelId || undefined,
                conversationId: latest.conversationId,
            };

            logger.debug(`Conversación cargada desde Appwrite: ${latest.conversationId}`);
            return context;
        } catch (error) {
            logger.warn(`Error cargando conversación desde Appwrite: ${getErrorMessage(error)}`);
            return null;
        }
    }

    /**
     * Obtener jerarquía de roles de un servidor
     */
    private async getGuildRoleHierarchy(guildId: string, client: any): Promise<string> {
        try {
            const guild = await client.guilds.fetch(guildId);
            if (!guild) return '';

            const roles = await guild.roles.fetch();
            const sortedRoles = roles
                .filter((role: any) => role.id !== guild.id) // Excluir @everyone
                .sort((a: any, b: any) => b.position - a.position)
                .map((role: any) => {
                    const permissions: string[] = [];
                    if (role.permissions.has('Administrator')) permissions.push('Admin');
                    if (role.permissions.has('ManageGuild')) permissions.push('Manage Server');
                    if (role.permissions.has('ManageChannels')) permissions.push('Manage Channels');
                    if (role.permissions.has('ManageMessages')) permissions.push('Manage Messages');
                    if (role.permissions.has('ModerateMembers')) permissions.push('Moderate Members');

                    const permStr = permissions.length > 0 ? ` (${permissions.join(', ')})` : '';
                    return `- ${role.name}${permStr}`;
                })
                .slice(0, 15) // Limitar a 15 roles principales
                .join('\n');

            return sortedRoles ? `\n## Jerarquía de roles del servidor:\n${sortedRoles}\n` : '';
        } catch (error) {
            logger.warn(`Error obteniendo jerarquía de roles: ${getErrorMessage(error)}`);
            return '';
        }
    }

    /**
     * Generar imagen usando la nueva API de @google/genai (basada en Google AI Studio)
     * Retorna un objeto con los bytes de la imagen y el tipo MIME.
     */
    public async generateImage(prompt: string, options?: {
        size?: 'square' | 'portrait' | 'landscape';
        mimeType?: string;
        numberOfImages?: number;
        personGeneration?: boolean;
    }): Promise<{ data: Buffer; mimeType: string; fileName: string; }> {
        if (!prompt?.trim()) {
            throw new Error('El prompt de imagen no puede estar vacío');
        }
        if (!this.genAIv2) {
            throw new Error('El SDK moderno (@google/genai) no está inicializado');
        }

        // Obtener/descubrir el modelo
        const model = this.imageModelName ?? (await this.detectImageModel());
        if (!model) {
            throw new Error('El generador de imágenes no está disponible para tu cuenta o región. Habilita Imagen 4.0 (imagen-4.0-generate-001) en Google AI Studio.');
        }

        const mimeType = options?.mimeType ?? 'image/jpeg';
        const size = options?.size ?? 'square';
        const numberOfImages = options?.numberOfImages ?? 1;
        const personGeneration = options?.personGeneration ?? true;

        // Mapear tamaño a aspectRatio según la nueva API
        const aspectRatio = size === 'portrait' ? '9:16' : size === 'landscape' ? '16:9' : '1:1';

        try {
            logger.info({ model, prompt: prompt.slice(0, 100) }, 'Generando imagen con nueva API');

            const response: any = await (this.genAIv2 as any).models.generateImages({
                model: model,
                prompt: prompt,
                config: {
                    numberOfImages: numberOfImages,
                    outputMimeType: mimeType,
                    personGeneration: personGeneration ? PersonGeneration.ALLOW_ALL : PersonGeneration.DONT_ALLOW,
                    aspectRatio: aspectRatio,
                    imageSize: '1K', // Usar 1K como tamaño estándar
                }
            });

            if (!response?.generatedImages || !Array.isArray(response.generatedImages) || response.generatedImages.length === 0) {
                logger.error({ response, model }, 'No se generaron imágenes en la respuesta');
                throw new Error('No se generaron imágenes');
            }

            // Tomar la primera imagen generada
            const generatedImage = response.generatedImages[0];
            if (!generatedImage?.image?.imageBytes) {
                logger.error({ generatedImage, model }, 'La imagen generada no contiene datos de bytes');
                throw new Error('La imagen generada no contiene datos válidos');
            }

            const base64Data = generatedImage.image.imageBytes;
            const buffer = Buffer.from(base64Data, 'base64');

            // Generar nombre de archivo basado en el tipo MIME
            let fileName = `gen_${Date.now()}`;
            if (mimeType.includes('png')) fileName += '.png';
            else if (mimeType.includes('jpeg') || mimeType.includes('jpg')) fileName += '.jpg';
            else if (mimeType.includes('webp')) fileName += '.webp';
            else fileName += '.img';

            logger.info({
                fileName,
                mimeType,
                bufferSize: buffer.length,
                model
            }, 'Imagen generada exitosamente');

            return {
                data: buffer,
                mimeType: mimeType,
                fileName: fileName
            };

        } catch (e) {
            logger.error({ err: e as any, model, prompt: prompt.slice(0, 100) }, 'Error en generateImage');
            const parsed = this.parseAPIError(e);
            const original = getErrorMessage(e);

            // Proporcionar mensajes de error más específicos
            if (original.includes('not found') || original.includes('404')) {
                throw new Error('El modelo de generación de imágenes no está disponible. Verifica que Imagen 4.0 esté habilitado en tu cuenta de Google AI Studio.');
            }
            if (original.includes('quota') || original.includes('limit')) {
                throw new Error('Has alcanzado el límite de generación de imágenes. Intenta más tarde.');
            }

            // Si el parser no aporta información útil, usar el mensaje original
            const message = parsed === 'Error temporal del servicio de IA. Intenta de nuevo' ? original : parsed;
            throw new Error(message || parsed);
        }
    }
}

// Instancia singleton
export const aiService = new AIService();
