#nullable enable
using Baya.Application.Contracts;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Identity;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Models.Common;
using Baya.Application.Models.Identity;
using Mediator;
using Microsoft.Extensions.Logging;

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

internal sealed class RefreshTokenCommandHandler(
    IUnitOfWork unitOfWork,
    IJwtService jwtService,
    IAppUserManager userManager,
    IPlatformConfig platformConfig,
    IFieldEncryptor fieldEncryptor,
    IDateTimeProvider clock,
    ICurrentUser currentUser,
    ILogger<RefreshTokenCommandHandler> logger)
    : IRequestHandler<RefreshTokenCommand, OperationResult<AuthTokensResult>>
{
    public async ValueTask<OperationResult<AuthTokensResult>> Handle(RefreshTokenCommand request, CancellationToken cancellationToken)
    {
        var tokenHash = fieldEncryptor.Hash(request.RefreshToken);
        var session = await unitOfWork.UserSessionRepository.GetByTokenHashAsync(tokenHash, cancellationToken);

        if (session is null)
            return OperationResult<AuthTokensResult>.UnauthorizedResult("Invalid refresh token.");

        var now = clock.UtcNow;

        if (session.IsRevoked)
        {
            // A token replayed against an already-rotated session is a stolen-token signal:
            // log the user out everywhere and refuse.
            var revoked = await unitOfWork.UserSessionRepository.RevokeAllActiveForUserAsync(session.UserId, now, cancellationToken);
            await unitOfWork.CommitAsync();

            logger.LogWarning(
                "Refresh-token reuse detected for user {UserId}; revoked {RevokedSessions} active session(s).",
                session.UserId, revoked);

            return OperationResult<AuthTokensResult>.UnauthorizedResult("Refresh token is no longer valid. Sign in again.");
        }

        if (session.ExpiresAt <= now)
        {
            session.Revoke(now);
            await unitOfWork.CommitAsync();
            return OperationResult<AuthTokensResult>.UnauthorizedResult("Refresh token expired. Sign in again.");
        }

        var user = session.User;
        if (user is null || !user.IsActive)
            return OperationResult<AuthTokensResult>.UnauthorizedResult("Invalid refresh token.");

        // Rotation: the presented session dies, a fresh pair is issued.
        session.Revoke(now);

        var roles = await userManager.GetRoleAsync(user);
        var accessToken = await jwtService.GenerateAccessTokenAsync(user);

        var sessionTtlDays = await platformConfig.GetConfig<int>(IdentityDefaults.SessionTtlDaysKey, cancellationToken);
        var (refreshToken, newSession) = IdentityDefaults.MintSession(
            fieldEncryptor, user.Id, now, sessionTtlDays,
            request.DeviceInfo ?? session.DeviceInfo, currentUser.IpAddress);

        await unitOfWork.UserSessionRepository.AddAsync(newSession, cancellationToken);
        await unitOfWork.CommitAsync();

        return OperationResult<AuthTokensResult>.SuccessResult(new AuthTokensResult(
            accessToken.Token,
            refreshToken,
            accessToken.ExpiresAt,
            newSession.ExpiresAt,
            IsNewUser: false,
            Roles: roles));
    }
}