<?php
/**
 * ASISMA CRM - Helper de WhatsApp (Evolution API)
 *
 * Cada franquiciado puede tener su propia instancia (tabla whatsapp_instancias).
 * Las instancias con franquiciado_id NULL son de la matriz y actúan como fallback.
 * Adaptado del WhatsAppHelper de ASISMA OPS a PDO y al esquema multi-tenant del CRM.
 */
class WhatsAppHelper {

    /** Último error de la API para diagnóstico */
    public static $lastError = null;

    // ─────────────────────────────────────────────────────────────────────────
    // Números
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Normaliza un número chileno al formato Evolution: 569XXXXXXXX
     */
    public static function formatearNumero($numero) {
        if (strpos($numero, '@') !== false) {
            return trim($numero); // ya es un JID
        }
        $numero = preg_replace('/[^0-9]/', '', trim($numero));
        if (substr($numero, 0, 2) === '56') {
            $numero = substr($numero, 2);
        }
        return '569' . substr($numero, -8);
    }

    /**
     * Últimos 8 dígitos, para matching flexible de teléfonos.
     */
    public static function ultimos8($numero) {
        $numero = preg_replace('/[^0-9]/', '', $numero);
        return substr($numero, -8);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Instancias
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Resuelve la instancia a usar para un lead. Las líneas de WhatsApp son
     * de los VENDEDORES (usuario_id); el franquiciado agrupa su servidor
     * Evolution. Prioridad:
     * 1. La instancia fijada en el lead (wa_instance_id).
     * 2. La línea del vendedor asignado al lead.
     * 3. Cualquier línea activa del franquiciado del lead.
     * 4. Una línea activa de la matriz (franquiciado_id NULL).
     * Devuelve el row de whatsapp_instancias o null.
     */
    public static function getInstanciaParaLead($lead) {
        $db = Database::getInstance();

        if (!empty($lead['wa_instance_id'])) {
            $inst = $db->fetchOne(
                "SELECT * FROM whatsapp_instancias WHERE id = :id AND activa = 1",
                array('id' => $lead['wa_instance_id'])
            );
            if ($inst) return $inst;
        }

        if (!empty($lead['usuario_asignado_id'])) {
            $inst = $db->fetchOne(
                "SELECT * FROM whatsapp_instancias WHERE usuario_id = :uid AND activa = 1 ORDER BY id ASC LIMIT 1",
                array('uid' => $lead['usuario_asignado_id'])
            );
            if ($inst) return $inst;
        }

        if (!empty($lead['franquiciado_id'])) {
            $inst = $db->fetchOne(
                "SELECT * FROM whatsapp_instancias WHERE franquiciado_id = :fid AND activa = 1 ORDER BY id ASC LIMIT 1",
                array('fid' => $lead['franquiciado_id'])
            );
            if ($inst) return $inst;
        }

        return $db->fetchOne(
            "SELECT * FROM whatsapp_instancias WHERE franquiciado_id IS NULL AND activa = 1 ORDER BY id ASC LIMIT 1"
        );
    }

    /** Resuelve la instancia por su token de webhook (?t=...). */
    public static function getInstanciaPorToken($token) {
        if (empty($token)) return null;
        $db = Database::getInstance();
        return $db->fetchOne(
            "SELECT * FROM whatsapp_instancias WHERE webhook_token = :t AND activa = 1 LIMIT 1",
            array('t' => $token)
        );
    }

    public static function getInstanciaPorNombre($nombre) {
        $db = Database::getInstance();
        return $db->fetchOne(
            "SELECT * FROM whatsapp_instancias WHERE nombre_instancia = :n AND activa = 1 LIMIT 1",
            array('n' => $nombre)
        );
    }

    /** Genera un token único para la URL del webhook de una instancia. */
    public static function generarWebhookToken() {
        if (function_exists('openssl_random_pseudo_bytes')) {
            return bin2hex(openssl_random_pseudo_bytes(20));
        }
        return md5(uniqid(mt_rand(), true));
    }

    /**
     * Consulta el estado de conexión de la instancia en Evolution API.
     * Devuelve array('status' => 'open|connecting|close|desconocido', 'raw' => ...).
     */
    public static function getEstadoInstancia($instancia) {
        $url = rtrim($instancia['api_url'], '/') . '/instance/connectionState/' . rawurlencode($instancia['nombre_instancia']);
        $res = self::curlJson($url, 'GET', $instancia['api_token']);

        if ($res['http_code'] === 200 && isset($res['data']['instance']['state'])) {
            return array('status' => $res['data']['instance']['state'], 'raw' => $res['data']);
        }
        return array('status' => 'desconocido', 'http_code' => $res['http_code'], 'raw' => $res['data']);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Aprovisionamiento de instancias (servidor Evolution global, config_sistema)
    // ─────────────────────────────────────────────────────────────────────────

    /** Claves globales del servidor Evolution (para CREAR/administrar instancias). */
    public static function configEvolutionGlobal() {
        $db = Database::getInstance();
        $filas = $db->fetchAll(
            "SELECT clave, valor FROM config_sistema WHERE clave IN ('EVOLUTION_URL_GLOBAL', 'EVOLUTION_APIKEY_GLOBAL')"
        );
        $cfg = array('EVOLUTION_URL_GLOBAL' => '', 'EVOLUTION_APIKEY_GLOBAL' => '');
        foreach ($filas as $f) {
            $cfg[$f['clave']] = trim($f['valor']);
        }
        return $cfg;
    }

    /** ¿Está configurado el servidor global? (vacío = solo registro manual). */
    public static function aprovisionamientoActivo() {
        $cfg = self::configEvolutionGlobal();
        return $cfg['EVOLUTION_URL_GLOBAL'] !== '' && $cfg['EVOLUTION_APIKEY_GLOBAL'] !== '';
    }

    /**
     * Crea la instancia en el servidor Evolution global con su webhook.
     * Devuelve array(success, api_url, api_token) o (success=false, detalle).
     */
    public static function crearInstanciaEvolution($nombreInstancia, $webhookUrl) {
        $cfg = self::configEvolutionGlobal();
        if ($cfg['EVOLUTION_URL_GLOBAL'] === '' || $cfg['EVOLUTION_APIKEY_GLOBAL'] === '') {
            return array('success' => false, 'detalle' => 'Falta configurar EVOLUTION_URL_GLOBAL y EVOLUTION_APIKEY_GLOBAL en Configuración del sistema.');
        }
        $base = rtrim($cfg['EVOLUTION_URL_GLOBAL'], '/');
        $res = self::curlJson($base . '/instance/create', 'POST', $cfg['EVOLUTION_APIKEY_GLOBAL'], array(
            'instanceName' => $nombreInstancia,
            'qrcode'       => true,
            'integration'  => 'WHATSAPP-BAILEYS',
            'webhook'      => array(
                'url'    => $webhookUrl,
                'base64' => true,
                'events' => array('MESSAGES_UPSERT', 'MESSAGES_UPDATE'),
            ),
        ));
        if ($res['http_code'] < 200 || $res['http_code'] >= 300) {
            error_log('[Evolution] instance/create falló (' . $res['http_code'] . '): ' . $res['body']);
            return array('success' => false, 'detalle' => 'El servidor Evolution rechazó la creación de la instancia (revisa el log del sistema).');
        }
        // Token propio de la instancia: Evolution v2 lo entrega en hash (string
        // en 2.1, objeto {apikey} en 2.0); si no llega, se usa la clave global.
        $token = '';
        if (isset($res['data']['hash'])) {
            $token = is_array($res['data']['hash'])
                ? (isset($res['data']['hash']['apikey']) ? $res['data']['hash']['apikey'] : '')
                : $res['data']['hash'];
        }
        if ($token === '') $token = $cfg['EVOLUTION_APIKEY_GLOBAL'];

        // Refuerzo del webhook por su endpoint dedicado: algunas versiones
        // ignoran el webhook del create. Si falla igual se continúa (se puede
        // configurar a mano en Evolution).
        self::configurarWebhookEvolution($nombreInstancia, $webhookUrl, $cfg);

        return array('success' => true, 'api_url' => $base, 'api_token' => $token);
    }

    /** Configura el webhook de la instancia (soporta formato v2.1 y el antiguo). */
    public static function configurarWebhookEvolution($nombreInstancia, $webhookUrl, $cfg = null) {
        if ($cfg === null) $cfg = self::configEvolutionGlobal();
        $base = rtrim($cfg['EVOLUTION_URL_GLOBAL'], '/');
        $destino = $base . '/webhook/set/' . rawurlencode($nombreInstancia);
        $res = self::curlJson($destino, 'POST', $cfg['EVOLUTION_APIKEY_GLOBAL'], array(
            'webhook' => array('enabled' => true, 'url' => $webhookUrl, 'base64' => true, 'events' => array('MESSAGES_UPSERT', 'MESSAGES_UPDATE'))
        ));
        if ($res['http_code'] >= 200 && $res['http_code'] < 300) return true;
        $res2 = self::curlJson($destino, 'POST', $cfg['EVOLUTION_APIKEY_GLOBAL'], array(
            'enabled' => true, 'url' => $webhookUrl, 'webhook_by_events' => false,
            'webhook_base64' => true, 'events' => array('MESSAGES_UPSERT', 'MESSAGES_UPDATE')
        ));
        if ($res2['http_code'] >= 200 && $res2['http_code'] < 300) return true;
        error_log('[Evolution] webhook/set falló para ' . $nombreInstancia . ': ' . $res['body'] . ' / ' . $res2['body']);
        return false;
    }

    /** QR (base64) y pairing code para vincular el teléfono; refrescable. */
    public static function qrInstancia($instancia) {
        $url = rtrim($instancia['api_url'], '/') . '/instance/connect/' . rawurlencode($instancia['nombre_instancia']);
        $res = self::curlJson($url, 'GET', $instancia['api_token']);
        if ($res['http_code'] === 200 && is_array($res['data'])) {
            $base64 = isset($res['data']['base64']) ? $res['data']['base64']
                : (isset($res['data']['qrcode']['base64']) ? $res['data']['qrcode']['base64'] : null);
            return array(
                'success'      => true,
                'base64'       => $base64,
                'pairing_code' => isset($res['data']['pairingCode']) ? $res['data']['pairingCode'] : null,
            );
        }
        return array('success' => false, 'detalle' => 'HTTP ' . $res['http_code']);
    }

    /** Número de teléfono conectado a la instancia (ownerJid), o null. */
    public static function numeroConectado($instancia) {
        $url = rtrim($instancia['api_url'], '/') . '/instance/fetchInstances?instanceName=' . rawurlencode($instancia['nombre_instancia']);
        $res = self::curlJson($url, 'GET', $instancia['api_token']);
        if ($res['http_code'] !== 200 || !is_array($res['data'])) return null;
        $filas = isset($res['data'][0]) ? $res['data'] : array($res['data']);
        foreach ($filas as $f) {
            $inst = isset($f['instance']) ? $f['instance'] : $f; // v2.0 anida, v2.1 no
            $jid = isset($inst['ownerJid']) ? $inst['ownerJid'] : (isset($inst['owner']) ? $inst['owner'] : '');
            if ($jid) return preg_replace('/@.*$/', '', $jid);
        }
        return null;
    }

    /**
     * Elimina PARA TODOS un mensaje enviado (retractar; ~48 h de plazo).
     * Patrón OPS: DELETE /chat/deleteMessageForEveryone/{instancia}.
     */
    public static function eliminarMensaje($instancia, $messageId, $numero) {
        $jid = self::formatearNumero($numero) . '@s.whatsapp.net';
        $url = rtrim($instancia['api_url'], '/') . '/chat/deleteMessageForEveryone/' . rawurlencode($instancia['nombre_instancia']);
        $res = self::curlJson($url, 'DELETE', $instancia['api_token'], array(
            'id'        => $messageId,
            'remoteJid' => $jid,
            'fromMe'    => true
        ));
        if ($res['http_code'] >= 200 && $res['http_code'] < 300) return true;
        error_log('[Evolution] deleteMessageForEveryone falló (' . $res['http_code'] . '): ' . $res['body']);
        return false;
    }

    /** Cierra la sesión del teléfono (la instancia queda esperando nuevo QR). */
    public static function logoutInstancia($instancia) {
        $url = rtrim($instancia['api_url'], '/') . '/instance/logout/' . rawurlencode($instancia['nombre_instancia']);
        $res = self::curlJson($url, 'DELETE', $instancia['api_token']);
        return $res['http_code'] >= 200 && $res['http_code'] < 300;
    }

    /** Elimina la instancia del servidor Evolution (best effort). */
    public static function eliminarInstanciaEvolution($instancia) {
        $cfg = self::configEvolutionGlobal();
        $token = $cfg['EVOLUTION_APIKEY_GLOBAL'] !== '' ? $cfg['EVOLUTION_APIKEY_GLOBAL'] : $instancia['api_token'];
        self::logoutInstancia($instancia);
        $url = rtrim($instancia['api_url'], '/') . '/instance/delete/' . rawurlencode($instancia['nombre_instancia']);
        $res = self::curlJson($url, 'DELETE', $token);
        return $res['http_code'] >= 200 && $res['http_code'] < 300;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Envío
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Envía un mensaje por Evolution API.
     *
     * @param array  $instancia Row de whatsapp_instancias
     * @param string $numero    Teléfono destino (se normaliza)
     * @param string $texto     Texto o caption
     * @param string $tipo      text|image|audio|document
     * @param string $media     Base64 puro o URL pública (para tipos con media)
     * @param string $fileName  Nombre de archivo para documentos
     * @param string $mimetype  Mimetype cuando media es base64
     * @return array('success' => bool, 'message_id' => string|null, 'error' => string|null)
     */
    public static function enviar($instancia, $numero, $texto, $tipo = 'text', $media = null, $fileName = null, $mimetype = null) {
        self::$lastError = null;

        if (!$instancia || empty($instancia['api_url']) || empty($instancia['api_token'])) {
            self::$lastError = 'Instancia de WhatsApp no configurada.';
            return array('success' => false, 'message_id' => null, 'error' => self::$lastError);
        }

        $numero = self::formatearNumero($numero);
        $base   = rtrim($instancia['api_url'], '/');
        $inst   = rawurlencode($instancia['nombre_instancia']);
        $delay  = rand(1200, 3500); // delay aleatorio anti-bloqueo

        switch ($tipo) {
            case 'image':
                // Evolution v2 no tiene sendWhatsAppImage: las imagenes van por
                // sendMedia con mediatype image
                $url  = $base . '/message/sendMedia/' . $inst;
                $data = array(
                    'number'    => $numero,
                    'mediatype' => 'image',
                    'media'     => $media,
                    'fileName'  => ($fileName ? $fileName : 'imagen.jpg'),
                    'caption'   => $texto,
                    'delay'     => $delay
                );
                if ($mimetype) $data['mimetype'] = $mimetype;
                break;

            case 'audio':
                $url  = $base . '/message/sendWhatsAppAudio/' . $inst;
                $data = array('number' => $numero, 'audio' => $media, 'ptt' => true, 'delay' => $delay);
                if ($mimetype) $data['mimetype'] = $mimetype;
                break;

            case 'video':
                $url  = $base . '/message/sendMedia/' . $inst;
                $data = array(
                    'number'    => $numero,
                    'mediatype' => 'video',
                    'media'     => $media,
                    'fileName'  => ($fileName ? $fileName : 'video.mp4'),
                    'caption'   => $texto,
                    'delay'     => $delay
                );
                if ($mimetype) $data['mimetype'] = $mimetype;
                break;

            case 'document':
                $url  = $base . '/message/sendMedia/' . $inst;
                $data = array(
                    'number'    => $numero,
                    'mediatype' => 'document',
                    'media'     => $media,
                    'fileName'  => ($fileName ? $fileName : 'archivo.pdf'),
                    'caption'   => $texto,
                    'delay'     => $delay
                );
                if ($mimetype) $data['mimetype'] = $mimetype;
                break;

            case 'text':
            default:
                $url  = $base . '/message/sendText/' . $inst;
                $data = array('number' => $numero, 'text' => $texto, 'delay' => $delay);
                break;
        }

        $res = self::curlJson($url, 'POST', $instancia['api_token'], $data);

        if ($res['http_code'] < 200 || $res['http_code'] >= 300) {
            self::$lastError = 'HTTP ' . $res['http_code'] . ': ' . substr($res['body'], 0, 500);
            error_log('[WhatsAppHelper] Error al enviar: ' . self::$lastError);
            return array('success' => false, 'message_id' => null, 'error' => self::$lastError);
        }

        $messageId = isset($res['data']['key']['id']) ? $res['data']['key']['id'] : ('local_' . uniqid());
        return array('success' => true, 'message_id' => $messageId, 'error' => null);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Persistencia
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Registra un mensaje en whatsapp_mensajes y su actividad espejo en actividades_crm.
     * Devuelve el id del mensaje insertado, o null si es duplicado.
     */
    public static function registrarMensaje($leadId, $instanciaId, $messageId, $direccion, $tipo, $contenido, $mediaUrl, $mediaPath, $remitente, $usuarioId = null) {
        $db = Database::getInstance();

        // Deduplicación por reintentos del webhook
        $existe = $db->fetchOne(
            "SELECT id FROM whatsapp_mensajes WHERE message_id = :mid AND instancia_id = :iid LIMIT 1",
            array('mid' => $messageId, 'iid' => $instanciaId)
        );
        if ($existe) return null;

        $msgId = $db->insert('whatsapp_mensajes', array(
            'lead_id'      => $leadId,
            'instancia_id' => $instanciaId,
            'message_id'   => $messageId,
            'direccion'    => $direccion,
            'tipo'         => $tipo,
            'contenido'    => $contenido,
            'media_url'    => $mediaUrl,
            'media_path'   => $mediaPath,
            'remitente'    => $remitente,
            'leido'        => ($direccion === 'saliente') ? 1 : 0
        ));

        $resumen = $contenido !== '' && $contenido !== null ? $contenido : '[' . $tipo . ']';
        if (function_exists('mb_substr')) {
            $resumen = mb_substr($resumen, 0, 500);
        } else {
            $resumen = substr($resumen, 0, 500);
        }

        $db->insert('actividades_crm', array(
            'lead_id'    => $leadId,
            'usuario_id' => $usuarioId,
            'tipo'       => 'whatsapp',
            'direccion'  => $direccion,
            'asunto'     => $direccion === 'entrante' ? 'WhatsApp recibido' : 'WhatsApp enviado',
            'detalles'   => $resumen,
            'metadata_json' => json_encode(array('whatsapp_mensaje_id' => $msgId, 'tipo' => $tipo))
        ));

        return $msgId;
    }

    /**
     * Guarda media entrante (base64 o URL) en storage/whatsapp_media.
     * Devuelve la ruta relativa (p. ej. "2026/07/in_xxx.jpg") o null.
     */
    public static function guardarMediaEntrante($base64OrUrl, $tipo, $mimetype) {
        $ext = self::extensionPorMime($mimetype, $tipo);
        $subdir = date('Y/m');
        $dirAbs = ROOT_DIR . '/storage/whatsapp_media/' . $subdir;
        if (!is_dir($dirAbs)) {
            @mkdir($dirAbs, 0775, true);
        }

        $nombre  = 'in_' . uniqid() . '.' . $ext;
        $rutaAbs = $dirAbs . '/' . $nombre;
        $rutaRel = $subdir . '/' . $nombre;

        $contenido = null;
        if (strpos($base64OrUrl, 'http://') === 0 || strpos($base64OrUrl, 'https://') === 0) {
            $contenido = self::descargarUrl($base64OrUrl);
        } else {
            // base64 puro o data-uri
            $b64 = $base64OrUrl;
            if (strpos($b64, 'base64,') !== false) {
                $b64 = substr($b64, strpos($b64, 'base64,') + 7);
            }
            $contenido = base64_decode($b64, true);
        }

        if ($contenido === null || $contenido === false || strlen($contenido) === 0) {
            return null;
        }

        return file_put_contents($rutaAbs, $contenido) !== false ? $rutaRel : null;
    }

    /**
     * Recupera la media completa de un mensaje desde Evolution API (base64).
     */
    public static function obtenerMediaDeApi($instancia, $messageId) {
        $url = rtrim($instancia['api_url'], '/') . '/chat/getBase64FromMediaMessage/' . rawurlencode($instancia['nombre_instancia']);
        $res = self::curlJson($url, 'POST', $instancia['api_token'], array(
            'message'      => array('key' => array('id' => $messageId)),
            'convertToMp4' => false
        ));
        // Evolution v2 responde 201 en este endpoint (igual que en el OPS se
        // aceptan 200 y 201)
        if ($res['http_code'] >= 200 && $res['http_code'] < 300) {
            if (isset($res['data']['base64'])) return $res['data']['base64'];
            // Respaldo: algunas versiones devuelven el base64 crudo como body
            if (is_string($res['body']) && strlen($res['body']) > 100 && preg_match('/^[A-Za-z0-9+\/=\s]+$/', substr($res['body'], 0, 200))) {
                return trim($res['body']);
            }
        }
        error_log('[WhatsAppHelper] getBase64FromMediaMessage fallo HTTP ' . $res['http_code'] . ': ' . substr((string)$res['body'], 0, 300));
        return null;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Utilitarios internos
    // ─────────────────────────────────────────────────────────────────────────

    public static function extensionPorMime($mimetype, $tipo = 'document') {
        $mapa = array(
            'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif',
            'image/bmp' => 'bmp',
            'audio/ogg' => 'ogg', 'audio/opus' => 'ogg', 'audio/mpeg' => 'mp3', 'audio/mp3' => 'mp3',
            'audio/mp4' => 'm4a', 'audio/x-m4a' => 'm4a', 'audio/aac' => 'aac', 'audio/wav' => 'wav',
            'audio/x-wav' => 'wav', 'audio/webm' => 'webm', 'audio/amr' => 'amr',
            'video/mp4' => 'mp4', 'video/webm' => 'webm', 'video/quicktime' => 'mov',
            'video/x-msvideo' => 'avi', 'video/3gpp' => '3gp',
            'application/pdf' => 'pdf',
            'application/msword' => 'doc',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
            'application/vnd.ms-excel' => 'xls',
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
            'application/vnd.ms-powerpoint' => 'ppt',
            'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
            'text/plain' => 'txt', 'text/csv' => 'csv',
            'application/zip' => 'zip', 'application/x-rar-compressed' => 'rar',
        );
        $mime = strtolower(trim(strtok((string)$mimetype, ';')));
        if (isset($mapa[$mime])) return $mapa[$mime];

        switch ($tipo) {
            case 'image': return 'jpg';
            case 'audio': return 'ogg';
            case 'video': return 'mp4';
            default:      return 'bin';
        }
    }

    /**
     * ¿Se acepta este tipo de archivo como adjunto? Imagenes, audios y videos
     * completos, mas los documentos de oficina tipicos. Todo lo demas (ej.
     * ejecutables o scripts) se rechaza.
     */
    public static function mimePermitido($mimetype) {
        $mime = strtolower(trim(strtok((string)$mimetype, ';')));
        if (strpos($mime, 'image/') === 0 || strpos($mime, 'audio/') === 0 || strpos($mime, 'video/') === 0) {
            return true;
        }
        $documentos = array(
            'application/pdf', 'application/msword',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'application/vnd.ms-excel',
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            'application/vnd.ms-powerpoint',
            'application/vnd.openxmlformats-officedocument.presentationml.presentation',
            'text/plain', 'text/csv', 'application/zip', 'application/x-rar-compressed',
        );
        return in_array($mime, $documentos);
    }

    private static function descargarUrl($url) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        $data = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        return ($code >= 200 && $code < 300) ? $data : null;
    }

    /**
     * Request JSON genérico a Evolution API.
     * Devuelve array('http_code' => int, 'body' => string, 'data' => array|null)
     */
    private static function curlJson($url, $method, $token, $payload = null) {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_TIMEOUT, 35);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array(
            'Authorization: Bearer ' . $token,
            'apikey: ' . $token,
            'Content-Type: application/json',
        ));

        if ($method === 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            if ($payload !== null) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
            }
        } elseif ($method !== 'GET') {
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
            if ($payload !== null) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
            }
        }

        $body = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err  = curl_error($ch);
        curl_close($ch);

        if ($body === false) {
            self::$lastError = 'cURL: ' . $err;
            return array('http_code' => 0, 'body' => '', 'data' => null);
        }

        return array('http_code' => $code, 'body' => $body, 'data' => json_decode($body, true));
    }
}
