<?php
/**
 * Servicio Bot - Manejo de conversaciones y menús
 * Fecha: 13 de noviembre de 2025
 */

class BotService {
    private $db;
    private $whatsappService;
    
    public function __construct() {
        $this->db = Database::getInstance();
        $this->whatsappService = new WhatsAppService();
    }
    
    /**
     * Procesar mensaje entrante
     */
    public function processMessage($user, $messageText, $messageType = 'text') {
        // Solo procesar mensajes de texto
        if ($messageType !== 'text') {
            return;
        }

        $messageText = trim($messageText);
        $phoneNumber = $user['phone_number'];
        $userId      = $user['id'] ?? null;

        // ── Advisory lock por usuario ─────────────────────────────────────────
        // Serializar el procesamiento para el mismo usuario y evitar race conditions
        // (WhatsApp puede entregar el mismo webhook dos veces en paralelo).
        $lockAcquired = false;
        if ($userId) {
            $lockKey  = 'bot_user_' . $userId;
            $lockRow  = $this->db->fetch("SELECT GET_LOCK(:k, 10) as ok", ['k' => $lockKey]);
            $lockAcquired = !empty($lockRow['ok']);
            if (!$lockAcquired) {
                error_log("[BotService] processMessage - lock timeout para user_id={$userId}, ignorando llamada concurrente");
                return;
            }
            // Recargar datos frescos del usuario después de adquirir el lock
            $fresh = $this->db->fetch("SELECT * FROM users WHERE id = :id", ['id' => $userId]);
            if ($fresh) $user = $fresh;
        }

        // ── Usuario bloqueado: descartar mensaje silenciosamente ──────────────
        if (isset($user['status']) && $user['status'] === 'blocked') {
            error_log("[BotService] processMessage - user_id={$userId} is blocked, ignoring message");
            return;
        }

        try {

        // Comprobar en la base de datos el estado más reciente de 'in_service' para evitar condiciones de carrera
        try {
            $fresh = $this->db->fetch("SELECT in_service, in_service_by FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber]);
            if (!empty($fresh['in_service'])) {
                try {
                    $userId = $user['id'] ?? 'unknown';
                    error_log("[BotService] processMessage - user_id={$userId} is currently in_service (DB), skipping bot processing");
                } catch (Throwable $t) {
                    // ignore logging errors
                }
                return;
            }
            // actualizar el array $user con valores frescos por si se usan más adelante
            $user['in_service'] = !empty($fresh['in_service']);
            $user['in_service_by'] = $fresh['in_service_by'] ?? null;
        } catch (Exception $e) {
            error_log('[BotService] processMessage - failed to refresh in_service: ' . $e->getMessage());
            // En caso de fallo al consultar, continuar con lógica previa (no bloquear)
        }

        // DEBUG: log basic context
        try {
            $userId = $user['id'] ?? 'unknown';
            error_log("[BotService] processMessage start - user_id={$userId} phone={$phoneNumber} type={$messageType} message='" . substr($messageText,0,200) . "' current_menu_id=" . ($user['current_menu_id'] ?? 'null') . " on_hold=" . ($user['on_hold'] ?? '0') . " advisor_requested=" . ($user['advisor_requested'] ?? '0') . " bot_paused_until=" . ($user['bot_paused_until'] ?? 'null') . " bot_enabled=" . (isset($user['bot_enabled']) ? $user['bot_enabled'] : 'null'));
        } catch (Throwable $t) {
            // ignore logging errors
        }
        
        // Si ha pasado suficiente tiempo (6 horas) desde la última actividad, reiniciar la conversación y permitir re-enviar el welcome
        try {
            if (!empty($user['id'])) {
                $this->maybeResetConversationAfterIdle($user['id']);
            }
        } catch (Exception $e) {
            error_log('[BotService] maybeResetConversationAfterIdle threw: ' . $e->getMessage());
        }

        // ── Flujo de Términos y Condiciones ──────────────────────────────────
        // 1. Si el usuario está esperando respuesta de términos, procesarla primero.
        if (!empty($user['terms_pending'])) {
            if ($this->processTermsResponse($user, $phoneNumber, $messageText)) {
                return;
            }
        }

        // 2. Si el usuario necesita aceptar (nuevo, expirado, o versión nueva con forzar_reenvio).
        if ($this->needsTermsAcceptance($user)) {
            $this->sendTermsMessage($user, $phoneNumber);
            return;
        }
        // ─────────────────────────────────────────────────────────────────────

        // Verificar si es un nuevo usuario (enviar mensaje de bienvenida)
        // Solo se envía si ya aceptó los términos, o si no hay términos activos configurados.
        $termsRequired = $this->getActiveTermsVersion() !== null;
        $termsAccepted = !empty($user['terms_accepted_at']);
        if ($this->isNewUser($user['id']) && (!$termsRequired || $termsAccepted)) {
            $this->sendWelcomeMessage($phoneNumber);
            return;
        }

        // Si el usuario escribe 'enviado' o 'enviados' en cualquier momento, enviar confirmación de recepción
        try {
            // Support masculino/femenino, singular/plural: enviado, enviada, enviados, enviadas
            if (preg_match('/\b(enviad[oa]s?)\b/iu', $messageText)) {
                // Cuando el usuario confirma que envió la documentación, notificarle y transferir a un asesor.
                try {
                    // Usar requestAdvisor para aplicar advisor_requested, pausar el bot y enviar notificación al usuario
                    $this->requestAdvisor($phoneNumber, 3);

                    // Registrar mensaje de sistema para trazabilidad (no altera flujo de notificación)
                    $sys = [
                        'system' => 'attention',
                        'type' => 'user_sent_documents',
                        'text' => 'El usuario indicó que ha enviado documentos o información; requiere atención.'
                    ];

                    $this->db->insert('conversations', [
                        'user_id' => $user['id'],
                        'direction' => 'incoming',
                        'message_type' => 'text',
                        'content' => json_encode($sys),
                        'status' => 'received',
                        'created_at' => date('Y-m-d H:i:s')
                    ]);

                } catch (Exception $e) {
                    error_log('[BotService] failed to request advisor on envio: ' . $e->getMessage());
                }

                error_log("[BotService] processMessage - requested advisor for user {$user['id']} (sent notification to user)");
                return;
            }
        } catch (Throwable $t) {
            // ignore regex errors
        }
        
        // Procesar comandos especiales
        if ($this->processSpecialCommands($phoneNumber, $messageText)) {
            return;
        }

        // Si el usuario está en espera por un asesor, y el periodo no expiró, no procesar.
        if (!empty($user['on_hold']) && $user['on_hold']) {
            $now = time();
            $until = null;
            if (!empty($user['bot_paused_until'])) {
                $until = strtotime($user['bot_paused_until']);
            }
            // Si hay un tiempo de expiración y aún no pasó, notificar y retornar
            if ($until && $until > $now) {
                try {
                    $this->whatsappService->sendTextMessage($phoneNumber, "⚠️ Se le ha puesto en espera por un asesor. Estará en espera hasta " . date('g:i a', $until) . ". Si no hay respuesta, podrá usar *MENU*." );
                } catch (Exception $e) {
                    // ignore
                }
                error_log("[BotService] processMessage - returning early because on_hold active for user {$user['id']}");
                return;
            } else {
                // El hold expiró, liberar y continuar
                $this->releaseHold($phoneNumber);
            }
        }

        // Si existe una solicitud de asesor (por parte del usuario) y aún no ha respondido un asesor, respetar la espera
        if (!empty($user['advisor_requested']) && $user['advisor_requested']) {
            $now = time();
            $until = !empty($user['bot_paused_until']) ? strtotime($user['bot_paused_until']) : null;
            if ($until && $until > $now) {
                // Permitir *navegación explícita* durante la espera: MENU, ATRÁS, números (opciones)
                $normalized = mb_strtolower(trim($messageText));
                $isNav = in_array($normalized, ['menu','menú','inicio','atras','atrás','volver','back'], true) || is_numeric($messageText);
                // No enviar notificación automática al usuario cuando advisor_requested está activa.
                // Mantener navegación explícita permitida (MENU/ATRÁS/números); no enviar mensajes proactivos.
                if ($isNav) {
                    try { error_log("[BotService] processMessage - advisor_requested active but navigation input allowed for user {$user['id']} message='{$messageText}'"); } catch (Throwable $t) {}

                    // Manejar la navegación inmediatamente (no esperar al flujo normal)
                    $normalized = mb_strtolower(trim($messageText));
                    // MENU
                    if (in_array($normalized, ['menu','menú','inicio'], true)) {
                        $this->showMainMenu($phoneNumber);
                        return;
                    }
                    // ATRÁS
                    if (in_array($normalized, ['atras','atrás','volver','back'], true)) {
                        // Si hay menú actual, intentar ir al padre, si no, usar historial
                        try {
                            $cur = $this->db->fetch("SELECT current_menu_id FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber]);
                            $currentMenuId = $cur ? $cur['current_menu_id'] : null;
                        } catch (Exception $e) {
                            $currentMenuId = null;
                        }

                        if (!empty($currentMenuId)) {
                            $this->goToParentMenu($phoneNumber, $currentMenuId);
                        } else {
                            $this->goToPreviousMenu($phoneNumber);
                        }
                        return;
                    }

                    // Si es número, intentar procesarlo contra current_menu_id o historial
                    if (is_numeric(trim($messageText))) {
                        try {
                            $stateSvc2 = new ConversationStateService();
                            $resolvedMenuId2 = $stateSvc2->getCurrentMenuId($phoneNumber);
                        } catch (Exception $e) {
                            $resolvedMenuId2 = null;
                        }

                        if (empty($resolvedMenuId2)) {
                            try {
                                $cur = $this->db->fetch("SELECT current_menu_id FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber]);
                                $resolvedMenuId2 = $cur ? $cur['current_menu_id'] : null;
                            } catch (Exception $e) {
                                $resolvedMenuId2 = null;
                            }
                        }

                        // Si no hay menú activo, intentar con historial
                        if (empty($resolvedMenuId2)) {
                            try {
                                $hist = (array)$stateSvc2->getStateData($phoneNumber, 'menu_history');
                                if (!empty($hist)) $resolvedMenuId2 = end($hist);
                            } catch (Exception $e) {
                                $resolvedMenuId2 = null;
                            }
                        }

                        if (!empty($resolvedMenuId2)) {
                            // Asegurar que la BD refleje el menu activo para que processMenuSelection lo resuelva correctamente
                            try {
                                $this->db->update('users', ['current_menu_id' => $resolvedMenuId2, 'current_step' => 1], 'phone_number = :phone', ['phone' => $phoneNumber]);
                                try {
                                    $stateSvc2->setCurrentMenu($phoneNumber, (int)$resolvedMenuId2);
                                } catch (Exception $e) {
                                    // ignore
                                }
                            } catch (Exception $e) {
                                error_log('[BotService] failed to set current_menu_id before processing numeric selection: ' . $e->getMessage());
                            }

                            $user['current_menu_id'] = $resolvedMenuId2;
                            $this->processMenuSelection($user, $messageText);
                            return;
                        }
                    }

                    // Si llegamos aquí no pudimos resolver navegación; permitir que el flujo normal lo trate
                } else {
                    error_log("[BotService] processMessage - returning early because advisor_requested active for user {$user['id']}");
                    return;
                }
            } else {
                // expiró la espera: limpiar flag
                try {
                    $this->db->update('users', ['advisor_requested' => 0], 'phone_number = :phone', ['phone' => $phoneNumber]);
                } catch (Exception $e) {
                    error_log('Failed to clear advisor_requested on expiry: ' . $e->getMessage());
                }
            }
        }

        // Si el bot está pausado por bot_paused_until, no responder automáticamente
        if (!empty($user['bot_paused_until']) && strtotime($user['bot_paused_until']) > time()) {
            // Allow explicit navigation commands and numeric menu selection during pause (better UX)
            $normalized = mb_strtolower(trim($messageText));
            $isNav = in_array($normalized, ['menu','menú','inicio','atras','atrás','volver','back'], true) || is_numeric(trim($messageText));
            if ($isNav) {
                try { error_log("[BotService] processMessage - bot_paused_until active but allowing navigation input for phone={$phoneNumber} message='{$messageText}'"); } catch (Throwable $t) {}

                // Si es un número, procesarlo inmediatamente como selección de menú
                if (is_numeric(trim($messageText))) {
                    try {
                        $stateSvc2 = new ConversationStateService();
                        $resolvedMenuId2 = $stateSvc2->getCurrentMenuId($phoneNumber);
                    } catch (Exception $e) {
                        $resolvedMenuId2 = null;
                    }

                    if (empty($resolvedMenuId2)) {
                        try {
                            $cur = $this->db->fetch("SELECT current_menu_id FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber]);
                            $resolvedMenuId2 = $cur ? $cur['current_menu_id'] : null;
                        } catch (Exception $e) {
                            $resolvedMenuId2 = null;
                        }
                    }

                    // Si aún no hay menú activo, intentar con historial
                    if (empty($resolvedMenuId2)) {
                        try {
                            $hist = (array)$stateSvc2->getStateData($phoneNumber, 'menu_history');
                            if (!empty($hist)) $resolvedMenuId2 = end($hist);
                        } catch (Exception $e) {
                            $resolvedMenuId2 = null;
                        }
                    }

                    if (!empty($resolvedMenuId2)) {
                        // Asegurar que la BD refleje el menu activo para que processMenuSelection lo resuelva correctamente
                        try {
                            $this->db->update('users', ['current_menu_id' => $resolvedMenuId2, 'current_step' => 1], 'phone_number = :phone', ['phone' => $phoneNumber]);
                            try {
                                $stateSvc2->setCurrentMenu($phoneNumber, (int)$resolvedMenuId2);
                            } catch (Exception $e) {
                                // ignore
                            }
                        } catch (Exception $e) {
                            error_log('[BotService] failed to set current_menu_id before processing numeric selection during pause: ' . $e->getMessage());
                        }

                        $user['current_menu_id'] = $resolvedMenuId2;
                        $this->processMenuSelection($user, $messageText);
                        return;
                    }
                }

                // proceed to let navigation/menus be processed
            } else {
                return;
            }
        }

        // Si el usuario ha desactivado el bot manualmente (toggle), evitar respuestas automáticas
        // pero permitir comandos explícitos (MENU, ATRÁS) y selección numérica dentro de menús.
        $botDisabled = (isset($user['bot_enabled']) && !$user['bot_enabled']);


        // Verificar si el asesor ya respondió después del último mensaje del usuario.
        // En ese caso NO enviaremos respuestas automáticas, pero sí permitiremos
        // que el usuario interactúe con menús manualmente.
        $lastIncoming = $this->db->fetch("SELECT created_at FROM conversations WHERE user_id = :uid AND direction = 'incoming' ORDER BY created_at DESC LIMIT 1", ['uid' => $user['id']]);
        $lastOutgoing = $this->db->fetch("SELECT created_at FROM conversations WHERE user_id = :uid AND direction = 'outgoing' ORDER BY created_at DESC LIMIT 1", ['uid' => $user['id']]);
        $advisorReplied = false;
        if ($lastOutgoing && $lastIncoming && strtotime($lastOutgoing['created_at']) >= strtotime($lastIncoming['created_at'])) {
            // El asesor ya respondió, marcar flag pero no retornar (permitir menús y comandos explícitos)
            $advisorReplied = true;
            error_log("[BotService] processMessage - advisorReplied=true for user {$user['id']}");
            try { error_log("[BotService] processMessage - lastOutgoing={$lastOutgoing['created_at']} lastIncoming={$lastIncoming['created_at']}"); } catch (Throwable $t) {}
        } else {
            try { error_log("[BotService] processMessage - advisorReplied=false lastOutgoing=" . ($lastOutgoing['created_at'] ?? 'null') . " lastIncoming=" . ($lastIncoming['created_at'] ?? 'null')) ; } catch (Throwable $t) {}
        }

        // Resolver current_menu_id fresco (evitar usar $user desactualizado que viene del webhook)
        $resolvedMenuId = null;
        try {
            $stateSvc = new ConversationStateService();
            $resolvedMenuId = $stateSvc->getCurrentMenuId($phoneNumber);
        } catch (Exception $e) {
            // ignore
        }

        if (empty($resolvedMenuId)) {
            try {
                $cur = $this->db->fetch("SELECT current_menu_id FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber]);
                $resolvedMenuId = $cur ? $cur['current_menu_id'] : ($user['current_menu_id'] ?? null);
            } catch (Exception $e) {
                $resolvedMenuId = $user['current_menu_id'] ?? null;
            }
        }

        // Actualizar el array $user para que funciones posteriores lo usen si es necesario
        $user['current_menu_id'] = $resolvedMenuId;

        try { error_log("[BotService] processMessage - resolved current_menu_id={$resolvedMenuId} for user {$user['id']} phone={$phoneNumber}"); } catch (Throwable $t) {}

        // Verificar si el usuario está en un menú
        if (!empty($resolvedMenuId)) {
            $this->processMenuSelection($user, $messageText);
            return;
        }

        // Si no hay menú actual pero el usuario envía un número, intentar interpretar el número
        // contra el último menú en el historial (por ejemplo: user vio opciones, eligió end y luego envía '6' para volver)
        if (is_numeric(trim($messageText))) {
            try {
                $stateSvc = new ConversationStateService();
                $history = (array)$stateSvc->getStateData($phoneNumber, 'menu_history');
                if (!empty($history)) {
                    $lastMenu = end($history);
                    try { error_log("[BotService] interpreting numeric input '{$messageText}' against lastMenu={$lastMenu} for phone={$phoneNumber}"); } catch (Throwable $t) {}
                    // simular que estamos en ese menú para procesar la selección
                    $user['current_menu_id'] = $lastMenu;
                    $this->processMenuSelection($user, $messageText);
                    return;
                }
            } catch (Exception $e) {
                // ignore and fallthrough to default
                error_log('[BotService] failed to check menu_history: ' . $e->getMessage());
            }
        }

        // No usar las "respuestas rápidas" (autoresponses) automáticamente desde el bot.
        // Estas respuestas solo se mostrarán en la UI del chat como "respuestas rápidas".
        // Si un asesor ya respondió recientemente, no enviar respuestas automáticas.
        if (empty($advisorReplied)) {
            if (empty($botDisabled)) {
                $this->sendDefaultNoMatch($phoneNumber);
            } else {
                try { error_log("[BotService] processMessage - bot disabled, skipping default no-match for user {$user['id']}"); } catch (Throwable $t) {}
            }
        } else {
            // Opcional: informar que el asesor ya respondió y no podemos enviar automáticas
            try {
                //$this->whatsappService->sendTextMessage($phoneNumber, "🔕 Un asesor ya ha intervenido en la conversación, por favor espera su respuesta o utiliza *MENU* para ver opciones.");
            } catch (Exception $e) {
                // ignore
            }
        }

        } finally {
            // Liberar advisory lock si fue adquirido
            if ($lockAcquired && isset($lockKey)) {
                try { $this->db->query("DO RELEASE_LOCK(:k)", ['k' => $lockKey]); } catch (Exception $e) {}
            }
        }
    }

    /**
     * Si la última actividad del usuario fue hace >= 6 horas, reinicia el estado de la conversación
     * para que el usuario vuelva a recibir la bienvenida y empiece desde cero. Esto incluye limpiar
     * menús, flags de espera/asesor, y `welcome_sent_at`.
     */
    private function maybeResetConversationAfterIdle($userId) {
        try {
            $sixHours = 6 * 3600;
            $row = $this->db->fetch("SELECT MAX(created_at) as last_at FROM conversations WHERE user_id = :uid", ['uid' => $userId]);
            $lastAt = $row && !empty($row['last_at']) ? strtotime($row['last_at']) : null;
            if (!$lastAt) return false;
            if ((time() - $lastAt) >= $sixHours) {
                try {
                    $this->db->update('users', [
                        'current_menu_id' => null,
                        'current_step' => 0,
                        'session_data' => null,
                        'welcome_sent_at' => null,
                        'advisor_requested' => 0,
                        'on_hold' => 0,
                        'in_service' => 0,
                        'in_service_by' => null,
                        'bot_paused_until' => null
                    ], 'id = :id', ['id' => $userId]);

                    // Registrar mensaje sistema para trazabilidad
                    $this->db->insert('conversations', [
                        'user_id' => $userId,
                        'direction' => 'outgoing',
                        'message_type' => 'text',
                        'content' => json_encode(['system' => 'system', 'type' => 'session_reset', 'text' => 'Sesión reiniciada por inactividad (6h).']),
                        'status' => 'sent',
                        'created_at' => date('Y-m-d H:i:s')
                    ]);

                    error_log("[BotService] maybeResetConversationAfterIdle: reset session for user_id={$userId} after inactivity");
                } catch (Exception $e) {
                    error_log('[BotService] maybeResetConversationAfterIdle -> failed to reset user state: ' . $e->getMessage());
                }
                return true;
            }
        } catch (Exception $e) {
            error_log('[BotService] maybeResetConversationAfterIdle failed: ' . $e->getMessage());
        }
        return false;
    }
    
    // =========================================================================
    // TÉRMINOS Y CONDICIONES
    // =========================================================================

    /**
     * Obtiene la versión activa de términos o null si no existe ninguna.
     */
    private function getActiveTermsVersion(): ?array {
        try {
            return $this->db->fetch(
                "SELECT * FROM terms_versions WHERE activa = 1 ORDER BY id DESC LIMIT 1"
            ) ?: null;
        } catch (Exception $e) {
            error_log('[BotService] getActiveTermsVersion error: ' . $e->getMessage());
            return null;
        }
    }

    /**
     * Decide si el usuario debe aceptar (o re-aceptar) los términos.
     *  - No hay aceptación previa (terms_accepted_at IS NULL)
     *  - Han pasado más de 6 meses desde la última aceptación
     *  - La versión activa tiene forzar_reenvio=1 y el usuario aceptó una versión anterior
     */
    private function needsTermsAcceptance(array $user): bool {
        $terms = $this->getActiveTermsVersion();
        if (!$terms) return false; // Sin versión activa → no hay términos que aceptar

        // ¿El usuario ya aceptó esta versión exacta?
        $acceptedVersionId = $user['terms_version_id'] ?? null;
        $acceptedAt        = $user['terms_accepted_at'] ?? null;

        // Nunca aceptó
        if (!$acceptedAt) return true;

        // Forzar re-aceptación si la versión activa lo requiere y el usuario tiene una versión diferente
        if (!empty($terms['forzar_reenvio']) && (int)$acceptedVersionId !== (int)$terms['id']) {
            return true;
        }

        // Han pasado más de 6 meses (26 semanas) desde la última aceptación
        $sixMonthsAgo = strtotime('-6 months');
        if (strtotime($acceptedAt) < $sixMonthsAgo) return true;

        return false;
    }

    /**
     * Envía el mensaje de términos al usuario y registra el envío en BD.
     */
    private function sendTermsMessage(array $user, string $phoneNumber): void {
        $terms = $this->getActiveTermsVersion();
        if (!$terms) return;

        // Prioridad: system_config (lo que edita el usuario en "Configuración del Sistema")
        // Fallback: terms_versions.mensaje_aceptacion (si el admin lo editó directamente en la BD)
        $msg = getConfigFromDB('terms_message', '');
        if (!$msg && !empty($terms['mensaje_aceptacion'])) {
            $msg = $terms['mensaje_aceptacion'];
        }

        if (!$msg) return;

        // Adjuntar URL del documento solo si es una URL pública y no está ya incluida en el mensaje
        // Normalizar protocolo para la comparación (http vs https no deben importar)
        if (!empty($terms['documento_url'])) {
            $docUrl = $terms['documento_url'];
            $isLocal = (
                strpos($docUrl, 'localhost') !== false ||
                strpos($docUrl, '127.0.0.1') !== false ||
                preg_match('/https?:\/\/\d+\.\d+\.\d+\.\d+:\d+/', $docUrl)
            );
            $docUrlNormalized = preg_replace('#^https?://#', '', $docUrl);
            $msgNormalized    = preg_replace('#https?://#', '', $msg);
            if (!$isLocal && strpos($msgNormalized, $docUrlNormalized) === false) {
                $msg .= "\n\n📄 Documento: " . $docUrl;
            }
        }

        $this->whatsappService->sendTextMessage($phoneNumber, $msg);

        // Marcar usuario como "esperando respuesta de términos"
        // También marcamos welcome_sent_at para evitar que una llamada concurrente
        // al webhook envíe el mensaje de bienvenida por race condition.
        try {
            $this->db->update(
                'users',
                ['terms_pending' => 1, 'welcome_sent_at' => date('Y-m-d H:i:s')],
                'id = :id',
                ['id' => $user['id']]
            );
        } catch (Exception $e) {
            error_log('[BotService] sendTermsMessage – update terms_pending: ' . $e->getMessage());
        }

        // Registrar envío en terms_acceptance con estado pendiente
        try {
            $this->db->insert('terms_acceptance', [
                'user_id'          => $user['id'],
                'terms_version_id' => $terms['id'],
                'phone_number'     => $phoneNumber,
                'estado'           => 'pendiente',
                'fecha_envio'      => date('Y-m-d H:i:s'),
            ]);
        } catch (Exception $e) {
            error_log('[BotService] sendTermsMessage – insert terms_acceptance: ' . $e->getMessage());
        }

        error_log("[BotService] sendTermsMessage – sent terms v{$terms['version']} to {$phoneNumber}");
    }

    /**
     * Procesa la respuesta del usuario cuando está en estado terms_pending.
     * Detecta "acepto" / "no acepto" / "rechazo".
     * Devuelve true si el mensaje fue manejado (para detener el procesamiento normal).
     */
    private function processTermsResponse(array $user, string $phoneNumber, string $messageText): bool {
        $terms = $this->getActiveTermsVersion();
        if (!$terms) {
            // Sin versión activa: limpiar el flag y continuar
            try {
                $this->db->update('users', ['terms_pending' => 0], 'id = :id', ['id' => $user['id']]);
            } catch (Exception $e) {}
            return false;
        }

        $normalized = strtolower(trim(
            iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $messageText) ?: $messageText
        ));

        // Detectar rechazo PRIMERO para evitar que "no acepto" haga match en el regex de aceptación
        $rejected = (bool) preg_match('/\b(no acepto|no|rechazo|rechazar|negar|0|rechaz[oa])\b/', $normalized);

        // Detectar aceptación solo si no hubo rechazo: "acepto", "si acepto", "sí", "1", etc.
        $accepted = !$rejected && (bool) preg_match('/\b(acepto|si acepto|si|sí|1|ok|aceptar|de acuerdo|confirmo)\b/', $normalized);

        if (!$accepted && !$rejected) {
            // Respuesta no reconocida → volver a preguntar
            $retry = "No entendí tu respuesta. Por favor responde *Acepto* o *No acepto*.";
            $this->whatsappService->sendTextMessage($phoneNumber, $retry);
            return true;
        }

        $estado = $accepted ? 'aceptado' : 'rechazado';
        $now    = date('Y-m-d H:i:s');

        // Actualizar el último registro pendiente de esta versión
        try {
            $this->db->query(
                "UPDATE terms_acceptance SET estado = :estado, fecha_respuesta = :now
                  WHERE user_id = :uid AND terms_version_id = :vid AND estado = 'pendiente'
                  ORDER BY id DESC LIMIT 1",
                ['estado' => $estado, 'now' => $now, 'uid' => $user['id'], 'vid' => $terms['id']]
            );
        } catch (Exception $e) {
            error_log('[BotService] processTermsResponse – update acceptance: ' . $e->getMessage());
        }

        if ($accepted) {
            // Actualizar usuario: aceptó términos
            try {
                $this->db->update('users', [
                    'terms_pending'     => 0,
                    'terms_accepted_at' => $now,
                    'terms_version_id'  => $terms['id'],
                ], 'id = :id', ['id' => $user['id']]);
            } catch (Exception $e) {
                error_log('[BotService] processTermsResponse – update user accepted: ' . $e->getMessage());
            }

            // Ir directo al menú principal.
            // El mensaje de T&C ya contiene el saludo de bienvenida,
            // NO se envía el "Mensaje de Bienvenida" por separado para evitar duplicados.
            $this->showMainMenu($phoneNumber);
        } else {
            // Rechazó — enviar mensaje de rechazo y dejar terms_pending=1 para que pueda aceptar después
            // Prioridad: system_config (lo que edita el usuario en "Configuración del Sistema")
            $rejMsg = getConfigFromDB('terms_rejected_message', '');
            if (!$rejMsg && !empty($terms['mensaje_rechazo'])) {
                $rejMsg = $terms['mensaje_rechazo'];
            }
            if (!$rejMsg) {
                $rejMsg = 'Lamentablemente no podemos proceder sin tu aceptación. Cuando estés listo, responde *Acepto*.';
            }

            $this->whatsappService->sendTextMessage($phoneNumber, $rejMsg);

            // Mantener terms_pending = 1 para que la próxima respuesta sea evaluada
        }

        error_log("[BotService] processTermsResponse – user {$user['id']} {$estado} terms v{$terms['version']}");
        return true;
    }

    // =========================================================================
    // FIN TÉRMINOS Y CONDICIONES
    // =========================================================================

    /**
     * Verificar si es un nuevo usuario
     * Ahora: reenvía el welcome si no se envió o si el último welcome fue hace >= 6 horas
     */
    private function isNewUser($userId) {
        $messageCount = $this->db->fetch(
            "SELECT COUNT(*) as count FROM conversations WHERE user_id = :user_id",
            ['user_id' => $userId]
        );
        // Evitar reenviar welcome si ya se envió recientemente
        $user = $this->db->fetch("SELECT welcome_sent_at FROM users WHERE id = :id", ['id' => $userId]);
        $welcomeSentAt = !empty($user['welcome_sent_at']) ? strtotime($user['welcome_sent_at']) : null;

        // Si nunca se envió el welcome, consideramos nuevo
        if (!$welcomeSentAt) return true;

        // Si el welcome fue hace 6 horas o más, consideramos que debe reenviarse
        $sixHours = 6 * 3600;
        if ((time() - $welcomeSentAt) >= $sixHours) return true;

        // En otros casos, no es nuevo
        return false;
    }
    
    /**
     * Enviar mensaje de bienvenida
     */
    public function sendWelcomeMessage($phoneNumber) {
        // No enviar bienvenida si hay términos activos y el usuario aún no los ha aceptado
        $termsVersion = $this->getActiveTermsVersion();
        if ($termsVersion) {
            $userRow = $this->db->fetch("SELECT terms_accepted_at FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber]);
            if (empty($userRow['terms_accepted_at'])) {
                error_log("[BotService] sendWelcomeMessage - skipped for {$phoneNumber}: terms not accepted yet");
                return;
            }
        }

        $enabled = getConfigFromDB('welcome_enabled', '1');
        $welcomeMessage = getConfigFromDB('welcome_message', '¡Hola! 👋 Bienvenido a nuestro servicio automatizado. Escriba *MENU* para ver las opciones disponibles.');
        
        if (!$enabled || !$welcomeMessage) return;

        $this->whatsappService->sendTextMessage($phoneNumber, $welcomeMessage);

        // Registrar que se envió el welcome
        try {
            $this->db->update('users', ['welcome_sent_at' => date('Y-m-d H:i:s')], 'phone_number = :phone', ['phone' => $phoneNumber]);
        
            // Asegurar que este envío no sea interpretado como respuesta de un asesor:
            // Limpiar banderas de in_service / advisor_requested y evitar que la UI marque la conversación como atendida.
            try {
                $this->db->update('users', ['in_service' => 0, 'in_service_by' => null, 'advisor_requested' => 0], 'phone_number = :phone', ['phone' => $phoneNumber]);
            } catch (Exception $e) {
                error_log('Failed to clear in_service/advisor flags after welcome: ' . $e->getMessage());
            }
        } catch (Exception $e) {
            error_log('Failed to set welcome_sent_at for ' . $phoneNumber . ': ' . $e->getMessage());
        }
    }
    
    /**
     * Procesar comandos especiales
     */
    private function processSpecialCommands($phoneNumber, $messageText) {
        $command = strtolower($messageText);
        try { error_log("[BotService] processSpecialCommands - command={$command} phone={$phoneNumber}"); } catch (Throwable $t) {}
        
        switch ($command) {
            case 'menu':
            case 'menú':
            case 'inicio':
                try { error_log("[BotService] processSpecialCommands - detected MENU command, calling showMainMenu for phone={$phoneNumber}"); } catch (Throwable $t) {}
                $this->showMainMenu($phoneNumber);
                return true;
                
            case 'salir':
            case 'exit':
            case 'cancelar':
                $this->exitMenu($phoneNumber);
                return true;

            case 'atras':
            case 'atrás':
            case 'volver':
            case 'back':
                try { error_log("[BotService] processSpecialCommands - detected BACK command in global handler for phone={$phoneNumber}"); } catch (Throwable $t) {}
                // Resolver current_menu_id desde users y decidir la acción más adecuada
                try {
                    $cur = $this->db->fetch("SELECT current_menu_id FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber]);
                    $currentMenuId = $cur ? $cur['current_menu_id'] : null;
                } catch (Exception $e) {
                    $currentMenuId = null;
                }

                if (!empty($currentMenuId)) {
                    $this->goToParentMenu($phoneNumber, $currentMenuId);
                } else {
                    // Intentar usar historial
                    $this->goToPreviousMenu($phoneNumber);
                }
                return true;
                
            case 'asesor':
            case 'ayuda':
                // Solicitar asesor: marcar solicitud y pausar brevemente (3 minutos)
                $this->requestAdvisor($phoneNumber, 3);
                return true; 
        }
        
        return false;
    }
    
    /**
     * Mostrar menú principal.
     * Si no hay asesores disponibles (advisor_available=0) y existe un menú de tipo
     * 'no_advisor' activo, lo muestra primero en lugar del menú raíz.
     */
    private function showMainMenu($phoneNumber) {
        try { error_log("[BotService] showMainMenu - phone={$phoneNumber} fetching root menu"); } catch (Throwable $t) {}

        // Verificar disponibilidad de asesores
        $advisorAvailable = true;
        try {
            $cfg = $this->db->fetch(
                "SELECT config_value FROM system_config WHERE config_key = 'advisor_available' LIMIT 1"
            );
            if ($cfg !== null && $cfg !== false) {
                $advisorAvailable = (bool)(int)$cfg['config_value'];
            }
        } catch (Exception $e) {
            error_log('[BotService] showMainMenu - failed to read advisor_available: ' . $e->getMessage());
        }

        // Si no hay asesor disponible, intentar mostrar el menú secundario primero
        if (!$advisorAvailable) {
            try {
                // Verificar que la columna menu_type ya existe (puede no existir si la migración aún no corrió).
                // Se cachea en static para no hacer SHOW COLUMNS en cada mensaje.
                static $menuTypeColChecked = null;
                if ($menuTypeColChecked === null) {
                    $colExists = $this->db->fetchAll("SHOW COLUMNS FROM menus LIKE 'menu_type'");
                    $menuTypeColChecked = !empty($colExists);
                    if (!$menuTypeColChecked) {
                        error_log('[BotService] showMainMenu - column menu_type not yet migrated, run run_20260527_menu_type.php');
                    }
                }

                if ($menuTypeColChecked) {
                    $noAdvisorMenu = $this->db->fetch(
                        "SELECT * FROM menus WHERE menu_type = 'no_advisor' AND is_active = 1 ORDER BY order_position, id LIMIT 1"
                    );
                    if ($noAdvisorMenu) {
                        try { error_log("[BotService] showMainMenu - no advisor available, showing no_advisor menu id={$noAdvisorMenu['id']} phone={$phoneNumber}"); } catch (Throwable $t) {}
                        $this->showMenu($phoneNumber, $noAdvisorMenu['id']);
                        return;
                    }
                    error_log('[BotService] showMainMenu - advisor absent but no no_advisor menu configured, falling back to root');
                }
            } catch (Exception $e) {
                error_log('[BotService] showMainMenu - error fetching no_advisor menu: ' . $e->getMessage());
            }
        }

        // Menú raíz principal (comportamiento normal)
        $mainMenu = $this->db->fetch(
            "SELECT * FROM menus WHERE is_root = 1 AND is_active = 1 ORDER BY order_position LIMIT 1"
        );
        try { error_log("[BotService] showMainMenu - fetched root menu=" . json_encode($mainMenu)); } catch (Throwable $t) {}

        if ($mainMenu) {
            $this->showMenu($phoneNumber, $mainMenu['id']);
        } else {
            try { error_log("[BotService] showMainMenu - no root menu found for phone={$phoneNumber}"); } catch (Throwable $t) {}
            $this->whatsappService->sendTextMessage(
                $phoneNumber,
                "❌ No hay menús configurados. Contacte con soporte."
            );
        }
    }
    
    /**
     * Mostrar menú específico
     */
    private function showMenu($phoneNumber, $menuId) {
        try { error_log("[BotService] showMenu - phone={$phoneNumber} menuId={$menuId} fetching menu"); } catch (Throwable $t) {}
        // Obtener información del menú
        $menu = $this->db->fetch(
            "SELECT * FROM menus WHERE id = :id AND is_active = 1",
            ['id' => $menuId]
        );
        try { error_log("[BotService] showMenu - fetched menu=" . json_encode($menu)); } catch (Throwable $t) {}
        
        if (!$menu) {
            try { error_log("[BotService] showMenu - menu not found id={$menuId} phone={$phoneNumber}"); } catch (Throwable $t) {}
            $this->whatsappService->sendTextMessage(
                $phoneNumber, 
                "❌ Menú no encontrado."
            );
            return;
        }
        
        // Obtener opciones del menú
        $options = $this->db->fetchAll(
            "SELECT * FROM menu_options WHERE menu_id = :menu_id AND is_active = 1 ORDER BY option_number",
            ['menu_id' => $menuId]
        );
        try { error_log("[BotService] showMenu - menuId={$menuId} options_count=" . count($options)); } catch (Throwable $t) {}
        
        if (empty($options)) {
            $this->whatsappService->sendTextMessage(
                $phoneNumber, 
                "❌ Este menú no tiene opciones configuradas."
            );
            return;
        }
        
        // Construir mensaje del menú
        $menuText = "📋 *" . $menu['title'] . "*\n\n";
        
        if ($menu['description']) {
            $menuText .= $menu['description'] . "\n\n";
        }
        
        foreach ($options as $option) {
            $menuText .= $option['option_number'] . ". " . $option['text'] . "\n";
        }
        
        // Añadir opción "Atrás" si el menú tiene un menú padre
        if (!empty($menu['parent_id'])) {
            $menuText .= "0. Atrás\n";
        }
        
        $menuText .= "\n💬 *Responda con el número de la opción que desea*";
        
        // Actualizar estado del usuario
        $this->updateUserMenuState($phoneNumber, $menuId);
        
        // Enviar mensaje
        $this->whatsappService->sendTextMessage($phoneNumber, $menuText);
    }
    
    /**
     * Procesar selección de menú
     */
    private function processMenuSelection($user, $messageText) {
        $phoneNumber = $user['phone_number'];
        $currentMenuId = null;

        // Intentar primero el servicio de estados (si existe) — esto permite flujos que usan user_states en lugar de la columna users.current_menu_id
        try {
            $stateSvc = new ConversationStateService();
            $currentMenuId = $stateSvc->getCurrentMenuId($phoneNumber);
        } catch (Exception $e) {
            // ignore if service not available
        }

        // Si no tenemos valor desde el servicio de estados, consultar columna users
        if (empty($currentMenuId)) {
            $cur = $this->db->fetch("SELECT current_menu_id FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber]);
            $currentMenuId = $cur ? $cur['current_menu_id'] : ($user['current_menu_id'] ?? null);
        }

        // DEBUG
        try { error_log("[BotService] processMenuSelection - RESOLVED menu_id={$currentMenuId} for phone={$phoneNumber} user_id={$user['id']} incoming='{$messageText}'"); } catch (Throwable $t) {}

        // Prioritario: si estamos esperando documentos para esta conversación, manejarlo aquí
        try {
            $stateSvcTemp = new ConversationStateService();
            $await = $stateSvcTemp->getStateData($phoneNumber, 'awaiting_documents');
        } catch (Exception $e) {
            $await = null;
        }

        if (!empty($await)) {
            // Si llega multimedia (no text), marcar recibido y enviar ACK
            if ($messageType !== 'text') {
                try {
                    $stateSvcTemp->updateStateData($phoneNumber, ['awaiting_documents' => ['since' => $await['since'], 'received' => true]]);
                } catch (Exception $e) {
                    // ignore
                }
                try {
                    $this->whatsappService->sendTextMessage($phoneNumber, "✅ Documento recibido. Cuando haya terminado, escriba *ENVIADA*.");
                } catch (Exception $e) {
                    // ignore
                }
                return; // no procesar más
            }

            // Si es texto y dice enviada => transferir a asesor
            if (preg_match('/\b(enviad[oa]s?)\b/iu', $messageText)) {
                try {
                    $this->requestAdvisor($phoneNumber, 3);
                    // limpiar estado awaiting_documents
                    $stateSvcTemp->updateStateData($phoneNumber, ['awaiting_documents' => null]);

                    // registrar sistema para trazabilidad
                    $sys = [
                        'system' => 'user_documents_sent',
                        'type' => 'user_confirmed_documents',
                        'text' => 'Usuario confirmó envío de documentos con la palabra ENVIADA.'
                    ];
                    $this->db->insert('conversations', [
                        'user_id' => $user['id'],
                        'direction' => 'incoming',
                        'message_type' => 'text',
                        'content' => json_encode($sys),
                        'status' => 'received',
                        'created_at' => date('Y-m-d H:i:s')
                    ]);
                } catch (Exception $e) {
                    error_log('[BotService] failed to process ENVIADA: ' . $e->getMessage());
                }
                return;
            }

            // Permitir navegación explícita (MENU/ATRÁS/números) y en los demás casos evitar respuestas automáticas
            $cmd = mb_strtolower(trim($messageText));
            $backCommands = ['atras', 'atrás', 'volver', 'back'];
            if (in_array($cmd, ['menu','menú','inicio'], true)) {
                $this->showMainMenu($phoneNumber);
                return;
            }
            if (in_array($cmd, $backCommands, true)) {
                try {
                    $cur = $this->db->fetch("SELECT current_menu_id FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber]);
                    $currentMenuId = $cur ? $cur['current_menu_id'] : null;
                } catch (Exception $e) {
                    $currentMenuId = null;
                }

                if (!empty($currentMenuId)) {
                    $this->goToParentMenu($phoneNumber, $currentMenuId);
                } else {
                    $this->goToPreviousMenu($phoneNumber);
                }
                return;
            }

            if (is_numeric(trim($messageText))) {
                try {
                    $stateSvc2 = new ConversationStateService();
                    $resolvedMenuId2 = $stateSvc2->getCurrentMenuId($phoneNumber);
                } catch (Exception $e) {
                    $resolvedMenuId2 = null;
                }

                if (empty($resolvedMenuId2)) {
                    try {
                        $cur = $this->db->fetch("SELECT current_menu_id FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber]);
                        $resolvedMenuId2 = $cur ? $cur['current_menu_id'] : null;
                    } catch (Exception $e) {
                        $resolvedMenuId2 = null;
                    }
                }

                if (empty($resolvedMenuId2)) {
                    try {
                        $hist = (array)$stateSvc2->getStateData($phoneNumber, 'menu_history');
                        if (!empty($hist)) $resolvedMenuId2 = end($hist);
                    } catch (Exception $e) {
                        $resolvedMenuId2 = null;
                    }
                }

                if (!empty($resolvedMenuId2)) {
                    try {
                        $this->db->update('users', ['current_menu_id' => $resolvedMenuId2, 'current_step' => 1], 'phone_number = :phone', ['phone' => $phoneNumber]);
                        try {
                            $stateSvc2->setCurrentMenu($phoneNumber, (int)$resolvedMenuId2);
                        } catch (Exception $e) {
                            // ignore
                        }
                    } catch (Exception $e) {
                        error_log('[BotService] failed to set current_menu_id before processing numeric selection: ' . $e->getMessage());
                    }

                    $user['current_menu_id'] = $resolvedMenuId2;
                    $this->processMenuSelection($user, $messageText);
                    return;
                }
            }

            // Si llegamos aquí y no es navegación, enviamos un aviso simple para que escriba ENVIADA
            try {
                $this->whatsappService->sendTextMessage($phoneNumber, "Si ya enviaste los documentos, escriba *ENVIADA* cuando hayas terminado. Si necesita volver al menú, escriba *MENU*. ");
            } catch (Exception $e) {
                // ignore
            }
            return;
        }

        // Soportar comando "atrás" o "volver" para navegar al menú padre o al menú previo
        $cmd = mb_strtolower(trim($messageText));
        $backCommands = ['atras', 'atrás', 'volver', 'back'];
        if (in_array($cmd, $backCommands, true)) {
            try { error_log("[BotService] processMenuSelection - BACK command detected for phone={$phoneNumber} resolved_menu={$currentMenuId}"); } catch (Throwable $t) {}
            if (!empty($currentMenuId)) {
                $this->goToParentMenu($phoneNumber, $currentMenuId);
            } else {
                // Si no hay menú actual, intentar volver al menú previo almacenado en historial
                $this->goToPreviousMenu($phoneNumber);
            }
            return;
        }
        
        // Verificar si es un número
        if (!is_numeric($messageText)) {
            $this->whatsappService->sendTextMessage(
                $phoneNumber, 
                "❌ Por favor, responda solo con el número de la opción deseada, la palabra *MENU* o *ATRÁS* para volver."
            );
            error_log("[BotService] processMenuSelection - non-numeric response from user {$user['id']}: '{$messageText}'");
            return;
        }
        
        $optionNumber = (int)$messageText;
        // Si eligió 0 => volver al menú padre
        if ($optionNumber === 0) {
            $this->goToParentMenu($phoneNumber, $currentMenuId);
            return;
        }
        
        // Buscar la opción seleccionada por número
        $option = $this->db->fetch(
            "SELECT * FROM menu_options WHERE menu_id = :menu_id AND option_number = :option_number AND is_active = 1",
            ['menu_id' => $currentMenuId, 'option_number' => $optionNumber]
        );
        
        // Si no encontramos por número, intentar buscar por texto (caso de replies interactivos que envían título)
        if (!$option) {
            $txt = strtolower(trim($messageText));
            if (!empty($txt)) {
                $option = $this->db->fetch(
                    "SELECT * FROM menu_options WHERE menu_id = :menu_id AND is_active = 1 AND (LOWER(text) = :txt OR LOWER(text) LIKE :like) LIMIT 1",
                    ['menu_id' => $currentMenuId, 'txt' => $txt, 'like' => "%{$txt}%"]
                );
                if ($option) {
                    error_log("[BotService] processMenuSelection - matched option by text for user {$user['id']} menu={$currentMenuId} text='{$txt}' => option_number={$option['option_number']}");
                }
            }
        }

        if (!$option) {
            $this->whatsappService->sendTextMessage(
                $phoneNumber, 
                "❌ Opción inválida. Por favor, seleccione una opción válida del menú."
            );
            error_log("[BotService] processMenuSelection - no option found for user {$user['id']} menu={$currentMenuId} option={$optionNumber}");
            return;
        }
        
        error_log("[BotService] processMenuSelection - option found for user {$user['id']} menu={$currentMenuId} option={$optionNumber} action={$option['action_type']}");
        // Procesar acción de la opción
        $this->processMenuAction($phoneNumber, $option);
    }
    
    /**
     * Procesar acción del menú
     */
    private function processMenuAction($phoneNumber, $option) {
        switch ($option['action_type']) {
            case 'menu':
                // Navegar a otro menú
                $targetMenu = $this->db->fetch(
                    "SELECT * FROM menus WHERE name = :name AND is_active = 1",
                    ['name' => $option['action_value']]
                );
                
                if ($targetMenu) {
                    $this->showMenu($phoneNumber, $targetMenu['id']);
                } else {
                    $this->whatsappService->sendTextMessage(
                        $phoneNumber, 
                        "❌ Menú no encontrado: " . $option['action_value']
                    );
                }
                break;
                
            case 'message':
                // Enviar mensaje de respuesta (procesando saltos de línea)
                $message = $this->processLineBreaks($option['action_value']);
                $this->whatsappService->sendTextMessage($phoneNumber, $message);
                $this->exitMenu($phoneNumber);
                break;
                
            case 'api_call':
                // Llamar API externa (implementar según necesidad)
                $this->processApiCall($phoneNumber, $option['action_value']);
                break;

            case 'request_documents':
                // Solicitar al usuario que envíe documentación y activar estado awaiting_documents
                $message = $option['action_value'] ?: "Por favor, envíe los documentos o fotos requeridas.";
                $message = $this->processLineBreaks($message);
                $this->whatsappService->sendTextMessage($phoneNumber, $message);
                try {
                    $stateSvc = new ConversationStateService();
                    $stateSvc->updateStateData($phoneNumber, ['awaiting_documents' => ['since' => date('c'), 'received' => false]]);
                    $this->whatsappService->sendTextMessage($phoneNumber, "Cuando hayas enviado los documentos, escribe *ENVIADA* para que un asesor lo revise.");
                    $sys = [
                        'system' => 'awaiting_documents_started',
                        'type' => 'awaiting_documents',
                        'text' => 'Se solicitó documentación y se espera confirmación ENVIADA.'
                    ];
                    $this->db->insert('conversations', [
                        'user_id' => $this->db->fetch('SELECT id FROM users WHERE phone_number = :phone', ['phone' => $phoneNumber])['id'] ?? null,
                        'direction' => 'outgoing',
                        'message_type' => 'text',
                        'content' => json_encode($sys),
                        'status' => 'sent',
                        'created_at' => date('Y-m-d H:i:s')
                    ]);
                } catch (Exception $e) {
                    error_log('[BotService] request_documents failed: ' . $e->getMessage());
                }
                $this->exitMenu($phoneNumber);
                break;
                
            case 'end':
                // Finalizar conversación (comportamiento por defecto)
                $message = $option['action_value'] ?: "Gracias por usar nuestro servicio. ¡Hasta pronto!";
                $message = $this->processLineBreaks($message);
                $this->whatsappService->sendTextMessage($phoneNumber, $message);
                $this->exitMenu($phoneNumber);
                // Pausar el bot para esta conversación por 3 minutos (antes eran 4 horas)
                $this->pauseConversationForMinutes($phoneNumber, 3);
                break;
        }
    }
    
    /**
     * Procesar texto para respetar saltos de línea
     * Convierte \n escapados a saltos reales
     */
    private function processLineBreaks($text) {
        if (empty($text)) return $text;
        
        // Convertir \n escapados (cadena literal) a saltos de línea reales
        $text = str_replace('\\n', "\n", $text);
        $text = str_replace('\n', "\n", $text);
        
        return $text;
    }

    /**
     * Procesar llamada API externa
     */
    private function processApiCall($phoneNumber, $apiEndpoint) {
        // Aquí puedes implementar llamadas a APIs externas
        // Por ejemplo, consultar saldos, procesar pagos, etc.
        $this->whatsappService->sendTextMessage(
            $phoneNumber, 
            "🔄 Procesando su solicitud... Un momento, por favor."
        );
        
        // Simular procesamiento
        sleep(2);
        
        $this->whatsappService->sendTextMessage(
            $phoneNumber, 
            "✅ Su solicitud ha sido procesada con éxito."
        );
        
        $this->exitMenu($phoneNumber);
    }
    
    /**
     * Procesar respuestas automáticas
     */
    private function processAutoResponses($phoneNumber, $messageText) {
        $keyword = strtolower($messageText);
        
        // Buscar respuesta automática por keyword
        $autoResponse = $this->db->fetch(
            "SELECT * FROM autoresponses WHERE trigger_type = 'keyword' AND LOWER(trigger_value) = :keyword AND is_active = 1",
            ['keyword' => $keyword]
        );
        
        if ($autoResponse) {
            $message = $this->processLineBreaks($autoResponse['response_text']);
            $this->whatsappService->sendTextMessage($phoneNumber, $message);
            
            // Si la keyword es 'menu', mostrar el menú
            if ($keyword === 'menu' || $keyword === 'menú') {
                $this->showMainMenu($phoneNumber);
            }
        } else {
            // Respuesta por defecto para mensajes no reconocidos cuando se llama explícitamente
            $this->sendDefaultNoMatch($phoneNumber);
        }
    }

    /**
     * Enviar mensaje por defecto cuando no hay coincidencias.
     * Si el asesor está marcado como ausente (advisor_available=0), muestra el menú
     * secundario en lugar del mensaje genérico, para que todos los clientes que escriban
     * vean el menú de asesor ausente hasta que se reactive la disponibilidad.
     */
    private function sendDefaultNoMatch($phoneNumber) {
        // Si el asesor está ausente, redirigir al menú secundario
        try {
            $cfg = $this->db->fetch(
                "SELECT config_value FROM system_config WHERE config_key = 'advisor_available' LIMIT 1"
            );
            $advisorAbsent = ($cfg !== null && $cfg !== false) && !(bool)(int)$cfg['config_value'];
            if ($advisorAbsent) {
                error_log("[BotService] sendDefaultNoMatch - advisor absent, redirecting to showMainMenu for phone={$phoneNumber}");
                $this->showMainMenu($phoneNumber);
                return;
            }
        } catch (Exception $e) {
            error_log('[BotService] sendDefaultNoMatch - failed to read advisor_available: ' . $e->getMessage());
        }

        $defaultMessage = "🤖 No entendí su mensaje. Escriba *MENU* para ver las opciones disponibles o *ASESOR* para obtener ayuda.";
        $this->whatsappService->sendTextMessage($phoneNumber, $defaultMessage);
    }
    
    /**
     * Salir del menú actual
     * Guarda el menú previo en el historial para permitir "ATRÁS" después de salir
     */
    private function exitMenu($phoneNumber) {
        // Obtener menú actual antes de limpiar
        try {
            $cur = $this->db->fetch("SELECT current_menu_id FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber]);
            $previousMenu = $cur ? $cur['current_menu_id'] : null;
            if (!empty($previousMenu)) {
                try {
                    $this->pushMenuToHistory($phoneNumber, $previousMenu);
                } catch (Exception $e) {
                    error_log('[BotService] exitMenu - failed to push previous menu: ' . $e->getMessage());
                }
            }
        } catch (Exception $e) {
            // ignore
        }

        $this->db->update(
            'users',
            ['current_menu_id' => null, 'current_step' => 0, 'session_data' => null],
            'phone_number = :phone',
            ['phone' => $phoneNumber]
        );

        // Mantener el estado en ConversationStateService (no borrar el historial)
        try {
            $stateSvc = new ConversationStateService();
            $stateSvc->setCurrentMenu($phoneNumber, null);
        } catch (Exception $e) {
            // ignore
        }
    }

    /**
     * Poner la conversación en pausa por X minutos (llamada desde opciones 'end')
     */
    private function pauseConversationForMinutes($phoneNumber, $minutes = 3) {
        try {
            $pausedUntil = date('Y-m-d H:i:s', strtotime("+{$minutes} minutes"));
            $this->db->update('users', ['bot_paused_until' => $pausedUntil], 'phone_number = :phone', ['phone' => $phoneNumber]);
            if (function_exists('writeLog')) writeLog('INFO', "Bot paused for {$minutes} minutes for {$phoneNumber}");
        } catch (Exception $e) {
            error_log('pauseConversationForMinutes failed: ' . $e->getMessage());
        }
    }

    // Compatibilidad: wrapper que acepta horas para llamadas antiguas
    private function pauseConversationForHours($phoneNumber, $hours = 4) {
        $minutes = intval($hours * 60);
        $this->pauseConversationForMinutes($phoneNumber, $minutes);
    }

    /**
     * Poner conversación en espera (asesor solicitado) por X minutos
     */
    public function putOnHold($phoneNumber, $minutes = 3) {
        try {
            $pausedUntil = date('Y-m-d H:i:s', strtotime("+{$minutes} minutes"));
            // Guardar bandera y tiempo de expiración (usamos bot_paused_until que ya existe)
            $this->db->update('users', ['on_hold' => 1, 'bot_paused_until' => $pausedUntil, 'advisor_requested' => 1, 'bot_enabled' => 0], 'phone_number = :phone', ['phone' => $phoneNumber]);
            // Enviar mensaje (bot) marcando la solicitud; no incluimos metadata de operador para evitar limpiar la marca
            $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Se ha solicitado un asesor. Se le ha puesto en espera por {$minutes} minutos. Si no hay respuesta, podrá volver a usar *MENU*." );
            if (function_exists('writeLog')) writeLog('INFO', "Advisor requested for $phoneNumber until $pausedUntil");
        } catch (Exception $e) {
            error_log('putOnHold failed: ' . $e->getMessage());
        }
    }

    /**
     * Registrar solicitud de asesor sin poner on_hold (usuario lo solicita, se notifica al equipo)
     */
    public function requestAdvisor($phoneNumber, $minutes = 3) {
        try {
            $pausedUntil = date('Y-m-d H:i:s', strtotime("+{$minutes} minutes"));
            // No ponemos on_hold para evitar bloqueo automático; usamos advisor_requested
            // Marcar solicitud de asesor y pausar bot temporalmente
            $this->db->update('users', ['advisor_requested' => 1, 'bot_paused_until' => $pausedUntil, 'bot_enabled' => 0], 'phone_number = :phone', ['phone' => $phoneNumber]);
            $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Le hemos transferido a un asesor; le responderemos en breve.");

            // Enviar automáticamente el menú de horario_asesor para que el cliente sepa cuándo esperar
            try {
                $horarioMenu = $this->db->fetch(
                    "SELECT id FROM menus WHERE name = :name AND is_active = 1",
                    ['name' => 'horario_asesor']
                );
                
                if ($horarioMenu && !empty($horarioMenu['id'])) {
                    $this->showMenu($phoneNumber, $horarioMenu['id']);
                    error_log("[BotService] Sent horario_asesor menu to {$phoneNumber} after advisor request");
                } else {
                    error_log("[BotService] horario_asesor menu not found when requesting advisor for {$phoneNumber}");
                }
            } catch (Exception $e) {
                error_log('Failed to send horario_asesor menu on advisor request: ' . $e->getMessage());
            }

            if (function_exists('writeLog')) writeLog('INFO', "Advisor solicited for $phoneNumber until $pausedUntil");
        } catch (Exception $e) {
            error_log('requestAdvisor failed: ' . $e->getMessage());
        }
    }

    public function releaseHold($phoneNumber, $notify = true) {
        try {
            // Clear hold/advisor flags and also ensure in_service is cleared for consistency
            // Clear hold and reactivate bot
            $this->db->update('users', ['on_hold' => 0, 'bot_paused_until' => null, 'advisor_requested' => 0, 'in_service' => 0, 'in_service_by' => null, 'in_service_at' => null, 'bot_enabled' => 1], 'phone_number = :phone', ['phone' => $phoneNumber]);

            // Send notification only when requested (avoid duplicate messages after finish)
            if ($notify) {
                try {
                    $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Su conversación ha sido retomada por el equipo. Puede usar *MENU* para continuar.");
                } catch (Exception $e) {
                    // ignore send errors
                }
            }

            if (function_exists('writeLog')) writeLog('INFO', "Advisor released hold for $phoneNumber notify=" . ($notify ? '1' : '0'));
        } catch (Exception $e) {
            error_log('releaseHold failed: ' . $e->getMessage());
        }
    }

    /**
     * Si ha pasado 6 horas desde la última actividad, reinicia el estado de la conversación para que
     * el bot vuelva a enviar el mensaje de bienvenida y el usuario empiece 

    /**
     * Marcar la conversación como "en servicio" por un operador (Atender)
     */
    public function attendConversation($phoneNumber, $operatorId = null) {
        try {
            $now = date('Y-m-d H:i:s');
            // Marcar como en servicio y desactivar el bot temporalmente (solo mientras el operador atiende)
            $update = ['in_service' => 1, 'in_service_at' => $now, 'advisor_requested' => 0, 'bot_enabled' => 0];
            if ($operatorId) $update['in_service_by'] = $operatorId;
            $this->db->update('users', $update, 'phone_number = :phone', ['phone' => $phoneNumber]);

            // Notificar al usuario
            try {
                $this->whatsappService->sendTextMessage($phoneNumber, "🔔 Un asesor está atendiendo su conversación. Por favor, espere su respuesta.");
                // ignore send errors
            } catch (Exception $e) {
                // Log and continue if notification fails
                error_log('attendConversation notify failed: ' . $e->getMessage());
            }

            // Registrar actividad del operador si hay información disponible
            $user = $this->db->fetch('SELECT id FROM users WHERE phone_number = :phone', ['phone' => $phoneNumber]);
            $userId = $user['id'] ?? null;
            $activity = [
                'user_id' => $userId,
                'operator_id' => $operatorId,
                'action' => 'attend',
                'details' => 'Operator started attending the conversation',
                'created_at' => $now
            ];
            try {
                $this->db->insert('operator_activity', $activity);
            } catch (Exception $e) {
                if (function_exists('writeLog')) writeLog('WARN', 'Failed to insert operator_activity: ' . $e->getMessage());
                error_log('Failed to insert operator_activity: ' . $e->getMessage());
            }

            if (function_exists('writeLog')) writeLog('INFO', "Operator {$operatorId} attending conversation for {$phoneNumber}");
        } catch (Exception $e) {
            error_log('attendConversation failed: ' . $e->getMessage());
            throw $e;
        }
    }

    /**
     * Finalizar la atención: limpiar estado 'in_service' y enviar plantilla de encuesta
     */
    public function finishAttendConversation($phoneNumber, $operatorId = null) {
        try {
            // Limpiar estado de 'in_service'
            // Limpiar in_service y reactivar el bot
            $this->db->update('users', ['in_service' => 0, 'in_service_by' => null, 'in_service_at' => null, 'advisor_requested' => 0, 'bot_enabled' => 1], 'phone_number = :phone', ['phone' => $phoneNumber]);

            // Enviar plantilla de encuesta (configurable)
            $template = getConfigFromDB('survey_template', 'encuesta_satisfaccin');
            $lang = getConfigFromDB('survey_template_language', 'es');

            try {
                $this->whatsappService->sendTemplateMessage($phoneNumber, $template, $lang, []);
            } catch (Exception $e) {
                // Ignorar errores de envío de plantilla pero loguear
                error_log('Failed to send survey template: ' . $e->getMessage());
                if (function_exists('writeLog')) writeLog('WARN', 'Failed to send survey template: ' . $e->getMessage());
            }

            // Registrar actividad del operador
            $user = $this->db->fetch('SELECT id FROM users WHERE phone_number = :phone', ['phone' => $phoneNumber]);
            $userId = $user['id'] ?? null;
            $now = date('Y-m-d H:i:s');
            $activity = [
                'user_id' => $userId,
                'operator_id' => $operatorId,
                'action' => 'finish',
                'details' => "Finished attend and sent survey: {$template}",
                'created_at' => $now
            ];
            try {
                $this->db->insert('operator_activity', $activity);
            } catch (Exception $e) {
                error_log('Failed to insert operator_activity on finish: ' . $e->getMessage());
            }

            if (function_exists('writeLog')) writeLog('INFO', "Operator {$operatorId} finished attending for {$phoneNumber}, survey={$template}");

            // Ensure consistent behavior: release hold / clear flags when finishing attend
            try {
                // Release hold silently to avoid duplicate notification after finishing attend
                $this->releaseHold($phoneNumber, false);
            } catch (Exception $e) {
                error_log('finishAttendConversation: releaseHold failed: ' . $e->getMessage());
            }
        } catch (Exception $e) {
            error_log('finishAttendConversation failed: ' . $e->getMessage());
            throw $e;
        }
    }

    /**
     * Ir al menú padre del menú actual
     */
    private function goToParentMenu($phoneNumber, $currentMenuId) {
        try {
            $parentId = null;
            $menu = null;

            // Intentar resolver por ID numérico
            if (!empty($currentMenuId) && is_numeric($currentMenuId)) {
                $menu = $this->db->fetch("SELECT id, parent_id, name, title FROM menus WHERE id = :id", ['id' => (int)$currentMenuId]);
            }

            // Si no existe, intentar resolver por nombre (caso action_value = 'main_menu')
            if (!$menu && !empty($currentMenuId)) {
                $menu = $this->db->fetch("SELECT id, parent_id, name, title FROM menus WHERE name = :name", ['name' => $currentMenuId]);
            }

            if ($menu) {
                $parentId = $menu['parent_id'] ?? null;
                try { error_log("[BotService] goToParentMenu - resolved menu id={$menu['id']} name={$menu['name']} title='{$menu['title']}' parent_id=" . ($parentId ?? 'null') . " for phone={$phoneNumber}"); } catch (Throwable $t) {}

                if (!empty($parentId)) {
                    $this->showMenu($phoneNumber, $parentId);
                    return;
                } else {
                    // Si no hay padre, re-mostrar el menú actual (mejor UX)
                    if (!empty($menu['id'])) {
                        $this->showMenu($phoneNumber, $menu['id']);
                        return;
                    }
                }
            } else {
                try { error_log("[BotService] goToParentMenu - could not resolve menu for currentMenuId='{$currentMenuId}' phone={$phoneNumber}"); } catch (Throwable $t) {}
            }

            // Fallback final a menú principal
            $this->showMainMenu($phoneNumber);
        } catch (Exception $e) {
            error_log('[BotService] goToParentMenu failed: ' . $e->getMessage());
            $this->showMainMenu($phoneNumber);
        }
    }

    /**
     * Actualizar estado del menú del usuario
     * - Guarda el menú actual en el historial (ConversationStateService) antes de actualizar
     */
    private function updateUserMenuState($phoneNumber, $menuId) {
        // Obtener menú previo
        try {
            $cur = $this->db->fetch("SELECT current_menu_id FROM users WHERE phone_number = :phone", ['phone' => $phoneNumber]);
            $previousMenu = $cur ? $cur['current_menu_id'] : null;
        } catch (Exception $e) {
            $previousMenu = null;
        }

        try { error_log("[BotService] updateUserMenuState - phone={$phoneNumber} previousMenu=" . json_encode($previousMenu) . " newMenu=" . json_encode($menuId)); } catch (Throwable $t) {}

        // Si hay un menú previo diferente al nuevo, empujarlo al historial
        if (!empty($previousMenu) && $previousMenu != $menuId) {
            try {
                $this->pushMenuToHistory($phoneNumber, $previousMenu);
            } catch (Exception $e) {
                error_log('[BotService] failed to push menu to history: ' . $e->getMessage());
            }
        }

        // Actualizar columna users
        $this->db->update(
            'users',
            ['current_menu_id' => $menuId, 'current_step' => 1],
            'phone_number = :phone',
            ['phone' => $phoneNumber]
        );

        // También actualizar ConversationStateService por compatibilidad
        try {
            $stateSvc = new ConversationStateService();
            $stateSvc->setCurrentMenu($phoneNumber, (int)$menuId);
        } catch (Exception $e) {
            // ignore if service not available
        }
    }
    

    /**
     * Empujar un menú al historial del usuario (se almacena en ConversationStateService.state_data.menu_history)
     */
    private function pushMenuToHistory(string $phoneNumber, $menuId) {
        try {
            $stateSvc = new ConversationStateService();
            $currentState = $stateSvc->getState($phoneNumber);

            // Si no existe una fila de estado, crearla con el historial inicial
            if (!$currentState) {
                try {
                    error_log("[BotService] pushMenuToHistory - no existing state, creating initial state with menu_history for phone={$phoneNumber} menuId={$menuId}");
                    $res = $stateSvc->setState($phoneNumber, ConversationStateService::STATE_INITIAL, ['menu_history' => [$menuId]]);
                    error_log('[BotService] pushMenuToHistory - setState result: ' . json_encode($res));
                    return $res;
                } catch (Exception $e) {
                    error_log('[BotService] pushMenuToHistory create initial state failed: ' . $e->getMessage());
                    return false;
                }
            }

            $history = (array)$stateSvc->getStateData($phoneNumber, 'menu_history');
            if (!is_array($history)) $history = [];

            // Añadir al final y limitar a 10 entradas
            $history[] = $menuId;
            if (count($history) > 10) array_shift($history);

            return $stateSvc->updateStateData($phoneNumber, ['menu_history' => $history]);
        } catch (Exception $e) {
            error_log('[BotService] pushMenuToHistory failed: ' . $e->getMessage());
            return false;
        }
    }

    /**
     * Sacar el último menú del historial (LIFO) y retornarlo
     */
    private function popMenuFromHistory(string $phoneNumber) {
        try {
            $stateSvc = new ConversationStateService();
            $history = (array)$stateSvc->getStateData($phoneNumber, 'menu_history');
            if (!is_array($history) || empty($history)) return null;

            $last = array_pop($history);
            $stateSvc->updateStateData($phoneNumber, ['menu_history' => $history]);
            return $last;
        } catch (Exception $e) {
            error_log('[BotService] popMenuFromHistory failed: ' . $e->getMessage());
            return null;
        }
    }

    /**
     * Ir al menú anterior según historial. Si no hay historial, fallback a main menu.
     */
    private function goToPreviousMenu($phoneNumber) {
        $prev = $this->popMenuFromHistory($phoneNumber);
        if (!empty($prev)) {
            // Intentar mostrar por ID primero
            if (is_numeric($prev)) {
                $this->showMenu($phoneNumber, (int)$prev);
                return;
            }
            // Si no es numérico, intentar resolver por nombre
            $menu = $this->db->fetch("SELECT id FROM menus WHERE name = :name", ['name' => $prev]);
            if ($menu && !empty($menu['id'])) {
                $this->showMenu($phoneNumber, $menu['id']);
                return;
            }
        }

        // Fallback
        $this->showMainMenu($phoneNumber);
    }

    /**
     * Obtener estadísticas del bot
     */
    public function getBotStats() {
        $stats = [];
        
        // Total de usuarios
        $stats['total_users'] = $this->db->fetch(
            "SELECT COUNT(*) as count FROM users"
        )['count'];
        
        // Usuarios activos (último mes)
        $stats['active_users'] = $this->db->fetch(
            "SELECT COUNT(DISTINCT user_id) as count FROM conversations 
             WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)"
        )['count'];
        
        // Total de mensajes
        $stats['total_conversations'] = $this->db->fetch(
            "SELECT COUNT(*) as count FROM conversations"
        )['count'];
        
        // Mensajes hoy
        $stats['conversations_today'] = $this->db->fetch(
            "SELECT COUNT(*) as count FROM conversations 
             WHERE DATE(created_at) = CURDATE()"
        )['count'];
        
        return $stats;
    }
}
?>