<?php
/**
 * api/notificaciones.php
 * Notificaciones dinámicas por ROL + reales de la tabla `notificaciones`
 * Roles: 1=administrador | 2=coordinador | 3=tutor | 4=secretaria
 *        5=estudiante | 6=gestor_ppp | 7=tutor_empresa | 9=facturacion
 */

// ── Capturar cualquier output previo (warnings PHP) ──────────
ob_start();

require_once __DIR__ . '/../config/app.php';
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/functions.php';

ob_clean();
header('Content-Type: application/json; charset=utf-8');

function apiError(string $msg): void {
    echo json_encode(['ok' => false, 'total' => 0, 'rows' => [], 'error' => $msg]);
    exit;
}

// ── CRÍTICO: iniciar sesión antes de leer $_SESSION ──────────
// currentUser() solo hace: return $_SESSION['user'] ?? [];
// Sin startSession() la sesión no está activa → devuelve [] → apiError
startSession();

// ── Autenticación ─────────────────────────────────────────────
try {
    $user = currentUser();
} catch (Throwable $e) { apiError('auth_fail'); }
if (empty($user)) apiError('no_session');

try {
    $db        = getDB();
    $idUser    = (int)$user['id'];
    $idRol     = (int)($user['id_rol'] ?? 0);
    // Instituto: leer de usuarios directamente (seguro en contexto API)
    $idInst    = (int)($user['id_instituto'] ?? 0);
} catch (Throwable $e) { apiError('db_fail'); }

// ── Helper: tiempo relativo ───────────────────────────────────
function hace(string $fecha): string {
    $diff = max(0, time() - strtotime($fecha));
    if ($diff < 60)     return 'hace un momento';
    if ($diff < 3600)   return 'hace ' . floor($diff/60) . ' min';
    if ($diff < 86400)  return 'hace ' . floor($diff/3600) . ' h';
    if ($diff < 604800) return 'hace ' . floor($diff/86400) . ' días';
    return date('d/m/Y', strtotime($fecha));
}

// ── Helper: query COUNT seguro ────────────────────────────────
function qCount(PDO $db, string $sql): int {
    try { return (int)$db->query($sql)->fetchColumn(); }
    catch (Throwable $e) { return 0; }
}

// ── Acumulador de notificaciones ─────────────────────────────
$rows = [];
$vId  = -1; // IDs negativos = virtuales

function addV(array &$rows, int &$vid,
              string $asunto, string $mensaje,
              string $url = '', string $icono = '🔔'): void {
    $rows[] = [
        'id_notif' => $vid--,
        'asunto'   => $asunto,
        'mensaje'  => $mensaje,
        'leida'    => 0,
        'hace'     => 'Ahora',
        'url'      => $url ?: null,
        'icono'    => $icono,
        'virtual'  => true,
    ];
}

// ════════════════════════════════════════════════════════════════
//  1. NOTIFICACIONES REALES de la tabla `notificaciones`
// ════════════════════════════════════════════════════════════════
try {
    $fInstN = $idInst
        ? 'AND (n.id_instituto = :iid OR n.id_instituto IS NULL)'
        : '';
    $st = $db->prepare("
        SELECT n.id_notif, n.asunto, n.mensaje, n.leida, n.created_at
        FROM   notificaciones n
        WHERE  n.id_usuario = :uid $fInstN
        ORDER  BY n.leida ASC, n.created_at DESC
        LIMIT  15
    ");
    $p = [':uid' => $idUser];
    if ($idInst) $p[':iid'] = $idInst;
    $st->execute($p);
    foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $r) {
        $rows[] = [
            'id_notif' => (int)$r['id_notif'],
            'asunto'   => $r['asunto'],
            'mensaje'  => $r['mensaje'],
            'leida'    => (int)$r['leida'],
            'hace'     => hace($r['created_at']),
            'url'      => null,
            'icono'    => '🔔',
            'virtual'  => false,
        ];
    }
} catch (Throwable $e) {}


// ════════════════════════════════════════════════════════════════
//  2. NOTIFICACIONES VIRTUALES POR ROL
// ════════════════════════════════════════════════════════════════

// ────────────────────────────────────────────────────────────────
//  ROL 1 — ADMINISTRADOR
// ────────────────────────────────────────────────────────────────
if ($idRol === 1) {

    $cnt = qCount($db, "SELECT COUNT(*) FROM licencias
        WHERE activa=1 AND DATEDIFF(fecha_fin, CURDATE()) BETWEEN 0 AND 30");
    if ($cnt) addV($rows, $vId,
        "⚠️ $cnt licencia" . ($cnt>1?'s':'') . " próxima" . ($cnt>1?'s':'') . " a vencer",
        "Hay $cnt licencia(s) que vencen en ≤ 30 días. Renuévalas para evitar interrupciones.",
        '../../modules/admin/licencias.php', '⚠️');

    $cnt = qCount($db, "SELECT COUNT(*) FROM usuarios WHERE activo=0");
    if ($cnt) addV($rows, $vId,
        "👤 $cnt usuario" . ($cnt>1?'s':'') . " inactivo" . ($cnt>1?'s':''),
        "$cnt cuenta(s) están desactivadas. Verifica si requieren activación o eliminación.",
        '../../modules/admin/usuarios.php', '👤');

    $cnt = qCount($db, "SELECT COUNT(*) FROM institutos i
        WHERE NOT EXISTS (SELECT 1 FROM carreras c WHERE c.id_instituto=i.id_instituto)");
    if ($cnt) addV($rows, $vId,
        "🏫 $cnt instituto" . ($cnt>1?'s':'') . " sin carreras",
        "Instituto(s) registrados sin carreras asignadas. Configura la estructura académica.",
        '../../modules/admin/carreras.php', '🏫');

    $cnt = qCount($db, "SELECT COUNT(*) FROM convenios
        WHERE estado='activo' AND fecha_fin < CURDATE()");
    if ($cnt) addV($rows, $vId,
        "📋 $cnt convenio" . ($cnt>1?'s':'') . " vencido" . ($cnt>1?'s':''),
        "$cnt convenio(s) marcados como activos ya tienen fecha de vencimiento pasada.",
        '../../modules/admin/convenios.php', '📋');
}


// ────────────────────────────────────────────────────────────────
//  ROL 2 — COORDINADOR
// ────────────────────────────────────────────────────────────────
if ($idRol === 2) {
    $fi  = $idInst ? "AND car.id_instituto = $idInst" : '';
    $fiv = $idInst ? "AND id_instituto = $idInst" : '';

    $cnt = qCount($db, "SELECT COUNT(*) FROM practicas p
        JOIN estudiantes est ON est.id_estudiante=p.id_estudiante
        JOIN carreras car ON car.id_carrera=est.id_carrera
        WHERE p.estado='postulado' $fi");
    if ($cnt) addV($rows, $vId,
        "📥 $cnt postulación" . ($cnt>1?'es':'') . " PPP pendiente" . ($cnt>1?'s':''),
        $cnt==1 ? 'Un estudiante espera aprobación para iniciar sus prácticas profesionales.'
                : "$cnt estudiantes esperan aprobación de sus prácticas profesionales.",
        'postulaciones.php?estado=postulado', '📥');

    $cnt = qCount($db, "SELECT COUNT(*) FROM practicas p
        JOIN estudiantes est ON est.id_estudiante=p.id_estudiante
        JOIN carreras car ON car.id_carrera=est.id_carrera
        WHERE p.estado='informe_enviado' $fi");
    if ($cnt) addV($rows, $vId,
        "📄 $cnt informe" . ($cnt>1?'s':'') . " final por revisar",
        "$cnt estudiante(s) enviaron su informe final de prácticas. Pendiente de aprobación.",
        'practicas.php?estado=informe_enviado', '📄');

    $cnt = qCount($db, "SELECT COUNT(*) FROM documentos_ppp_gestion dpg
        JOIN practicas p ON p.id_practica=dpg.id_practica
        JOIN estudiantes est ON est.id_estudiante=p.id_estudiante
        JOIN carreras car ON car.id_carrera=est.id_carrera
        WHERE dpg.estado='enviado' $fi");
    if ($cnt) addV($rows, $vId,
        "📎 $cnt documento" . ($cnt>1?'s':'') . " PPP recibido" . ($cnt>1?'s':''),
        "$cnt documento(s) PPP de estudiantes están pendientes de tu validación.",
        'expedientes.php', '📎');

    $cnt = qCount($db, "SELECT COUNT(*) FROM proyectos_vinculacion
        WHERE estado='planificacion' $fiv");
    if ($cnt) addV($rows, $vId,
        "🤝 $cnt proyecto" . ($cnt>1?'s':'') . " de vinculación en planificación",
        "$cnt proyecto(s) de vinculación requieren revisión y aprobación.",
        'vinculacion.php?estado=planificacion', '🤝');

    $cnt = qCount($db, "SELECT COUNT(*) FROM convalidaciones cv
        JOIN estudiantes est ON est.id_estudiante=cv.id_estudiante
        JOIN carreras car ON car.id_carrera=est.id_carrera
        WHERE cv.estado='pendiente' $fi");
    if ($cnt) addV($rows, $vId,
        "⇄ $cnt convalidación" . ($cnt>1?'es':'') . " pendiente" . ($cnt>1?'s':''),
        "$cnt solicitud(es) de convalidación esperan tu revisión y resolución.",
        'convalidaciones.php?estado=pendiente', '⇄');

    $cnt = qCount($db, "SELECT COUNT(*) FROM titulaciones WHERE estado='enviada' $fiv");
    if ($cnt) addV($rows, $vId,
        "🎓 $cnt titulación" . ($cnt>1?'es':'') . " por revisar",
        "$cnt proceso(s) de titulación enviados esperan revisión del coordinador.",
        'titulacion.php?estado=enviada', '🎓');
}


// ────────────────────────────────────────────────────────────────
//  ROL 3 — TUTOR ACADÉMICO
// ────────────────────────────────────────────────────────────────
if ($idRol === 3) {
    $idTutor = 0;
    try {
        $st = $db->prepare("SELECT id_tutor FROM tutores WHERE id_usuario=:u LIMIT 1");
        $st->execute([':u' => $idUser]);
        $idTutor = (int)($st->fetchColumn() ?: 0);
    } catch (Throwable $e) {}

    if ($idTutor) {
        $cnt = qCount($db, "SELECT COUNT(*) FROM practicas
            WHERE id_tutor=$idTutor AND estado IN ('aprobado','en_progreso')");
        if ($cnt) addV($rows, $vId,
            "👨‍🎓 $cnt estudiante" . ($cnt>1?'s':'') . " con práctica activa",
            "Tienes $cnt estudiante(s) asignados en prácticas que requieren seguimiento académico.",
            '../tutor/practicas.php', '👨‍🎓');

        $cnt = qCount($db, "SELECT COUNT(*) FROM bitacoras b
            JOIN practicas p ON p.id_practica=b.id_practica
            WHERE p.id_tutor=$idTutor AND b.estado_tutor='pendiente'");
        if ($cnt) addV($rows, $vId,
            "📓 $cnt bitácora" . ($cnt>1?'s':'') . " por revisar",
            "$cnt bitácora(s) semanales de tus estudiantes están pendientes de revisión.",
            '../tutor/bitacoras.php', '📓');

        $cnt = qCount($db, "SELECT COUNT(*) FROM practicas p
            WHERE p.id_tutor=$idTutor AND p.estado='informe_enviado'
            AND NOT EXISTS (SELECT 1 FROM evaluaciones_satisfaccion ev
                WHERE ev.id_practica=p.id_practica AND ev.tipo_evaluador='tutor_academico')");
        if ($cnt) addV($rows, $vId,
            "⭐ $cnt evaluación" . ($cnt>1?'es':'') . " pendiente" . ($cnt>1?'s':''),
            "$cnt estudiante(s) esperan que completes su evaluación de desempeño en prácticas.",
            '../tutor/evaluaciones.php', '⭐');

        $cnt = qCount($db, "SELECT COUNT(*) FROM titulaciones
            WHERE id_tutor=$idTutor AND estado_trabajo='entregado'");
        if ($cnt) addV($rows, $vId,
            "🎓 $cnt trabajo" . ($cnt>1?'s':'') . " de titulación por revisar",
            "$cnt estudiante(s) entregaron su trabajo de investigación. Revisa y califica.",
            '../tutor/titulacion.php', '🎓');
    }
}


// ────────────────────────────────────────────────────────────────
//  ROL 4 — SECRETARIA
// ────────────────────────────────────────────────────────────────
if ($idRol === 4) {
    $fi  = $idInst ? "AND id_instituto = $idInst" : '';
    $fic = $idInst ? "AND car.id_instituto = $idInst" : '';

    $cnt = qCount($db, "SELECT COUNT(*) FROM practicas p
        WHERE p.estado='completado' $fi
        AND NOT EXISTS (SELECT 1 FROM certificados c WHERE c.id_practica=p.id_practica)");
    if ($cnt) addV($rows, $vId,
        "📜 $cnt certificado" . ($cnt>1?'s':'') . " pendiente" . ($cnt>1?'s':'') . " de generar",
        "$cnt práctica(s) completadas sin certificado generado. Emite los documentos.",
        '../secretaria/certificados.php', '📜');

    $cnt = qCount($db, "SELECT COUNT(*) FROM certificados c
        JOIN practicas p ON p.id_practica=c.id_practica
        WHERE c.firmado=0 $fi");
    if ($cnt) addV($rows, $vId,
        "✍️ $cnt certificado" . ($cnt>1?'s':'') . " sin firmar",
        "$cnt certificado(s) están generados pero pendientes de firma oficial.",
        '../secretaria/certificados.php?filtro=sin_firmar', '✍️');

    $cnt = qCount($db, "SELECT COUNT(*) FROM titulaciones WHERE estado='aprobado' $fi");
    if ($cnt) addV($rows, $vId,
        "🎓 $cnt titulación" . ($cnt>1?'es':'') . " aprobada" . ($cnt>1?'s':'') . " — pendiente documental",
        "$cnt proceso(s) de titulación aprobados requieren gestión de documentos oficiales.",
        '../secretaria/titulacion.php', '🎓');

    $cnt = qCount($db, "SELECT COUNT(*) FROM convalidaciones cv
        JOIN estudiantes est ON est.id_estudiante=cv.id_estudiante
        JOIN carreras car ON car.id_carrera=est.id_carrera
        WHERE cv.estado='aprobada' $fic");
    if ($cnt) addV($rows, $vId,
        "⇄ $cnt resolución" . ($cnt>1?'es':'') . " de convalidación por emitir",
        "$cnt convalidación(es) aprobadas esperan la emisión de resolución formal.",
        '../secretaria/convalidaciones.php', '⇄');
}


// ────────────────────────────────────────────────────────────────
//  ROL 5 — ESTUDIANTE
// ────────────────────────────────────────────────────────────────
if ($idRol === 5) {
    $idEst = 0;
    try {
        $st = $db->prepare("SELECT id_estudiante FROM estudiantes WHERE id_usuario=:u LIMIT 1");
        $st->execute([':u' => $idUser]);
        $idEst = (int)($st->fetchColumn() ?: 0);
    } catch (Throwable $e) {}

    if ($idEst) {
        // Estado de su práctica más reciente
        try {
            $st = $db->prepare("SELECT estado, horas_cumplidas, horas_requeridas
                FROM practicas WHERE id_estudiante=:e ORDER BY created_at DESC LIMIT 1");
            $st->execute([':e' => $idEst]);
            $pr = $st->fetch(PDO::FETCH_ASSOC);
            if ($pr) {
                $pct = $pr['horas_requeridas'] > 0
                    ? round($pr['horas_cumplidas'] / $pr['horas_requeridas'] * 100) : 0;
                $map = [
                    'postulado'       => ['📤 Tu postulación PPP está en revisión',
                        'El coordinador revisará tu solicitud próximamente.',
                        '../estudiante/practicas.php'],
                    'aprobado'        => ['✅ ¡Práctica aprobada! Avance: '.$pct.'%',
                        "Tu práctica fue aprobada. Lleva ".$pr['horas_cumplidas']."/".$pr['horas_requeridas']." h. Sube tus bitácoras.",
                        '../estudiante/practicas.php'],
                    'en_progreso'     => ['⚡ Práctica en progreso — '.$pct.'%',
                        "Llevas ".$pr['horas_cumplidas']."/".$pr['horas_requeridas']." h completadas. ¡Sigue así!",
                        '../estudiante/practicas.php'],
                    'informe_enviado' => ['📄 Informe enviado — esperando revisión',
                        'Tu informe fue enviado al coordinador. Te notificaremos cuando esté revisado.',
                        '../estudiante/practicas.php'],
                    'rechazado'       => ['❌ Postulación rechazada — revisa y vuelve a postular',
                        'Tu postulación fue rechazada. Lee las observaciones y presenta una nueva.',
                        '../estudiante/postulaciones.php'],
                    'completado'      => ['🏆 ¡Prácticas completadas exitosamente!',
                        "Completaste tus prácticas profesionales con $pct% de avance. ¡Felicidades!",
                        '../estudiante/practicas.php'],
                ];
                if (isset($map[$pr['estado']])) {
                    [$a, $m, $u] = $map[$pr['estado']];
                    // mb_substr: corta por CARACTERES no bytes (emojis son 3-4 bytes)
                    $icono = mb_substr($a, 0, 1, 'UTF-8');
                    addV($rows, $vId, $a, $m, $u, $icono);
                }
            }
        } catch (Throwable $e) {}

        // Documentos observados (necesitan corrección)
        $cnt = qCount($db, "SELECT COUNT(*) FROM documentos_ppp_gestion dpg
            JOIN practicas p ON p.id_practica=dpg.id_practica
            WHERE p.id_estudiante=$idEst AND dpg.estado='observado'");
        if ($cnt) addV($rows, $vId,
            "⚠️ $cnt documento" . ($cnt>1?'s':'') . " devuelto" . ($cnt>1?'s':'') . " con observación",
            "El coordinador devolvió $cnt documento(s) con correcciones. Revisa y vuelve a subirlos.",
            '../estudiante/expediente.php', '⚠️');

        // Convalidación resuelta pendiente de lectura
        $cnt = qCount($db, "SELECT COUNT(*) FROM convalidaciones
            WHERE id_estudiante=$idEst AND estado IN ('aprobada','rechazada')
            AND leida_estudiante=0");
        if ($cnt) addV($rows, $vId,
            "📋 Tienes " . ($cnt>1?"$cnt resoluciones":"una resolución") . " de convalidación",
            "El coordinador emitió una resolución sobre tu solicitud de convalidación. Revísala.",
            '../estudiante/convalidaciones.php', '📋');

        // Titulación con comprobante pendiente
        $cnt = qCount($db, "SELECT COUNT(*) FROM titulaciones
            WHERE id_estudiante=$idEst AND estado='pendiente' AND comprobante_pago IS NULL");
        if ($cnt) addV($rows, $vId,
            "🎓 Completa tu proceso de titulación",
            "Debes subir el comprobante de pago para continuar con tu proceso de titulación.",
            '../estudiante/titulacion.php', '🎓');
    }
}


// ────────────────────────────────────────────────────────────────
//  ROL 6 — GESTOR PPP
// ────────────────────────────────────────────────────────────────
if ($idRol === 6) {
    $fi = $idInst ? "AND car.id_instituto = $idInst" : '';

    $cnt = qCount($db, "SELECT COUNT(*) FROM informes_finales inf
        JOIN practicas p ON p.id_practica=inf.id_practica
        JOIN estudiantes est ON est.id_estudiante=p.id_estudiante
        JOIN carreras car ON car.id_carrera=est.id_carrera
        WHERE inf.estado_coordinador='pendiente' $fi");
    if ($cnt) addV($rows, $vId,
        "📄 $cnt informe" . ($cnt>1?'s':'') . " final por calificar",
        "$cnt informe(s) final(es) de prácticas esperan calificación y aprobación.",
        '../gestor/informes.php', '📄');

    $cnt = qCount($db, "SELECT COUNT(*) FROM documentos_ppp_gestion dpg
        JOIN practicas p ON p.id_practica=dpg.id_practica
        JOIN estudiantes est ON est.id_estudiante=p.id_estudiante
        JOIN carreras car ON car.id_carrera=est.id_carrera
        WHERE dpg.estado='enviado' $fi");
    if ($cnt) addV($rows, $vId,
        "📎 $cnt documento" . ($cnt>1?'s':'') . " PPP por validar",
        "$cnt documento(s) subidos por estudiantes están pendientes de validación.",
        '../gestor/documentos.php', '📎');

    $cnt = qCount($db, "SELECT COUNT(*) FROM bitacoras b
        JOIN practicas p ON p.id_practica=b.id_practica
        JOIN estudiantes est ON est.id_estudiante=p.id_estudiante
        JOIN carreras car ON car.id_carrera=est.id_carrera
        WHERE b.estado_tutor='pendiente' $fi");
    if ($cnt) addV($rows, $vId,
        "📓 $cnt bitácora" . ($cnt>1?'s':'') . " sin revisar",
        "$cnt bitácora(s) semanales de estudiantes esperan revisión y comentarios.",
        '../gestor/bitacoras.php', '📓');

    $cnt = qCount($db, "SELECT COUNT(*) FROM practicas p
        JOIN estudiantes est ON est.id_estudiante=p.id_estudiante
        JOIN carreras car ON car.id_carrera=est.id_carrera
        WHERE p.estado='completado'
        AND NOT EXISTS (SELECT 1 FROM informes_finales inf
            WHERE inf.id_practica=p.id_practica AND inf.estado_coordinador='aprobado')
        $fi");
    if ($cnt) addV($rows, $vId,
        "🏅 $cnt práctica" . ($cnt>1?'s':'') . " sin nota final",
        "$cnt práctica(s) completadas no tienen informe final aprobado.",
        '../gestor/practicas.php?estado=completado', '🏅');
}


// ────────────────────────────────────────────────────────────────
//  ROL 7 — TUTOR EMPRESA
// ────────────────────────────────────────────────────────────────
if ($idRol === 7) {
    $idTE = 0;
    try {
        $st = $db->prepare("SELECT id FROM tutores_empresa WHERE id_usuario=:u LIMIT 1");
        $st->execute([':u' => $idUser]);
        $idTE = (int)($st->fetchColumn() ?: 0);
    } catch (Throwable $e) {}

    if ($idTE) {
        $cnt = qCount($db, "SELECT COUNT(*) FROM practicas
            WHERE id_tutor_empresa=$idTE AND estado IN ('aprobado','en_progreso')");
        if ($cnt) addV($rows, $vId,
            "👷 $cnt estudiante" . ($cnt>1?'s':'') . " a tu cargo",
            "Tienes $cnt estudiante(s) asignados para supervisión en prácticas en tu empresa.",
            '../empresa/estudiantes.php', '👷');

        $cnt = qCount($db, "SELECT COUNT(*) FROM practicas p
            WHERE p.id_tutor_empresa=$idTE AND p.estado='informe_enviado'
            AND NOT EXISTS (SELECT 1 FROM evaluaciones_satisfaccion ev
                WHERE ev.id_practica=p.id_practica AND ev.tipo_evaluador='tutor_empresa')");
        if ($cnt) addV($rows, $vId,
            "⭐ $cnt evaluación" . ($cnt>1?'es':'') . " de empresa pendiente" . ($cnt>1?'s':''),
            "$cnt estudiante(s) esperan tu evaluación de desempeño. Complétala para cerrar el proceso.",
            '../empresa/evaluaciones.php', '⭐');

        $cnt = qCount($db, "SELECT COUNT(*) FROM bitacoras b
            JOIN practicas p ON p.id_practica=b.id_practica
            WHERE p.id_tutor_empresa=$idTE AND b.estado_tutor='pendiente'");
        if ($cnt) addV($rows, $vId,
            "📓 $cnt bitácora" . ($cnt>1?'s':'') . " por revisar",
            "$cnt bitácora(s) semanales de tus estudiantes en prácticas listas para revisar.",
            '../empresa/bitacoras.php', '📓');
    }
}


// ────────────────────────────────────────────────────────────────
//  ROL 9 — FACTURACIÓN
// ────────────────────────────────────────────────────────────────
if ($idRol === 9) {
    $fi = $idInst ? "AND id_instituto = $idInst" : '';

    $cnt = qCount($db, "SELECT COUNT(*) FROM titulaciones
        WHERE estado='enviada' AND comprobante_pago IS NOT NULL $fi");
    if ($cnt) addV($rows, $vId,
        "💳 $cnt comprobante" . ($cnt>1?'s':'') . " de titulación por validar",
        "$cnt estudiante(s) subieron comprobante de pago para titulación. Valida o rechaza.",
        '../facturacion/titulaciones.php', '💳');

    $cnt = qCount($db, "SELECT COUNT(*) FROM mat_pagos
        WHERE estado='pendiente' AND comprobante_img IS NOT NULL");
    if ($cnt) addV($rows, $vId,
        "💰 $cnt pago" . ($cnt>1?'s':'') . " de matrícula por validar",
        "$cnt estudiante(s) subieron comprobante de pago de matrícula pendiente de validación.",
        '../facturacion/matriculas.php', '💰');

    $cnt = qCount($db, "SELECT COUNT(*) FROM titulaciones
        WHERE estado='rechazado' $fi");
    if ($cnt) addV($rows, $vId,
        "❌ $cnt pago" . ($cnt>1?'s':'') . " rechazado" . ($cnt>1?'s':'') . " — en espera",
        "$cnt pago(s) rechazados. El estudiante debe reenviar comprobante corregido.",
        '../facturacion/titulaciones.php?filtro=rechazado', '❌');
}


// ════════════════════════════════════════════════════════════════
//  3. TOTAL no leídas (reales no leídas + todas las virtuales)
// ════════════════════════════════════════════════════════════════
$total = 0;
foreach ($rows as $r) {
    if (($r['virtual'] ?? false) || (int)$r['leida'] === 0) $total++;
}

$rows = array_slice($rows, 0, 20);

// Encode con sustitución segura de UTF-8 inválido
ob_clean();
$json = json_encode(
    ['ok' => true, 'total' => $total, 'rows' => $rows],
    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE
);

// Guardia: si json_encode falla por algún carácter raro, devolver respuesta mínima válida
if ($json === false) {
    // Limpiar strings problemáticos y reintentar
    array_walk_recursive($rows, function(&$v) {
        if (is_string($v)) {
            $v = mb_convert_encoding($v, 'UTF-8', 'UTF-8');
        }
    });
    $json = json_encode(
        ['ok' => true, 'total' => $total, 'rows' => $rows],
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE
    );
}
if ($json === false) {
    $json = '{"ok":true,"total":0,"rows":[],"warn":"json_encode_failed"}';
}

echo $json;