using Microsoft.EntityFrameworkCore;
using Tiku.Application.Auth;
using Tiku.Domain.Identity;
using Tiku.Domain.Tenancy;

namespace Tiku.Infrastructure.Auth;

public sealed partial class AuthService
{
    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);
    }

    public async Task<SmsSendResult> RequestPasswordResetAsync(
        PasswordResetCodeRequest request,
        CancellationToken cancellationToken = default)
    {
        var phone = SmsCodeHashing.NormalizePhone(request.Phone);
        var userId = await dbContext.Users.AsNoTracking()
            .Where(user => user.Phone == phone && user.Status == UserStatus.Active)
            .Select(user => (Guid?)user.Id)
            .SingleOrDefaultAsync(cancellationToken);
        var eligible = userId.HasValue && await dbContext.TenantMemberships.AsNoTracking().AnyAsync(
            membership => membership.TenantId == request.TenantId && membership.UserId == userId.Value &&
                          membership.Status == MembershipStatus.Active,
            cancellationToken);
        if (!eligible) return new SmsSendResult(Guid.NewGuid(), DateTimeOffset.UtcNow.AddMinutes(5));

        return await smsVerificationService.CreateCodeAsync(
            new SendSmsCodeRequest(
                request.TenantId,
                phone,
                SmsPurpose.ResetPassword,
                request.IpAddress,
                request.UserAgent,
                request.DeviceId),
            cancellationToken);
    }

    public async Task ResetPasswordAsync(
        PasswordResetRequest request,
        CancellationToken cancellationToken = default)
    {
        var phone = SmsCodeHashing.NormalizePhone(request.Phone);
        var user = await dbContext.Users.SingleOrDefaultAsync(
            item => item.Phone == phone && item.Status == UserStatus.Active,
            cancellationToken);
        if (user is null || !await dbContext.TenantMemberships.AnyAsync(
                membership => membership.TenantId == request.TenantId && membership.UserId == user.Id &&
                              membership.Status == MembershipStatus.Active,
                cancellationToken))
            throw new InvalidCredentialsException();

        await smsVerificationService.VerifyCodeAsync(
            request.TenantId,
            phone,
            SmsPurpose.ResetPassword,
            request.Code,
            cancellationToken);
        var token = await userManager.GeneratePasswordResetTokenAsync(user);
        var reset = await userManager.ResetPasswordAsync(user, token, 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 finalize the password reset.");

        await sessionStore.RevokeAllAsync(user.Id, "password_reset", cancellationToken);
        await AddSecurityAuditAsync(
            user.Id,
            request.TenantId,
            "auth.password.reset",
            null,
            request.IpAddress,
            request.UserAgent,
            cancellationToken);
    }

    public async Task<AuthenticationResult> ChangePasswordAsync(
        AuthenticatedPasswordChangeRequest request,
        CancellationToken cancellationToken = default)
    {
        var session = await sessionStore.ResolveActiveSessionAsync(
            request.SessionId,
            request.UserId,
            cancellationToken) ?? throw new SessionRevokedException();
        var user = await userManager.FindByIdAsync(request.UserId.ToString())
                   ?? throw new InvalidCredentialsException();
        var changed = await userManager.ChangePasswordAsync(user, request.CurrentPassword, request.NewPassword);
        if (!changed.Succeeded)
        {
            var currentPasswordInvalid = changed.Errors.Any(error =>
                string.Equals(error.Code, "PasswordMismatch", StringComparison.OrdinalIgnoreCase));
            throw new InvalidCredentialsException(currentPasswordInvalid
                ? "invalid_credentials"
                : "invalid_new_password");
        }

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

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