<?php

namespace MintyPHP\Service\Auth;

use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
use MintyPHP\Service\User\UserAssignmentService;
use MintyPHP\Service\User\UserAvatarService;

class SsoUserLinkService
{
    public function __construct(
        private readonly UserReadRepositoryInterface $userReadRepository,
        private readonly UserWriteRepositoryInterface $userWriteRepository,
        private readonly UserAssignmentService $userAssignmentService,
        private readonly UserTenantRepositoryInterface $userTenantRepository,
        private readonly AuthSettingsGateway $settingsGateway,
        private readonly TenantSsoService $tenantSsoService,
        private readonly UserAvatarService $avatarService,
        private readonly AuthExternalIdentityGateway $externalIdentityGateway
    ) {
    }

    public function linkOrProvisionMicrosoftUser(array $tenant, array $claims): array
    {
        $tenantId = (int) ($tenant['id'] ?? 0);
        $tid = trim((string) ($claims['tid'] ?? ''));
        $oid = trim((string) ($claims['oid'] ?? ''));
        $issuer = trim((string) ($claims['issuer'] ?? ''));
        $subject = trim((string) ($claims['subject'] ?? ''));
        $email = strtolower(trim((string) ($claims['email'] ?? '')));

        if ($tenantId <= 0 || $tid === '' || $oid === '' || $issuer === '' || $subject === '') {
            return ['ok' => false, 'error' => 'identity_invalid'];
        }

        $providerKey = $this->tenantSsoService->microsoftProviderKey();
        // Look up by OID first (stable Microsoft object ID), fall back to issuer+subject
        // to handle tenants that migrated from an older OIDC setup without OID.
        $identity = $this->externalIdentityGateway->findByProviderTidOid(
            $providerKey,
            $tid,
            $oid
        );
        if (!$identity) {
            $identity = $this->externalIdentityGateway->findByProviderIssuerSubject(
                $providerKey,
                $issuer,
                $subject
            );
        }

        if ($identity) {
            $userId = (int) ($identity['user_id'] ?? 0);
            $user = $this->userReadRepository->find($userId);
            if (!$user || empty($user['active'])) {
                return ['ok' => false, 'error' => 'user_inactive'];
            }
            $tenantIds = array_values(array_map('intval', $this->userTenantRepository->listTenantIdsByUserId($userId)));
            if (!in_array($tenantId, $tenantIds, true)) {
                return ['ok' => false, 'error' => 'tenant_not_assigned'];
            }
            $this->syncProfileFromMicrosoft($userId, $claims);
            return ['ok' => true, 'user_id' => $userId, 'created' => false];
        }

        if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return ['ok' => false, 'error' => 'email_missing'];
        }

        $user = $this->userReadRepository->findByEmail($email);
        if ($user) {
            $userId = (int) ($user['id'] ?? 0);
            if ($userId <= 0 || empty($user['active'])) {
                return ['ok' => false, 'error' => 'user_inactive'];
            }

            $this->ensureTenantAssignment($userId, $tenantId);
            $this->createIdentityLink($userId, $tid, $oid, $issuer, $subject, $email);
            $this->syncProfileFromMicrosoft($userId, $claims);
            return ['ok' => true, 'user_id' => $userId, 'created' => false];
        }

        $firstName = trim((string) ($claims['given_name'] ?? ''));
        $lastName = trim((string) ($claims['family_name'] ?? ''));
        $fullName = trim((string) ($claims['name'] ?? ''));
        if ($firstName === '' && $lastName === '' && $fullName !== '') {
            $parts = preg_split('/\s+/', $fullName) ?: [];
            $firstName = (string) ($parts[0] ?? '');
            $lastName = implode(' ', array_slice($parts, 1));
        }
        if ($firstName === '') {
            $firstName = ucfirst(explode('@', $email, 2)[0]);
        }

        $createdId = $this->userWriteRepository->create([
            'first_name' => $firstName,
            'last_name' => $lastName,
            'email' => $email,
            'password' => $this->randomPassword(),
            'locale' => $this->normalizeLocale((string) ($claims['preferred_language'] ?? '')),
            'totp_secret' => '',
            'theme' => $this->resolveInitialTheme(null),
            'primary_tenant_id' => $tenantId,
            'current_tenant_id' => $tenantId,
            'active' => 1,
            'created_by' => null,
            'active_changed_at' => gmdate('Y-m-d H:i:s'),
            'active_changed_by' => null,
        ]);

        if (!$createdId) {
            return ['ok' => false, 'error' => 'user_create_failed'];
        }

        $userId = (int) $createdId;
        $this->userWriteRepository->setEmailVerified($userId);
        $this->userAssignmentService->syncTenants($userId, [$tenantId]);
        $defaultRoleId = $this->settingsGateway->getDefaultRoleId();
        if ($defaultRoleId) {
            $this->userAssignmentService->syncRoles($userId, [$defaultRoleId]);
        }
        $defaultDepartmentId = $this->settingsGateway->getDefaultDepartmentId();
        if ($defaultDepartmentId) {
            $this->userAssignmentService->syncDepartments($userId, [$defaultDepartmentId]);
        }

        $this->createIdentityLink($userId, $tid, $oid, $issuer, $subject, $email);
        $this->syncProfileFromMicrosoft($userId, $claims);
        return ['ok' => true, 'user_id' => $userId, 'created' => true];
    }

    private function syncProfileFromMicrosoft(int $userId, array $claims): void
    {
        if ($userId <= 0 || empty($claims['sync_profile_on_login'])) {
            return;
        }

        $syncFields = $this->tenantSsoService->normalizeProfileSyncFields($claims['sync_profile_fields'] ?? []);
        if (!$syncFields) {
            $syncFields = $this->tenantSsoService->defaultProfileSyncFields();
        }
        if (!$syncFields) {
            return;
        }
        $syncAvatar = in_array('avatar', $syncFields, true);

        $mappedValues = [
            'first_name' => trim((string) ($claims['given_name'] ?? '')) !== ''
                ? trim((string) ($claims['given_name'] ?? ''))
                : trim((string) ($claims['graph_given_name'] ?? '')),
            'last_name' => trim((string) ($claims['family_name'] ?? '')) !== ''
                ? trim((string) ($claims['family_name'] ?? ''))
                : trim((string) ($claims['graph_family_name'] ?? '')),
            'display_name' => trim((string) ($claims['graph_display_name'] ?? '')),
            'job_title' => trim((string) ($claims['graph_job_title'] ?? '')),
            'phone' => trim((string) ($claims['graph_phone'] ?? '')),
            'mobile' => trim((string) ($claims['graph_mobile'] ?? '')),
        ];

        $updates = [];
        foreach ($syncFields as $field) {
            if ($field === 'avatar') {
                continue;
            }
            $value = trim((string) ($mappedValues[$field] ?? ''));
            if ($value === '') {
                continue;
            }
            $updates[$field] = $value;
        }
        if (!$updates) {
            if ($syncAvatar) {
                $this->syncAvatarFromMicrosoft($userId, $claims);
            }
            return;
        }

        $this->userWriteRepository->updateProfileFieldsFromSso($userId, $updates);
        if ($syncAvatar) {
            $this->syncAvatarFromMicrosoft($userId, $claims);
        }
    }

    private function syncAvatarFromMicrosoft(int $userId, array $claims): void
    {
        $avatarBase64 = trim((string) ($claims['graph_avatar_data_base64'] ?? ''));
        if ($avatarBase64 === '') {
            return;
        }

        $avatarMime = strtolower(trim((string) ($claims['graph_avatar_mime'] ?? '')));
        if (!in_array($avatarMime, ['image/jpeg', 'image/png', 'image/webp'], true)) {
            return;
        }

        $binary = base64_decode($avatarBase64, true);
        if (!is_string($binary) || $binary === '') {
            return;
        }

        $user = $this->userReadRepository->find($userId);
        if (!$user) {
            return;
        }
        $userUuid = (string) ($user['uuid'] ?? '');
        if (!$this->avatarService->isValidUuid($userUuid)) {
            return;
        }

        // Keep avatar current with Microsoft profile photo on each login.
        $this->avatarService->saveBinary($userUuid, $binary, $avatarMime);
    }

    /**
     * Link or provision a user from LDAP authentication result.
     *
     * @param array $tenant     Tenant record
     * @param array $ldapResult Result from LdapAuthService::authenticate()
     * @param array $ldapConfig Effective LDAP provider config
     * @return array{ok: bool, user_id?: int, created?: bool, error?: string}
     */
    public function linkOrProvisionLdapUser(array $tenant, array $ldapResult, array $ldapConfig): array
    {
        $tenantId = (int) ($tenant['id'] ?? 0);
        $uniqueId = trim((string) ($ldapResult['unique_id'] ?? ''));
        $dn = trim((string) ($ldapResult['dn'] ?? ''));
        $attributes = $ldapResult['attributes'] ?? [];
        $host = trim((string) ($ldapConfig['host'] ?? ''));

        $email = strtolower(trim((string) ($attributes['email'] ?? '')));

        if ($tenantId <= 0 || $host === '') {
            return ['ok' => false, 'error' => 'identity_invalid'];
        }

        $providerKey = $this->tenantSsoService->ldapProviderKey();
        $tid = $host;
        $oid = $uniqueId !== '' ? $uniqueId : $dn;
        $issuer = $host;
        $subject = $dn;

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

        $identity = $this->externalIdentityGateway->findByProviderTidOid($providerKey, $tid, $oid);

        if ($identity) {
            $userId = (int) ($identity['user_id'] ?? 0);
            $user = $this->userReadRepository->find($userId);
            if (!$user || empty($user['active'])) {
                return ['ok' => false, 'error' => 'user_inactive'];
            }
            $tenantIds = array_values(array_map('intval', $this->userTenantRepository->listTenantIdsByUserId($userId)));
            if (!in_array($tenantId, $tenantIds, true)) {
                return ['ok' => false, 'error' => 'tenant_not_assigned'];
            }
            $this->syncProfileFromLdap($userId, $attributes, $ldapConfig);
            return ['ok' => true, 'user_id' => $userId, 'created' => false];
        }

        if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $user = $this->userReadRepository->findByEmail($email);
            if ($user) {
                $userId = (int) ($user['id'] ?? 0);
                if ($userId <= 0 || empty($user['active'])) {
                    return ['ok' => false, 'error' => 'user_inactive'];
                }

                $this->ensureTenantAssignment($userId, $tenantId);
                $this->createIdentityLinkForProvider($providerKey, $userId, $tid, $oid, $issuer, $subject, $email);
                $this->syncProfileFromLdap($userId, $attributes, $ldapConfig);
                return ['ok' => true, 'user_id' => $userId, 'created' => false];
            }
        }

        if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return ['ok' => false, 'error' => 'email_missing'];
        }

        $firstName = trim((string) ($attributes['first_name'] ?? ''));
        $lastName = trim((string) ($attributes['last_name'] ?? ''));
        if ($firstName === '') {
            $firstName = ucfirst(explode('@', $email, 2)[0]);
        }

        $createdId = $this->userWriteRepository->create([
            'first_name' => $firstName,
            'last_name' => $lastName,
            'email' => $email,
            'password' => $this->randomPassword(),
            'locale' => null,
            'totp_secret' => '',
            'theme' => $this->resolveInitialTheme(null),
            'primary_tenant_id' => $tenantId,
            'current_tenant_id' => $tenantId,
            'active' => 1,
            'created_by' => null,
            'active_changed_at' => gmdate('Y-m-d H:i:s'),
            'active_changed_by' => null,
        ]);

        if (!$createdId) {
            return ['ok' => false, 'error' => 'user_create_failed'];
        }

        $userId = (int) $createdId;
        $this->userWriteRepository->setEmailVerified($userId);
        $this->userAssignmentService->syncTenants($userId, [$tenantId]);
        $defaultRoleId = $this->settingsGateway->getDefaultRoleId();
        if ($defaultRoleId) {
            $this->userAssignmentService->syncRoles($userId, [$defaultRoleId]);
        }
        $defaultDepartmentId = $this->settingsGateway->getDefaultDepartmentId();
        if ($defaultDepartmentId) {
            $this->userAssignmentService->syncDepartments($userId, [$defaultDepartmentId]);
        }

        $this->createIdentityLinkForProvider($providerKey, $userId, $tid, $oid, $issuer, $subject, $email);
        $this->syncProfileFromLdap($userId, $attributes, $ldapConfig);
        return ['ok' => true, 'user_id' => $userId, 'created' => true];
    }

    private function syncProfileFromLdap(int $userId, array $attributes, array $ldapConfig): void
    {
        if ($userId <= 0 || empty($ldapConfig['sync_profile_on_login'])) {
            return;
        }

        $syncFields = $this->tenantSsoService->normalizeLdapProfileSyncFields($ldapConfig['sync_profile_fields'] ?? []);
        if (!$syncFields) {
            $syncFields = $this->tenantSsoService->defaultProfileSyncFields();
        }
        if (!$syncFields) {
            return;
        }

        $updates = [];
        foreach ($syncFields as $field) {
            $value = trim((string) ($attributes[$field] ?? ''));
            if ($value === '') {
                continue;
            }
            $updates[$field] = $value;
        }

        if ($updates) {
            $this->userWriteRepository->updateProfileFieldsFromSso($userId, $updates);
        }
    }

    private function createIdentityLinkForProvider(
        string $providerKey,
        int $userId,
        string $tid,
        string $oid,
        string $issuer,
        string $subject,
        string $email
    ): void {
        try {
            $this->externalIdentityGateway->createLink([
                'user_id' => $userId,
                'provider' => $providerKey,
                'oid' => $oid,
                'tid' => $tid,
                'issuer' => $issuer,
                'subject' => $subject,
                'email_at_link_time' => $email,
            ]);
        } catch (\Throwable) {
            // Ignore duplicate-link race conditions.
        }
    }

    private function ensureTenantAssignment(int $userId, int $tenantId): void
    {
        $tenantIds = toIntIds($this->userTenantRepository->listTenantIdsByUserId($userId));
        if (!in_array($tenantId, $tenantIds, true)) {
            $tenantIds[] = $tenantId;
            $this->userAssignmentService->syncTenants($userId, $tenantIds);
        }
    }

    private function createIdentityLink(
        int $userId,
        string $tid,
        string $oid,
        string $issuer,
        string $subject,
        string $email
    ): void {
        try {
            $providerKey = $this->tenantSsoService->microsoftProviderKey();
            $this->externalIdentityGateway->createLink([
                'user_id' => $userId,
                'provider' => $providerKey,
                'oid' => $oid,
                'tid' => $tid,
                'issuer' => $issuer,
                'subject' => $subject,
                'email_at_link_time' => $email,
            ]);
        } catch (\Throwable $exception) {
            // Ignore duplicate-link race conditions.
        }
    }

    private function normalizeLocale(string $locale): ?string
    {
        $locale = strtolower(trim($locale));
        if ($locale === '') {
            return null;
        }
        if (str_contains($locale, '-')) {
            $locale = explode('-', $locale, 2)[0];
        }
        $allowed = defined('APP_LOCALES') ? APP_LOCALES : [];
        if ($allowed && !in_array($locale, $allowed, true)) {
            return null;
        }
        return $locale;
    }

    // SSO users never log in with a password, but the DB column is NOT NULL.
    // The suffix ensures the hash satisfies any strength validators.
    private function randomPassword(): string
    {
        return 'sso-' . bin2hex(random_bytes(24)) . '-A1!';
    }

    private function resolveInitialTheme(?string $theme): string
    {
        return $this->settingsGateway->normalizeTheme($theme);
    }
}
