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

namespace Tiku.Infrastructure.Auth;

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

    protected async Task ConsumeChallengeAsync(AuthChallenge challenge, CancellationToken cancellationToken)
    {
        var now = DateTimeOffset.UtcNow;
        var consumed = await identityPersistence.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();
    }

    protected 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 tenancyPersistence.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);
    }

    protected 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));
    }

    protected 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 = identityPersistence.Database.CurrentTransaction is null
            ? await identityPersistence.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 identityPersistence.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);
    }

    protected 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);
    }

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

        if (user is null)
        {
            user = new User
            {
                Name = wechatIdentity.Nickname,
                AvatarUrl = wechatIdentity.AvatarUrl,
                PrimaryRole = "student",
                RawProfile = CreateWechatRawProfile(wechatIdentity)
            };
            identityPersistence.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
            };
            identityPersistence.UserIdentities.Add(existingIdentity);
        }

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

        return user;
    }

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

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

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

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

        if (activeMembershipExists) return;

        var studentMembership = await identityPersistence.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 tenancyPersistence.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);

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

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

    protected 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);
        identityPersistence.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 identityPersistence.SaveChangesAsync(cancellationToken);
        await AddSecurityAuditAsync(
            user.Id, tenantId, "auth.challenge.issued", status.ToString(),
            ipAddress, userAgent, cancellationToken);
        return new AuthenticationResult(status, ChallengeToken: rawToken, ChallengeExpiresAt: expiresAt);
    }

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

        if (!tenantId.HasValue) return false;

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

    protected static string HashChallengeToken(string token)
    {
        return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token ?? string.Empty))).ToLowerInvariant();
    }

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

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

        await identityPersistence.SaveChangesAsync(cancellationToken);
    }

    protected 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;
    }

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