<?php
/**
 * LDAP configuration test — AJAX endpoint (POST → JSON).
 *
 * Tests, dans l'ordre :
 *   1. Présence de l'extension PHP ldap
 *   2. Initialisation de la connexion + options LDAPv3
 *   3. Bind de service (si bind_dn renseigné) ou bind anonyme
 *   4. Bind utilisateur (si test_user/test_pass fournis) — reproduit le flux
 *      réel d'attemptLdap() : DN = "uid={user},{base_dn}"
 *
 * Réponse JSON : { ok: bool, steps: [{name, ok, msg, details?}] }
 */
require_once dirname(__DIR__) . '/bootstrap.php';

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

/* ── Auth + CSRF ────────────────────────────────────────────────────────────── */
if (!$auth->isLoggedIn() || !$auth->hasRole(ROLE_APP_ADMIN)) {
    echo json_encode(['ok' => false, 'msg' => t('msg.access_denied')]);
    exit;
}
if (!hash_equals(Session::get('csrf_token', ''), $_POST['csrf_token'] ?? '')) {
    echo json_encode(['ok' => false, 'msg' => t('msg.error')]);
    exit;
}

/* ── Paramètres ─────────────────────────────────────────────────────────────── */
$host         = trim($_POST['host']            ?? '');
$port         = (int)($_POST['port']           ?? 389);
$baseDn       = trim($_POST['base_dn']         ?? '');
$bindDn       = trim($_POST['bind_dn']         ?? '');
$bindPass     = $_POST['bind_pass']            ?? '';
// Champ vide ⇒ on retombe sur la valeur chiffrée stockée (admin n'a pas re-saisi).
if ($bindPass === '') {
    $stored   = $auth->getMethodConfig(AUTH_LDAP) ?? [];
    $bindPass = Crypto::decrypt($stored['bind_password'] ?? '') ?? '';
}
$userPattern  = trim($_POST['user_dn_pattern'] ?? 'uid={user},{base_dn}');
$tlsInsecure  = !empty($_POST['tls_insecure']);
$testUser     = trim($_POST['test_user']       ?? '');
$testPass     = $_POST['test_pass']            ?? '';

/* ── Helper d'ajout d'étape ─────────────────────────────────────────────────── */
$steps = [];
$addStep = function (string $name, bool $ok, string $msg, ?string $details = null) use (&$steps): void {
    $entry = ['name' => $name, 'ok' => $ok, 'msg' => $msg];
    if ($details !== null && $details !== '') $entry['details'] = $details;
    $steps[] = $entry;
};

/* ── Étape 1 : extension PHP ldap ───────────────────────────────────────────── */
if (!function_exists('ldap_connect')) {
    $addStep('extension', false, t('settings.ldap_test_step_ext_ko'));
    echo json_encode(['ok' => false, 'steps' => $steps]);
    exit;
}
$addStep('extension', true, t('settings.ldap_test_step_ext_ok'));

/* ── Étape 2 : paramètres minimum ───────────────────────────────────────────── */
if ($host === '') {
    $addStep('config', false, t('settings.ldap_test_step_host_ko'));
    echo json_encode(['ok' => false, 'steps' => $steps]);
    exit;
}
$addStep('config', true, "$host:$port" . ($baseDn !== '' ? " — Base DN : $baseDn" : ''));

/* ── Étape 3 : pré-test TCP (réseau / firewall / DNS) ───────────────────────── */
$sockHost = preg_replace('#^ldaps?://#i', '', $host);
$tcpErrno = 0; $tcpErr = '';
$sock = @fsockopen($sockHost, $port, $tcpErrno, $tcpErr, 5);
if (!$sock) {
    $hint = $tcpErrno === 0
        ? 'Vérifiez le nom DNS, le firewall ou la VPN'
        : "errno=$tcpErrno — $tcpErr";
    $addStep('reachable', false, "Port $port injoignable sur $sockHost", $hint);
    echo json_encode(['ok' => false, 'steps' => $steps]);
    exit;
}
fclose($sock);
$addStep('reachable', true, "Port $port accessible sur $sockHost (TCP OK)");

/* ── Étape 4 : ldap_connect + options ───────────────────────────────────────── */
$uri    = Auth::buildLdapUri($host, $port);
$useTls = Auth::isLdapsUri($uri);

if ($useTls && $tlsInsecure) {
    @ldap_set_option(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
}

$conn = @ldap_connect($uri);
if (!$conn) {
    $addStep('connect', false, t('settings.ldap_test_step_connect_ko'), "URI : $uri");
    echo json_encode(['ok' => false, 'steps' => $steps]);
    exit;
}
@ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3);
@ldap_set_option($conn, LDAP_OPT_REFERRALS, 0);
@ldap_set_option($conn, LDAP_OPT_NETWORK_TIMEOUT, 5);

$schemeNote = "URI : $uri" . ($useTls ? ($tlsInsecure ? ' (TLS — vérification cert désactivée)' : ' (TLS)') : '');
$addStep('connect', true, t('settings.ldap_test_step_connect_ok'), $schemeNote);

/* ── Helpers diagnostic ─────────────────────────────────────────────────────── */
// Sous-codes AD courants (dans la chaîne diagnostic : "data XXX")
$adSubcodes = [
    '52e' => '52e — identifiants invalides (mot de passe erroné)',
    '525' => '525 — utilisateur introuvable dans l\'annuaire',
    '530' => '530 — connexion non autorisée à cette heure',
    '531' => '531 — connexion non autorisée depuis ce poste',
    '532' => '532 — mot de passe expiré',
    '533' => '533 — compte désactivé',
    '701' => '701 — compte expiré',
    '773' => '773 — l\'utilisateur doit changer son mot de passe',
    '775' => '775 — compte verrouillé',
];

$ldapDiag = function ($conn) use ($adSubcodes): string {
    $no    = @ldap_errno($conn);
    $err   = @ldap_error($conn);
    $extra = '';
    @ldap_get_option($conn, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extra);
    $msg = "#$no $err";
    if ($extra !== '' && $extra !== null) {
        $extra = trim($extra);
        $msg  .= ' — ' . $extra;
        if (preg_match('/data\s+([0-9a-fA-F]+)/', $extra, $m)) {
            $code = strtolower($m[1]);
            if (isset($adSubcodes[$code])) {
                $msg .= "\nAD : " . $adSubcodes[$code];
            }
        }
    }
    return $msg;
};

// Indice contextuel à afficher après une erreur de bind
$bindHint = function (int $no, string $dn) use ($useTls, $tlsInsecure): string {
    if ($no === 49) {
        // Invalid credentials
        if (stripos($dn, 'uid=') === 0) {
            return "Indice : si le serveur est Active Directory, le préfixe `uid=` n'est pas reconnu. "
                 . "Essayez le pattern `{user}@{domain}` (UPN) ou `CN={user},OU=Users,{base_dn}`.";
        }
        return "Indice : mot de passe incorrect, ou DN ne correspondant à aucun objet (vérifier `data XXX` ci-dessus).";
    }
    if ($no === 32)  return "Indice : objet introuvable — la Base DN ne contient pas cet utilisateur.";
    if ($no === 34)  return "Indice : DN mal formé (`Invalid DN syntax`).";
    if ($no === 81)  return "Indice : serveur injoignable malgré que le TCP réponde — version LDAP ou TLS requis ?";
    if ($no === -1) {
        if ($useTls && !$tlsInsecure) {
            return "Indice : LDAPS actif mais le certificat du serveur n'est pas reconnu par le système. "
                 . "Cochez « Ignorer la vérification du certificat TLS » pour confirmer ce diagnostic, "
                 . "ou installez le certificat de la CA dans le magasin de confiance du serveur web.";
        }
        if (!$useTls) {
            return "Indice : le serveur ferme la connexion en clair. Si c'est de l'AD, le port 636 (LDAPS) "
                 . "est probablement le bon — le code détecte LDAPS automatiquement quand port=636.";
        }
        return "Indice : la couche libldap n'a pas pu communiquer avec le serveur.";
    }
    return '';
};

$overallOk = true;

/* ── Étape 5 : bind de service ou anonyme ──────────────────────────────────── */
if ($bindDn !== '') {
    if (@ldap_bind($conn, $bindDn, $bindPass)) {
        $addStep('service_bind', true, t('settings.ldap_test_step_svc_ok'), $bindDn);
    } else {
        $no   = @ldap_errno($conn);
        $det  = $ldapDiag($conn) . "\nDN : $bindDn";
        $hint = $bindHint($no, $bindDn);
        if ($hint) $det .= "\n" . $hint;
        $addStep('service_bind', false, t('settings.ldap_test_step_svc_ko'), $det);
        $overallOk = false;
    }
} else {
    if (@ldap_bind($conn)) {
        $addStep('anon_bind', true, t('settings.ldap_test_step_anon_ok'));
    } else {
        // Bind anonyme refusé : courant sur AD, ne fait pas échouer le test global
        $addStep('anon_bind', false, t('settings.ldap_test_step_anon_warn'), $ldapDiag($conn));
    }
}

/* ── Étape 6 : bind utilisateur (optionnel) ─────────────────────────────────── */
if ($testUser !== '' && $testPass !== '') {
    // Le pattern peut référencer {base_dn} ; vérifier que ce dernier est renseigné si nécessaire
    if (str_contains($userPattern, '{base_dn}') && $baseDn === '') {
        $addStep('user_bind', false, t('settings.ldap_test_step_user_no_base'), "Pattern : $userPattern");
        $overallOk = false;
    } else {
        $userDn = Auth::buildUserDn($userPattern, $testUser, $baseDn);
        if (@ldap_bind($conn, $userDn, $testPass)) {
            $addStep('user_bind', true, t('settings.ldap_test_step_user_ok'), "Pattern : $userPattern\nDN : $userDn");
        } else {
            $no   = @ldap_errno($conn);
            $det  = $ldapDiag($conn) . "\nPattern : $userPattern\nDN : $userDn";
            $hint = $bindHint($no, $userDn);
            if ($hint) $det .= "\n" . $hint;
            $addStep('user_bind', false, t('settings.ldap_test_step_user_ko'), $det);
            $overallOk = false;
        }
    }
}

@ldap_close($conn);

echo json_encode(['ok' => $overallOk, 'steps' => $steps]);
