<?php

/**
 * Servicio de WhatsApp - Envío de mensajes
 * Fecha: 13 de noviembre de 2025
 */

// Asegurar que las funciones de configuración estén cargadas si este archivo
// se incluye directamente sin pasar por `config.php`.
if (!function_exists('getWhatsAppConfigFromDB')) {
    $possible = __DIR__ . '/../config/config.php';
    if (file_exists($possible)) {
        require_once $possible;
    }
}

class WhatsAppService
{
    private $token;
    private $phoneNumberId;
    private $apiUrl;
    private $db;
    private $rateLimitMonitor;

    public function __construct($canal = null)
    {
        // Obtener configuración desde base de datos
        $config = getWhatsAppConfigFromDB();

        $this->token = $config['token'];
        $this->phoneNumberId = $config['phone_number_id'];
        $this->apiUrl = $config['api_url'] ?: 'https://graph.facebook.com/v22.0/';

        // Si se especifica canal 'turnero', usar el phone ID del número turnero
        if ($canal === 'turnero') {
            $turneroPhoneId = getConfigFromDB('whatsapp_phone_number_id_turnero', '');
            if ($turneroPhoneId) {
                $this->phoneNumberId = $turneroPhoneId;
            }
        }

        $this->db = Database::getInstance();
        
        // Inicializar monitor de rate limit
        try {
            // Cargar autoloader si no está cargado
            if (!class_exists('Predis\Client')) {
                $autoloadPath = __DIR__ . '/../vendor/autoload.php';
                if (file_exists($autoloadPath)) {
                    require_once $autoloadPath;
                }
            }
            
            if (file_exists(__DIR__ . '/RateLimitMonitor.php')) {
                require_once __DIR__ . '/RateLimitMonitor.php';
                $this->rateLimitMonitor = new RateLimitMonitor();
            }
        } catch (Exception $e) {
            error_log("WhatsAppService: No se pudo inicializar RateLimitMonitor - " . $e->getMessage());
            $this->rateLimitMonitor = null;
        }
    }

    /**
     * Enviar mensaje de texto
     */
    public function sendTextMessage($to, $message, $meta = null)
    {
        $data = [
            'messaging_product' => 'whatsapp',
            'to' => $this->formatPhoneNumber($to),
            'type' => 'text',
            'text' => [
                'body' => $message
            ]
        ];

        if (!empty($meta) && is_array($meta)) {
            // internal metadata used by our application (not sent to WhatsApp API)
            $data['__app_meta'] = $meta;
        }

        return $this->sendMessage($data);
    }

    /**
     * Enviar mensaje con template
     */
    public function sendTemplateMessage(
        $to,
        $templateName,
        $language = 'es',
        $bodyParameters = [],
        $headerParameters = [],
        $rawComponents = null,
        $meta = null,
        $dryRun = false
    ) {
        // Si se pasan components completos (por ejemplo flow/button/image), úsalos tal cual
        if (is_array($rawComponents) && !empty($rawComponents)) {
            $template = [
                'name' => $templateName,
                'language' => [ 'code' => $language ],
                'components' => $rawComponents
            ];

            $data = [
                'messaging_product' => 'whatsapp',
                'to' => $this->formatPhoneNumber($to),
                'type' => 'template',
                'template' => $template
            ];

            if (!empty($meta) && is_array($meta)) {
                $data['__app_meta'] = $meta;
            }

            return $dryRun ? $data : $this->sendMessage($data);
        }

        $components = [];

        // Normalizar parámetros de header (si vienen como strings -> convertir a objeto de texto)
        if (!empty($headerParameters)) {
            $normalizedHeader = [];
            foreach ($headerParameters as $hp) {
                if (is_array($hp) && isset($hp['type'])) {
                    $normalizedHeader[] = $hp;
                } elseif (is_string($hp)) {
                    $normalizedHeader[] = [
                        'type' => 'text',
                        'text' => $hp
                    ];
                }
            }
            if (!empty($normalizedHeader)) {
                $components[] = [
                    'type' => 'header',
                    'parameters' => $normalizedHeader
                ];
            }
        }

        // Normalizar parámetros de body: soportar tres formatos:
        // 1) Indexed array of strings: ['Alice','+123'] => converted to text params in order
        // 2) Indexed array of param objects: [['type'=>'text','text'=>'Alice'], ...] => used as-is
        // 3) Associative array for NAMED params: ['name' => 'Alice'] => converted to {type:text, name: 'name', text: 'Alice'}
        if (!empty($bodyParameters)) {
            $normalizedBody = [];

            // Detect associative (named) arrays: if array has string keys
            $isNamed = false;
            foreach ($bodyParameters as $k => $v) {
                if (!is_int($k)) { $isNamed = true; break; }
            }

            if ($isNamed) {
                // bodyParameters is like ['name' => 'Alice', ...]
                // Convert named params to the WhatsApp API expected parameter objects
                foreach ($bodyParameters as $paramName => $paramValue) {
                    if (is_array($paramValue) && isset($paramValue['type'])) {
                        // already detailed param object: remove any unexpected keys like 'name'
                        $paramObj = $paramValue;
                        // ensure parameter name is set using 'parameter_name' (preferred) or fall back to 'name'
                        if (!isset($paramObj['parameter_name']) && !isset($paramObj['name'])) {
                            $paramObj['parameter_name'] = $paramName;
                        }
                        // remove legacy 'name' to avoid API errors
                        if (isset($paramObj['name'])) unset($paramObj['name']);
                        $normalizedBody[] = $paramObj;
                    } else {
                        // include parameter_name so WhatsApp recognizes named params
                        $normalizedBody[] = [
                            'type' => 'text',
                            'parameter_name' => $paramName,
                            'text' => (string)$paramValue
                        ];
                    }
                }
            } else {
                // Indexed array: preserve objects or convert strings
                foreach ($bodyParameters as $bp) {
                    if (is_array($bp) && isset($bp['type'])) {
                        // ensure no unsupported keys like 'name' are passed through
                        $bpCopy = $bp;
                        unset($bpCopy['name']);
                        $normalizedBody[] = $bpCopy; // already detailed
                    } elseif (is_array($bp) && isset($bp['name'], $bp['value'])) {
                        // {name: 'param_name', value: 'text'} — include parameter_name for named-variable templates
                        $normalizedBody[] = [
                            'type'           => 'text',
                            'parameter_name' => (string)$bp['name'],
                            'text'           => (string)$bp['value'],
                        ];
                    } elseif (is_string($bp) || is_numeric($bp)) {
                        $normalizedBody[] = [
                            'type' => 'text',
                            'text' => (string)$bp
                        ];
                    }
                }
            }

            if (!empty($normalizedBody)) {
                $components[] = [
                    'type' => 'body',
                    'parameters' => $normalizedBody
                ];
            }
        }

        $template = [
            'name' => $templateName,
            'language' => [
                'code' => $language
            ]
        ];

        if (!empty($components)) {
            $template['components'] = $components;
        }

        $data = [
            'messaging_product' => 'whatsapp',
            'to' => $this->formatPhoneNumber($to),
            'type' => 'template',
            'template' => $template
        ];

        if (!empty($meta) && is_array($meta)) {
            // internal metadata used by our application (not sent to WhatsApp API)
            $data['__app_meta'] = $meta;
        }

        return $dryRun ? $data : $this->sendMessage($data);
    }

    /**
     * Enviar mensaje con botones interactivos
     */
    /**
     * Pedirle a la persona que comparta su número de teléfono.
     *
     * Quien activó el nombre de usuario de WhatsApp llega sin teléfono, y sin él
     * no se le puede cruzar con su historia ni con el turnero. Este botón es la
     * vía que da Meta para pedírselo: la persona decide si lo comparte, y si
     * acepta llega un mensaje de tipo `contacts` con el número.
     *
     * Solo tiene sentido dentro de la ventana de 24 horas; fuera de ella hay que
     * usar una plantilla con el botón REQUEST_CONTACT_INFO.
     */
    public function pedirContacto($to, $bodyText)
    {
        return $this->sendMessage([
            'messaging_product' => 'whatsapp',
            'recipient_type'    => 'individual',
            'to'                => $this->formatPhoneNumber($to),
            'type'              => 'interactive',
            'interactive'       => [
                'type'   => 'request_contact_info',
                'body'   => ['text' => $bodyText],
                'action' => ['name' => 'request_contact_info'],
            ],
        ]);
    }

    public function sendInteractiveMessage($to, $bodyText, $buttons, $header = null, $footer = null)
    {
        $interactive = [
            'type' => 'button',
            'body' => [
                'text' => $bodyText
            ],
            'action' => [
                'buttons' => $buttons
            ]
        ];

        if ($header) {
            $interactive['header'] = [
                'type' => 'text',
                'text' => $header
            ];
        }

        if ($footer) {
            $interactive['footer'] = [
                'text' => $footer
            ];
        }

        $data = [
            'messaging_product' => 'whatsapp',
            'recipient_type' => 'individual',
            'to' => $this->formatPhoneNumber($to),
            'type' => 'interactive',
            'interactive' => $interactive
        ];

        return $this->sendMessage($data);
    }

    /**
     * Enviar mensaje con lista
     */
    public function sendListMessage($to, $bodyText, $buttonText, $sections, $header = null, $footer = null)
    {
        $interactive = [
            'type' => 'list',
            'body' => [
                'text' => $bodyText
            ],
            'action' => [
                'button' => $buttonText,
                'sections' => $sections
            ]
        ];

        if ($header) {
            $interactive['header'] = [
                'type' => 'text',
                'text' => $header
            ];
        }

        if ($footer) {
            $interactive['footer'] = [
                'text' => $footer
            ];
        }

        $data = [
            'messaging_product' => 'whatsapp',
            'recipient_type' => 'individual',
            'to' => $this->formatPhoneNumber($to),
            'type' => 'interactive',
            'interactive' => $interactive
        ];

        return $this->sendMessage($data);
    }

    /**
     * Marcar mensaje como leído
     */
    public function markAsRead($messageId)
    {
        $data = [
            'messaging_product' => 'whatsapp',
            'status' => 'read',
            'message_id' => $messageId
        ];

        $url = $this->apiUrl . $this->phoneNumberId . '/conversations';
        return $this->makeRequest('POST', $url, $data);
    }

    /**
     * Enviar imagen
     */
    public function sendImageMessage($to, $imageUrl, $caption = null)
    {
        $image = ['link' => $imageUrl];
        
        if ($caption) {
            $image['caption'] = $caption;
        }

        $data = [
            'messaging_product' => 'whatsapp',
            'to' => $this->formatPhoneNumber($to),
            'type' => 'image',
            'image' => $image
        ];

        return $this->sendMessage($data);
    }

    /**
     * Enviar video
     */
    public function sendVideoMessage($to, $videoUrl, $caption = null, $skipAutoSave = false)
    {
        $video = ['link' => $videoUrl];
        
        if ($caption) {
            $video['caption'] = $caption;
        }

        $data = [
            'messaging_product' => 'whatsapp',
            'to' => $this->formatPhoneNumber($to),
            'type' => 'video',
            'video' => $video
        ];

        if ($skipAutoSave) {
            $data['__skip_auto_save'] = true;
        }

        return $this->sendMessage($data);
    }

    /**
     * Enviar audio
     * @param string $to Número de destinatario
     * @param string $audioUrl URL del audio
     * @param bool $skipAutoSave Omitir guardado automático
     * @param bool $isVoice True para enviar como nota de voz (requiere .ogg con códec OPUS)
     */
    public function sendAudioMessage($to, $audioUrl, $skipAutoSave = false, $isVoice = false)
    {
        $audioData = [
            'link' => $audioUrl
        ];
        
        // Si es nota de voz, agregar parámetro ptt (push-to-talk)
        // Requiere archivo .ogg con códec OPUS
        if ($isVoice) {
            // NOTA: WhatsApp Cloud API no tiene parámetro 'ptt' directo para links.
            // Para notas de voz se recomienda subir el archivo y usar sendMediaById con is_voice.
            error_log('sendAudioMessage: is_voice=true pero usando link. Para notas de voz, subir archivo primero.');
        }

        $data = [
            'messaging_product' => 'whatsapp',
            'to' => $this->formatPhoneNumber($to),
            'type' => 'audio',
            'audio' => $audioData
        ];

        if ($skipAutoSave) {
            $data['__skip_auto_save'] = true;
        }

        return $this->sendMessage($data);
    }

    /**
     * Enviar documento
     */
    public function sendDocumentMessage($to, $documentUrl, $filename = null, $caption = null, $skipAutoSave = false)
    {
        $document = ['link' => $documentUrl];
        
        if ($filename) {
            $document['filename'] = $filename;
        }
        
        if ($caption) {
            $document['caption'] = $caption;
        }

        $data = [
            'messaging_product' => 'whatsapp',
            'to' => $this->formatPhoneNumber($to),
            'type' => 'document',
            'document' => $document
        ];

        if ($skipAutoSave) {
            $data['__skip_auto_save'] = true;
        }

        return $this->sendMessage($data);
    }

    /**
     * Subir archivo multimedia a WhatsApp
     * Intenta primero con PHP curl. Si falla por SSL/conexión (errno 35, 56, 77),
     * reintenta con distintas opciones SSL y usa stream_context/proc_open como fallback.
     */
    public function uploadMedia($filePath, $mimeType)
    {
        if (!file_exists($filePath)) {
            throw new Exception("Archivo no encontrado: $filePath");
        }

        $url = $this->apiUrl . $this->phoneNumberId . '/media';
        $errors = [];

        // ── Intento 1: PHP curl TLS 1.2 + sin ALPN + IPv4 (hasta 3 reintentos) ──
        // curl1 logra conectar por TCP pero puede fallar en el envío (errno 55).
        // Reintentar es útil: el error es del lado del CDN de Meta y suele desaparecer.
        $a1Opts = [
            CURLOPT_SSLVERSION      => CURL_SSLVERSION_TLSv1_2,
            CURLOPT_SSL_ENABLE_ALPN => false,   // sin ALPN → HTTP/1.1 → CDN diferente (alcanzable)
            CURLOPT_HTTP_VERSION    => CURL_HTTP_VERSION_1_1,
            CURLOPT_IPRESOLVE       => CURL_IPRESOLVE_V4,
        ];
        $a1 = null;
        for ($try = 1; $try <= 3; $try++) {
            $a1 = $this->_curlUploadAttempt($url, $filePath, $mimeType, $a1Opts);
            if ($a1['ok']) return $a1['result'];
            error_log("WhatsAppService::uploadMedia a1 intento{$try} fail: errno={$a1['errno']} http={$a1['http']}");
            if ($try < 3) sleep(1);
        }
        $errors[] = "curl1: #{$a1['errno']}({$a1['error']}) http={$a1['http']}";

        // ── Intento 2: PHP curl SSL deshabilitado + sin ALPN + HTTP/1.1 ──────────
        // IMPORTANTE: ALPN desactivado es crítico — con ALPN activo curl resuelve a un
        // IP de graph.facebook.com que no tiene ruta en el contenedor (errno 7 inmediato).
        $a2 = $this->_curlUploadAttempt($url, $filePath, $mimeType, [
            CURLOPT_SSL_VERIFYPEER  => false,
            CURLOPT_SSL_VERIFYHOST  => 0,
            CURLOPT_SSL_ENABLE_ALPN => false,   // mismo truco que curl1
            CURLOPT_HTTP_VERSION    => CURL_HTTP_VERSION_1_1,
            CURLOPT_IPRESOLVE       => CURL_IPRESOLVE_V4,
        ]);
        if ($a2['ok']) return $a2['result'];
        $errors[] = "curl2: #{$a2['errno']}({$a2['error']}) http={$a2['http']}";
        error_log("WhatsAppService::uploadMedia a2 fail: {$errors[1]}");

        // ── Intento 3: PHP stream_context (evita libcurl/OpenSSL completamente) ──
        // Usa el wrapper SSL de PHP (diferente al de libcurl), sin ALPN issues
        try {
            $result = $this->_streamContextUpload($url, $filePath, $mimeType);
            if ($result) return $result;
        } catch (Exception $e) {
            $errors[] = "stream: " . $e->getMessage();
            error_log("WhatsAppService::uploadMedia stream fail: " . $e->getMessage());
        }

        // ── Intento 4: proc_open + curl CLI con --ipv4 ──────────────────────────
        if (function_exists('proc_open')) {
            try {
                $result = $this->_procOpenUpload($url, $filePath, $mimeType);
                if ($result) return $result;
            } catch (Exception $e) {
                $errors[] = "proc_open: " . $e->getMessage();
                error_log("WhatsAppService::uploadMedia proc_open fail: " . $e->getMessage());
            }
        }

        throw new Exception(
            "Error al subir archivo a WhatsApp tras 4 intentos: " . implode(' | ', $errors) .
            ". Solución permanente: docker compose up --build (aplica parche OpenSSL en contenedor)."
        );
    }

    /**
     * Helper interno: upload con PHP curl + opciones extra.
     * IMPORTANTE: $extraOpts siempre sobrescriben los base. IPv4 se fuerza en base.
     */
    private function _curlUploadAttempt($url, $filePath, $mimeType, array $extraOpts = [])
    {
        $ch = curl_init();
        // Base siempre incluye IPRESOLVE_V4; extraOpts puede sobreescribirlo si es necesario
        $base = [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => [
                'messaging_product' => 'whatsapp',
                'file'              => new CURLFile($filePath, $mimeType),
                'type'              => $mimeType,
            ],
            CURLOPT_HTTPHEADER     => [
                'Authorization: Bearer ' . $this->token,
                'Expect:',  // deshabilitar 100-Continue: evita errno 55 (Send failure) con Meta
            ],
            CURLOPT_TIMEOUT        => 300,
            CURLOPT_CONNECTTIMEOUT => 30,
            CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
            CURLOPT_IPRESOLVE      => CURL_IPRESOLVE_V4, // siempre IPv4 salvo que extraOpts lo cambie
        ];
        curl_setopt_array($ch, $base + $extraOpts);

        $response = curl_exec($ch);
        $http     = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error    = curl_error($ch);
        $errno    = curl_errno($ch);
        curl_close($ch);

        if ($errno !== 0 || ($http !== 200 && $http !== 201)) {
            return ['ok' => false, 'errno' => $errno, 'error' => $error, 'http' => $http];
        }
        $result = json_decode($response, true);
        if (!isset($result['id'])) {
            return ['ok' => false, 'errno' => 0, 'error' => 'No ID: ' . substr($response, 0, 200), 'http' => $http];
        }
        return ['ok' => true, 'result' => $result];
    }

    /**
     * Upload usando PHP stream_context (evita libcurl y su OpenSSL completamente).
     * Construye manualmente el body multipart/form-data.
     */
    private function _streamContextUpload($url, $filePath, $mimeType)
    {
        $boundary = '----WA' . bin2hex(random_bytes(16));
        $fileContent = file_get_contents($filePath);
        if ($fileContent === false) {
            throw new Exception("No se pudo leer el archivo: $filePath");
        }
        $filename = basename($filePath);

        // Construir multipart body manualmente
        $body  = "--{$boundary}\r\n";
        $body .= "Content-Disposition: form-data; name=\"messaging_product\"\r\n\r\n";
        $body .= "whatsapp\r\n";
        $body .= "--{$boundary}\r\n";
        $body .= "Content-Disposition: form-data; name=\"type\"\r\n\r\n";
        $body .= "{$mimeType}\r\n";
        $body .= "--{$boundary}\r\n";
        $body .= "Content-Disposition: form-data; name=\"file\"; filename=\"{$filename}\"\r\n";
        $body .= "Content-Type: {$mimeType}\r\n\r\n";
        $body .= $fileContent . "\r\n";
        $body .= "--{$boundary}--\r\n";

        $opts = [
            'http' => [
                'method'           => 'POST',
                'header'           => implode("\r\n", [
                    'Authorization: Bearer ' . $this->token,
                    'Content-Type: multipart/form-data; boundary=' . $boundary,
                    'Content-Length: ' . strlen($body),
                ]),
                'content'          => $body,
                'timeout'          => 300,
                'ignore_errors'    => true,
                'protocol_version' => '1.1',
            ],
            'ssl' => [
                'verify_peer'       => true,
                'verify_peer_name'  => true,
                'crypto_method'     => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
                'cafile'            => '/etc/ssl/certs/ca-certificates.crt',
            ],
        ];

        $ctx      = stream_context_create($opts);
        $response = @file_get_contents($url, false, $ctx);

        // Obtener HTTP code de los response headers
        $httpCode = 0;
        if (isset($http_response_header) && is_array($http_response_header)) {
            foreach ($http_response_header as $h) {
                if (preg_match('#^HTTP/\S+ (\d+)#', $h, $m)) {
                    $httpCode = intval($m[1]);
                }
            }
        }

        error_log("WhatsAppService::_streamContextUpload http={$httpCode} response=" . substr((string)$response, 0, 300));

        if ($response === false || ($httpCode !== 200 && $httpCode !== 201)) {
            $err = error_get_last();
            throw new Exception("stream_context HTTP {$httpCode}: " . ($err['message'] ?? 'sin respuesta'));
        }

        $result = json_decode($response, true);
        if (!isset($result['id'])) {
            throw new Exception("stream_context sin ID: " . substr($response, 0, 300));
        }
        return $result;
    }

    /**
     * Upload usando proc_open + curl CLI con --ipv4 explícito.
     * proc_open hereda el entorno de red completo del proceso PHP-FPM.
     */
    private function _procOpenUpload($url, $filePath, $mimeType)
    {
        // Localizar curl binary
        $curlBin = '/usr/bin/curl';
        if (!file_exists($curlBin)) {
            foreach (['/usr/local/bin/curl', '/bin/curl'] as $c) {
                if (file_exists($c)) { $curlBin = $c; break; }
            }
        }
        if (!file_exists($curlBin)) {
            throw new Exception("curl CLI no encontrado");
        }

        $tmpResponse = tempnam(sys_get_temp_dir(), 'wa_up_');

        $args = [
            $curlBin,
            '--silent', '--show-error',
            '--ipv4',           // forzar IPv4
            '--tlsv1.2',
            '--http1.1',        // sin ALPN→HTTP/2: graph.facebook.com CDN alcanzable
            '--no-alpn',        // desactiva ALPN extension (igual que CURLOPT_SSL_ENABLE_ALPN=false)
            '--insecure',       // bypass SSL verify (fallback de último recurso)
            '--max-time', '300',
            '-o', $tmpResponse,
            '-w', '%{http_code}',
            '-X', 'POST',
            '-H', 'Authorization: Bearer ' . $this->token,
            '-H', 'Expect:',   // deshabilitar 100-Continue: evita errno 55 con Meta API
            '-F', 'messaging_product=whatsapp',
            '-F', 'type=' . $mimeType,
            '-F', 'file=@' . $filePath . ';type=' . $mimeType,
            $url,
        ];

        $cmd = implode(' ', array_map('escapeshellarg', $args));
        $descriptors = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
        $env = array_merge(
            $_ENV ?: [],
            ['HOME' => '/tmp', 'PATH' => '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin']
        );
        $proc = proc_open($cmd, $descriptors, $pipes, null, $env);

        if (!is_resource($proc)) {
            @unlink($tmpResponse);
            throw new Exception("proc_open no pudo iniciar el proceso");
        }

        fclose($pipes[0]);
        $stdout   = stream_get_contents($pipes[1]);
        $stderr   = stream_get_contents($pipes[2]);
        fclose($pipes[1]);
        fclose($pipes[2]);
        $exitCode = proc_close($proc);

        $httpCode = intval(trim($stdout));
        $body     = @file_get_contents($tmpResponse);
        @unlink($tmpResponse);

        error_log("WhatsAppService::_procOpenUpload exit={$exitCode} http={$httpCode} stderr=" . substr($stderr, 0, 300));

        if ($exitCode !== 0) {
            throw new Exception("curl CLI exit {$exitCode}: " . trim($stderr));
        }
        if ($httpCode !== 200 && $httpCode !== 201) {
            throw new Exception("CLI HTTP {$httpCode}: " . substr((string)$body, 0, 300));
        }
        $result = json_decode($body, true);
        if (!isset($result['id'])) {
            throw new Exception("CLI sin ID: " . substr((string)$body, 0, 300));
        }
        return $result;
    }

    /**
     * Enviar mensaje usando media_id (archivo ya subido a WhatsApp)
     * @param string $to Número de destinatario
     * @param string $mediaId ID del archivo en WhatsApp
     * @param string $mediaType Tipo: image, video, audio, document
     * @param string|null $caption Texto opcional (no aplica para audio)
     * @param string|null $filename Nombre archivo (solo document)
     * @param bool $skipAutoSave Omitir guardado automático
     * @param bool $isVoice Para audio: true = nota de voz con onda verde, false = audio normal
     */
    public function sendMediaById($to, $mediaId, $mediaType, $caption = null, $filename = null, $skipAutoSave = false, $isVoice = false, $meta = null)
    {
        $data = [
            'messaging_product' => 'whatsapp',
            'to' => $this->formatPhoneNumber($to),
            'type' => $mediaType
        ];

        $mediaData = ['id' => $mediaId];

        if ($caption && in_array($mediaType, ['image', 'video', 'document'])) {
            $mediaData['caption'] = $caption;
        }

        if ($filename && $mediaType === 'document') {
            $mediaData['filename'] = $filename;
        }

        if ($mediaType === 'audio' && $isVoice) {
            error_log('sendMediaById: is_voice=true, pero ptt no soportado en Cloud API. Audio se enviará como archivo.');
        }

        $data[$mediaType] = $mediaData;

        if (!empty($meta) && is_array($meta)) {
            $data['__app_meta'] = $meta;
        }

        if ($skipAutoSave) {
            $data['__skip_auto_save'] = true;
        }

        return $this->sendMessage($data);
    }

    /**
     * Enviar mensaje principal
     */
    private function sendMessage($data)
    {
        // Construir URL correcto para enviar mensajes (use /messages)
        $url = rtrim($this->apiUrl, '/') . '/' . $this->phoneNumberId . '/messages';
        
        // Prepare payload (remove internal app meta before sending to WhatsApp API)
        $payload = $data;
        if (isset($payload['__app_meta'])) {
            unset($payload['__app_meta']);
        }

        // A quien oculta su teléfono se le responde por BSUID, y Meta exige que vaya
        // en `recipient` en lugar de `to`. Se corrige aquí, que es por donde pasan
        // todos los envíos, en vez de en cada método que arma un payload.
        if (isset($payload['to']) && self::esBsuid($payload['to'])) {
            $payload['recipient'] = $payload['to'];
            unset($payload['to']);
        }

        // Debug log (payload sent to WhatsApp)
        error_log("WhatsApp API URL: " . $url);
        error_log("WhatsApp Token length: " . strlen($this->token));
        error_log("WhatsApp Payload: " . json_encode($payload));
        
        $response = $this->makeRequest('POST', $url, $payload);

        // Guardar mensaje enviado en la base de datos (respuesta puede contener 'messages' o 'conversations')
        // Solo guardar si no se solicitó skipAutoSave
        $skipAutoSave = isset($data['__skip_auto_save']) && $data['__skip_auto_save'];
        
        $sentMessageId = null;
        if ($response) {
            if (isset($response['messages'][0]['id'])) {
                $sentMessageId = $response['messages'][0]['id'];
            } elseif (isset($response['conversations'][0]['id'])) {
                $sentMessageId = $response['conversations'][0]['id'];
            }
        }

        if ($response && $sentMessageId && !$skipAutoSave) {
            // Asegurar que la estructura de respuesta para saveOutgoingMessage se mantenga pasando el id
            $responseWrapper = $response;
            // Normalizar para compatibilidad con saveOutgoingMessage
            if (!isset($responseWrapper['conversations']) && $sentMessageId) {
                $responseWrapper['conversations'][0]['id'] = $sentMessageId;
            }
            $this->saveOutgoingMessage($data, $responseWrapper);
        }

        return $response;
    }

    /**
     * Realizar petición HTTP
     */
    private function makeRequest($method, $url, $data = null)
    {
        $ch = curl_init();

        $headers = [
            'Authorization: Bearer ' . $this->token,
            'Content-Type: application/json',
            'User-Agent: WhatsApp-Bot/1.0'
        ];

        curl_setopt_array($ch, [
            CURLOPT_URL             => $url,
            CURLOPT_RETURNTRANSFER  => true,
            CURLOPT_HTTPHEADER      => $headers,
            CURLOPT_TIMEOUT         => 30,
            CURLOPT_CONNECTTIMEOUT  => 10,
            CURLOPT_SSL_VERIFYPEER  => true,
            CURLOPT_SSL_VERIFYHOST  => 2,
            CURLOPT_FOLLOWLOCATION  => true,
            CURLOPT_MAXREDIRS       => 3,
            CURLOPT_HEADER          => true, // Capturar headers de respuesta
            CURLOPT_IPRESOLVE       => CURL_IPRESOLVE_V4,
            CURLOPT_HTTP_VERSION    => CURL_HTTP_VERSION_1_1,
            // Forzar TLS 1.2: evita el bug errno-56 de OpenSSL 3.x con Meta/Facebook CDN
            CURLOPT_SSLVERSION      => CURL_SSLVERSION_TLSv1_2,
        ]);

        if ($method === 'POST' && $data) {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
        }

        $fullResponse = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $error = curl_error($ch);
        
        // Separar headers y body
        $headerText = substr($fullResponse, 0, $headerSize);
        $response = substr($fullResponse, $headerSize);
        
        // curl_close is deprecated in PHP 8.5+ (has no effect). Avoid calling it on newer versions.
        if (version_compare(PHP_VERSION, '8.5.0', '<')) {
            curl_close($ch);
        }
        
        // Capturar rate limit info de los headers
        if ($this->rateLimitMonitor && $headerText) {
            $endpoint = parse_url($url, PHP_URL_PATH);
            // Parsear headers string a array
            $headersArray = array_filter(explode("\r\n", $headerText));
            $this->rateLimitMonitor->captureFromHeaders($headersArray, $endpoint);
        }

        if ($error) {
            error_log("cURL Error: " . $error);
            throw new Exception("Error de comunicación con WhatsApp: " . $error);
        }

        $decoded = json_decode($response, true);

        if ($httpCode >= 400) {
            $err     = $decoded['error'] ?? [];
            $errorMsg = $err['message'] ?? 'Error desconocido';
            $details  = $err['error_data']['details'] ?? ($err['error_data']['description'] ?? null);
            $subcode  = $err['error_subcode'] ?? null;

            error_log("WhatsApp API Error: HTTP={$httpCode} code={$err['code']} sub={$subcode} msg={$errorMsg} details={$details} body=" . substr($response, 0, 400));

            $full = $errorMsg;
            if ($subcode) $full .= " (sub:{$subcode})";
            if ($details) $full .= " — {$details}";
            throw new Exception("Error de WhatsApp API: " . $full);
        }

        return $decoded;
    }

    /**
     * ¿El destinatario es un BSUID (identificador de usuario por empresa) y no un teléfono?
     * La definición vive en config.php, que es donde la ven también las clases del
     * laboratorio; aquí solo se expone para quien ya tiene el servicio a la mano.
     */
    public static function esBsuid($destinatario)
    {
        return esBsuid($destinatario);
    }

    /**
     * Formatear número de teléfono
     */
    private function formatPhoneNumber($phone)
    {
        // Un BSUID no es un teléfono: se devuelve intacto, porque quitarle
        // el punto y las letras lo convertiría en un destinatario inexistente.
        if (self::esBsuid($phone)) {
            return $phone;
        }

        // Remover caracteres especiales
        $phone = preg_replace('/[^0-9]/', '', $phone);

        // Si empieza con +57 (Colombia), mantenerlo
        if (substr($phone, 0, 2) === '57' && strlen($phone) >= 12) {
            return $phone;
        }

        // Si es número colombiano sin código de país
        if (strlen($phone) === 10 && substr($phone, 0, 1) === '3') {
            return '57' . $phone;
        }

        return $phone;
    }

    /**
     * Guardar mensaje enviado en BD
     */
    private function saveOutgoingMessage($data, $response)
    {
        try {
            $user = $this->getUserByPhone($data['to']);
            if ($user) {
                $appMeta = $data['__app_meta'] ?? [];
                $messageData = [
                    'user_id' => $user['id'],
                    'message_id' => $response['conversations'][0]['id'],
                    'direction' => 'outgoing',
                    'message_type' => $data['type'],
                    'content' => $this->extractMessageContent($data),
                    'status' => 'sent',
                    'created_at' => date('Y-m-d H:i:s'),
                    'canal' => $appMeta['canal'] ?? 'bot',
                ];

                // Para mensajes multimedia, guardar el media_id si está disponible
                if (in_array($data['type'], ['image', 'video', 'audio', 'document'])) {
                    $mediaId = $data[$data['type']]['id'] ?? null;
                    $mediaLink = $data[$data['type']]['link'] ?? null;
                    
                    // Prioridad: usar media_id de WhatsApp, sino link
                    if ($mediaId) {
                        $messageData['media_url'] = $mediaId;
                        $messageData['whatsapp_media_id'] = $mediaId;
                    } elseif ($mediaLink) {
                        $messageData['media_url'] = $mediaLink;
                    }
                    
                    // Asegurar que content tenga el filename si está disponible
                    $filename = $data[$data['type']]['filename'] ?? null;
                    if ($filename) {
                        // Si content está vacío o es una descripción genérica, usar filename
                        if (empty($messageData['content']) || 
                            in_array($messageData['content'], ['[Imagen]', '[Video]', '[Audio]', '[Documento]'])) {
                            $messageData['content'] = $filename;
                        }
                    }
                }

                // Log detallado para multimedia
                if (in_array($data['type'], ['image', 'video', 'audio', 'document'])) {
                    error_log("WhatsAppService.saveOutgoingMessage - Multimedia:");
                    error_log("  - type: " . $data['type']);
                    error_log("  - content: " . ($messageData['content'] ?? 'empty'));
                    error_log("  - media_url: " . ($messageData['media_url'] ?? 'empty'));
                    error_log("  - whatsapp_media_id: " . ($messageData['whatsapp_media_id'] ?? 'empty'));
                }

                // Si es reply (context)
                if (isset($data['context']['message_id'])) {
                    // WhatsApp message IDs can be alphanumeric (e.g., wamid...), store as string
                    $messageData['reply_to_message_id'] = trim((string)$data['context']['message_id']) ?: null;
                }

                // Si es reaction
                if ($data['type'] === 'reaction' && isset($data['reaction'])) {
                    $messageData['reaction_to_message_id'] = isset($data['reaction']['message_id']) ? trim((string)$data['reaction']['message_id']) : null;
                    $messageData['reaction_emoji'] = $data['reaction']['emoji'] ?? null;
                }

                $this->db->insert('conversations', $messageData);

                // Solo limpiar advisor_requested si el mensaje fue enviado por un asesor (operator)
                try {
                    $cleared = false;
                    if (isset($data['__app_meta']) && is_array($data['__app_meta']) && !empty($data['__app_meta']['operator_id'])) {
                        $this->db->update('users', ['advisor_requested' => 0, 'on_hold' => 0, 'bot_paused_until' => null], 'id = :id', ['id' => $user['id']]);
                        $cleared = true;
                    }
                    if ($cleared && function_exists('writeLog')) writeLog('INFO', "Cleared advisor_requested for user {$user['id']} after outgoing message by operator");
                } catch (Exception $e) {
                    error_log('Failed to clear advisor_requested after outgoing: ' . $e->getMessage());
                }
            }
        } catch (Exception $e) {
            error_log("Error saving outgoing message: " . $e->getMessage());
        }
    }

    /**
     * Extraer contenido del mensaje para guardar
     */
    private function extractMessageContent($data)
    {
        switch ($data['type']) {
            case 'text':
                return $data['text']['body'];
            case 'template':
                return 'Template: ' . $data['template']['name'];
            case 'interactive':
                if (isset($data['interactive']['body']['text'])) {
                    return $data['interactive']['body']['text'];
                }
                break;
            case 'reaction':
                return isset($data['reaction']['emoji']) ? $data['reaction']['emoji'] : '';
            case 'image':
                // Prioridad: caption, filename, descripción por defecto
                if (!empty($data['image']['caption'])) {
                    return $data['image']['caption'];
                }
                if (!empty($data['image']['filename'])) {
                    return $data['image']['filename'];
                }
                return '[Imagen]';
            case 'video':
                if (!empty($data['video']['caption'])) {
                    return $data['video']['caption'];
                }
                if (!empty($data['video']['filename'])) {
                    return $data['video']['filename'];
                }
                return '[Video]';
            case 'audio':
                if (!empty($data['audio']['filename'])) {
                    return $data['audio']['filename'];
                }
                if (!empty($data['audio']['caption'])) {
                    return $data['audio']['caption'];
                }
                return '[Audio]';
            case 'document':
                // Prioridad: filename, caption, descripción por defecto
                if (!empty($data['document']['filename'])) {
                    return $data['document']['filename'];
                }
                if (!empty($data['document']['caption'])) {
                    return $data['document']['caption'];
                }
                return '[Documento]';
        }
        return '';
    }

    /**
     * Enviar reacción (emoji) a un mensaje previo
     * @param string $to Teléfono del destinatario
     * @param string $messageId ID del mensaje al que se reacciona
     * @param string $emoji Emoji de reacción
     * @param bool $dryRun Si true, no hace la petición y devuelve el payload
     */
    public function sendReaction($to, $messageId, $emoji, $dryRun = false)
    {
        $data = [
            'messaging_product' => 'whatsapp',
            'to' => $this->formatPhoneNumber($to),
            'type' => 'reaction',
            'reaction' => [
                'message_id' => $messageId,
                'emoji' => $emoji
            ]
        ];

        if ($dryRun) return $data;
        return $this->sendMessage($data);
    }

    /**
     * Enviar texto como respuesta (con contexto) a un mensaje existente
     * @param string $to Teléfono del destinatario
     * @param string $messageId ID del mensaje al que se responde
     * @param string $text Contenido del mensaje de respuesta
     * @param bool $preview_url Incluir preview_url en body.text
     * @param bool $dryRun Si true, no hace la petición y devuelve el payload
     */
    public function sendTextReply($to, $messageId, $text, $preview_url = false, $dryRun = false, $meta = null)
    {
        $data = [
            'messaging_product' => 'whatsapp',
            'to' => $this->formatPhoneNumber($to),
            'type' => 'text',
            'context' => [
                'message_id' => $messageId
            ],
            'text' => [
                'preview_url' => (bool)$preview_url,
                'body' => $text
            ]
        ];

        if (!empty($meta) && is_array($meta)) {
            $data['__app_meta'] = $meta;
        }

        if ($dryRun) return $data;
        return $this->sendMessage($data);
    }

    /**
     * Obtener usuario por teléfono
     */
    private function getUserByPhone($phone)
    {
        return $this->db->fetch(
            "SELECT * FROM users WHERE phone_number = :phone",
            ['phone' => $phone]
        );
    }

    /**
     * Descargar archivo multimedia
     */
    public function downloadMedia($mediaId)
    {
        // Validar que mediaId no esté vacío ANTES de hacer la llamada
        if (empty($mediaId) || !is_string($mediaId) || trim($mediaId) === '') {
            error_log("downloadMedia: mediaId inválido o vacío: " . var_export($mediaId, true));
            throw new Exception("Media ID no puede estar vacío");
        }
        
        $url = $this->apiUrl . $mediaId;

        // Primero obtener información del archivo
        $mediaInfo = $this->makeRequest('GET', $url);

        if (!$mediaInfo || !isset($mediaInfo['url'])) {
            throw new Exception("No se pudo obtener información del archivo");
        }

        // Descargar el archivo
        $fileUrl = $mediaInfo['url'];
        $ch = curl_init();

        curl_setopt_array($ch, [
            CURLOPT_URL             => $fileUrl,
            CURLOPT_RETURNTRANSFER  => true,
            CURLOPT_HTTPHEADER      => [
                'Authorization: Bearer ' . $this->token
            ],
            CURLOPT_TIMEOUT         => 60,
            CURLOPT_FOLLOWLOCATION  => true,
            CURLOPT_IPRESOLVE       => CURL_IPRESOLVE_V4,
            CURLOPT_HTTP_VERSION    => CURL_HTTP_VERSION_1_1,
            CURLOPT_SSLVERSION      => CURL_SSLVERSION_TLSv1_2,
        ]);

        $fileContent = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            throw new Exception("Error descargando archivo multimedia");
        }

        return [
            'content' => $fileContent,
            'mime_type' => $mediaInfo['mime_type'] ?? 'application/octet-stream',
            'file_size' => $mediaInfo['file_size'] ?? strlen($fileContent)
        ];
    }
}
