<?php
if (!function_exists('creer_admin')) {
    /**
     * Crée un compte admin avec mot de passe haché (jamais stocké en clair).
     */
    function creer_admin(PDO $pdo, string $login, string $pass): void {
        $pdo->prepare('INSERT INTO admins (login, pass_hash) VALUES (?,?)')
            ->execute([$login, password_hash($pass, PASSWORD_BCRYPT)]);
    }
}
if (!function_exists('verifier_identifiants')) {
    /**
     * Vérifie un couple login/mot de passe contre le hash stocké.
     *
     * Revue Task 9 — fix 1 : un login inconnu passe quand même par password_verify()
     * contre un hash factice. Sans cela, le retour anticipé (pas de SELECT trouvé =>
     * pas de vérification bcrypt ~renvoi immédiat) est mesurablement plus rapide qu'un
     * login connu + mauvais mot de passe (qui, lui, exécute bcrypt) : un attaquant
     * pourrait chronométrer les réponses pour énumérer les logins admin valides.
     * Non couvert par un test automatisé : une assertion de timing serait instable
     * (charge machine, coût bcrypt variable) ; vérifié manuellement (voir rapport).
     */
    function verifier_identifiants(PDO $pdo, string $login, string $pass): bool {
        $q = $pdo->prepare('SELECT pass_hash FROM admins WHERE login = ?');
        $q->execute([$login]);
        $h = $q->fetchColumn();
        if ($h === false) {
            password_verify($pass, '$2y$10$abcdefghijklmnopqrstuOQoWrq0V5vqhqXk1yq1Kx1zq1Kx1zq1K');
            return false;
        }
        return password_verify($pass, $h);
    }
}
if (!function_exists('journaliser_connexion')) {
    /**
     * Revue Task 9 — verrou anti-force-brute : journalise une tentative de
     * connexion admin pour l'IP donnée ($ok = identifiants acceptés).
     * Miroir de journaliser_envoi() (antispam.php).
     */
    function journaliser_connexion(PDO $pdo, string $ip, bool $ok): void {
        $pdo->prepare('INSERT INTO connexions_log (ip, ok) VALUES (?,?)')->execute([$ip, (int)$ok]);
    }
}
if (!function_exists('connexion_bloquee')) {
    /**
     * Vrai si l'IP a atteint $max tentatives de connexion dans la fenêtre
     * glissante d'une heure. Comme ip_bloquee(), toutes les tentatives comptent
     * (succès inclus) : un flot de connexions, même réussies, est anormal pour
     * un back-office et les tentatives faites pendant le blocage prolongent le
     * verrou.
     *
     * Contrat : $max >= 1 — avec 0, COUNT(*) >= 0 est toujours vrai, donc tout
     * est bloqué par construction.
     */
    function connexion_bloquee(PDO $pdo, string $ip, int $max = 5): bool {
        $sqlite = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'sqlite';
        $sql = $sqlite
            ? "SELECT COUNT(*) FROM connexions_log WHERE ip = ? AND created_at > datetime('now','-1 hour')"
            : 'SELECT COUNT(*) FROM connexions_log WHERE ip = ? AND created_at > NOW() - INTERVAL 1 HOUR';
        $q = $pdo->prepare($sql); $q->execute([$ip]);
        return (int)$q->fetchColumn() >= $max;
    }
}
if (!function_exists('session_admin_valide')) {
    /**
     * Revue Task 9 — expiration de session. Helper pur (aucune lecture de
     * $_SESSION ni de time() : tout est passé en paramètre, donc testable
     * unitairement sans session réelle).
     *
     * Deux plafonds : 8 h après la connexion (absolu, même si la session est
     * active) et 1 h sans activité. Bornes inclusives : à exactement 8 h ou
     * 1 h, la session est encore valide. Un marqueur absent ou d'un type
     * inattendu invalide la session (fail-closed).
     */
    function session_admin_valide(array $session, int $maintenant): bool {
        $depuis = $session['admin_depuis'] ?? null;
        $actif = $session['admin_actif'] ?? null;
        if (!is_int($depuis) || !is_int($actif)) {
            return false;
        }
        return $maintenant - $depuis <= 8 * 3600 && $maintenant - $actif <= 3600;
    }
}
if (!function_exists('demarrer_session_admin')) {
    /**
     * Démarre (ou rejoint) la session admin avec des attributs durcis.
     *
     * Revue Task 9 : httponly (inaccessible en JS, réduit l'impact d'un XSS),
     * SameSite=Lax (limite l'envoi du cookie sur navigation cross-site),
     * « secure » seulement si la requête est déjà en HTTPS — le forcer
     * inconditionnellement casserait la session en dev local (http://localhost).
     *
     * Nom ISITADMIN + path=/admin : cookie totalement distinct du PHPSESSID du
     * site public (flash PRG de /admission) — les deux sessions ne peuvent plus
     * se partager ni se contaminer. use_strict_mode refuse les identifiants de
     * session non initiés par le serveur (complète session_regenerate_id()
     * contre la fixation).
     *
     * Tolère un appel alors qu'une session est déjà active (double inclusion) :
     * on rejoint la session en cours au lieu d'émettre un warning.
     */
    function demarrer_session_admin(): void {
        if (session_status() === PHP_SESSION_ACTIVE) {
            return;
        }
        ini_set('session.use_strict_mode', '1');
        session_name('ISITADMIN');
        session_set_cookie_params([
            'path' => '/admin',
            'httponly' => true,
            'samesite' => 'Lax',
            'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
        ]);
        session_start();
    }
}
if (!function_exists('exiger_admin')) {
    /**
     * Garde de session pour les pages admin : redirige vers la connexion si
     * l'utilisateur n'est pas authentifié ou si sa session a expiré
     * (?expiree=1 fait afficher un message d'information par login.php).
     * Une requête sur une session valide rafraîchit le marqueur d'activité.
     */
    function exiger_admin(): void {
        demarrer_session_admin();
        if (empty($_SESSION['admin'])) {
            header('Location: /admin/login.php');
            exit;
        }
        if (!session_admin_valide($_SESSION, time())) {
            session_destroy();
            header('Location: /admin/login.php?expiree=1');
            exit;
        }
        $_SESSION['admin_actif'] = time();
    }
}
