using System.Text.Json;
using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens;
using Tiku.Application.Auth;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
using Tiku.Domain.Identity;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Auth;

public sealed class AuthService(
    TikuDbContext dbContext,
    SignInManager<User> signInManager,
    UserManager<User> userManager,
    ISmsVerificationService smsVerificationService,
    IAuthSessionStore sessionStore,
    IWechatOAuthClient wechatOAuthClient,
    ITenantExternalProviderConfigService providerConfigService,
    IFeatureAccessService featureAccessService) : IAuthService
{
    private const string PasswordProvider = "password";
    private const string SmsProvider = "sms";
    private const string WechatWebProvider = "wechat_web";
    private const string WechatMiniAppProvider = "wechat_miniapp";
    private static readonly string[] WechatWebProviderAliases = ["wechat_web", "wechat-web", "wechat"];
    private static readonly string[] WechatMiniAppProviderAliases = ["wechat-miniapp", "wechat_miniapp", "wechat-mini", "wechatMiniapp"];
    private static readonly string[] WechatIdentityProviders = ["wechat_web", "wechat-web", "wechat", "wechat-miniapp", "wechat_miniapp", "wechat-mini", "wechatMiniapp"];

    public async Task<AuthenticationResult> LoginWithPasswordAsync(
        PasswordLoginRequest request,
        CancellationToken cancellationToken = default)
    {
        var identifier = request.Phone.Trim();
        var normalizedEmail = userManager.NormalizeEmail(identifier);
        var normalizedUserName = userManager.NormalizeName(identifier);
        var user = await dbContext.Users
            .SingleOrDefaultAsync(entity =>
                entity.Phone == identifier ||
                entity.NormalizedEmail == normalizedEmail ||
                entity.NormalizedUserName == normalizedUserName,
                cancellationToken);
        var passwordResult = user is null || user.Status != UserStatus.Active
            ? SignInResult.Failed
            : await signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true);

        if (!passwordResult.Succeeded)
        {
            var loginResult = passwordResult.IsLockedOut
                ? AuthLoginResult.Blocked
                : AuthLoginResult.Failed;
            var failureCode = passwordResult.IsLockedOut
                ? "account_locked"
                : "invalid_credentials";
            await AddLoginEventAsync(
                request.TenantId,
                user?.Id,
                PasswordProvider,
                identifier,
                loginResult,
                failureCode,
                request.IpAddress,
                request.UserAgent,
                cancellationToken);
            throw new InvalidCredentialsException();
        }

        return await CompleteSuccessfulLoginAsync(
            request.Realm,
            request.TenantId,
            user!,
            PasswordProvider,
            identifier,
            request.IpAddress,
            request.UserAgent,
            cancellationToken);
    }

    public async Task<AuthenticationResult> LoginWithSmsAsync(
        SmsLoginRequest request,
        CancellationToken cancellationToken = default)
    {
        var phone = SmsCodeHashing.NormalizePhone(request.Phone);
        var user = await dbContext.Users
            .SingleOrDefaultAsync(entity => entity.Phone == phone, cancellationToken);

        try
        {
            if (!request.TenantId.HasValue)
            {
                throw new InvalidCredentialsException("tenant_required_for_sms");
            }
            await smsVerificationService.VerifyCodeAsync(
                request.TenantId.Value,
                phone,
                SmsPurpose.Login,
                request.Code,
                cancellationToken);
        }
        catch (InvalidCredentialsException exception)
        {
            await AddLoginEventAsync(
                request.TenantId,
                user?.Id,
                SmsProvider,
                phone,
                AuthLoginResult.Failed,
                exception.Code,
                request.IpAddress,
                request.UserAgent,
                cancellationToken);
            throw;
        }

        if (user is null)
        {
            await AddLoginEventAsync(
                request.TenantId,
                null,
                SmsProvider,
                phone,
                AuthLoginResult.Failed,
                "user_not_found",
                request.IpAddress,
                request.UserAgent,
                cancellationToken);
            throw new InvalidCredentialsException();
        }

        return await CompleteSuccessfulLoginAsync(
            request.Realm,
            request.TenantId,
            user,
            SmsProvider,
            phone,
            request.IpAddress,
            request.UserAgent,
            cancellationToken);
    }

    public Task<AuthenticationResult> LoginWithWechatWebAsync(
        WechatLoginRequest request,
        CancellationToken cancellationToken = default)
    {
        return LoginWithWechatAsync(
            request,
            WechatWebProvider,
            WechatWebProviderAliases,
            (options, code, token) => wechatOAuthClient.ExchangeWebCodeAsync(options, code, token),
            cancellationToken);
    }

    public Task<AuthenticationResult> LoginWithWechatMiniAppAsync(
        WechatLoginRequest request,
        CancellationToken cancellationToken = default)
    {
        return LoginWithWechatAsync(
            request,
            WechatMiniAppProvider,
            WechatMiniAppProviderAliases,
            (options, code, token) => wechatOAuthClient.ExchangeMiniAppCodeAsync(options, code, token),
            cancellationToken);
    }

    public async Task<AuthTokenPair> RefreshAsync(
        RefreshSessionRequest request,
        CancellationToken cancellationToken = default)
    {
        return await sessionStore.RotateAsync(
            request.RefreshToken, request.IpAddress, request.UserAgent, cancellationToken);
    }

    public async Task LogoutAsync(
        LogoutSessionRequest request,
        CancellationToken cancellationToken = default)
    {
        await sessionStore.RevokeFamilyAsync(request.RefreshToken, "logout", cancellationToken);
    }

    public async Task LogoutAllAsync(Guid userId, CancellationToken cancellationToken = default)
    {
        var user = await userManager.FindByIdAsync(userId.ToString())
            ?? throw new InvalidCredentialsException();
        var stampResult = await userManager.UpdateSecurityStampAsync(user);
        if (!stampResult.Succeeded)
        {
            throw new InvalidOperationException("Unable to update the user's security stamp.");
        }

        await sessionStore.RevokeAllAsync(userId, "logout_all", cancellationToken);
    }

    public async Task<AuthenticationResult> ChangeRequiredPasswordAsync(
        PasswordChangeChallengeRequest request,
        CancellationToken cancellationToken = default)
    {
        var challenge = await FindChallengeAsync(
            request.ChallengeToken, AuthChallengePurpose.PasswordChange, cancellationToken);
        var user = await userManager.FindByIdAsync(challenge.UserId.ToString())
            ?? throw new InvalidAuthChallengeException();
        var resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
        var reset = await userManager.ResetPasswordAsync(user, resetToken, request.NewPassword);
        if (!reset.Succeeded)
        {
            throw new InvalidCredentialsException("invalid_new_password");
        }

        user.ForcePasswordChange = false;
        var updated = await userManager.UpdateAsync(user);
        if (!updated.Succeeded)
        {
            throw new InvalidOperationException("Unable to clear the password-change requirement.");
        }

        await sessionStore.RevokeAllAsync(user.Id, "password_changed", cancellationToken);
        await ConsumeChallengeAsync(challenge, cancellationToken);
        await AddSecurityAuditAsync(
            user.Id, challenge.TenantId, "auth.password.changed", null,
            request.IpAddress, request.UserAgent, cancellationToken);
        return await CompleteSuccessfulLoginAsync(
            challenge.Realm, challenge.TenantId, user, challenge.Provider, user.Email ?? user.Phone ?? user.Id.ToString(),
            request.IpAddress, request.UserAgent, cancellationToken);
    }

    private async Task<AuthChallenge> FindChallengeAsync(
        string token,
        AuthChallengePurpose purpose,
        CancellationToken cancellationToken)
    {
        var tokenHash = HashChallengeToken(token);
        var now = DateTimeOffset.UtcNow;
        return await dbContext.AuthChallenges.SingleOrDefaultAsync(
                   item => item.TokenHash == tokenHash && item.Purpose == purpose &&
                           item.ConsumedAt == null && item.ExpiresAt > now &&
                           dbContext.Users.Any(user =>
                               user.Id == item.UserId && user.Status == UserStatus.Active &&
                               user.SecurityStamp == item.SecurityStamp),
                   cancellationToken)
               ?? throw new InvalidAuthChallengeException();
    }

    private async Task ConsumeChallengeAsync(AuthChallenge challenge, CancellationToken cancellationToken)
    {
        var now = DateTimeOffset.UtcNow;
        var consumed = await dbContext.AuthChallenges
            .Where(item => item.Id == challenge.Id && item.ConsumedAt == null && item.ExpiresAt > now)
            .ExecuteUpdateAsync(setters => setters.SetProperty(item => item.ConsumedAt, now), cancellationToken);
        if (consumed != 1)
        {
            throw new InvalidAuthChallengeException();
        }
    }

    private async Task<AuthenticationResult> CompleteSuccessfulLoginAsync(
        AuthRealm realm,
        Guid? tenantId,
        User user,
        string provider,
        string identifier,
        string? ipAddress,
        string? userAgent,
        CancellationToken cancellationToken)
    {
        if (user.Status != UserStatus.Active)
        {
            await AddLoginEventAsync(
                tenantId, user.Id, provider, identifier, AuthLoginResult.Failed,
                "user_disabled", ipAddress, userAgent, cancellationToken);
            throw new InvalidCredentialsException();
        }

        TenantMembership? membership = null;
        Tenant? tenant = null;
        if (realm == AuthRealm.Tenant && tenantId.HasValue)
        {
            membership = await FindActiveMembershipAsync(tenantId.Value, user.Id, cancellationToken);
            tenant = await dbContext.Tenants.SingleOrDefaultAsync(
                item => item.Id == tenantId.Value && item.Status == TenantStatus.Active, cancellationToken);
            if (membership is null || tenant is null)
            {
                await AddLoginEventAsync(tenantId, user.Id, provider, identifier, AuthLoginResult.Failed,
                    "tenant_access_denied", ipAddress, userAgent, cancellationToken);
                throw new TenantAccessDeniedException();
            }
        }
        else if (realm == AuthRealm.Platform)
        {
            if (!await HasBackendPermissionsAsync(realm, tenantId, user.Id, cancellationToken))
            {
                await AddLoginEventAsync(
                    null, user.Id, provider, identifier, AuthLoginResult.Failed,
                    "platform_access_denied", ipAddress, userAgent, cancellationToken);
                throw new TenantAccessDeniedException();
            }
        }
        else
        {
            throw new TenantAccessDeniedException();
        }

        if (user.ForcePasswordChange)
        {
            return await CreateChallengeResultAsync(
                user, realm, tenantId, AuthChallengePurpose.PasswordChange, provider,
                AuthenticationStatus.PasswordChangeRequired, ipAddress, userAgent, cancellationToken);
        }

        return await IssueAuthenticatedResultAsync(
            user, realm, tenant, membership, provider,
            identifier, ipAddress, userAgent, cancellationToken);
    }

    private async Task<AuthenticationResult> IssueAuthenticatedResultAsync(
        User user,
        AuthRealm realm,
        Tenant? tenant,
        TenantMembership? membership,
        string provider,
        string? identifier,
        string? ipAddress,
        string? userAgent,
        CancellationToken cancellationToken)
    {
        var tokens = await sessionStore.IssueAsync(
            new AuthSessionIssueRequest(
                user.Id,
                user.Phone,
                user.Email,
                user.SecurityStamp ?? string.Empty,
                realm,
                tenant?.Id,
                provider,
                ipAddress,
                userAgent),
            cancellationToken);

        await AddLoginEventAsync(
            tenant?.Id,
            user.Id,
            provider,
            identifier,
            AuthLoginResult.Success,
            null,
            ipAddress,
            userAgent,
            cancellationToken);

        var tenantSummary = tenant is not null && membership is not null
            ? new TenantMembershipSummary(tenant.Id, tenant.Name, membership.Role, membership.Status)
            : null;
        return new AuthenticationResult(
            AuthenticationStatus.Authenticated,
            new AuthenticatedUser(user.Id, user.Phone, user.Email, user.Name, realm, tenantSummary, tokens));
    }

    private async Task<AuthenticationResult> LoginWithWechatAsync(
        WechatLoginRequest request,
        string provider,
        IReadOnlyList<string> providerAliases,
        Func<WechatProviderOptions, string, CancellationToken, Task<WechatIdentity>> exchangeCodeAsync,
        CancellationToken cancellationToken)
    {
        if (request.Realm != AuthRealm.Tenant || !request.TenantId.HasValue)
        {
            throw new InvalidCredentialsException("tenant_realm_required_for_wechat");
        }

        var config = await LoadWechatProviderOptionsAsync(
            request.TenantId.Value,
            provider,
            providerAliases,
            cancellationToken);

        WechatIdentity identity;
        try
        {
            identity = await exchangeCodeAsync(config, request.Code, cancellationToken);
        }
        catch (AuthException exception)
        {
            await AddLoginEventAsync(
                request.TenantId,
                null,
                provider,
                null,
                AuthLoginResult.Failed,
                exception.Code,
                request.IpAddress,
                request.UserAgent,
                cancellationToken);
            throw;
        }

        await using var transaction = dbContext.Database.CurrentTransaction is null
            ? await dbContext.Database.BeginTransactionAsync(cancellationToken)
            : null;
        var providerSubject = $"{config.AppId}:{identity.OpenId}";
        var user = await UpsertWechatUserAsync(
            provider,
            providerSubject,
            config.AppId,
            identity,
            cancellationToken);
        await EnsureTenantMembershipAsync(
            request.TenantId.Value,
            user.Id,
            cancellationToken);
        // Persist the external identity and membership together only after the
        // tenant policy and existing membership state have accepted the login.
        // A denied first login must not leave a user or provider identity behind.
        await dbContext.SaveChangesAsync(cancellationToken);
        if (transaction is not null)
        {
            await transaction.CommitAsync(cancellationToken);
        }

        return await CompleteSuccessfulLoginAsync(
            request.Realm,
            request.TenantId,
            user,
            provider,
            identity.OpenId,
            request.IpAddress,
            request.UserAgent,
            cancellationToken);
    }

    private async Task<WechatProviderOptions> LoadWechatProviderOptionsAsync(
        Guid tenantId,
        string provider,
        IReadOnlyList<string> aliases,
        CancellationToken cancellationToken)
    {
        TenantExternalProviderAccount? account = null;
        foreach (var alias in aliases)
        {
            try
            {
                account = await providerConfigService.GetActiveProviderAsync(
                    tenantId,
                    TenantExternalProviderCapability.Identity,
                    alias,
                    cancellationToken);
                break;
            }
            catch (TenantExternalProviderException)
            {
            }
        }

        if (account is null)
        {
            throw new AuthProviderNotConfiguredException(provider);
        }

        var appId = GetJsonString(account.ConfigPublic, "appId", "clientId");
        var appSecret = GetJsonString(account.SecretPayload, "appSecret", "clientSecret", "secret");
        if (string.IsNullOrWhiteSpace(appId) || string.IsNullOrWhiteSpace(appSecret))
        {
            throw new AuthProviderNotConfiguredException(provider);
        }

        return new WechatProviderOptions(appId, appSecret);
    }

    private async Task<User> UpsertWechatUserAsync(
        string provider,
        string providerSubject,
        string appId,
        WechatIdentity wechatIdentity,
        CancellationToken cancellationToken)
    {
        var existingIdentity = await dbContext.UserIdentities
            .SingleOrDefaultAsync(
                identity =>
                    identity.Provider == provider &&
                    identity.ProviderSubject == providerSubject,
                cancellationToken);
        var user = existingIdentity is null
            ? await FindUserByWechatUnionIdAsync(wechatIdentity.UnionId, cancellationToken)
            : await dbContext.Users.FindAsync([existingIdentity.UserId], cancellationToken);

        if (user is null)
        {
            user = new User
            {
                Name = wechatIdentity.Nickname,
                AvatarUrl = wechatIdentity.AvatarUrl,
                PrimaryRole = "student",
                RawProfile = CreateWechatRawProfile(wechatIdentity)
            };
            dbContext.Users.Add(user);
        }
        else
        {
            user.Name = string.IsNullOrWhiteSpace(user.Name) ? wechatIdentity.Nickname : user.Name;
            user.AvatarUrl = string.IsNullOrWhiteSpace(user.AvatarUrl) ? wechatIdentity.AvatarUrl : user.AvatarUrl;
        }

        if (existingIdentity is null)
        {
            existingIdentity = new UserIdentity
            {
                UserId = user.Id,
                Provider = provider,
                ProviderSubject = providerSubject
            };
            dbContext.UserIdentities.Add(existingIdentity);
        }

        existingIdentity.UserId = user.Id;
        existingIdentity.OpenId = wechatIdentity.OpenId;
        existingIdentity.UnionId = wechatIdentity.UnionId;

        return user;
    }

    private async Task<User?> FindUserByWechatUnionIdAsync(
        string? unionId,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(unionId))
        {
            return null;
        }

        var identity = await dbContext.UserIdentities
            .Where(entity =>
                entity.UnionId == unionId &&
                WechatIdentityProviders.Contains(entity.Provider))
            .OrderBy(entity => entity.CreatedAt)
            .FirstOrDefaultAsync(cancellationToken);

        return identity is null
            ? null
            : await dbContext.Users.FindAsync([identity.UserId], cancellationToken);
    }

    private async Task EnsureTenantMembershipAsync(
        Guid tenantId,
        Guid userId,
        CancellationToken cancellationToken)
    {
        var activeMembershipExists = await dbContext.TenantMemberships.AnyAsync(
            membership =>
                membership.TenantId == tenantId &&
                membership.UserId == userId &&
                membership.Status == MembershipStatus.Active,
            cancellationToken);

        if (activeMembershipExists)
        {
            return;
        }

        var studentMembership = await dbContext.TenantMemberships
            .FirstOrDefaultAsync(
                membership =>
                    membership.TenantId == tenantId &&
                    membership.UserId == userId &&
                    membership.Role == TenantRole.Student,
                cancellationToken);
        if (studentMembership is not null)
        {
            // Invited and Disabled memberships require an explicit administrator action.
            throw new TenantAccessDeniedException();
        }

        var policy = await dbContext.TenantAuthPolicies.AsNoTracking()
            .SingleOrDefaultAsync(item => item.TenantId == tenantId, cancellationToken);
        if (policy is not null && !policy.AllowExternalStudentSelfRegistration)
        {
            throw new TenantAccessDeniedException();
        }

        await featureAccessService.ConsumeQuotaIfConfiguredAsync(
            tenantId,
            SaasQuotaMetricCatalog.StudentCount,
            cancellationToken: cancellationToken);

        dbContext.TenantMemberships.Add(new TenantMembership
        {
            TenantId = tenantId,
            UserId = userId,
            Role = TenantRole.Student,
            Status = MembershipStatus.Active
        });
    }

    private async Task<TenantMembership?> FindActiveMembershipAsync(
        Guid tenantId,
        Guid userId,
        CancellationToken cancellationToken)
    {
        return await dbContext.TenantMemberships
            .Where(entity =>
                entity.TenantId == tenantId &&
                entity.UserId == userId &&
                entity.Status == MembershipStatus.Active)
            .OrderBy(entity => entity.Role)
            .FirstOrDefaultAsync(cancellationToken);
    }

    private async Task<AuthenticationResult> CreateChallengeResultAsync(
        User user,
        AuthRealm realm,
        Guid? tenantId,
        AuthChallengePurpose purpose,
        string provider,
        AuthenticationStatus status,
        string? ipAddress,
        string? userAgent,
        CancellationToken cancellationToken)
    {
        var realmCode = realm == AuthRealm.Tenant ? "t" : "p";
        var tenantCode = tenantId?.ToString("N") ?? "-";
        var rawToken = $"c1.{realmCode}.{tenantCode}.{Base64UrlEncoder.Encode(RandomNumberGenerator.GetBytes(48))}";
        var expiresAt = DateTimeOffset.UtcNow.AddMinutes(5);
        dbContext.AuthChallenges.Add(new AuthChallenge
        {
            UserId = user.Id,
            Realm = realm,
            TenantId = tenantId,
            Purpose = purpose,
            TokenHash = HashChallengeToken(rawToken),
            SecurityStamp = user.SecurityStamp ?? string.Empty,
            Provider = provider,
            ExpiresAt = expiresAt,
            IpAddress = ipAddress,
            UserAgent = userAgent
        });
        await dbContext.SaveChangesAsync(cancellationToken);
        await AddSecurityAuditAsync(
            user.Id, tenantId, "auth.challenge.issued", status.ToString(),
            ipAddress, userAgent, cancellationToken);
        return new AuthenticationResult(status, ChallengeToken: rawToken, ChallengeExpiresAt: expiresAt);
    }

    private async Task<bool> HasBackendPermissionsAsync(
        AuthRealm realm,
        Guid? tenantId,
        Guid userId,
        CancellationToken cancellationToken)
    {
        if (realm == AuthRealm.Platform)
        {
            return await (
                from userRole in dbContext.PlatformBackendUserRoles
                join role in dbContext.PlatformBackendRoles on userRole.RoleId equals role.Id
                join binding in dbContext.PlatformBackendRolePermissions on role.Id equals binding.RoleId
                join permission in dbContext.BackendPermissions on binding.PermissionCode equals permission.Code
                where userRole.UserId == userId &&
                      role.Status == Tiku.Domain.Operations.BackendRoleStatus.Active &&
                      (permission.Area == Tiku.Domain.Operations.BackendPermissionArea.Platform ||
                       permission.Area == Tiku.Domain.Operations.BackendPermissionArea.Both)
                select permission.Id).AnyAsync(cancellationToken);
        }

        if (!tenantId.HasValue)
        {
            return false;
        }

        return await (
            from userRole in dbContext.TenantBackendUserRoles
            join role in dbContext.TenantBackendRoles on userRole.RoleId equals role.Id
            join binding in dbContext.TenantBackendRolePermissions on role.Id equals binding.RoleId
            join permission in dbContext.BackendPermissions on binding.PermissionCode equals permission.Code
            where userRole.TenantId == tenantId.Value && userRole.UserId == userId &&
                  binding.TenantId == tenantId.Value &&
                  role.Status == Tiku.Domain.Operations.BackendRoleStatus.Active &&
                  (permission.Area == Tiku.Domain.Operations.BackendPermissionArea.Tenant ||
                   permission.Area == Tiku.Domain.Operations.BackendPermissionArea.Both)
            select permission.Id).AnyAsync(cancellationToken);
    }

    private static string HashChallengeToken(string token) =>
        Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token ?? string.Empty))).ToLowerInvariant();

    private async Task AddSecurityAuditAsync(
        Guid userId,
        Guid? tenantId,
        string action,
        string? reason,
        string? ipAddress,
        string? userAgent,
        CancellationToken cancellationToken)
    {
        dbContext.AuditLogs.Add(new Tiku.Domain.Operations.AuditLog
        {
            TenantId = tenantId,
            ActorUserId = userId,
            Action = action,
            TargetType = "user",
            TargetId = userId.ToString(),
            Details = JsonSerializer.SerializeToElement(new { reason }),
            IpAddress = ipAddress,
            UserAgent = userAgent
        });
        await dbContext.SaveChangesAsync(cancellationToken);
    }

    private async Task AddLoginEventAsync(
        Guid? tenantId,
        Guid? userId,
        string provider,
        string? identifier,
        AuthLoginResult result,
        string? failureCode,
        string? ipAddress,
        string? userAgent,
        CancellationToken cancellationToken)
    {
        dbContext.AuthLoginEvents.Add(new AuthLoginEvent
        {
            TenantId = tenantId,
            UserId = userId,
            Provider = provider,
            Identifier = identifier,
            Result = result,
            FailureCode = failureCode,
            IpAddress = ipAddress,
            UserAgent = userAgent
        });

        await dbContext.SaveChangesAsync(cancellationToken);
    }

    private static string? GetJsonString(JsonElement element, params string[] names)
    {
        if (element.ValueKind != JsonValueKind.Object)
        {
            return null;
        }

        foreach (var name in names)
        {
            if (element.TryGetProperty(name, out var property) &&
                property.ValueKind == JsonValueKind.String &&
                !string.IsNullOrWhiteSpace(property.GetString()))
            {
                return property.GetString()!.Trim();
            }
        }

        return null;
    }

    private static JsonElement CreateWechatRawProfile(WechatIdentity identity)
    {
        return JsonSerializer.SerializeToElement(new
        {
            openId = identity.OpenId,
            unionId = identity.UnionId,
            nickname = identity.Nickname,
            avatarUrl = identity.AvatarUrl
        });
    }

}
