<?php

namespace MintyPHP\Service\Auth;

use MintyPHP\Repository\Tenant\TenantLdapAuthRepositoryInterface;
use MintyPHP\Repository\Tenant\TenantMicrosoftAuthRepositoryInterface;

class TenantSsoService
{
    public const PROVIDER_MICROSOFT = 'microsoft';
    public const PROVIDER_LDAP = 'ldap';

    private const PROFILE_SYNC_FIELD_OPTIONS = [
        'first_name' => 'Sync first name',
        'last_name' => 'Sync last name',
        'display_name' => 'Sync display name',
        'job_title' => 'Sync job title',
        'phone' => 'Sync phone',
        'mobile' => 'Sync mobile',
        'avatar' => 'Sync avatar image',
    ];

    private const LDAP_PROFILE_SYNC_FIELD_OPTIONS = [
        'first_name' => 'Sync first name',
        'last_name' => 'Sync last name',
        'phone' => 'Sync phone',
        'mobile' => 'Sync mobile',
        'email' => 'Sync email',
    ];

    private const DEFAULT_PROFILE_SYNC_FIELDS = ['first_name', 'last_name'];

    private const VALID_LDAP_ENCRYPTION_MODES = ['none', 'starttls', 'ldaps'];
    private const VALID_LDAP_BIND_METHODS = ['search_then_bind', 'direct_bind'];
    private const VALID_LDAP_SEARCH_SCOPES = ['sub', 'one'];

    public function __construct(
        private readonly AuthTenantGateway $tenantGateway,
        private readonly TenantMicrosoftAuthRepositoryInterface $tenantMicrosoftAuthRepository,
        private readonly TenantLdapAuthRepositoryInterface $tenantLdapAuthRepository,
        private readonly AuthSettingsGateway $settingsGateway,
        private readonly AuthCryptoGateway $cryptoGateway
    ) {
    }

    public function microsoftProviderKey(): string
    {
        return self::PROVIDER_MICROSOFT;
    }

    public function ldapProviderKey(): string
    {
        return self::PROVIDER_LDAP;
    }

    public function findTenantByLoginSlug(string $slug): ?array
    {
        $slug = trim(strtolower($slug));
        if ($slug === '') {
            return null;
        }

        if (preg_match('/^[a-f0-9-]{36}$/', $slug)) {
            $tenant = $this->tenantGateway->findByUuid($slug);
            if ($tenant && (string) ($tenant['status'] ?? 'active') === 'active') {
                return $tenant;
            }
        }

        $matches = [];
        foreach ($this->tenantGateway->list() as $tenant) {
            if ((string) ($tenant['status'] ?? 'active') !== 'active') {
                continue;
            }
            if ($this->slugify((string) ($tenant['description'] ?? '')) === $slug) {
                $matches[] = $tenant;
            }
        }

        // Reject ambiguous slugs — if two tenants have the same name-slug, neither matches.
        if (count($matches) !== 1) {
            return null;
        }

        return $matches[0];
    }

    public function tenantLoginSlug(array $tenant): string
    {
        $slug = $this->slugify((string) ($tenant['description'] ?? ''));
        if ($slug !== '') {
            return $slug;
        }

        return strtolower((string) ($tenant['uuid'] ?? ''));
    }

    public function getTenantMicrosoftAuth(int $tenantId): array
    {
        $row = $this->tenantMicrosoftAuthRepository->findByTenantId($tenantId) ?? [];
        $syncProfileOnLogin = !empty($row['sync_profile_on_login']);
        $syncFields = $this->normalizeProfileSyncFields($row['sync_profile_fields'] ?? []);
        if ($syncProfileOnLogin && !$syncFields) {
            $syncFields = $this->defaultProfileSyncFields();
        }

        return [
            'enabled' => !empty($row['enabled']),
            'enforce_microsoft_login' => !empty($row['enforce_microsoft_login']),
            'sync_profile_on_login' => $syncProfileOnLogin,
            'sync_profile_fields' => $this->profileSyncFieldsCsv($syncFields),
            'sync_profile_fields_list' => $syncFields,
            'entra_tenant_id' => strtolower(trim((string) ($row['entra_tenant_id'] ?? ''))),
            'allowed_domains' => $this->normalizeDomains((string) ($row['allowed_domains'] ?? '')),
            'use_shared_app' => !array_key_exists('use_shared_app', $row) || !empty($row['use_shared_app']),
            'client_id_override' => trim((string) ($row['client_id_override'] ?? '')),
            'client_secret_override_enc' => trim((string) ($row['client_secret_override_enc'] ?? '')),
            'auto_remember_mode' => array_key_exists('auto_remember_mode', $row) && $row['auto_remember_mode'] !== null
                ? (int) $row['auto_remember_mode']
                : null,
        ];
    }

    public function buildMicrosoftUiState(int $tenantId, array $input = []): array
    {
        $existing = $this->getTenantMicrosoftAuth($tenantId);
        $state = $this->normalizeMicrosoftInputState($input, $existing);
        $configState = $this->evaluateMicrosoftConfigState($state);

        return [
            'enabled' => $state['enabled'],
            'config_complete' => !$state['enabled'] || $configState['complete'],
            'config_error_code' => (string) ($configState['error_code'] ?? ''),
            'config_error_label' => $this->microsoftConfigErrorLabel((string) ($configState['error_code'] ?? '')),
            'password_mode' => !$state['enabled']
                ? 'local_only'
                : ($state['enforce_microsoft_login'] ? 'microsoft_only' : 'local_and_microsoft'),
            'credential_source' => $state['use_shared_app'] ? 'shared' : 'override',
            'sync_needs_graph' => $state['sync_profile_on_login']
                && $this->profileSyncFieldsRequireGraph($state['sync_profile_fields']),
        ];
    }

    public function isLocalPasswordLoginAllowed(int $tenantId): bool
    {
        $microsoftAuth = $this->getTenantMicrosoftAuth($tenantId);
        if ($microsoftAuth['enabled'] && $microsoftAuth['enforce_microsoft_login']) {
            return false;
        }

        $ldapAuth = $this->getTenantLdapAuth($tenantId);
        if ($ldapAuth['enabled'] && $ldapAuth['enforce_ldap_login']) {
            return false;
        }

        return true;
    }

    /**
     * Discover tenants that accept SSO login for the given email domain.
     * Only tenants with explicitly configured (non-empty) allowed_domains are returned.
     * Returns SSO-only methods (local is always false since the user has no local account).
     *
     * @return list<array{id: int, methods: array}> Tenant candidates with SSO methods
     */
    public function discoverTenantsByEmailDomain(string $email): array
    {
        $email = strtolower(trim($email));
        $parts = explode('@', $email);
        if (count($parts) !== 2 || $parts[1] === '') {
            return [];
        }
        $domain = $parts[1];

        $microsoftMatches = $this->tenantMicrosoftAuthRepository->findEnabledWithExplicitDomain($domain);
        $ldapMatches = $this->tenantLdapAuthRepository->findEnabledWithExplicitDomain($domain);

        $tenantIds = [];
        foreach ($microsoftMatches as $match) {
            $tenantIds[(int) $match['tenant_id']] = true;
        }
        foreach ($ldapMatches as $match) {
            $tenantIds[(int) $match['tenant_id']] = true;
        }

        if (!$tenantIds) {
            return [];
        }

        $candidates = [];
        foreach (array_keys($tenantIds) as $tenantId) {
            $tenant = $this->tenantGateway->findById($tenantId);
            if (!$tenant || (string) ($tenant['status'] ?? 'active') !== 'active') {
                continue;
            }

            $methods = $this->resolveTenantLoginMethods($tenantId);

            // Discovery candidates never offer local password (user has no local account)
            $methods['local'] = false;

            // Skip tenants where no SSO method is actually functional
            if (empty($methods['microsoft']) && empty($methods['ldap'])) {
                continue;
            }

            $candidates[] = [
                'id' => $tenantId,
                'tenant' => $tenant,
                'methods' => $methods,
            ];
        }

        return $candidates;
    }

    public function resolveTenantLoginMethods(int $tenantId): array
    {
        $tenant = $this->tenantGateway->findById($tenantId);
        if (!$tenant || (string) ($tenant['status'] ?? 'active') !== 'active') {
            return [
                'local' => false,
                'microsoft' => false,
                'microsoft_reason' => 'tenant_not_found',
                'ldap' => false,
                'ldap_reason' => 'tenant_not_found',
            ];
        }

        $microsoftResult = $this->getEffectiveMicrosoftProviderConfig($tenant);
        $ldapResult = $this->getEffectiveLdapProviderConfig($tenant);

        return [
            'local' => $this->isLocalPasswordLoginAllowed($tenantId),
            'microsoft' => !empty($microsoftResult['ok']),
            'microsoft_reason' => $microsoftResult['ok'] ?? false
                ? ''
                : (string) ($microsoftResult['error'] ?? 'sso_disabled'),
            'ldap' => !empty($ldapResult['ok']),
            'ldap_reason' => $ldapResult['ok'] ?? false
                ? ''
                : (string) ($ldapResult['error'] ?? 'ldap_disabled'),
        ];
    }

    public function getEffectiveMicrosoftProviderConfig(array $tenant): array
    {
        $tenantId = (int) ($tenant['id'] ?? 0);
        if ($tenantId <= 0) {
            return ['ok' => false, 'error' => 'tenant_not_found'];
        }

        $auth = $this->getTenantMicrosoftAuth($tenantId);
        if (!$auth['enabled']) {
            return ['ok' => false, 'error' => 'sso_disabled'];
        }

        $entraTenantId = $auth['entra_tenant_id'];
        if (!preg_match('/^[a-f0-9-]{36}$/', $entraTenantId)) {
            return ['ok' => false, 'error' => 'tenant_id_invalid'];
        }

        $useSharedApp = $auth['use_shared_app'];
        $clientId = '';
        $clientSecret = '';

        if ($useSharedApp) {
            $clientId = $this->settingsGateway->getMicrosoftSharedClientId();
            $clientSecret = $this->settingsGateway->getMicrosoftSharedClientSecret();
            if (trim($clientId) === '' || trim($clientSecret) === '') {
                return ['ok' => false, 'error' => 'shared_credentials_missing'];
            }
        } else {
            $clientId = (string) $auth['client_id_override'];
            if (trim($clientId) === '') {
                return ['ok' => false, 'error' => 'client_id_override_required'];
            }
            if ($auth['client_secret_override_enc'] === '') {
                return ['ok' => false, 'error' => 'client_secret_override_required'];
            }
            try {
                $clientSecret = $this->cryptoGateway->decryptString($auth['client_secret_override_enc']);
            } catch (\Throwable $exception) {
                return ['ok' => false, 'error' => 'secret_invalid'];
            }
            if (trim($clientSecret) === '') {
                return ['ok' => false, 'error' => 'client_secret_override_required'];
            }
        }

        $clientId = trim($clientId);
        $clientSecret = trim($clientSecret);

        return [
            'ok' => true,
            'config' => [
                'provider' => self::PROVIDER_MICROSOFT,
                'tenant_id' => $entraTenantId,
                'client_id' => $clientId,
                'client_secret' => $clientSecret,
                'allowed_domains' => $auth['allowed_domains'],
                'enforce_microsoft_login' => $auth['enforce_microsoft_login'],
                'sync_profile_on_login' => $auth['sync_profile_on_login'],
                'sync_profile_fields' => $auth['sync_profile_fields_list'],
                'sync_profile_needs_graph' => $auth['sync_profile_on_login']
                    && $this->profileSyncFieldsRequireGraph($auth['sync_profile_fields_list']),
                'authority' => $this->settingsGateway->getMicrosoftAuthority(),
                'use_shared_app' => $useSharedApp,
            ],
        ];
    }

    public function saveTenantMicrosoftAuth(int $tenantId, array $input): array
    {
        $tenant = $this->tenantGateway->findById($tenantId);
        if (!$tenant) {
            return ['ok' => false, 'errors' => [t('Tenant not found')]];
        }

        $enabled = !empty($input['microsoft_enabled']);
        $enforce = !empty($input['enforce_microsoft_login']);
        $entraTenantId = strtolower(trim((string) ($input['entra_tenant_id'] ?? '')));
        $allowedDomains = $this->normalizeDomains((string) ($input['allowed_domains'] ?? ''));
        $syncProfileOnLogin = !empty($input['sync_profile_on_login']);
        $syncProfileFields = $this->normalizeProfileSyncFields($input['sync_profile_fields'] ?? []);
        if ($syncProfileOnLogin && !$syncProfileFields) {
            $syncProfileFields = $this->defaultProfileSyncFields();
        }
        $syncProfileFieldsCsv = $this->profileSyncFieldsCsv($syncProfileFields);
        $useSharedApp = !empty($input['use_shared_app']);
        $clientIdOverride = trim((string) ($input['client_id_override'] ?? ''));
        $clientSecretOverride = trim((string) ($input['client_secret_override'] ?? ''));
        $clearSecret = !empty($input['clear_client_secret_override']);

        $errors = [];

        $existing = $this->getTenantMicrosoftAuth($tenantId);
        $clientSecretOverrideEnc = (string) ($existing['client_secret_override_enc'] ?? '');
        if ($clearSecret) {
            $clientSecretOverrideEnc = '';
        } elseif ($clientSecretOverride !== '') {
            if (!$this->cryptoGateway->isConfigured()) {
                $errors[] = t('APP_CRYPTO_KEY is missing or invalid');
            } else {
                try {
                    $clientSecretOverrideEnc = $this->cryptoGateway->encryptString($clientSecretOverride);
                } catch (\Throwable $exception) {
                    $errors[] = t('Client secret could not be encrypted');
                }
            }
        }

        $stateForValidation = [
            'enabled' => $enabled,
            'enforce_microsoft_login' => $enforce,
            'sync_profile_on_login' => $syncProfileOnLogin,
            'sync_profile_fields' => $syncProfileFields,
            'entra_tenant_id' => $entraTenantId,
            'use_shared_app' => $useSharedApp,
            'client_id_override' => $clientIdOverride,
            'client_secret_override_enc' => $clientSecretOverrideEnc,
        ];
        $configState = $this->evaluateMicrosoftConfigState($stateForValidation);
        if ($enabled && !($configState['complete'] ?? false)) {
            $label = $this->microsoftConfigErrorLabel((string) ($configState['error_code'] ?? ''));
            if ($label !== '') {
                $errors[] = $label;
            }
        }
        if ($enabled && $enforce && !($configState['complete'] ?? false)) {
            $errors[] = t('Microsoft-only login requires a complete configuration');
        }

        if ($errors) {
            return ['ok' => false, 'errors' => $errors];
        }

        $autoRememberMode = $this->normalizeAutoRememberMode($input['microsoft_auto_remember_mode'] ?? null);

        $saved = $this->tenantMicrosoftAuthRepository->upsertByTenantId($tenantId, [
            'enabled' => $enabled,
            'enforce_microsoft_login' => $enabled ? $enforce : false,
            'sync_profile_on_login' => $enabled ? $syncProfileOnLogin : false,
            'sync_profile_fields' => $syncProfileFieldsCsv !== '' ? $syncProfileFieldsCsv : null,
            'entra_tenant_id' => $enabled ? $entraTenantId : null,
            'allowed_domains' => $allowedDomains === '' ? null : $allowedDomains,
            'use_shared_app' => $useSharedApp,
            'client_id_override' => $useSharedApp ? null : ($clientIdOverride !== '' ? $clientIdOverride : null),
            'client_secret_override_enc' => $useSharedApp ? null : ($clientSecretOverrideEnc !== '' ? $clientSecretOverrideEnc : null),
            'auto_remember_mode' => $autoRememberMode,
        ]);

        if (!$saved) {
            return ['ok' => false, 'errors' => [t('Tenant SSO settings could not be saved')]];
        }

        return ['ok' => true];
    }

    // Empty allowedDomains list means all email domains are permitted.
    public function isEmailDomainAllowed(string $email, array $allowedDomains): bool
    {
        $allowedDomains = array_values(array_filter(array_map(
            static fn ($domain): string => strtolower(trim((string) $domain)),
            $allowedDomains
        ), static fn (string $domain): bool => $domain !== ''));

        if (!$allowedDomains) {
            return true;
        }

        $parts = explode('@', strtolower(trim($email)));
        if (count($parts) !== 2) {
            return false;
        }

        return in_array($parts[1], $allowedDomains, true);
    }

    public function profileSyncFieldOptions(): array
    {
        return self::PROFILE_SYNC_FIELD_OPTIONS;
    }

    public function defaultProfileSyncFields(): array
    {
        return self::DEFAULT_PROFILE_SYNC_FIELDS;
    }

    public function normalizeProfileSyncFields($raw): array
    {
        if (is_string($raw)) {
            $raw = preg_split('/[\s,;]+/', $raw) ?: [];
        } elseif (!is_array($raw)) {
            return [];
        }

        $allowed = array_fill_keys(array_keys(self::PROFILE_SYNC_FIELD_OPTIONS), true);
        $normalized = [];
        foreach ($raw as $item) {
            $key = strtolower(trim((string) $item));
            if ($key === '' || !isset($allowed[$key])) {
                continue;
            }
            $normalized[$key] = true;
        }

        return array_keys($normalized);
    }

    public function profileSyncFieldsCsv(array $fields): string
    {
        $fields = $this->normalizeProfileSyncFields($fields);
        return implode(',', $fields);
    }

    public function profileSyncFieldsRequireGraph(array $fields): bool
    {
        $fields = $this->normalizeProfileSyncFields($fields);
        return in_array('phone', $fields, true)
            || in_array('mobile', $fields, true)
            || in_array('avatar', $fields, true)
            || in_array('job_title', $fields, true)
            || in_array('display_name', $fields, true);
    }

    public function shouldAutoRememberOnMicrosoftLogin(int $tenantId): bool
    {
        $auth = $this->getTenantMicrosoftAuth($tenantId);
        $mode = $auth['auto_remember_mode'] ?? null;
        if ($mode === 1) {
            return true;
        }
        if ($mode === 0) {
            return false;
        }

        return $this->settingsGateway->isMicrosoftAutoRememberDefault();
    }

    private function normalizeAutoRememberMode(mixed $value): ?int
    {
        if ($value === null || $value === '' || $value === 'inherit') {
            return null;
        }
        $intVal = (int) $value;
        return in_array($intVal, [0, 1], true) ? $intVal : null;
    }

    private function normalizeDomains(string $raw): string
    {
        $parts = preg_split('/[,\n;\r]+/', $raw) ?: [];
        $domains = [];
        foreach ($parts as $part) {
            $domain = strtolower(trim($part));
            $domain = preg_replace('/^@+/', '', $domain);
            if ($domain === '') {
                continue;
            }
            if (!preg_match('/^[a-z0-9.-]+\.[a-z]{2,}$/i', $domain)) {
                continue;
            }
            $domains[] = $domain;
        }
        $domains = array_values(array_unique($domains));
        return implode(',', $domains);
    }

    private function normalizeMicrosoftInputState(array $input, array $existing): array
    {
        $enabled = array_key_exists('microsoft_enabled', $input)
            ? !empty($input['microsoft_enabled'])
            : !empty($existing['enabled']);
        $enforce = array_key_exists('enforce_microsoft_login', $input)
            ? !empty($input['enforce_microsoft_login'])
            : !empty($existing['enforce_microsoft_login']);
        $syncProfileOnLogin = array_key_exists('sync_profile_on_login', $input)
            ? !empty($input['sync_profile_on_login'])
            : !empty($existing['sync_profile_on_login']);
        $syncProfileFields = array_key_exists('sync_profile_fields', $input)
            ? $this->normalizeProfileSyncFields($input['sync_profile_fields'] ?? [])
            : $this->normalizeProfileSyncFields($existing['sync_profile_fields_list'] ?? []);
        if ($syncProfileOnLogin && !$syncProfileFields) {
            $syncProfileFields = $this->defaultProfileSyncFields();
        }

        $entraTenantId = array_key_exists('entra_tenant_id', $input)
            ? strtolower(trim((string) ($input['entra_tenant_id'] ?? '')))
            : strtolower(trim((string) ($existing['entra_tenant_id'] ?? '')));
        $useSharedApp = array_key_exists('use_shared_app', $input)
            ? !empty($input['use_shared_app'])
            : !empty($existing['use_shared_app']);
        $clientIdOverride = array_key_exists('client_id_override', $input)
            ? trim((string) ($input['client_id_override'] ?? ''))
            : trim((string) ($existing['client_id_override'] ?? ''));

        $clientSecretOverrideEnc = trim((string) ($existing['client_secret_override_enc'] ?? ''));
        if (array_key_exists('clear_client_secret_override', $input) && !empty($input['clear_client_secret_override'])) {
            $clientSecretOverrideEnc = '';
        } elseif (trim((string) ($input['client_secret_override'] ?? '')) !== '') {
            // For UI status, a newly typed secret is considered present before persisting.
            $clientSecretOverrideEnc = '__pending_secret__';
        }

        return [
            'enabled' => $enabled,
            'enforce_microsoft_login' => $enforce,
            'sync_profile_on_login' => $syncProfileOnLogin,
            'sync_profile_fields' => $syncProfileFields,
            'entra_tenant_id' => $entraTenantId,
            'use_shared_app' => $useSharedApp,
            'client_id_override' => $clientIdOverride,
            'client_secret_override_enc' => $clientSecretOverrideEnc,
        ];
    }

    private function evaluateMicrosoftConfigState(array $state): array
    {
        if (empty($state['enabled'])) {
            return ['complete' => true, 'error_code' => ''];
        }

        $entraTenantId = strtolower(trim((string) ($state['entra_tenant_id'] ?? '')));
        if (!preg_match('/^[a-f0-9-]{36}$/', $entraTenantId)) {
            return ['complete' => false, 'error_code' => 'tenant_id_invalid'];
        }

        if (!empty($state['use_shared_app'])) {
            $sharedClientId = trim($this->settingsGateway->getMicrosoftSharedClientId());
            $sharedClientSecret = trim($this->settingsGateway->getMicrosoftSharedClientSecret());
            if ($sharedClientId === '' || $sharedClientSecret === '') {
                return ['complete' => false, 'error_code' => 'shared_credentials_missing'];
            }
            return ['complete' => true, 'error_code' => ''];
        }

        $clientIdOverride = trim((string) ($state['client_id_override'] ?? ''));
        if ($clientIdOverride === '') {
            return ['complete' => false, 'error_code' => 'client_id_override_required'];
        }

        $secretEnc = trim((string) ($state['client_secret_override_enc'] ?? ''));
        if ($secretEnc === '') {
            return ['complete' => false, 'error_code' => 'client_secret_override_required'];
        }
        if ($secretEnc !== '__pending_secret__') {
            try {
                $secret = trim($this->cryptoGateway->decryptString($secretEnc));
            } catch (\Throwable $exception) {
                return ['complete' => false, 'error_code' => 'secret_invalid'];
            }
            if ($secret === '') {
                return ['complete' => false, 'error_code' => 'client_secret_override_required'];
            }
        }

        return ['complete' => true, 'error_code' => ''];
    }

    private function microsoftConfigErrorLabel(string $errorCode): string
    {
        return match ($errorCode) {
            'tenant_id_invalid' => t('Entra tenant ID is invalid'),
            'shared_credentials_missing' => t('Shared credentials missing'),
            'client_id_override_required' => t('Client ID override is required'),
            'client_secret_override_required' => t('Client secret override is required'),
            'secret_invalid' => t('Client secret override is invalid'),
            'client_credentials_missing' => t('Client credentials are missing'),
            default => '',
        };
    }

    // ── LDAP provider methods ─────────────────────────────────────────

    public function getTenantLdapAuth(int $tenantId): array
    {
        $row = $this->tenantLdapAuthRepository->findByTenantId($tenantId) ?? [];
        $syncProfileOnLogin = !empty($row['sync_profile_on_login']);
        $syncFields = $this->normalizeLdapProfileSyncFields($row['sync_profile_fields'] ?? []);
        if ($syncProfileOnLogin && !$syncFields) {
            $syncFields = self::DEFAULT_PROFILE_SYNC_FIELDS;
        }

        return [
            'enabled' => !empty($row['enabled']),
            'enforce_ldap_login' => !empty($row['enforce_ldap_login']),
            'host' => trim((string) ($row['host'] ?? '')),
            'port' => (int) ($row['port'] ?? 389),
            'encryption_mode' => $this->normalizeLdapEnum($row['encryption_mode'] ?? 'starttls', self::VALID_LDAP_ENCRYPTION_MODES, 'starttls'),
            'verify_tls_certificate' => !array_key_exists('verify_tls_certificate', $row) || !empty($row['verify_tls_certificate']),
            'base_dn' => trim((string) ($row['base_dn'] ?? '')),
            'bind_dn_enc' => trim((string) ($row['bind_dn_enc'] ?? '')),
            'bind_password_enc' => trim((string) ($row['bind_password_enc'] ?? '')),
            'user_search_filter' => trim((string) ($row['user_search_filter'] ?? '(&(objectClass=user)(sAMAccountName=%s))')),
            'user_search_scope' => $this->normalizeLdapEnum($row['user_search_scope'] ?? 'sub', self::VALID_LDAP_SEARCH_SCOPES, 'sub'),
            'bind_method' => $this->normalizeLdapEnum($row['bind_method'] ?? 'search_then_bind', self::VALID_LDAP_BIND_METHODS, 'search_then_bind'),
            'bind_username_format' => trim((string) ($row['bind_username_format'] ?? '')),
            'unique_id_attribute' => trim((string) ($row['unique_id_attribute'] ?? 'objectGUID')),
            'attribute_map' => $this->normalizeLdapAttributeMap($row['attribute_map'] ?? null),
            'sync_profile_on_login' => $syncProfileOnLogin,
            'sync_profile_fields' => $this->profileSyncFieldsCsv($syncFields),
            'sync_profile_fields_list' => $syncFields,
            'allowed_domains' => $this->normalizeDomains((string) ($row['allowed_domains'] ?? '')),
            'auto_remember_mode' => array_key_exists('auto_remember_mode', $row) && $row['auto_remember_mode'] !== null
                ? (int) $row['auto_remember_mode']
                : null,
            'network_timeout' => (int) ($row['network_timeout'] ?? 5),
        ];
    }

    public function getEffectiveLdapProviderConfig(array $tenant): array
    {
        $tenantId = (int) ($tenant['id'] ?? 0);
        if ($tenantId <= 0) {
            return ['ok' => false, 'error' => 'tenant_not_found'];
        }

        $auth = $this->getTenantLdapAuth($tenantId);
        if (!$auth['enabled']) {
            return ['ok' => false, 'error' => 'ldap_disabled'];
        }

        if ($auth['host'] === '') {
            return ['ok' => false, 'error' => 'ldap_host_missing'];
        }

        if ($auth['base_dn'] === '') {
            return ['ok' => false, 'error' => 'ldap_base_dn_missing'];
        }

        $bindDn = '';
        $bindPassword = '';
        if ($auth['bind_dn_enc'] !== '') {
            try {
                $bindDn = $this->cryptoGateway->decryptString($auth['bind_dn_enc']);
            } catch (\Throwable) {
                return ['ok' => false, 'error' => 'ldap_bind_dn_decrypt_failed'];
            }
        }
        if ($auth['bind_password_enc'] !== '') {
            try {
                $bindPassword = $this->cryptoGateway->decryptString($auth['bind_password_enc']);
            } catch (\Throwable) {
                return ['ok' => false, 'error' => 'ldap_bind_password_decrypt_failed'];
            }
        }

        return [
            'ok' => true,
            'config' => [
                'provider' => self::PROVIDER_LDAP,
                'host' => $auth['host'],
                'port' => $auth['port'],
                'encryption_mode' => $auth['encryption_mode'],
                'verify_tls_certificate' => $auth['verify_tls_certificate'],
                'base_dn' => $auth['base_dn'],
                'bind_dn' => $bindDn,
                'bind_password' => $bindPassword,
                'user_search_filter' => $auth['user_search_filter'],
                'user_search_scope' => $auth['user_search_scope'],
                'bind_method' => $auth['bind_method'],
                'bind_username_format' => $auth['bind_username_format'],
                'unique_id_attribute' => $auth['unique_id_attribute'],
                'attribute_map' => $auth['attribute_map'],
                'sync_profile_on_login' => $auth['sync_profile_on_login'],
                'sync_profile_fields' => $auth['sync_profile_fields_list'],
                'allowed_domains' => $auth['allowed_domains'],
                'enforce_ldap_login' => $auth['enforce_ldap_login'],
                'network_timeout' => $auth['network_timeout'],
            ],
        ];
    }

    public function buildLdapUiState(int $tenantId, array $input = []): array
    {
        $existing = $this->getTenantLdapAuth($tenantId);
        $enabled = array_key_exists('ldap_enabled', $input)
            ? !empty($input['ldap_enabled'])
            : $existing['enabled'];
        $enforce = array_key_exists('enforce_ldap_login', $input)
            ? !empty($input['enforce_ldap_login'])
            : $existing['enforce_ldap_login'];
        $host = array_key_exists('ldap_host', $input)
            ? trim((string) ($input['ldap_host'] ?? ''))
            : $existing['host'];
        $baseDn = array_key_exists('ldap_base_dn', $input)
            ? trim((string) ($input['ldap_base_dn'] ?? ''))
            : $existing['base_dn'];

        $configComplete = !$enabled || ($host !== '' && $baseDn !== '');
        $configErrorCode = '';
        if ($enabled && $host === '') {
            $configErrorCode = 'ldap_host_missing';
        } elseif ($enabled && $baseDn === '') {
            $configErrorCode = 'ldap_base_dn_missing';
        }

        return [
            'enabled' => $enabled,
            'config_complete' => $configComplete,
            'config_error_code' => $configErrorCode,
            'config_error_label' => $this->ldapConfigErrorLabel($configErrorCode),
            'password_mode' => !$enabled
                ? 'local_only'
                : ($enforce ? 'ldap_only' : 'local_and_ldap'),
        ];
    }

    public function saveTenantLdapAuth(int $tenantId, array $input): array
    {
        $tenant = $this->tenantGateway->findById($tenantId);
        if (!$tenant) {
            return ['ok' => false, 'errors' => [t('Tenant not found')]];
        }

        $enabled = !empty($input['ldap_enabled']);
        $enforce = !empty($input['enforce_ldap_login']);
        $host = trim((string) ($input['ldap_host'] ?? ''));
        $port = (int) ($input['ldap_port'] ?? 389);
        $encryptionMode = $this->normalizeLdapEnum($input['ldap_encryption_mode'] ?? 'starttls', self::VALID_LDAP_ENCRYPTION_MODES, 'starttls');
        $verifyTlsCertificate = !empty($input['ldap_verify_tls_certificate']);
        $baseDn = trim((string) ($input['ldap_base_dn'] ?? ''));
        $bindDn = trim((string) ($input['ldap_bind_dn'] ?? ''));
        $bindPassword = trim((string) ($input['ldap_bind_password'] ?? ''));
        $clearBindPassword = !empty($input['clear_ldap_bind_password']);
        $userSearchFilter = trim((string) ($input['ldap_user_search_filter'] ?? '(&(objectClass=user)(sAMAccountName=%s))'));
        $userSearchScope = $this->normalizeLdapEnum($input['ldap_user_search_scope'] ?? 'sub', self::VALID_LDAP_SEARCH_SCOPES, 'sub');
        $bindMethod = $this->normalizeLdapEnum($input['ldap_bind_method'] ?? 'search_then_bind', self::VALID_LDAP_BIND_METHODS, 'search_then_bind');
        $bindUsernameFormat = trim((string) ($input['ldap_bind_username_format'] ?? ''));
        $uniqueIdAttribute = trim((string) ($input['ldap_unique_id_attribute'] ?? 'objectGUID'));
        $attributeMap = $this->normalizeLdapAttributeMap($input['ldap_attribute_map'] ?? null);
        $syncProfileOnLogin = !empty($input['ldap_sync_profile_on_login']);
        $syncProfileFields = $this->normalizeLdapProfileSyncFields($input['ldap_sync_profile_fields'] ?? []);
        if ($syncProfileOnLogin && !$syncProfileFields) {
            $syncProfileFields = self::DEFAULT_PROFILE_SYNC_FIELDS;
        }
        $syncProfileFieldsCsv = $this->profileSyncFieldsCsv($syncProfileFields);
        $allowedDomains = $this->normalizeDomains((string) ($input['ldap_allowed_domains'] ?? ''));
        $networkTimeout = max(1, min(30, (int) ($input['ldap_network_timeout'] ?? 5)));

        $errors = [];

        if ($enabled && $host === '') {
            $errors[] = t('LDAP host is required');
        }
        if ($enabled && $baseDn === '') {
            $errors[] = t('LDAP base DN is required');
        }
        if ($enabled && ($port < 1 || $port > 65535)) {
            $errors[] = t('LDAP port must be between 1 and 65535');
        }
        if ($enabled && !str_contains($userSearchFilter, '%s')) {
            $errors[] = t('Search filter must contain %s placeholder');
        }
        if ($enabled && $enforce && ($host === '' || $baseDn === '')) {
            $errors[] = t('LDAP-only login requires a complete configuration');
        }
        if ($bindMethod === 'direct_bind' && $bindUsernameFormat === '') {
            $errors[] = t('Direct bind method requires a username format');
        }

        $existing = $this->getTenantLdapAuth($tenantId);

        $bindDnEnc = (string) ($existing['bind_dn_enc'] ?? '');
        if ($bindDn !== '') {
            if (!$this->cryptoGateway->isConfigured()) {
                $errors[] = t('APP_CRYPTO_KEY is missing or invalid');
            } else {
                try {
                    $bindDnEnc = $this->cryptoGateway->encryptString($bindDn);
                } catch (\Throwable) {
                    $errors[] = t('Bind DN could not be encrypted');
                }
            }
        }

        $bindPasswordEnc = (string) ($existing['bind_password_enc'] ?? '');
        if ($clearBindPassword) {
            $bindPasswordEnc = '';
        } elseif ($bindPassword !== '') {
            if (!$this->cryptoGateway->isConfigured()) {
                $errors[] = t('APP_CRYPTO_KEY is missing or invalid');
            } else {
                try {
                    $bindPasswordEnc = $this->cryptoGateway->encryptString($bindPassword);
                } catch (\Throwable) {
                    $errors[] = t('Bind password could not be encrypted');
                }
            }
        }

        if ($errors) {
            return ['ok' => false, 'errors' => $errors];
        }

        $autoRememberMode = $this->normalizeAutoRememberMode($input['ldap_auto_remember_mode'] ?? null);
        try {
            $attributeMapJson = json_encode($attributeMap, JSON_THROW_ON_ERROR);
        } catch (\JsonException) {
            return ['ok' => false, 'errors' => [t('LDAP attribute map could not be encoded')]];
        }

        $saved = $this->tenantLdapAuthRepository->upsertByTenantId($tenantId, [
            'enabled' => $enabled,
            'enforce_ldap_login' => $enabled ? $enforce : false,
            'host' => $host,
            'port' => $port,
            'encryption_mode' => $encryptionMode,
            'verify_tls_certificate' => $verifyTlsCertificate,
            'base_dn' => $baseDn,
            'bind_dn_enc' => $bindDnEnc !== '' ? $bindDnEnc : null,
            'bind_password_enc' => $bindPasswordEnc !== '' ? $bindPasswordEnc : null,
            'user_search_filter' => $userSearchFilter,
            'user_search_scope' => $userSearchScope,
            'bind_method' => $bindMethod,
            'bind_username_format' => $bindUsernameFormat !== '' ? $bindUsernameFormat : null,
            'unique_id_attribute' => $uniqueIdAttribute,
            'attribute_map' => $attributeMapJson,
            'sync_profile_on_login' => $enabled ? $syncProfileOnLogin : false,
            'sync_profile_fields' => $syncProfileFieldsCsv !== '' ? $syncProfileFieldsCsv : null,
            'allowed_domains' => $allowedDomains === '' ? null : $allowedDomains,
            'auto_remember_mode' => $autoRememberMode,
            'network_timeout' => $networkTimeout,
        ]);

        if (!$saved) {
            return ['ok' => false, 'errors' => [t('Tenant LDAP settings could not be saved')]];
        }

        return ['ok' => true];
    }

    public function shouldAutoRememberOnLdapLogin(int $tenantId): bool
    {
        $auth = $this->getTenantLdapAuth($tenantId);
        $mode = $auth['auto_remember_mode'] ?? null;
        if ($mode === 1) {
            return true;
        }
        if ($mode === 0) {
            return false;
        }

        return false;
    }

    public function ldapProfileSyncFieldOptions(): array
    {
        return self::LDAP_PROFILE_SYNC_FIELD_OPTIONS;
    }

    public function normalizeLdapProfileSyncFields($raw): array
    {
        if (is_string($raw)) {
            $raw = preg_split('/[\s,;]+/', $raw) ?: [];
        } elseif (!is_array($raw)) {
            return [];
        }

        $allowed = array_fill_keys(array_keys(self::LDAP_PROFILE_SYNC_FIELD_OPTIONS), true);
        $normalized = [];
        foreach ($raw as $item) {
            $key = strtolower(trim((string) $item));
            if ($key === '' || !isset($allowed[$key])) {
                continue;
            }
            $normalized[$key] = true;
        }

        return array_keys($normalized);
    }

    public function normalizeLdapAttributeMap(mixed $raw): array
    {
        $default = [
            'first_name' => 'givenName',
            'last_name' => 'sn',
            'email' => 'mail',
            'phone' => 'telephoneNumber',
            'mobile' => 'mobile',
        ];

        if ($raw === null || $raw === '') {
            return $default;
        }

        if (is_string($raw)) {
            $decoded = json_decode($raw, true);
            if (!is_array($decoded)) {
                return $default;
            }
            $raw = $decoded;
        }

        if (!is_array($raw)) {
            return $default;
        }

        $result = [];
        foreach ($raw as $key => $value) {
            $key = trim((string) $key);
            $value = trim((string) $value);
            if ($key !== '' && $value !== '') {
                $result[$key] = $value;
            }
        }

        return $result ?: $default;
    }

    private function ldapConfigErrorLabel(string $errorCode): string
    {
        return match ($errorCode) {
            'ldap_host_missing' => t('LDAP host is required'),
            'ldap_base_dn_missing' => t('LDAP base DN is required'),
            'ldap_bind_dn_decrypt_failed' => t('Bind DN could not be decrypted'),
            'ldap_bind_password_decrypt_failed' => t('Bind password could not be decrypted'),
            default => '',
        };
    }

    private function normalizeLdapEnum(string $value, array $valid, string $default): string
    {
        $value = strtolower(trim($value));
        return in_array($value, $valid, true) ? $value : $default;
    }

    private function slugify(string $value): string
    {
        $value = strtolower(trim($value));
        if ($value === '') {
            return '';
        }
        $value = preg_replace('/[^a-z0-9]+/', '-', $value) ?? '';
        return trim($value, '-');
    }
}
