#nullable enable
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Identity;
using Baya.Application.Models.Common;
using Baya.Application.Models.Identity;
using Baya.Domain.Entities.User;
using Mediator;

namespace Baya.Application.Features.Identity.Commands.RequestOtp;

internal sealed class RequestOtpCommandHandler(
    IAppUserManager userManager,
    ISmsSender smsSender,
    IPlatformConfig platformConfig,
    ICacheService cache,
    IFieldEncryptor fieldEncryptor,
    IDateTimeProvider clock)
    : IRequestHandler<RequestOtpCommand, OperationResult<RequestOtpResult>>
{
    public async ValueTask<OperationResult<RequestOtpResult>> Handle(RequestOtpCommand request, CancellationToken cancellationToken)
    {
        var phone = IranianPhone.Normalize(request.Phone);
        if (phone is null)
            return OperationResult<RequestOtpResult>.FailureResult(nameof(request.Phone), "A valid Iranian mobile number is required.");

        var resendSeconds = await platformConfig.GetConfig<int>(IdentityDefaults.OtpResendSecondsKey, cancellationToken);

        // Per-phone resend window (the per-IP OTP rate-limit policy guards the endpoint separately).
        // Same behaviour whether or not the phone has an account — no enumeration.
        var resendKey = IdentityDefaults.OtpResendCacheKey(fieldEncryptor.Hash(phone));
        var windowEndsAt = await cache.GetAsync<DateTimeOffset>(resendKey, cancellationToken);
        var now = clock.UtcNow;
        if (windowEndsAt > now)
            return OperationResult<RequestOtpResult>.SuccessResult(
                new RequestOtpResult(false, (int)Math.Ceiling((windowEndsAt - now).TotalSeconds),
                    IdentityDefaults.OtpCodeLength, IdentityDefaults.OtpExpirySeconds));

        var user = await userManager.GetUserByPhoneNumber(phone);
        if (user is null)
        {
            // Inactive-until-verified shell account; no PII beyond the (encrypted) phone. The surrogate
            // username keeps the plaintext phone out of Identity's UserName/NormalizedUserName columns.
            user = new User
            {
                UserName = $"u_{Guid.NewGuid():N}",
                PhoneNumber = phone,
                IsActive = false
            };

            var createResult = await userManager.CreateUser(user);
            if (!createResult.Succeeded)
                return OperationResult<RequestOtpResult>.FailureResult("Unable to process the request. Try again.");
        }

        // A fresh code voids the previous attempt counter; brute force stays bounded by the endpoint's
        // per-IP rate limit plus the TOTP window.
        await userManager.ResetUserLockoutAsync(user);

        var code = user.PhoneNumberConfirmed
            ? await userManager.GenerateOtpCode(user)
            : await userManager.GeneratePhoneNumberConfirmationToken(user, phone);

        await smsSender.SendOtpAsync(phone, code, cancellationToken);

        await cache.SetAsync(resendKey, now.AddSeconds(resendSeconds), TimeSpan.FromSeconds(resendSeconds), cancellationToken);

        return OperationResult<RequestOtpResult>.SuccessResult(
            new RequestOtpResult(true, resendSeconds, IdentityDefaults.OtpCodeLength, IdentityDefaults.OtpExpirySeconds));
    }
}