<?php
/**
 * Webhook para recibir mensajes de WhatsApp
 * Fecha: 13 de noviembre de 2025
 */

require_once __DIR__ . '/../config/config.php';



// Headers para API
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
header('Access-Control-Allow-Headers: Content-Type');

class WhatsAppWebhook {
    private $db;
    private $whatsappService;
    private $botService;
    
    public function __construct() {
        error_log('[webhook] __construct start');
        try {
            $this->db = Database::getInstance();
            $this->whatsappService = new WhatsAppService();
            $this->botService = new BotService();
        } catch (Throwable $t) {
            error_log('[webhook] constructor failed: ' . $t->getMessage());
            if (function_exists('writeLog')) {
                writeLog('ERROR', 'Webhook constructor failed: ' . $t->getMessage(), ['trace' => $t->getTraceAsString()]);
            }
            throw $t;
        }
        error_log('[webhook] __construct end');
    }
    
    public function handleRequest() {
        error_log('[webhook] handleRequest start, method=' . ($_SERVER['REQUEST_METHOD'] ?? 'unknown'));
        $method = $_SERVER['REQUEST_METHOD'];
        
        if ($method === 'GET') {
            $this->verifyWebhook();
        } elseif ($method === 'POST') {
            $this->processIncomingMessage();
        } else {
            http_response_code(405);
            echo json_encode(['error' => 'Método no permitido']);
        }
    }
    
    private function verifyWebhook() {
        $verifyToken = $_GET['hub_verify_token'] ?? '';
        $challenge = $_GET['hub_challenge'] ?? '';
        $mode = $_GET['hub_mode'] ?? '';
        
        if ($mode === 'subscribe' && $verifyToken === WEBHOOK_VERIFY_TOKEN) {
            echo $challenge;
            exit;
        } else {
            http_response_code(403);
            echo json_encode(['error' => 'Token de verificación inválido']);
        }
    }
    
    private function processIncomingMessage() {
        $input = file_get_contents('php://input');
        $data = json_decode($input, true);
        
        // Registrar webhook en logs
        $this->logWebhook($input, json_encode(['status' => 'received']), 200);
        
        if (!$data || !isset($data['entry'])) {
            http_response_code(400);
            echo json_encode(['error' => 'Datos inválidos']);
            return;
        }
        
        try {
            foreach ($data['entry'] as $entry) {
                if (isset($entry['changes'])) {
                    foreach ($entry['changes'] as $change) {
                        // Soportar payloads con field 'conversations' y 'messages' (WhatsApp puede variar)
                        if ($change['field'] === 'conversations' || $change['field'] === 'messages' || isset($change['value']['messages'])) {
                            $this->processconversations($change['value']);
                        }
                    }
                }
            }
            
            echo json_encode(['status' => 'success']);
            
        } catch (Exception $e) {
            error_log("Error processing webhook: " . $e->getMessage());
            http_response_code(500);
            echo json_encode(['error' => 'Error interno del servidor']);
        }
    }
    
    private function processconversations($value) {
        // Detectar si este mensaje llegó al número turnero
        $receivingPhoneId = $value['metadata']['phone_number_id'] ?? null;
        $turneroPhoneId   = getConfigFromDB('whatsapp_phone_number_id_turnero', '');
        $isTurnero = $receivingPhoneId && $turneroPhoneId && ($receivingPhoneId === $turneroPhoneId);

        // Aceptar tanto payloads con 'conversations' como con 'messages' (WhatsApp varía según la integración)
        $items = [];
        if (isset($value['conversations']) && is_array($value['conversations'])) {
            $items = $value['conversations'];
        } elseif (isset($value['messages']) && is_array($value['messages'])) {
            $items = $value['messages'];
        } else {
            return;
        }

        // Extraer nombres de contactos del payload (contacts[].profile.name).
        // Se indexa por wa_id y también por user_id (BSUID): desde que Meta desplegó
        // los nombres de usuario, quien oculta su teléfono llega sin wa_id.
        $contactNames = [];
        if (isset($value['contacts']) && is_array($value['contacts'])) {
            foreach ($value['contacts'] as $contact) {
                $name = $contact['profile']['name'] ?? null;
                if (!$name) continue;
                foreach ([$contact['wa_id'] ?? null, $contact['user_id'] ?? null] as $clave) {
                    if ($clave) $contactNames[$clave] = $name;
                }
            }
        }

        foreach ($items as $message) {
            // En algunos payloads la estructura key es 'from' y 'id' (mensajes), en otros puede venir distinta; normalizamos.
            $phoneNumber = $message['from'] ?? ($message['wa_id'] ?? null);
            $messageId = $message['id'] ?? ($message['message_id'] ?? null);
            $timestamp = $message['timestamp'] ?? null;

            // Meta manda el BSUID en todos los mensajes, traigan teléfono o no.
            $bsuid = $message['from_user_id'] ?? null;

            // Si la persona oculta su teléfono, `from` y `wa_id` no llegan. En ese caso
            // se busca por BSUID: si ya escribió antes mostrando su número, se le
            // reconoce y se le sigue respondiendo a ese teléfono.
            if (empty($phoneNumber) && $bsuid) {
                $conocido = $this->db->fetch(
                    "SELECT phone_number FROM users WHERE bsuid = :b", ['b' => $bsuid]
                );
                // Si no se le conoce, el BSUID hace de identificador: sirve para responderle,
                // aunque no permita cruzarlo con el paciente ni con el turnero.
                $phoneNumber = $conocido['phone_number'] ?? $bsuid;
            }

            // Si falta lo crítico, saltar
            if (empty($phoneNumber) || empty($messageId)) {
                continue;
            }

            // Verificar si ya procesamos este mensaje
            $existing = $this->db->fetch(
                "SELECT id FROM conversations WHERE message_id = :message_id",
                ['message_id' => $messageId]
            );

            if ($existing) {
                continue; // Ya procesamos este mensaje
            }

            // Obtener o crear usuario
            $user = $this->getUserByPhone($phoneNumber);
            $contactName = $contactNames[$phoneNumber] ?? ($bsuid ? ($contactNames[$bsuid] ?? null) : null);
            if (!$user) {
                $userId = $this->createUser($phoneNumber);
                $user = $this->getUserById($userId);
                // Guardar nombre del contacto de WhatsApp al crear usuario
                if ($contactName && $userId) {
                    try {
                        $this->db->update('users', ['name' => $contactName], 'id = ? AND (name IS NULL OR name = ?)', [$userId, '']);
                        $user['name'] = $contactName;
                    } catch (Exception $e) {
                        error_log('[webhook] Error guardando nombre contacto: ' . $e->getMessage());
                    }
                }
            } else {
                // Actualizar nombre si el usuario no tiene uno guardado
                if ($contactName && (empty($user['name']))) {
                    try {
                        $this->db->update('users', ['name' => $contactName], 'id = ? AND (name IS NULL OR name = ?)', [$user['id'], '']);
                        $user['name'] = $contactName;
                    } catch (Exception $e) {
                        error_log('[webhook] Error actualizando nombre contacto: ' . $e->getMessage());
                    }
                }
            }

            // Guardar la equivalencia BSUID↔usuario mientras la persona todavía muestra
            // su teléfono. El día que lo oculte, ese registro es lo único que permitirá
            // reconocerla, así que se anota en cada mensaje y no solo la primera vez.
            if ($bsuid && !empty($user['id']) && ($user['bsuid'] ?? null) !== $bsuid) {
                try {
                    $this->db->update('users', ['bsuid' => $bsuid], 'id = ?', [$user['id']]);
                    $user['bsuid'] = $bsuid;
                } catch (Exception $e) {
                    // Choca si ese BSUID ya está en otro usuario (la persona cambió de
                    // número). No es motivo para perder el mensaje: se sigue adelante.
                    error_log('[webhook] No se pudo guardar el BSUID ' . $bsuid . ': ' . $e->getMessage());
                }
            }

            // Procesar diferentes tipos de mensaje
            $messageText = '';
            $messageType = 'text';
            $mediaUrl = null;

            if (isset($message['reaction']) || (isset($message['type']) && $message['type'] === 'reaction')) {
                // Reacción (emoji) a un mensaje anterior
                $messageType = 'reaction';
                $emoji = $message['reaction']['emoji'] ?? ($message['emoji'] ?? '');
                $reactionTo = $message['reaction']['message_id'] ?? ($message['context']['id'] ?? '');
                $messageText = json_encode(['emoji' => $emoji, 'message_id' => $reactionTo]);

                // Guardar campos específicos para reacciones
                $extraFields = [
                    'reaction_to_message_id' => $reactionTo ? intval($reactionTo) : null,
                    'reaction_emoji' => $emoji
                ];

            } elseif (($message['type'] ?? '') === 'contacts') {
                // La persona compartió su contacto, sea por el botón que se le pidió
                // o a mano. Es la única forma de obtener el teléfono de quien lo oculta.
                $messageType = 'contacts';
                $messageText = json_encode($message['contacts'] ?? []);

                $telefonoCompartido = null;
                foreach ($message['contacts'] ?? [] as $c) {
                    foreach ($c['phones'] ?? [] as $t) {
                        // wa_id ya viene normalizado; `phone` puede traer espacios y signos
                        $candidato = $t['wa_id'] ?? ($t['phone'] ?? null);
                        if ($candidato) {
                            $telefonoCompartido = preg_replace('/[^0-9]/', '', $candidato);
                            break 2;
                        }
                    }
                }

                if ($telefonoCompartido && !empty($user['id'])) {
                    $this->vincularTelefonoCompartido($user, $telefonoCompartido, $bsuid);
                }

            } elseif (isset($message['interactive'])) {
                // Interactive replies (list or button) - normalize to text so bot can process
                $messageType = 'text';
                if (isset($message['interactive']['list_reply']['title'])) {
                    $messageText = $message['interactive']['list_reply']['title'];
                    // If title starts with a number ("1. Option"), convert to the number for menu selection
                    if (preg_match('/^\s*(\d+)\b/', $messageText, $m)) {
                        $messageText = $m[1];
                    }
                } elseif (isset($message['interactive']['button_reply']['title'])) {
                    $messageText = $message['interactive']['button_reply']['title'];
                    if (preg_match('/^\s*(\d+)\b/', $messageText, $m)) {
                        $messageText = $m[1];
                    }
                } else {
                    $messageText = json_encode($message['interactive']);
                }
                error_log(sprintf('[webhook] interactive received title=%s mapped=%s', substr($message['interactive']['list_reply']['title'] ?? ($message['interactive']['button_reply']['title'] ?? json_encode($message['interactive'])),0,200), substr($messageText,0,200)));
            } elseif (isset($message['text']) || ($message['type'] ?? '') === 'text') {
                $messageText = isset($message['text']['body']) ? $message['text']['body'] : ($message['body'] ?? '');
                $messageType = 'text';
            } elseif (isset($message['image'])) {
                $messageText = $message['image']['caption'] ?? '';
                $messageType = 'image';
                // Preferir media ID (estable) sobre URL (expira en minutos)
                $mediaUrl = $message['image']['id'] ?? ($message['image']['url'] ?? null);
                $mimeType = $message['image']['mime_type'] ?? null;
                $filename = $message['image']['filename'] ?? null;

            } elseif (isset($message['audio'])) {
                $messageType = 'audio';
                $mediaUrl = $message['audio']['id'] ?? ($message['audio']['url'] ?? null);
                $mimeType = $message['audio']['mime_type'] ?? null;
                $filename = $message['audio']['filename'] ?? null;

            } elseif (isset($message['video'])) {
                $messageText = $message['video']['caption'] ?? '';
                $messageType = 'video';
                $mediaUrl = $message['video']['id'] ?? ($message['video']['url'] ?? null);
                $mimeType = $message['video']['mime_type'] ?? null;
                $filename = $message['video']['filename'] ?? null;

            } elseif (isset($message['document'])) {
                $messageText = $message['document']['filename'] ?? '';
                $messageType = 'document';
                $mediaUrl = $message['document']['id'] ?? ($message['document']['url'] ?? null);
                $mimeType = $message['document']['mime_type'] ?? null;
                $filename = $message['document']['filename'] ?? null;

            } elseif (isset($message['sticker'])) {
                $messageType = 'sticker';
                $mediaUrl = $message['sticker']['id'] ?? ($message['sticker']['url'] ?? null);
                $mimeType = $message['sticker']['mime_type'] ?? null;
                $filename = $message['sticker']['filename'] ?? null;

            }

            // Log debug (temporal) para verificar cómo llegan media_url/filename
            error_log(sprintf('[webhook] messageId=%s type=%s mediaUrl=%s filename=%s mime=%s', $messageId, $messageType, $mediaUrl ?? 'NULL', $filename ?? 'NULL', $mimeType ?? 'NULL'));

            // ── Descargar media localmente de inmediato ──
            // Si hay un media ID (numérico) o URL de Graph, descargarlo y guardarlo en uploads/media/
            $localFile = null;
            $localThumb = null;
            if (!empty($mediaUrl) && in_array($messageType, ['image', 'audio', 'video', 'document', 'sticker'])) {
                try {
                    set_time_limit(180); // Tiempo extra para descargar media pesado
                    $mediaService = new MediaService();
                    $subdir = date('Y/m'); // Organizar por año/mes

                    // Determinar si es un media ID (numérico) o una URL directa
                    if (preg_match('/^\d+$/', $mediaUrl)) {
                        // Es un media ID de WhatsApp → resolver vía Graph API y descargar
                        $stored = $mediaService->fetchAndStoreFromGraph($mediaUrl, $subdir);
                    } else {
                        // Es una URL directa → descargar directamente
                        $stored = $mediaService->fetchAndStoreFromUrl($mediaUrl, $subdir);
                    }

                    if (!empty($stored['local_file'])) {
                        $localFile = $stored['local_file'];
                        error_log("[webhook] Media descargado localmente: {$localFile}");
                    }
                    if (!empty($stored['local_thumb'])) {
                        $localThumb = $stored['local_thumb'];
                    }
                } catch (Exception $mediaEx) {
                    error_log("[webhook] Error descargando media (se encolará para retry): " . $mediaEx->getMessage());
                    // Encolar en media_queue para reintento posterior
                    try {
                        $this->db->insert('media_queue', [
                            'media_id' => preg_match('/^\d+$/', $mediaUrl) ? $mediaUrl : '',
                            'media_url' => $mediaUrl,
                            'subdir' => date('Y/m'),
                            'status' => 'pending',
                            'created_at' => date('Y-m-d H:i:s')
                        ]);
                        error_log("[webhook] Media encolado en media_queue para retry");
                    } catch (Exception $qEx) {
                        error_log("[webhook] Error encolando media: " . $qEx->getMessage());
                    }
                }
            }

            // Guardar mensaje en base de datos
            $saveData = [
                'user_id' => $user['id'],
                'message_id' => $messageId,
                'direction' => 'incoming',
                'message_type' => $messageType,
                'content' => $messageText,
                'media_url' => $mediaUrl,
                'status' => 'received',
                'is_read' => 0,
                'canal' => $isTurnero ? 'turnero' : 'bot',
            ];

            // Guardar whatsapp_media_id si el mediaUrl es un ID numérico de WhatsApp
            if (!empty($mediaUrl) && preg_match('/^\d+$/', $mediaUrl)) {
                $saveData['whatsapp_media_id'] = $mediaUrl;
            }

            // Añadir campos opcionales solo si existen para evitar errores en esquemas antiguos
            if (isset($filename) && $filename !== null) {
                $saveData['filename'] = $filename;
            }
            if (isset($mimeType) && $mimeType !== null) {
                $saveData['mime_type'] = $mimeType;
            }
            // Si se descargó y almacenó localmente, añadir las rutas locales
            if (isset($localFile) && $localFile) {
                $saveData['local_file'] = $localFile;
            }
            if (isset($localThumb) && $localThumb) {
                $saveData['local_thumb'] = $localThumb;
            }

            // Si el mensaje incluye contexto (es respuesta a otro mensaje)
            if (isset($message['context']) && isset($message['context']['id'])) {
                $saveData['reply_to_message_id'] = $message['context']['id'];
            }

            // Special handling for reaction messages: apply reaction to referenced message instead of creating separate record
            if (isset($extraFields) && is_array($extraFields) && isset($messageType) && $messageType === 'reaction' && !empty($reactionTo)) {
                try {
                    // Try to update the referenced message (by message_id or id)
                    $updated = $this->db->update(
                        'conversations',
                        ['reaction_emoji' => $emoji, 'reaction_to_message_id' => $reactionTo],
                        'message_id = :mid OR id = :mid',
                        ['mid' => $reactionTo]
                    );

                    // Optionally create a small notification about the reaction
                    try {
                        $this->db->insert('notifications', [
                            'user_id' => $user['id'],
                            'type' => 'reaction',
                            'message' => "Reacción recibida: {$emoji}",
                            'data' => json_encode(['message_id' => $reactionTo, 'emoji' => $emoji]),
                            'is_read' => 0,
                            'created_at' => date('Y-m-d H:i:s')
                        ]);
                    } catch (Exception $ne) {
                        error_log('Failed to create reaction notification: ' . $ne->getMessage());
                    }

                    // We handled the reaction by updating the target message - skip creating a separate reaction record
                    continue;
                } catch (Exception $e) {
                    // If update failed, fallback to saving the reaction as a message
                    error_log('Failed to apply reaction update: ' . $e->getMessage());
                    $saveData = array_merge($saveData, $extraFields);
                }
            } else {
                // Añadir campos extra (si se detectaron)
                if (isset($extraFields) && is_array($extraFields)) {
                    $saveData = array_merge($saveData, $extraFields);
                }
            }

            // Filtrar campos según columnas existentes para evitar errores en esquemas antiguos
            $saveData = $this->filterColumns('conversations', $saveData);
            // Guardar mensaje con INSERT IGNORE para deduplicar webhooks duplicados.
            // Si el message_id ya existe (segunda llamada paralela), rowCount=0 y saltamos el bot.
            $conversationId = $this->saveMessage($saveData);
            if (!$conversationId) {
                // Mensaje ya procesado por una llamada concurrente, ignorar
                error_log("[webhook] Duplicate webhook ignorado para message_id={$messageId}");
                continue;
            }

            // Si el media se encoló (no se pudo descargar), actualizar media_queue con el conversation_id
            if (!empty($mediaUrl) && empty($localFile) && in_array($messageType, ['image', 'audio', 'video', 'document', 'sticker'])) {
                try {
                    $this->db->query(
                        "UPDATE media_queue SET conversation_id = :cid WHERE media_id = :mid AND conversation_id IS NULL ORDER BY id DESC LIMIT 1",
                        ['cid' => $conversationId, 'mid' => (preg_match('/^\d+$/', $mediaUrl) ? $mediaUrl : '')]
                    );
                } catch (Exception $e) {
                    error_log("[webhook] Error actualizando media_queue con conversation_id: " . $e->getMessage());
                }
            }

            // Notificaciones y SSE solo para el canal principal (no turnero)
            if (!$isTurnero) {
                try {
                    $this->db->insert('notifications', [
                        'user_id' => $user['id'],
                        'type' => 'incoming_message',
                        'message' => substr($messageText, 0, 250),
                        'data' => json_encode(['message_id' => $messageId]),
                        'is_read' => 0,
                        'created_at' => date('Y-m-d H:i:s')
                    ]);
                } catch (Exception $e) {
                    error_log('Failed to create notification: ' . $e->getMessage());
                }

                $this->pushSSEEvent('new_message', [
                    'user_id' => $user['id'],
                    'phone_number' => $user['phone_number'],
                    'name' => $user['name'] ?? ($phoneNumber ?? ''),
                    'message' => substr($messageText, 0, 250),
                    'message_type' => $messageType,
                    'timestamp' => date('Y-m-d H:i:s')
                ]);
            }
            
            // Procesar con bot solo si el mensaje llegó al número principal
            if (!$isTurnero) {
                try {
                    $this->botService->processMessage($user, $messageText, $messageType);
                } catch (Exception $e) {
                    error_log("Bot processing failed: " . $e->getMessage());
                }
            }
        }
        
        // Procesar estados de mensajes (entregado, leído, etc.)
        if (isset($value['statuses'])) {
            $this->processconversationstatuses($value['statuses']);
        }
    }
    
    private function processconversationstatuses($statuses) {
        foreach ($statuses as $status) {
            $messageId = $status['id'];
            $newStatus = $status['status']; // sent, delivered, read, failed
            
            $this->db->update(
                'conversations',
                ['status' => $newStatus],
                'message_id = :message_id',
                ['message_id' => $messageId]
            );
        }
    }
    
    /**
     * Vincula el teléfono que la persona acaba de compartir con el usuario que
     * hasta ahora solo se conocía por su BSUID.
     *
     * Puede haber dos registros de la misma persona: el viejo, de cuando escribía
     * mostrando el número, y el nuevo creado con el BSUID de identificador. No se
     * fusionan aquí (implicaría mover conversaciones, estados y aceptación de
     * términos, y una fusión mal hecha mezcla historias clínicas de dos personas):
     * se deja el registro con el teléfono como el bueno y se marca el otro, para
     * que alguien lo revise.
     *
     * @return bool si el teléfono quedó vinculado
     */
    private function vincularTelefonoCompartido(&$user, $telefono, $bsuid) {
        // Ya lo teníamos: nada que hacer
        if (($user['phone_number'] ?? null) === $telefono) {
            return true;
        }

        $existente = $this->db->fetch(
            "SELECT id FROM users WHERE phone_number = :t AND id <> :id",
            ['t' => $telefono, 'id' => $user['id']]
        );

        try {
            if ($existente) {
                // El registro bueno es el que tiene el teléfono. Se le pasa el BSUID
                // para que a partir de ahora se le reconozca por ahí.
                if ($bsuid) {
                    $this->db->update('users', ['bsuid' => null], 'id = ?', [$user['id']]);
                    $this->db->update('users', ['bsuid' => $bsuid], 'id = ?', [$existente['id']]);
                }
                error_log(sprintf(
                    '[webhook] BSUID %s compartió el teléfono %s, que ya era del usuario %d. ' .
                    'El usuario %d queda duplicado y hay que revisarlo a mano.',
                    $bsuid, $telefono, $existente['id'], $user['id']
                ));
                $user = $this->getUserById($existente['id']) ?: $user;
                return true;
            }

            // No había otro registro: el placeholder pasa a tener el teléfono real
            $this->db->update('users', ['phone_number' => $telefono], 'id = ?', [$user['id']]);
            $user['phone_number'] = $telefono;
            error_log(sprintf('[webhook] BSUID %s quedó vinculado al teléfono %s', $bsuid, $telefono));
            return true;

        } catch (Exception $e) {
            error_log('[webhook] Error vinculando el teléfono compartido: ' . $e->getMessage());
            return false;
        }
    }

    private function getUserByPhone($phoneNumber) {
        return $this->db->fetch(
            "SELECT * FROM users WHERE phone_number = :phone",
            ['phone' => $phoneNumber]
        );
    }
    
    private function getUserById($userId) {
        return $this->db->fetch(
            "SELECT * FROM users WHERE id = :id",
            ['id' => $userId]
        );
    }
    
    private function createUser($phoneNumber, $sendWelcome = true) {
        $id = $this->db->insert('users', [
            'phone_number' => $phoneNumber,
            'status' => 'active',
            'created_at' => date('Y-m-d H:i:s')
        ]);

        if ($sendWelcome && $id) {
            try {
                // Enviar mensaje de bienvenida usando el BotService (que consulta system_config)
                $this->botService->sendWelcomeMessage($phoneNumber);
            } catch (Throwable $t) {
                error_log('Failed to send welcome message: ' . $t->getMessage());
            }
        }

        return $id;
    }
    
    private function saveMessage($messageData) {
        // INSERT IGNORE: si message_id ya existe (webhook duplicado), no inserta y devuelve 0
        $cols = implode(', ', array_map(fn($c) => "`{$c}`", array_keys($messageData)));
        $placeholders = implode(', ', array_map(fn($c) => ":{$c}", array_keys($messageData)));
        $sql = "INSERT IGNORE INTO `conversations` ({$cols}) VALUES ({$placeholders})";
        $stmt = $this->db->getConnection()->prepare($sql);
        $stmt->execute($messageData);
        if ($stmt->rowCount() === 0) {
            return 0; // duplicado
        }
        return $this->db->getConnection()->lastInsertId();
    }

    /**
     * Filtrar los datos para dejar solo las columnas que existen en la tabla
     * Evita errores cuando la BD está en un esquema más antiguo
     */
    private function filterColumns($table, array $data) {
        static $columnsCache = [];
        if (!isset($columnsCache[$table])) {
            $cols = $this->db->fetchAll("SHOW COLUMNS FROM {$table}");
            $columnsCache[$table] = array_map(function($c){ return $c['Field']; }, $cols);
        }
        $allowed = $columnsCache[$table];
        return array_filter($data, function($v, $k) use ($allowed) {
            return in_array($k, $allowed, true);
        }, ARRAY_FILTER_USE_BOTH);
    }
    
    private function logWebhook($requestBody, $responseBody, $statusCode) {
        if (ENABLE_LOGGING) {
            $this->db->insert('webhook_logs', [
                'request_body' => $requestBody,
                'response_body' => $responseBody,
                'status_code' => $statusCode,
                'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
                'created_at' => date('Y-m-d H:i:s')
            ]);
        }
    }

    /**
     * Empujar evento a través de SSE (Server-Sent Events)
     * Notifica a todos los operadores conectados sobre nuevos eventos
     */
    private function pushSSEEvent($eventType, $data) {
        try {
            // Llamar al endpoint push_event interno
            $pushUrl = 'http://127.0.0.1' . dirname($_SERVER['SCRIPT_NAME']) . '/push_event.php';
            
            $payload = json_encode([
                'event_type' => $eventType,
                'data' => $data,
                'target_user_id' => 'all' // Notificar a todos los operadores
            ]);
            
            // Hacer llamada asíncrona (fire and forget)
            $ch = curl_init($pushUrl);
            curl_setopt_array($ch, [
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => $payload,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT_MS => 500, // Timeout corto
                CURLOPT_HTTPHEADER => [
                    'Content-Type: application/json',
                    'X-Push-Token: internal_push_secret_2026'
                ]
            ]);
            
            // Ejecutar sin bloquear
            curl_exec($ch);
            curl_close($ch);
            
        } catch (Exception $e) {
            // No fallar si el push falla
            error_log('pushSSEEvent failed: ' . $e->getMessage());
        }
    }

    /**
     * Procesar un payload (array) recibido manualmente (replay)
     * Útil para reproducir entradas desde la UI o scripts.
     */
    public function processPayload(array $data) {
        error_log("processPayload START");
        // Registrar como recibido (reproducción)
        $this->logWebhook(json_encode($data), json_encode(['status' => 'replayed']), 200);

        if (empty($data) || !isset($data['entry'])) {
            error_log("processPayload: empty or missing entry");
            return false;
        }

        foreach ($data['entry'] as $entry) {
            if (isset($entry['changes'])) {
                foreach ($entry['changes'] as $change) {
                    error_log("processPayload: processing change field=" . ($change['field'] ?? ''));
                    if ((($change['field'] ?? '') === 'conversations' || ($change['field'] ?? '') === 'messages' || isset($change['value']['messages'])) && isset($change['value'])) {
                        try {
                            $this->processconversations($change['value']);
                        } catch (Exception $e) {
                            error_log("processPayload: processconversations failed: " . $e->getMessage());
                        }
                    }
                }
            }
        }

        error_log("processPayload END");
        return true;
    }
}

// Procesar la solicitud (solo si no estamos en CLI)
if (php_sapi_name() !== 'cli') {
    try {
        $webhook = new WhatsAppWebhook();
        $webhook->handleRequest();
    } catch (Throwable $t) {
        // Manejar excepciones fatales y errores (Throwable)
        error_log("Fatal error in webhook: " . $t->getMessage());
        error_log($t->getTraceAsString());
        if (function_exists('writeLog')) {
            writeLog('ERROR', 'Fatal error in webhook: ' . $t->getMessage(), ['trace' => $t->getTraceAsString()]);
        }
        http_response_code(500);
        echo json_encode(['error' => 'Error fatal del servidor', 'detail' => $t->getMessage()]);
    }
}
?>