<?php
// ─────────────────────────────────────────
//  SIGVP — Auth Helper  (v5 — multi-instituto)
//  CAMBIOS v5:
//  - login() resuelve ?inst=TOKEN → id_instituto y filtra la búsqueda.
//    Permite que el mismo usuario (misma cédula) exista en varios
//    institutos con distinto estado activo/inactivo.
//  - logout() recupera inst_token antes de destruir la sesión y redirige
//    con ?inst=TOKEN para volver al login correcto.
//  - requireLogin() preserva inst_token al redirigir por sesión expirada.
//
//  REQUISITO BD (ejecutar UNA sola vez):
//    ALTER TABLE institutos ADD COLUMN token VARCHAR(20) NULL UNIQUE AFTER nombre;
// ─────────────────────────────────────────
require_once __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'app.php';
require_once __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'database.php';

function startSession(): void {
    if (session_status() === PHP_SESSION_NONE) {
        session_name(SESSION_NAME);
        session_set_cookie_params([
            'lifetime' => SESSION_LIFETIME,
            'path'     => '/',
            'secure'   => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
            'httponly' => true,
            'samesite' => 'Strict',
        ]);
        session_start();
    }
}

// ── HTTPS: forzar conexión segura ────────────────────────────
function forzarHttps(): void {
    if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') {
        $url = 'https://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . ($_SERVER['REQUEST_URI'] ?? '/');
        header('Location: ' . $url, true, 301);
        exit;
    }
}

// ── HEADERS DE SEGURIDAD ─────────────────────────────────────
function securityHeaders(): void {
    header('X-Frame-Options: DENY');
    header('X-Content-Type-Options: nosniff');
    header('X-XSS-Protection: 1; mode=block');
    header('Referrer-Policy: strict-origin-when-cross-origin');
    header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' cdnjs.cloudflare.com fonts.googleapis.com; style-src 'self' 'unsafe-inline' cdnjs.cloudflare.com fonts.googleapis.com fonts.gstatic.com; font-src 'self' fonts.gstatic.com cdnjs.cloudflare.com; img-src 'self' data:; frame-src 'none';");
    header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
}

// ── BRUTE FORCE: verificar intentos fallidos (4 máx / 15 min) ──
// Bloquea por IP + id_instituto: fallar en instituto A NO bloquea instituto B
function verificarBruteForce(string $ipAddr, ?int $idInstituto): bool {
    try {
        $db = getDB();

        // Filtro base: IP + accion + ventana de tiempo
        $filtroInst = $idInstituto
            ? ' AND id_instituto = :inst'
            : ' AND id_instituto IS NULL';

        $st = $db->prepare("
            SELECT COUNT(*) FROM audit_log
            WHERE  accion      = 'login_fallido'
              AND  ip_address  = :ip
              {$filtroInst}
              AND  created_at >= DATE_SUB(NOW(), INTERVAL 15 MINUTE)
        ");

        $params = [':ip' => $ipAddr];
        if ($idInstituto) $params[':inst'] = $idInstituto;

        $st->execute($params);
        return (int)$st->fetchColumn() >= 4;

    } catch (Exception) {
        // Fallback a sesión: la clave incluye IP + instituto para aislarlos
        startSession();
        $sk = 'lf_' . md5($ipAddr . '_inst_' . ($idInstituto ?? 'none'));
        $sf = $_SESSION[$sk] ?? ['n' => 0, 't' => 0];
        if (time() - $sf['t'] > 900) $sf = ['n' => 0, 't' => 0];
        return $sf['n'] >= 4;
    }
}

// ── LOGIN ────────────────────────────────
// Acepta email O cédula. Si viene ?inst=TOKEN filtra por instituto,
// resolviendo el conflicto cuando la misma cédula existe en varios institutos.
function login(string $email, string $password): array {
    $db     = getDB();
    $input  = trim($email);
    $ipAddr = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';

    // ── 1. Resolver token → id_instituto (PRIMERO, necesario para brute force) ──
    $instToken   = preg_replace('/[^A-Za-z0-9_\-]/', '', $_POST['inst'] ?? $_GET['inst'] ?? '');
    $idInstituto = null;

    if ($instToken !== '') {
        $stInst = $db->prepare(
            "SELECT id_instituto FROM institutos WHERE token = :tk AND activo = 1 LIMIT 1"
        );
        $stInst->execute([':tk' => $instToken]);
        $row = $stInst->fetch();
        if ($row) {
            $idInstituto = (int) $row['id_instituto'];
        }
    }

    // ── 2. Verificar bloqueo por intentos fallidos (IP + instituto) ──
    // Fallar en instituto A NO bloquea el acceso al instituto B
    if (verificarBruteForce($ipAddr, $idInstituto)) {
        return ['ok' => false, 'msg' => 'Demasiados intentos fallidos. Espera 15 minutos e intenta de nuevo.'];
    }

    // ── 3. Buscar usuario (filtrado por instituto) ───────────────────
    $porCedula     = (bool) preg_match('/^\d{10,13}$/', $input);
    $campoBusqueda = $porCedula ? 'u.cedula' : 'u.email';
    $filtroInst    = $idInstituto ? ' AND u.id_instituto = :inst' : '';

    $sql = "SELECT u.id_usuario, u.nombres, u.apellidos, u.email,
                   u.password_hash, u.activo, u.cambiar_password,
                   u.id_rol, r.nombre AS rol_nombre, u.id_instituto
            FROM   usuarios u
            JOIN   roles r ON r.id_rol = u.id_rol
            WHERE  {$campoBusqueda} = :email
            {$filtroInst}
            LIMIT  1";

    $params = [':email' => $input];
    if ($idInstituto) $params[':inst'] = $idInstituto;

    $stmt = $db->prepare($sql);
    $stmt->execute($params);
    $user = $stmt->fetch();

    if (!$user) {
        // Registrar fallo con id_instituto para que el bloqueo sea por instituto
        registrarAudit(null, 'login_fallido', 'usuarios', null, null, null, $idInstituto);
        return ['ok' => false, 'msg' => 'Credenciales incorrectas.'];
    }
    if (!$user['activo']) {
        return ['ok' => false, 'msg' => 'Cuenta desactivada. Contacta al administrador.'];
    }
    if (!password_verify($password, $user['password_hash'])) {
        // Registrar fallo con id_instituto del usuario encontrado
        $instReal = $user['id_instituto'] ? (int)$user['id_instituto'] : $idInstituto;
        registrarAudit($user['id_usuario'], 'login_fallido', 'usuarios', $user['id_usuario'], null, null, $instReal);
        return ['ok' => false, 'msg' => 'Credenciales incorrectas.'];
    }

    // ── 3. Prevenir session fixation y guardar sesión ────────────────
    session_regenerate_id(true); // Nueva ID de sesión al autenticarse
    $_SESSION['user'] = [
        'id'               => $user['id_usuario'],
        'nombre'           => $user['nombres'] . ' ' . $user['apellidos'],
        'email'            => $user['email'],
        'id_rol'           => (int) $user['id_rol'],
        'rol'              => $user['rol_nombre'],
        'id_instituto'     => $user['id_instituto'] ? (int) $user['id_instituto'] : null,
        'cambiar_password' => (bool) $user['cambiar_password'],
        'login_at'         => time(),
    ];

    // Guardar inst_token en sesión para recuperarlo en logout
    if ($instToken !== '') {
        $_SESSION['inst_token'] = $instToken;
    }

    // ── 4. Registrar sesión en BD ────────────────────────────────────
    try {
        $db->prepare("
            INSERT INTO sesiones (id_sesion, id_usuario, ip_address, user_agent, ultimo_acceso)
            VALUES (:sid, :uid, :ip, :ua, NOW())
            ON DUPLICATE KEY UPDATE
                ultimo_acceso = NOW(),
                ip_address    = VALUES(ip_address),
                user_agent    = VALUES(user_agent)
        ")->execute([
            ':sid' => session_id(),
            ':uid' => $user['id_usuario'],
            ':ip'  => $_SERVER['REMOTE_ADDR']     ?? '0.0.0.0',
            ':ua'  => $_SERVER['HTTP_USER_AGENT'] ?? '',
        ]);
    } catch (Exception) {}

    // ── 5. Actualizar ultimo_login ───────────────────────────────────
    try {
        $db->prepare("UPDATE usuarios SET ultimo_login = NOW() WHERE id_usuario = :id")
           ->execute([':id' => $user['id_usuario']]);
    } catch (Exception) {}

    registrarAudit($user['id_usuario'], 'login_exitoso', 'usuarios', $user['id_usuario']);

    return ['ok' => true, 'redirect' => ROL_REDIRECT[$user['id_rol']] ?? (BASE_URL . '/auth/login.php')];
}

// ── LOGOUT ───────────────────────────────
function logout(): void {
    startSession();

    // Recuperar inst_token ANTES de destruir la sesión
    $instToken = $_SESSION['inst_token'] ?? '';

    if (isset($_SESSION['user'])) {
        registrarAudit($_SESSION['user']['id'], 'logout', 'usuarios', $_SESSION['user']['id']);
        try {
            $db = getDB();
            $db->prepare("DELETE FROM sesiones WHERE id_sesion = :sid")
               ->execute([':sid' => session_id()]);
        } catch (Exception) {}
    }

    $_SESSION = [];
    session_destroy();

    // Redirigir al login con el inst_token para volver al instituto correcto
    $loginUrl = BASE_URL . '/auth/login.php?msg=logout';
    if ($instToken !== '') {
        $loginUrl .= '&inst=' . urlencode($instToken);
    }
    header('Location: ' . $loginUrl);
    exit;
}

// ── GUARDS ───────────────────────────────
function requireLogin(): void {
    startSession();
    if (empty($_SESSION['user'])) {
        $instToken = $_SESSION['inst_token'] ?? '';
        $url = BASE_URL . '/auth/login.php?msg=no_auth';
        if ($instToken !== '') {
            $url .= '&inst=' . urlencode($instToken);
        }
        header('Location: ' . $url);
        exit;
    }
    if ((time() - $_SESSION['user']['login_at']) > SESSION_LIFETIME) {
        logout();
    }
}

function requireRol(int|array $roles): void {
    requireLogin();
    $rolActual  = (int) $_SESSION['user']['id_rol'];
    $permitidos = is_array($roles) ? $roles : [$roles];
    if (!in_array($rolActual, $permitidos, true)) {
        http_response_code(403);
        include BASE_PATH . DIRECTORY_SEPARATOR . 'includes' . DIRECTORY_SEPARATOR . '403.php';
        exit;
    }
}

// ── HELPERS ──────────────────────────────
function currentUser(): array  { return $_SESSION['user'] ?? []; }
function isLoggedIn(): bool    { startSession(); return !empty($_SESSION['user']); }

// ── AUDIT LOG ────────────────────────────
function registrarAudit(
    ?int    $idUsuario,
    string  $accion,
    ?string $tabla       = null,
    ?int    $idRegistro  = null,
    ?array  $antes       = null,
    ?array  $despues     = null,
    ?int    $idInstituto = null   // ← para aislar bloqueos por instituto
): void {
    try {
        $db = getDB();
        $db->prepare("
            INSERT INTO audit_log
                (id_usuario, accion, tabla_afectada, id_registro,
                 datos_antes, datos_despues, ip_address, user_agent, id_instituto)
            VALUES
                (:uid, :accion, :tabla, :id_reg,
                 :antes, :despues, :ip, :ua, :inst)
        ")->execute([
            ':uid'     => $idUsuario,
            ':accion'  => $accion,
            ':tabla'   => $tabla,
            ':id_reg'  => $idRegistro,
            ':antes'   => $antes    ? json_encode($antes)    : null,
            ':despues' => $despues  ? json_encode($despues)  : null,
            ':ip'      => $_SERVER['REMOTE_ADDR']     ?? '0.0.0.0',
            ':ua'      => $_SERVER['HTTP_USER_AGENT'] ?? '',
            ':inst'    => $idInstituto,
        ]);
    } catch (Exception) {}
}

// ── HELPERS MULTI-TENANT ─────────────────────────────────────────
function instId(): int {
    startSession();
    return (int)($_SESSION['user']['id_instituto'] ?? 0);
}

function instCond(string $alias = '', int $id = 0): string {
    $idInst = $id ?: instId();
    if (!$idInst) return '';
    $col = $alias ? "{$alias}.id_instituto" : 'id_instituto';
    return " AND {$col} = {$idInst}";
}

function instWhere(string $alias = '', int $id = 0): string {
    $idInst = $id ?: instId();
    if (!$idInst) return 'WHERE 1=1';
    $col = $alias ? "{$alias}.id_instituto" : 'id_instituto';
    return "WHERE {$col} = {$idInst}";
}

function instParam(array $params = [], int $id = 0): array {
    $idInst = $id ?: instId();
    if ($idInst) $params[':inst'] = $idInst;
    return $params;
}