<?php
/**
 * ASISMA CRM - WhatsApp Controller (Evolution API)
 *
 * - Bandeja de chats por lead (multi-tenant).
 * - Envío de texto y adjuntos (imagen, audio, documento).
 * - Webhook público para mensajes entrantes (api/whatsapp/webhook).
 * - Gestión de instancias por franquiciado.
 */
class WhatsAppController extends Controller {

    /**
     * Bandeja de conversaciones + panel de chat.
     * Acepta ?lead_id= para abrir directo la conversación de un lead.
     */
    public function index() {
        $db = Database::getInstance();
        $leadIdSel = (int)$this->input('lead_id', 0);
        $esAdmin = SessionHelper::isAdmin();

        $tenantFilter = $this->getTenantFilter('l');

        // Filtros de segmentación: SOLO la matriz (ve los chats de todos los
        // franquiciados y necesita acotarlos por vertical/marca/franquicia)
        $filtros = array(
            'vertical_id'     => $esAdmin ? (int)$this->input('vertical_id', 0) : 0,
            'marca_id'        => $esAdmin ? (int)$this->input('marca_id', 0) : 0,
            'franquiciado_id' => $esAdmin ? (int)$this->input('franquiciado_id', 0) : 0,
        );
        $condiciones = '';
        $params = array('sel' => $leadIdSel);
        if ($filtros['vertical_id'])     { $condiciones .= " AND l.vertical_interes = :f_vertical";    $params['f_vertical'] = $filtros['vertical_id']; }
        if ($filtros['marca_id'])        { $condiciones .= " AND l.marca_interes = :f_marca";          $params['f_marca'] = $filtros['marca_id']; }
        if ($filtros['franquiciado_id']) { $condiciones .= " AND l.franquiciado_id = :f_franquiciado"; $params['f_franquiciado'] = $filtros['franquiciado_id']; }

        // Conversaciones: leads con mensajes + el lead seleccionado aunque no tenga
        $conversaciones = $db->fetchAll(
            "SELECT l.id, l.estado_embudo, l.datos_liberados, l.telefono_enmascarado, l.telefono_real,
                    c.nombre AS contacto_nombre,
                    COALESCE(ef.nombre_fantasia, '') AS franquiciado_nombre,
                    COALESCE(u.nombre, '') AS vendedor_nombre,
                    (SELECT contenido FROM whatsapp_mensajes wm WHERE wm.lead_id = l.id ORDER BY wm.id DESC LIMIT 1) AS ultimo_mensaje,
                    (SELECT tipo FROM whatsapp_mensajes wm WHERE wm.lead_id = l.id ORDER BY wm.id DESC LIMIT 1) AS ultimo_tipo,
                    (SELECT created_at FROM whatsapp_mensajes wm WHERE wm.lead_id = l.id ORDER BY wm.id DESC LIMIT 1) AS ultimo_at,
                    (SELECT COUNT(*) FROM whatsapp_mensajes wm WHERE wm.lead_id = l.id AND wm.direccion = 'entrante' AND wm.leido = 0) AS no_leidos
             FROM leads l
             INNER JOIN contactos c ON l.contacto_id = c.id
             LEFT JOIN franquiciados f ON l.franquiciado_id = f.id
             LEFT JOIN empresas_franquiciados ef ON f.empresa_madre_id = ef.id
             LEFT JOIN usuarios u ON l.usuario_asignado_id = u.id
             WHERE l.activo = 1 AND {$tenantFilter}{$condiciones}
               AND (EXISTS (SELECT 1 FROM whatsapp_mensajes wm WHERE wm.lead_id = l.id) OR l.id = :sel)
             ORDER BY ultimo_at DESC, l.updated_at DESC
             LIMIT 100",
            $params
        );

        $leadSel = null;
        $puedeEnviar = false;
        $duenoInstancia = '';
        if ($leadIdSel > 0) {
            $leadSel = $db->fetchOne(
                "SELECT l.*, c.nombre AS contacto_nombre FROM leads l
                 INNER JOIN contactos c ON l.contacto_id = c.id
                 WHERE l.id = :id AND {$tenantFilter}",
                array('id' => $leadIdSel)
            );
            // Solo el DUEÑO de la instancia del chat puede escribir; el resto
            // de la franquicia (y la matriz) lo ve en modo lectura (regla 22-07)
            if ($leadSel) {
                $instanciaSel = WhatsAppHelper::getInstanciaParaLead($leadSel);
                if ($instanciaSel) {
                    $puedeEnviar = (int)$instanciaSel['usuario_id'] === (int)SessionHelper::getUsuarioId();
                    if (!$puedeEnviar && !empty($instanciaSel['usuario_id'])) {
                        $filaDueno = $db->fetchOne("SELECT nombre FROM usuarios WHERE id = :id", array('id' => $instanciaSel['usuario_id']));
                        if ($filaDueno) $duenoInstancia = $filaDueno['nombre'];
                    }
                }
            }
        }

        // Catálogos para los selects de filtro (solo matriz)
        $verticales = array();
        $marcas = array();
        $franquiciados = array();
        if ($esAdmin) {
            $verticales = $db->fetchAll("SELECT id, nombre FROM verticales WHERE activa = 1 ORDER BY nombre ASC");
            $marcas     = $db->fetchAll("SELECT id, vertical_id, nombre FROM marcas WHERE activa = 1 ORDER BY nombre ASC");
            $franquiciados = $db->fetchAll(
                "SELECT f.id, ef.nombre_fantasia FROM franquiciados f
                 INNER JOIN empresas_franquiciados ef ON f.empresa_madre_id = ef.id
                 WHERE f.estado = 'activo' ORDER BY ef.nombre_fantasia ASC"
            );
        }

        $this->render('whatsapp/index', array(
            'title'          => 'Chats de WhatsApp',
            'conversaciones' => $conversaciones,
            'leadSel'        => $leadSel,
            'puedeEnviar'    => $puedeEnviar,
            'duenoInstancia' => $duenoInstancia,
            'filtros'        => $filtros,
            'verticales'     => $verticales,
            'marcas'         => $marcas,
            'franquiciados'  => $franquiciados
        ));
    }

    /**
     * AJAX: historial de mensajes de un lead (JSON) + marca entrantes como leídos.
     */
    public function chat() {
        $leadId = (int)$this->input('lead_id', 0);
        if ($leadId <= 0) {
            $this->json(array('success' => false, 'message' => 'Lead inválido.'));
        }

        $lead = $this->getLeadConTenant($leadId);
        if (!$lead) {
            $this->json(array('success' => false, 'message' => 'Sin acceso a este lead.'), 403);
        }

        $db = Database::getInstance();
        $mensajes = $db->fetchAll(
            "SELECT id, direccion, tipo, contenido, media_path, created_at, estado_entrega, eliminado
             FROM whatsapp_mensajes
             WHERE lead_id = :lid
             ORDER BY id ASC
             LIMIT 500",
            array('lid' => $leadId)
        );

        $out = array();
        foreach ($mensajes as $m) {
            $out[] = array(
                'id'             => (int)$m['id'],
                'direccion'      => $m['direccion'],
                'tipo'           => $m['tipo'],
                'contenido'      => $m['contenido'],
                'media'          => !empty($m['media_path']) ? (APP_URL . '/whatsapp/media?id=' . $m['id']) : null,
                'created_at'     => $m['created_at'],
                'estado_entrega' => $m['estado_entrega'],
                'eliminado'      => (int)$m['eliminado']
            );
        }

        $db->update('whatsapp_mensajes', array('leido' => 1),
            "lead_id = :lid AND direccion = 'entrante' AND leido = 0", array('lid' => $leadId));

        $instancia = WhatsAppHelper::getInstanciaParaLead($lead);

        $this->json(array(
            'success'   => true,
            'mensajes'  => $out,
            'instancia' => $instancia ? $instancia['nombre_instancia'] : null
        ));
    }

    /**
     * POST: enviar mensaje (texto o adjunto multipart 'adjunto') a un lead.
     */
    public function enviarMensaje() {
        // Si el POST superó post_max_size, PHP llega acá con $_POST y $_FILES
        // VACIOS: sin este aviso el error diría "Lead inválido" y confundiría.
        if (empty($_POST) && empty($_FILES) && !empty($_SERVER['CONTENT_LENGTH'])) {
            $this->json(array('success' => false, 'message' => 'El archivo es demasiado grande para subirlo (máximo 16 MB).'));
        }

        $leadId = (int)$this->input('lead_id', 0);
        // Texto sin sanitizar HTML (se escapa al renderizar); solo trim
        $texto  = isset($_POST['mensaje']) ? trim($_POST['mensaje']) : '';

        if ($leadId <= 0) {
            $this->json(array('success' => false, 'message' => 'Lead inválido.'));
        }

        $lead = $this->getLeadConTenant($leadId);
        if (!$lead) {
            $this->json(array('success' => false, 'message' => 'Sin acceso a este lead.'), 403);
        }

        $instancia = WhatsAppHelper::getInstanciaParaLead($lead);
        if (!$instancia) {
            $this->json(array('success' => false, 'message' => 'No hay una instancia de WhatsApp activa configurada para esta franquicia. Contacte al administrador.'));
        }

        // Solo el DUEÑO de la instancia envía por su línea (regla 22-07). El
        // resto de la franquicia puede leer el chat, pero no escribir.
        if ((int)$instancia['usuario_id'] !== (int)SessionHelper::getUsuarioId()) {
            $this->json(array('success' => false, 'message' => 'Solo lectura: esta línea de WhatsApp pertenece a otro usuario y solo su dueño puede enviar mensajes.'), 403);
        }

        $tipo = 'text';
        $media = null; $fileName = null; $mimetype = null; $mediaPath = null;

        // Adjunto opcional
        if (!empty($_FILES['adjunto']) && $_FILES['adjunto']['error'] === UPLOAD_ERR_OK) {
            $arch = $_FILES['adjunto'];
            if ($arch['size'] > 16 * 1024 * 1024) {
                $this->json(array('success' => false, 'message' => 'El archivo supera el máximo de 16 MB.'));
            }

            $finfo = function_exists('finfo_open') ? finfo_open(FILEINFO_MIME_TYPE) : null;
            $mimetype = $finfo ? finfo_file($finfo, $arch['tmp_name']) : $arch['type'];
            if ($finfo) finfo_close($finfo);

            $esNotaVoz = isset($_POST['es_nota_voz']) && $_POST['es_nota_voz'] == '1';

            if ($esNotaVoz) {
                // Grabación del micrófono: se envía como nota de voz (ptt) con el
                // mimetype REAL del binario (Chrome suele producir webm/opus, que
                // finfo detecta como video/webm; se corrige a audio/webm).
                $tipo = 'audio';
                if (strpos($mimetype, 'video/webm') === 0)      $mimetype = 'audio/webm';
                elseif (strpos($mimetype, 'audio/') !== 0)      $mimetype = 'audio/ogg';
            } elseif (strpos($mimetype, 'image/') === 0)  $tipo = 'image';
            elseif (strpos($mimetype, 'audio/') === 0)    $tipo = 'audio';
            elseif (strpos($mimetype, 'video/') === 0)    $tipo = 'video';
            else                                          $tipo = 'document';

            // Solo tipos de archivo conocidos (nada de ejecutables/scripts)
            if (!$esNotaVoz && !WhatsAppHelper::mimePermitido($mimetype)) {
                $this->json(array('success' => false, 'message' => 'Tipo de archivo no permitido (' . $mimetype . '). Se aceptan imágenes, audios, videos y documentos de oficina.'));
            }

            $ext    = WhatsAppHelper::extensionPorMime($mimetype, $tipo);
            $subdir = date('Y/m');
            $dirAbs = ROOT_DIR . '/storage/whatsapp_media/' . $subdir;
            if (!is_dir($dirAbs)) @mkdir($dirAbs, 0775, true);

            $nombre    = 'out_' . uniqid() . '.' . $ext;
            $mediaPath = $subdir . '/' . $nombre;
            if (!move_uploaded_file($arch['tmp_name'], $dirAbs . '/' . $nombre)) {
                $this->json(array('success' => false, 'message' => 'No se pudo guardar el archivo adjunto.'));
            }

            $media    = base64_encode(file_get_contents($dirAbs . '/' . $nombre));
            $fileName = $arch['name'];
        }

        if ($tipo === 'text' && $texto === '') {
            $this->json(array('success' => false, 'message' => 'Escribe un mensaje antes de enviar.'));
        }

        $resultado = WhatsAppHelper::enviar($instancia, $lead['telefono_real'], $texto, $tipo, $media, $fileName, $mimetype);

        if (!$resultado['success']) {
            // "Connection Closed" = la sesion de WhatsApp esta caida; el vigilante
            // (cron cada 1 min) la reconecta solo — avisar claro en vez del error crudo
            if (stripos($resultado['error'], 'Connection Closed') !== false) {
                $this->json(array('success' => false, 'message' => 'La línea de WhatsApp está desconectada en este momento. Se está reconectando automáticamente: reintenta en un minuto (si persiste, hay que escanear el QR de la instancia).'));
            }
            $this->json(array('success' => false, 'message' => 'Evolution API rechazó el envío. ' . $resultado['error']));
        }

        WhatsAppHelper::registrarMensaje(
            $leadId,
            $instancia['id'],
            $resultado['message_id'],
            'saliente',
            $tipo,
            $texto,
            null,
            $mediaPath,
            WhatsAppHelper::formatearNumero($lead['telefono_real']),
            SessionHelper::getUsuarioId()
        );

        // Fijar la instancia en el lead para enrutar respuestas entrantes
        if (empty($lead['wa_instance_id'])) {
            Database::getInstance()->update('leads',
                array('wa_instance_id' => $instancia['id']),
                "id = :id", array('id' => $leadId));
        }

        // Estado automático: el primer WhatsApp saliente marca el lead "Contactado"
        EmbudoHelper::avanzarAutomatico($leadId, ESTADO_CONTACTADO, 'se envió un mensaje de WhatsApp al cliente.');

        $this->json(array('success' => true, 'message' => 'Mensaje enviado.'));
    }

    /**
     * GET: stream seguro de la media de un mensaje (con control de tenant).
     */
    public function media() {
        $msgId = (int)$this->input('id', 0);
        if ($msgId <= 0) { http_response_code(404); exit; }

        $db = Database::getInstance();
        $msg = $db->fetchOne(
            "SELECT wm.media_path, wm.tipo, l.franquiciado_id
             FROM whatsapp_mensajes wm
             INNER JOIN leads l ON wm.lead_id = l.id
             WHERE wm.id = :id",
            array('id' => $msgId)
        );

        if (!$msg || empty($msg['media_path'])) { http_response_code(404); exit; }

        if (!SessionHelper::isAdmin() && $msg['franquiciado_id'] != SessionHelper::getFranquiciadoId()) {
            http_response_code(403); exit;
        }

        $ruta = ROOT_DIR . '/storage/whatsapp_media/' . $msg['media_path'];
        $real = realpath($ruta);
        $baseDir = realpath(ROOT_DIR . '/storage/whatsapp_media');
        if (!$real || strpos($real, $baseDir) !== 0 || !is_file($real)) { http_response_code(404); exit; }

        $finfo = function_exists('finfo_open') ? finfo_open(FILEINFO_MIME_TYPE) : null;
        $mime  = $finfo ? finfo_file($finfo, $real) : 'application/octet-stream';
        if ($finfo) finfo_close($finfo);

        header('Content-Type: ' . $mime);
        header('Content-Length: ' . filesize($real));
        header('Cache-Control: private, max-age=86400');
        readfile($real);
        exit;
    }

    /**
     * Webhook público de Evolution API (POST api/whatsapp/webhook).
     * Procesa messages.upsert; ignora el resto con 200 OK.
     */
    public function webhook() {
        $raw = file_get_contents('php://input');
        $payload = json_decode($raw, true);

        if (!$payload || empty($payload['event'])) {
            $this->json(array('success' => false, 'message' => 'Payload inválido.'), 400);
        }

        $this->logWebhook($payload['event'], $raw);

        // Estados de entrega (los "vistos"): messages.update trae el status de
        // cada mensaje enviado (mapa del OPS: 1 sent, 2 delivered, 3 read,
        // 4 played, 5 error). Se actualiza por message_id y listo.
        if ($payload['event'] === 'messages.update') {
            $this->procesarEstadosEntrega($payload);
            $this->json(array('success' => true, 'message' => 'Estados actualizados.'));
        }

        if ($payload['event'] !== 'messages.upsert') {
            $this->json(array('success' => true, 'message' => 'Evento ignorado.'));
        }

        // Resolución de instancia: primero por token único de la URL (?t=...),
        // que identifica la línea del vendedor; fallback por nombre de instancia
        // del payload (compatibilidad).
        $token     = isset($_GET['t']) ? trim($_GET['t']) : '';
        $instancia = WhatsAppHelper::getInstanciaPorToken($token);
        if (!$instancia) {
            $instNombre = isset($payload['instance']) ? $payload['instance'] : '';
            $instancia  = WhatsAppHelper::getInstanciaPorNombre($instNombre);
        }
        if (!$instancia) {
            $this->json(array('success' => true, 'message' => 'Instancia no registrada en el CRM.'));
        }

        $data = isset($payload['data']) ? $payload['data'] : array();
        $key  = isset($data['key']) ? $data['key'] : array();
        $remoteJid = isset($key['remoteJid']) ? $key['remoteJid'] : '';

        // Ignorar grupos y estados
        if (strpos($remoteJid, '@g.us') !== false || strpos($remoteJid, 'status@') === 0) {
            $this->json(array('success' => true, 'message' => 'Chat grupal/estado ignorado.'));
        }

        $telefono  = str_replace(array('@s.whatsapp.net', '@c.us'), '', $remoteJid);
        $fromMe    = !empty($key['fromMe']);
        $messageId = isset($key['id']) ? $key['id'] : ('wh_' . uniqid());
        $mensaje   = isset($data['message']) ? $data['message'] : array();
        $msgType   = isset($data['messageType']) ? $data['messageType'] : (is_array($mensaje) ? key($mensaje) : 'conversation');

        // Parsear tipo y contenido. El base64 puede venir en data.base64 o en
        // data.message.base64 según la versión/configuración de Evolution.
        $tipo = 'text'; $contenido = ''; $mimetype = null;
        $b64Inline = null;
        if (isset($payload['data']['base64'])) $b64Inline = $payload['data']['base64'];
        elseif (isset($mensaje['base64']))      $b64Inline = $mensaje['base64'];
        $mediaB64 = null;

        if ($msgType === 'conversation' && isset($mensaje['conversation'])) {
            $contenido = $mensaje['conversation'];
        } elseif ($msgType === 'extendedTextMessage' && isset($mensaje['extendedTextMessage']['text'])) {
            $contenido = $mensaje['extendedTextMessage']['text'];
        } elseif ($msgType === 'imageMessage') {
            $tipo      = 'image';
            $contenido = isset($mensaje['imageMessage']['caption']) ? $mensaje['imageMessage']['caption'] : '';
            $mimetype  = isset($mensaje['imageMessage']['mimetype']) ? $mensaje['imageMessage']['mimetype'] : 'image/jpeg';
            $mediaB64  = $b64Inline;
        } elseif ($msgType === 'audioMessage') {
            $tipo     = 'audio';
            $mimetype = isset($mensaje['audioMessage']['mimetype']) ? $mensaje['audioMessage']['mimetype'] : 'audio/ogg';
            $mediaB64 = $b64Inline;
        } elseif ($msgType === 'videoMessage') {
            $tipo      = 'video';
            $contenido = isset($mensaje['videoMessage']['caption']) ? $mensaje['videoMessage']['caption'] : '';
            $mimetype  = isset($mensaje['videoMessage']['mimetype']) ? $mensaje['videoMessage']['mimetype'] : 'video/mp4';
            $mediaB64  = $b64Inline;
        } elseif ($msgType === 'documentMessage') {
            $tipo      = 'document';
            $contenido = isset($mensaje['documentMessage']['fileName']) ? $mensaje['documentMessage']['fileName'] : '';
            $mimetype  = isset($mensaje['documentMessage']['mimetype']) ? $mensaje['documentMessage']['mimetype'] : 'application/pdf';
            $mediaB64  = $b64Inline;
        } elseif ($msgType === 'stickerMessage') {
            $tipo     = 'sticker';
            $mimetype = isset($mensaje['stickerMessage']['mimetype']) ? $mensaje['stickerMessage']['mimetype'] : 'image/webp';
            $mediaB64 = $b64Inline;
        } elseif ($msgType === 'reactionMessage') {
            $tipo      = 'reaction';
            $contenido = isset($mensaje['reactionMessage']['text']) ? $mensaje['reactionMessage']['text'] : '';
        } else {
            // Tipo no soportado: registrar como texto informativo
            $contenido = '[Mensaje de tipo no soportado: ' . $msgType . ']';
        }

        // Buscar lead por teléfono (últimos 8 dígitos), priorizando la instancia
        $lead = $this->buscarLeadPorTelefono($telefono, $instancia);
        if (!$lead) {
            $this->json(array('success' => true, 'message' => 'Sin lead asociado a este número.'));
        }

        // Media: descargar si viene en el payload o pedirla a la API
        $mediaPath = null;
        if (in_array($tipo, array('image', 'audio', 'video', 'document', 'sticker'))) {
            if (!$mediaB64) {
                $mediaB64 = WhatsAppHelper::obtenerMediaDeApi($instancia, $messageId);
            }
            if ($mediaB64) {
                $mediaPath = WhatsAppHelper::guardarMediaEntrante($mediaB64, $tipo, $mimetype);
            }
            if ($contenido === '') $contenido = '[' . ucfirst($tipo) . ']';
        }

        WhatsAppHelper::registrarMensaje(
            $lead['id'],
            $instancia['id'],
            $messageId,
            $fromMe ? 'saliente' : 'entrante',
            $tipo,
            $contenido,
            null,
            $mediaPath,
            WhatsAppHelper::formatearNumero($telefono)
        );

        // Fijar instancia en el lead si aún no la tiene
        if (empty($lead['wa_instance_id'])) {
            Database::getInstance()->update('leads',
                array('wa_instance_id' => $instancia['id']),
                "id = :id", array('id' => $lead['id']));
        }

        $this->json(array('success' => true));
    }

    /**
     * messages.update de Evolution → whatsapp_mensajes.estado_entrega.
     * Acepta status numérico (1..5) o string (READ/DELIVERED...), y el id del
     * mensaje en key.id, keyId o messageId (formatos Baileys/Evolution).
     */
    private function procesarEstadosEntrega($payload) {
        $mapa = array(1 => 'sent', 2 => 'delivered', 3 => 'read', 4 => 'played', 5 => 'error');
        $validos = array('sent', 'delivered', 'read', 'played', 'error');

        $data = isset($payload['data']) ? $payload['data'] : array();
        $updates = isset($data[0]) ? $data : array($data);

        $db = Database::getInstance();
        foreach ($updates as $upd) {
            if (!is_array($upd)) continue;
            $msgId = '';
            if (isset($upd['key']['id']))        $msgId = $upd['key']['id'];
            elseif (isset($upd['keyId']))        $msgId = $upd['keyId'];
            elseif (isset($upd['messageId']))    $msgId = $upd['messageId'];
            if ($msgId === '') continue;

            $statusRaw = null;
            if (isset($upd['update']['status'])) $statusRaw = $upd['update']['status'];
            elseif (isset($upd['status']))       $statusRaw = $upd['status'];
            if ($statusRaw === null) continue;

            $estado = 'sent';
            if (is_numeric($statusRaw)) {
                $estado = isset($mapa[(int)$statusRaw]) ? $mapa[(int)$statusRaw] : 'sent';
            } elseif (is_string($statusRaw)) {
                $estado = strtolower($statusRaw);
            }
            if (!in_array($estado, $validos)) $estado = 'sent';

            // Los estados solo avanzan (un 'delivered' tardío no pisa un 'read')
            $orden = array('sent' => 1, 'delivered' => 2, 'read' => 3, 'played' => 4, 'error' => 9);
            $db->query(
                "UPDATE whatsapp_mensajes SET estado_entrega = :nuevo
                 WHERE message_id = :mid
                   AND (CASE estado_entrega WHEN 'sent' THEN 1 WHEN 'delivered' THEN 2
                        WHEN 'read' THEN 3 WHEN 'played' THEN 4 ELSE 9 END) < :orden",
                array('nuevo' => $estado, 'mid' => $msgId, 'orden' => $orden[$estado])
            );
        }
    }

    /**
     * POST: eliminar PARA TODOS un mensaje saliente (retractar, patrón OPS).
     * Solo el dueño de la instancia puede hacerlo; WhatsApp lo permite hasta
     * ~48 horas después del envío.
     */
    public function mensajeEliminar() {
        $id = (int)$this->input('id', 0);
        if ($id <= 0) $this->json(array('success' => false, 'message' => 'Mensaje inválido.'));

        $db = Database::getInstance();
        $msg = $db->fetchOne("SELECT * FROM whatsapp_mensajes WHERE id = :id", array('id' => $id));
        if (!$msg) $this->json(array('success' => false, 'message' => 'Mensaje no encontrado.'));
        if ($msg['direccion'] !== 'saliente') {
            $this->json(array('success' => false, 'message' => 'Solo puedes eliminar mensajes enviados por ti.'));
        }
        if (!empty($msg['eliminado'])) {
            $this->json(array('success' => false, 'message' => 'El mensaje ya está eliminado.'));
        }

        $lead = $this->getLeadConTenant((int)$msg['lead_id']);
        if (!$lead) $this->json(array('success' => false, 'message' => 'Sin acceso a este lead.'), 403);

        $instancia = $db->fetchOne("SELECT * FROM whatsapp_instancias WHERE id = :id", array('id' => $msg['instancia_id']));
        if (!$instancia) $this->json(array('success' => false, 'message' => 'La instancia de este mensaje ya no existe.'));

        // Misma regla que el envío: solo el DUEÑO de la línea
        if ((int)$instancia['usuario_id'] !== (int)SessionHelper::getUsuarioId()) {
            $this->json(array('success' => false, 'message' => 'Solo el dueño de la línea puede eliminar sus mensajes.'), 403);
        }

        $ok = WhatsAppHelper::eliminarMensaje($instancia, $msg['message_id'], $lead['telefono_real']);
        if (!$ok) {
            $this->json(array('success' => false, 'message' => 'WhatsApp no permitió eliminarlo (normalmente porque pasaron más de 48 horas).'));
        }

        $db->update('whatsapp_mensajes', array('eliminado' => 1), "id = :id", array('id' => $id));
        AuditHelper::log('eliminar_mensaje_whatsapp', 'whatsapp_mensajes', $id, null,
            array('lead_id' => (int)$msg['lead_id'], 'message_id' => $msg['message_id']));
        $this->json(array('success' => true, 'message' => 'Mensaje eliminado para todos.'));
    }

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

    /**
     * Vista de gestión de instancias (admin ve todas; franquiciado las suyas).
     */
    public function instancias() {
        $db = Database::getInstance();

        $where = "1=1";
        if (!SessionHelper::isAdmin()) {
            $fid = (int)SessionHelper::getFranquiciadoId();
            $where = "wi.franquiciado_id = " . $fid;
        }

        $instancias = $db->fetchAll(
            "SELECT wi.*, ef.nombre_fantasia AS franquiciado_nombre, u.nombre AS usuario_nombre
             FROM whatsapp_instancias wi
             LEFT JOIN franquiciados f ON wi.franquiciado_id = f.id
             LEFT JOIN empresas_franquiciados ef ON f.empresa_madre_id = ef.id
             LEFT JOIN usuarios u ON wi.usuario_id = u.id
             WHERE {$where}
             ORDER BY wi.activa DESC, wi.id ASC"
        );

        // Instancias antiguas sin token: generarlo al vuelo
        foreach ($instancias as $k => $inst) {
            if (empty($inst['webhook_token'])) {
                $token = WhatsAppHelper::generarWebhookToken();
                $db->update('whatsapp_instancias', array('webhook_token' => $token), "id = :id", array('id' => $inst['id']));
                $instancias[$k]['webhook_token'] = $token;
            }
        }

        $franquiciados = array();
        if (SessionHelper::isAdmin()) {
            $franquiciados = $db->fetchAll(
                "SELECT f.id, ef.nombre_fantasia FROM franquiciados f
                 INNER JOIN empresas_franquiciados ef ON f.empresa_madre_id = ef.id
                 ORDER BY ef.nombre_fantasia ASC"
            );
        }

        // ¿Servidor Evolution global configurado? Habilita "Conectar línea (QR)"
        $aprovisionamiento = WhatsAppHelper::aprovisionamientoActivo();

        // Vendedores para asignar como dueños de línea (admin: todos; franquiciado: los suyos)
        if (SessionHelper::isAdmin()) {
            $usuarios = $db->fetchAll(
                "SELECT u.id, u.nombre, u.franquiciado_id FROM usuarios u WHERE u.activo = 1 ORDER BY u.nombre ASC"
            );
        } else {
            $usuarios = $db->fetchAll(
                "SELECT u.id, u.nombre, u.franquiciado_id FROM usuarios u
                 WHERE u.activo = 1 AND u.franquiciado_id = :fid ORDER BY u.nombre ASC",
                array('fid' => (int)SessionHelper::getFranquiciadoId())
            );
        }

        $this->render('whatsapp/instancias', array(
            'title'             => 'Instancias de WhatsApp',
            'instancias'        => $instancias,
            'franquiciados'     => $franquiciados,
            'usuarios'          => $usuarios,
            'defaultApiUrl'     => EVOLUTION_API_URL,
            'aprovisionamiento' => $aprovisionamiento
        ));
    }

    /** POST: crear o editar instancia (línea de WhatsApp de un vendedor). */
    public function instanciaGuardar() {
        $id        = (int)$this->input('id', 0);
        $nombre    = trim($this->input('nombre_instancia', ''));
        $apiUrl    = trim($this->input('api_url', ''));
        $apiToken  = trim($this->input('api_token', ''));
        $telefono  = trim($this->input('telefono', ''));
        $franqId   = (int)$this->input('franquiciado_id', 0);
        $usuarioId = (int)$this->input('usuario_id', 0);

        if ($nombre === '' || $apiUrl === '' || $apiToken === '') {
            $this->json(array('success' => false, 'message' => 'Nombre de instancia, URL y token son obligatorios.'));
        }

        // Franquiciado: solo puede administrar instancias propias
        if (!SessionHelper::isAdmin()) {
            $franqId = (int)SessionHelper::getFranquiciadoId();
            if ($franqId <= 0) {
                $this->json(array('success' => false, 'message' => 'Su cuenta no tiene franquicia asociada.'), 403);
            }
        }

        $db = Database::getInstance();

        // La instancia es POR USUARIO (regla 22-07): dueño OBLIGATORIO, la
        // franquicia se deriva siempre del dueño, y la relación es 1:1 (un
        // usuario = una instancia; lo respalda el índice único en la BD).
        if ($usuarioId <= 0) {
            $this->json(array('success' => false, 'message' => 'Cada instancia debe tener un usuario dueño: es SU línea de WhatsApp.'));
        }
        $usr = $db->fetchOne("SELECT id, nombre, franquiciado_id FROM usuarios WHERE id = :id AND activo = 1", array('id' => $usuarioId));
        if (!$usr) {
            $this->json(array('success' => false, 'message' => 'El usuario seleccionado no existe o está inactivo.'));
        }
        if (!SessionHelper::isAdmin() && (int)$usr['franquiciado_id'] !== (int)SessionHelper::getFranquiciadoId()) {
            $this->json(array('success' => false, 'message' => 'El usuario no pertenece a tu franquicia.'), 403);
        }
        $franqId = !empty($usr['franquiciado_id']) ? (int)$usr['franquiciado_id'] : 0;

        $otra = $db->fetchOne(
            "SELECT id, nombre_instancia FROM whatsapp_instancias WHERE usuario_id = :uid AND id != :id",
            array('uid' => $usuarioId, 'id' => $id)
        );
        if ($otra) {
            $this->json(array('success' => false, 'message' => htmlspecialchars($usr['nombre']) . ' ya tiene la instancia "' . htmlspecialchars($otra['nombre_instancia']) . '". Un usuario solo puede tener UNA instancia.'));
        }

        $data = array(
            'franquiciado_id'  => $franqId > 0 ? $franqId : null,
            'usuario_id'       => $usuarioId > 0 ? $usuarioId : null,
            'nombre_instancia' => $nombre,
            'api_url'          => $apiUrl,
            'api_token'        => $apiToken,
            'telefono'         => $telefono !== '' ? normalizar_telefono($telefono) : null
        );

        if ($id > 0) {
            $inst = $db->fetchOne("SELECT * FROM whatsapp_instancias WHERE id = :id", array('id' => $id));
            if (!$inst) $this->json(array('success' => false, 'message' => 'Instancia no encontrada.'));
            if (!SessionHelper::isAdmin() && $inst['franquiciado_id'] != SessionHelper::getFranquiciadoId()) {
                $this->json(array('success' => false, 'message' => 'Sin permisos sobre esta instancia.'), 403);
            }
            $db->update('whatsapp_instancias', $data, "id = :id", array('id' => $id));
            AuditHelper::log('edit_whatsapp_instancia', 'whatsapp_instancias', $id, null, array('nombre' => $nombre));
            $this->json(array('success' => true, 'message' => 'Instancia actualizada.'));
        }

        // Token único para el webhook de esta línea
        $data['webhook_token'] = WhatsAppHelper::generarWebhookToken();

        $newId = $db->insert('whatsapp_instancias', $data);
        AuditHelper::log('create_whatsapp_instancia', 'whatsapp_instancias', $newId, null, array('nombre' => $nombre));
        $this->json(array('success' => true, 'message' => 'Instancia registrada. Configura su webhook único en Evolution API.'));
    }

    /** POST: activar/desactivar instancia. */
    public function instanciaToggle() {
        $id = (int)$this->input('id', 0);
        $db = Database::getInstance();

        $inst = $db->fetchOne("SELECT * FROM whatsapp_instancias WHERE id = :id", array('id' => $id));
        if (!$inst) $this->json(array('success' => false, 'message' => 'Instancia no encontrada.'));
        if (!SessionHelper::isAdmin() && $inst['franquiciado_id'] != SessionHelper::getFranquiciadoId()) {
            $this->json(array('success' => false, 'message' => 'Sin permisos sobre esta instancia.'), 403);
        }

        $nueva = $inst['activa'] ? 0 : 1;
        $db->update('whatsapp_instancias', array('activa' => $nueva), "id = :id", array('id' => $id));
        AuditHelper::log('toggle_whatsapp_instancia', 'whatsapp_instancias', $id, array('activa' => $inst['activa']), array('activa' => $nueva));
        $this->json(array('success' => true, 'message' => $nueva ? 'Instancia activada.' : 'Instancia desactivada.'));
    }

    /**
     * POST: crea la línea PERSONAL de un usuario DIRECTO en el servidor
     * Evolution global (instancia + webhook) y deja todo listo para escanear
     * el QR. El usuario es obligatorio y la relación es 1:1.
     */
    public function instanciaCrearAuto() {
        $usuarioId = (int)$this->input('usuario_id', 0);
        if ($usuarioId <= 0) {
            $this->json(array('success' => false, 'message' => 'Elige el usuario dueño de la línea.'));
        }
        if (!WhatsAppHelper::aprovisionamientoActivo()) {
            $this->json(array('success' => false, 'message' => 'Falta configurar el servidor Evolution global (EVOLUTION_URL_GLOBAL y EVOLUTION_APIKEY_GLOBAL en Configuración).'));
        }

        $db = Database::getInstance();
        $usr = $db->fetchOne("SELECT id, nombre, franquiciado_id FROM usuarios WHERE id = :id AND activo = 1", array('id' => $usuarioId));
        if (!$usr) {
            $this->json(array('success' => false, 'message' => 'El usuario seleccionado no existe o está inactivo.'));
        }
        if (!SessionHelper::isAdmin() && (int)$usr['franquiciado_id'] !== (int)SessionHelper::getFranquiciadoId()) {
            $this->json(array('success' => false, 'message' => 'El usuario no pertenece a tu franquicia.'), 403);
        }
        $otra = $db->fetchOne("SELECT nombre_instancia FROM whatsapp_instancias WHERE usuario_id = :uid", array('uid' => $usuarioId));
        if ($otra) {
            $this->json(array('success' => false, 'message' => htmlspecialchars($usr['nombre']) . ' ya tiene la instancia "' . htmlspecialchars($otra['nombre_instancia']) . '". Un usuario solo puede tener UNA línea.'));
        }

        $webhookToken = WhatsAppHelper::generarWebhookToken();
        $webhookUrl = APP_URL . '/api/whatsapp/webhook?t=' . $webhookToken;
        $nombreInstancia = 'crm_u' . $usuarioId . '_' . substr(md5(uniqid(mt_rand(), true)), 0, 6);

        $res = WhatsAppHelper::crearInstanciaEvolution($nombreInstancia, $webhookUrl);
        if (!$res['success']) {
            $this->json(array('success' => false, 'message' => $res['detalle']));
        }

        $newId = $db->insert('whatsapp_instancias', array(
            'franquiciado_id'  => !empty($usr['franquiciado_id']) ? (int)$usr['franquiciado_id'] : null,
            'usuario_id'       => $usuarioId,
            'nombre_instancia' => $nombreInstancia,
            'api_url'          => $res['api_url'],
            'api_token'        => $res['api_token'],
            'webhook_token'    => $webhookToken,
            'estado'           => 'desconectada',
            'activa'           => 1
        ));
        AuditHelper::log('crear_instancia_evolution', 'whatsapp_instancias', $newId, null,
            array('nombre' => $nombreInstancia, 'usuario' => $usr['nombre']));

        $this->json(array('success' => true, 'id' => (int)$newId, 'message' => 'Instancia creada. Escanea el QR para vincular el teléfono.'));
    }

    /**
     * AJAX: QR/estado para el modal de vinculación. Si la instancia ya quedó
     * conectada, captura el número del teléfono y lo guarda.
     */
    public function instanciaQr() {
        $id = (int)$this->input('id', 0);
        $db = Database::getInstance();
        $inst = $db->fetchOne("SELECT * FROM whatsapp_instancias WHERE id = :id", array('id' => $id));
        if (!$inst) $this->json(array('success' => false, 'message' => 'Instancia no encontrada.'));
        if (!SessionHelper::isAdmin() && $inst['franquiciado_id'] != SessionHelper::getFranquiciadoId()) {
            $this->json(array('success' => false, 'message' => 'Sin permisos.'), 403);
        }

        $estado = WhatsAppHelper::getEstadoInstancia($inst);
        if ($estado['status'] === 'open') {
            $numero = WhatsAppHelper::numeroConectado($inst);
            $upd = array('estado' => 'conectada');
            if ($numero) $upd['telefono'] = $numero;
            $db->update('whatsapp_instancias', $upd, "id = :id", array('id' => $id));
            $this->json(array('success' => true, 'estado' => 'open', 'telefono' => $numero));
        }

        $qr = WhatsAppHelper::qrInstancia($inst);
        $this->json(array(
            'success' => $qr['success'],
            'estado'  => $estado['status'],
            'qr'      => isset($qr['base64']) ? $qr['base64'] : null,
            'pairing' => isset($qr['pairing_code']) ? $qr['pairing_code'] : null,
            'message' => isset($qr['detalle']) ? $qr['detalle'] : null
        ));
    }

    /** POST: cerrar la sesión del teléfono (después se re-vincula con QR). */
    public function instanciaLogout() {
        $id = (int)$this->input('id', 0);
        $db = Database::getInstance();
        $inst = $db->fetchOne("SELECT * FROM whatsapp_instancias WHERE id = :id", array('id' => $id));
        if (!$inst) $this->json(array('success' => false, 'message' => 'Instancia no encontrada.'));
        if (!SessionHelper::isAdmin() && $inst['franquiciado_id'] != SessionHelper::getFranquiciadoId()) {
            $this->json(array('success' => false, 'message' => 'Sin permisos.'), 403);
        }
        $ok = WhatsAppHelper::logoutInstancia($inst);
        $db->update('whatsapp_instancias', array('estado' => 'desconectada'), "id = :id", array('id' => $id));
        AuditHelper::log('logout_instancia_whatsapp', 'whatsapp_instancias', $id, null, array('ok' => $ok));
        $this->json(array(
            'success' => true,
            'message' => $ok ? 'Sesión cerrada. Usa "Vincular (QR)" para conectar el teléfono de nuevo.'
                             : 'No se pudo confirmar el cierre en Evolution, la instancia quedó marcada desconectada.'
        ));
    }

    /**
     * POST: eliminar la línea. Se borra del servidor Evolution (best effort) y
     * en el CRM la fila queda inactiva y SIN dueño (conserva el historial de
     * mensajes y libera al usuario para una nueva línea).
     */
    public function instanciaEliminar() {
        $id = (int)$this->input('id', 0);
        $db = Database::getInstance();
        $inst = $db->fetchOne("SELECT * FROM whatsapp_instancias WHERE id = :id", array('id' => $id));
        if (!$inst) $this->json(array('success' => false, 'message' => 'Instancia no encontrada.'));
        if (!SessionHelper::isAdmin() && $inst['franquiciado_id'] != SessionHelper::getFranquiciadoId()) {
            $this->json(array('success' => false, 'message' => 'Sin permisos.'), 403);
        }
        $okRemoto = WhatsAppHelper::eliminarInstanciaEvolution($inst);
        $db->update('whatsapp_instancias',
            array('activa' => 0, 'estado' => 'desconectada', 'usuario_id' => null),
            "id = :id", array('id' => $id));
        AuditHelper::log('eliminar_instancia_whatsapp', 'whatsapp_instancias', $id,
            array('nombre' => $inst['nombre_instancia'], 'usuario_id' => $inst['usuario_id']),
            array('eliminada_en_evolution' => $okRemoto));
        $this->json(array(
            'success' => true,
            'message' => 'Línea eliminada' . ($okRemoto ? ' (también en el servidor Evolution).' : '. OJO: no se pudo confirmar el borrado en Evolution; revísalo allá.')
        ));
    }

    /** AJAX: estado de conexión de una instancia contra Evolution API. */
    public function instanciaEstado() {
        $id = (int)$this->input('id', 0);
        $db = Database::getInstance();

        $inst = $db->fetchOne("SELECT * FROM whatsapp_instancias WHERE id = :id", array('id' => $id));
        if (!$inst) $this->json(array('success' => false, 'message' => 'Instancia no encontrada.'));
        if (!SessionHelper::isAdmin() && $inst['franquiciado_id'] != SessionHelper::getFranquiciadoId()) {
            $this->json(array('success' => false, 'message' => 'Sin permisos.'), 403);
        }

        $estado = WhatsAppHelper::getEstadoInstancia($inst);
        $estadoDb = ($estado['status'] === 'open') ? 'conectada' : (($estado['status'] === 'desconocido') ? 'error' : 'desconectada');
        $db->update('whatsapp_instancias', array('estado' => $estadoDb), "id = :id", array('id' => $id));

        $this->json(array('success' => true, 'estado' => $estado['status'], 'estado_db' => $estadoDb));
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Privados
    // ─────────────────────────────────────────────────────────────────────────

    private function getLeadConTenant($leadId) {
        $db = Database::getInstance();
        $tenantFilter = $this->getTenantFilter('l');
        return $db->fetchOne(
            "SELECT l.* FROM leads l WHERE l.id = :id AND l.activo = 1 AND {$tenantFilter}",
            array('id' => $leadId)
        );
    }

    /**
     * Matching de lead por últimos 8 dígitos del teléfono.
     * Prioridad: lead ya vinculado a la instancia > lead del VENDEDOR dueño
     * de la línea > lead del franquiciado dueño del servidor > lead activo
     * más reciente.
     */
    private function buscarLeadPorTelefono($telefono, $instancia) {
        $db = Database::getInstance();
        $sufijo = '%' . WhatsAppHelper::ultimos8($telefono);

        $lead = $db->fetchOne(
            "SELECT * FROM leads WHERE telefono_real LIKE :tel AND wa_instance_id = :iid AND activo = 1
             ORDER BY updated_at DESC LIMIT 1",
            array('tel' => $sufijo, 'iid' => $instancia['id'])
        );
        if ($lead) return $lead;

        if (!empty($instancia['usuario_id'])) {
            $lead = $db->fetchOne(
                "SELECT * FROM leads WHERE telefono_real LIKE :tel AND usuario_asignado_id = :uid AND activo = 1
                 ORDER BY updated_at DESC LIMIT 1",
                array('tel' => $sufijo, 'uid' => $instancia['usuario_id'])
            );
            if ($lead) return $lead;
        }

        if (!empty($instancia['franquiciado_id'])) {
            $lead = $db->fetchOne(
                "SELECT * FROM leads WHERE telefono_real LIKE :tel AND franquiciado_id = :fid AND activo = 1
                 ORDER BY updated_at DESC LIMIT 1",
                array('tel' => $sufijo, 'fid' => $instancia['franquiciado_id'])
            );
            if ($lead) return $lead;
        }

        return $db->fetchOne(
            "SELECT * FROM leads WHERE telefono_real LIKE :tel AND activo = 1
             ORDER BY updated_at DESC LIMIT 1",
            array('tel' => $sufijo)
        );
    }

    private function logWebhook($evento, $raw) {
        $linea = '[' . date('Y-m-d H:i:s') . "] {$evento} " . substr($raw, 0, 2000) . PHP_EOL;
        @file_put_contents(LOG_DIR . '/whatsapp_webhook.log', $linea, FILE_APPEND);
    }
}
