<?php
declare(strict_types=1);

/**
 * Hourly sweep that flips local users to is_active=0 when their LDAP
 * counterpart no longer matches the active filter (deleted, disabled, locked,
 * removed from required group). Runs from cron via the cron_tick hook with
 * its own throttle so it can be wired into a generic per-minute cron.
 *
 * The login-time filter (LdapAuthAdapter::tryAuthenticate) is what actually
 * blocks logins — this sweep keeps the UI consistent (member pickers,
 * workload, mention autocomplete) and gives admins a security-log trail.
 */
class LdapSync
{
    /** Minimum interval between sweeps, in seconds. */
    private const INTERVAL = 3600;

    public static function runIfDue(): void
    {
        if (!extension_loaded('ldap')) return;
        $last = (int)Helper::getSetting('ldap_sync_last_run', '0');
        if ($last !== 0 && (time() - $last) < self::INTERVAL) return;
        self::run();
    }

    public static function run(): array
    {
        $cfg = LdapAuthAdapter::config();
        if ($cfg === null) {
            return ['ok' => false, 'reason' => 'not_configured'];
        }

        $ds = LdapAuthAdapter::connect($cfg);
        if (!$ds) {
            error_log('[Plenvo LDAP] sync: connect failed to ' . $cfg['uri']);
            return ['ok' => false, 'reason' => 'connect_failed'];
        }

        $disabled = 0;
        $checked  = 0;
        try {
            $bindPw = $cfg['bind_password_enc'] !== '' ? Crypto::decrypt($cfg['bind_password_enc']) : '';
            if ($cfg['bind_dn'] !== '' && !@ldap_bind($ds, $cfg['bind_dn'], $bindPw)) {
                error_log('[Plenvo LDAP] sync: service bind failed');
                return ['ok' => false, 'reason' => 'service_bind_failed'];
            }

            $db = Database::getInstance();
            $users = $db->fetchAll(
                "SELECT id, email, external_id FROM users WHERE auth_provider = 'ldap' AND is_active = 1"
            );
            $checked = count($users);

            $activeFilter = trim((string)$cfg['active_filter']);
            $userFilter   = (string)$cfg['user_filter'] ?: '(mail=%s)';

            foreach ($users as $u) {
                $stillActive = self::isStillActive($ds, $cfg, $u, $userFilter, $activeFilter);
                if ($stillActive === null) continue;          // search error, do not punish the user
                if ($stillActive === true) {
                    $db->execute(
                        'UPDATE users SET ldap_synced_at = ? WHERE id = ?',
                        [time(), $u['id']]
                    );
                    continue;
                }
                $db->execute(
                    'UPDATE users SET is_active = 0, ldap_synced_at = ? WHERE id = ?',
                    [time(), $u['id']]
                );
                SecurityLog::log('ldap_user_deactivated', SecurityLog::SEV_WARN, [
                    'email_hint' => Helper::maskEmail((string)$u['email']),
                    'reason'     => 'not_in_directory_or_disabled',
                ], (int)$u['id']);
                $disabled++;
            }
        } finally {
            @ldap_unbind($ds);
        }

        Helper::setSetting('ldap_sync_last_run',      (string)time());
        Helper::setSetting('ldap_sync_last_checked',  (string)$checked);
        Helper::setSetting('ldap_sync_last_disabled', (string)$disabled);

        return ['ok' => true, 'checked' => $checked, 'disabled' => $disabled];
    }

    /**
     * Returns true if the user is still present and matches the active filter,
     * false if vanished/disabled, null if the LDAP search itself errored
     * (network, ACL on this entry — do not deactivate on those).
     */
    private static function isStillActive($ds, array $cfg, array $u, string $userFilter, string $activeFilter): ?bool
    {
        $email = (string)$u['email'];
        $emailSafe = ldap_escape($email, '', LDAP_ESCAPE_FILTER);
        $base   = $userFilter !== '' ? str_replace('%s', $emailSafe, $userFilter) : '(mail=' . $emailSafe . ')';
        $filter = $activeFilter !== '' ? '(&' . $base . $activeFilter . ')' : $base;

        $sr = @ldap_search($ds, $cfg['base_dn'], $filter, ['dn']);
        if (!$sr) return null;
        return ldap_count_entries($ds, $sr) > 0;
    }
}
