<?php

// Strenge Typprüfung für Parameter- und Rückgabetypen aktivieren.
declare(strict_types=1);

namespace App\Services\Ldap;

use RuntimeException;

/**
 * Service zum Lesen von Objekten aus dem Active Directory.
 *
 * Aktueller Umfang:
 * - Liste von Benutzern (sAMAccountName, displayName, mail)
 * - Liste von Gruppen (sAMAccountName, cn, description)
 *
 * Technische Details:
 * - Verwendet ein technisches Konto (bind_dn + bind_password) für Lesezugriffe
 * - Nutzt LdapConnectionHelper zum Aufbau der Verbindung
 */
class LdapDirectoryService
{
    /** @var array<string, mixed> LDAP-Konfiguration (inkl. base_dn, bind_dn, bind_password) */
    private array $config;

    /** @var LdapConnectionHelper Zentrale Hilfsklasse für den Aufbau von LDAP-Verbindungen */
    private LdapConnectionHelper $connectionHelper;

    /**
     * @param array<string, mixed> $ldapConfig Teilbereich "ldap" aus der config.php
     */
    public function __construct(array $ldapConfig)
    {
        // Vollständige LDAP-Konfiguration speichern.
        $this->config = $ldapConfig;

        // Gemeinsamen Verbindungs-Helper initialisieren.
        // Dieser kümmert sich um ldap_connect und die grundlegenden Optionen.
        $this->connectionHelper = new LdapConnectionHelper($ldapConfig);
    }

    /**
     * Stellt eine LDAP-Verbindung her und bindet sich mit dem technischen Konto.
     *
     * @return resource LDAP-Verbindungs-Handle
     *
     * @throws RuntimeException wenn Bind-Daten fehlen oder der Bind fehlschlägt
     */
    private function connect()
    {
        // Technischen Bind-DN (Service-Account) aus der Konfiguration lesen.
        $bindDn       = (string)($this->config['bind_dn'] ?? '');
        $bindPassword = (string)($this->config['bind_password'] ?? '');

        // Ohne Bind-DN oder Passwort kann kein technischer Bind durchgeführt werden.
        if ($bindDn === '' || $bindPassword === '') {
            throw new RuntimeException('LDAP-Binddaten für technisches Konto sind nicht konfiguriert.');
        }

        // Verbindung über den zentralen Helper erstellen (Server, Port, Optionen).
        $connection = $this->connectionHelper->createConnection();

        // Mit dem technischen Konto binden, um Lesezugriffe durchführen zu können.
        // Das @-Zeichen unterdrückt PHP-Warnings bei Fehlversuchen.
        if (@ldap_bind($connection, $bindDn, $bindPassword) !== true) {
            // Bei fehlgeschlagenem Bind Verbindung wieder schließen.
            ldap_unbind($connection);
            throw new RuntimeException('LDAP-Bind mit technischem Konto ist fehlgeschlagen.');
        }

        // Erfolgreich gebundene Verbindung zurückgeben.
        return $connection;
    }

    /**
     * Liefert eine Liste von Benutzern aus dem AD.
     *
     * @return array<int, array<string, string>> Liste von Benutzer-Datensätzen
     *               [
     *                  [
     *                      'samaccountname' => 'user1',
     *                      'displayname'    => 'User Eins',
     *                      'mail'           => 'user1@example.local',
     *                  ],
     *                  ...
     *               ]
     *
     * @throws RuntimeException bei Konfigurations- oder Verbindungsproblemen
     */
    public function getUsers(): array
    {
        // Base-DN ist der Startpunkt der Suche im Verzeichnisbaum (z. B. DC=...,DC=...).
        $baseDn = (string)($this->config['base_dn'] ?? '');
        if ($baseDn === '') {
            throw new RuntimeException('LDAP base_dn für Benutzersuche ist nicht konfiguriert.');
        }

        // Verbindung mit technischem Bind herstellen.
        $connection = $this->connect();

        // Standard-Filter für Benutzer in AD:
        // - objectClass=user
        // - objectCategory=person
        $filter     = '(&(objectClass=user)(objectCategory=person))';

        // Nur relevante Attribute auslesen, um Datenmenge klein zu halten.
        $attributes = ['sAMAccountName', 'displayName', 'mail'];

        // LDAP-Suche unterhalb des Base-DN durchführen.
        $search = @ldap_search($connection, $baseDn, $filter, $attributes);
        if ($search === false) {
            // Bei einem Fehler die Verbindung schließen und Exception werfen.
            ldap_unbind($connection);
            throw new RuntimeException('LDAP-Suche nach Benutzern ist fehlgeschlagen.');
        }

        // Suchergebnisse in ein PHP-Array umwandeln.
        $entries = ldap_get_entries($connection, $search);

        // Verbindung wieder schließen, da sie nicht mehr benötigt wird.
        ldap_unbind($connection);

        // Ergebnisliste für die View vorbereiten.
        $users = [];

        // Wenn die Struktur unerwartet ist, leere Liste zurückgeben.
        if (!is_array($entries) || !isset($entries['count'])) {
            return $users;
        }

        // Jeden einzelnen Eintrag aus dem Ergebnis verarbeiten.
        for ($i = 0; $i < $entries['count']; $i++) {
            $entry = $entries[$i];

            // Attribute sind im Ergebnis verschachtelt, daher Zugriff über [...][0].
            $users[] = [
                'samaccountname' => isset($entry['samaccountname'][0]) ? (string)$entry['samaccountname'][0] : '',
                'displayname'    => isset($entry['displayname'][0]) ? (string)$entry['displayname'][0] : '',
                'mail'           => isset($entry['mail'][0]) ? (string)$entry['mail'][0] : '',
            ];
        }

        return $users;
    }

    /**
     * Liefert eine Liste von Gruppen aus dem AD.
     *
     * @return array<int, array<string, string>> Liste von Gruppen-Datensätzen
     *
     * @throws RuntimeException bei Konfigurations- oder Verbindungsproblemen
     */
    public function getGroups(): array
    {
        // Base-DN für die Gruppensuche aus der Konfiguration lesen.
        $baseDn = (string)($this->config['base_dn'] ?? '');
        if ($baseDn === '') {
            throw new RuntimeException('LDAP base_dn für Gruppensuche ist nicht konfiguriert.');
        }

        // Verbindung mit technischem Bind herstellen.
        $connection = $this->connect();

        // Filter für Gruppenobjekte im AD.
        $filter     = '(objectClass=group)';

        // Attribute, die für die Darstellung relevant sind.
        $attributes = ['sAMAccountName', 'cn', 'description'];

        // LDAP-Suche ausführen.
        $search = @ldap_search($connection, $baseDn, $filter, $attributes);
        if ($search === false) {
            ldap_unbind($connection);
            throw new RuntimeException('LDAP-Suche nach Gruppen ist fehlgeschlagen.');
        }

        // Ergebnisse holen.
        $entries = ldap_get_entries($connection, $search);

        // Verbindung schließen.
        ldap_unbind($connection);

        $groups = [];

        // Struktur prüfen, leere Liste zurückgeben, falls unerwartet.
        if (!is_array($entries) || !isset($entries['count'])) {
            return $groups;
        }

        // Alle Einträge in ein flacheres Array-Format transformieren.
        for ($i = 0; $i < $entries['count']; $i++) {
            $entry = $entries[$i];

            $groups[] = [
                'samaccountname' => isset($entry['samaccountname'][0]) ? (string)$entry['samaccountname'][0] : '',
                'cn'             => isset($entry['cn'][0]) ? (string)$entry['cn'][0] : '',
                'description'    => isset($entry['description'][0]) ? (string)$entry['description'][0] : '',
            ];
        }

        return $groups;
    }
}
