using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using TeleWave.Application.Auth;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Domain.Auth;
using TeleWave.Infrastructure.Persistence;

namespace TeleWave.Infrastructure.Identity;

internal sealed class RefreshTokenService(AppDbContext dbContext, IOptions<JwtOptions> options)
    : IRefreshTokenService
{
    private readonly JwtOptions _options = options.Value;

    public async Task<IssuedRefreshToken> IssueAsync(
        Guid userId,
        CancellationToken cancellationToken
    )
    {
        var rawToken = GenerateRawToken();
        var expiresAt = DateTimeOffset.UtcNow.AddDays(_options.RefreshTokenDays);

        dbContext.RefreshTokens.Add(RefreshToken.Issue(userId, Hash(rawToken), expiresAt));
        await dbContext.SaveChangesAsync(cancellationToken);

        return new IssuedRefreshToken(rawToken, expiresAt);
    }

    public async Task<Result<RotatedRefreshToken>> RotateAsync(
        string rawToken,
        CancellationToken cancellationToken
    )
    {
        var hash = Hash(rawToken);
        var existing = await dbContext.RefreshTokens.SingleOrDefaultAsync(
            t => t.TokenHash == hash,
            cancellationToken
        );

        if (existing is null)
            return Result.Failure<RotatedRefreshToken>(AuthErrors.InvalidRefreshToken);

        if (!existing.IsActive)
        {
            if (existing.RevokedAt is not null)
            {
                // Повторное использование уже отозванного токена — признак компрометации: гасим все токены пользователя.
                await RevokeAllForUserAsync(existing.UserId, cancellationToken);
            }
            return Result.Failure<RotatedRefreshToken>(AuthErrors.InvalidRefreshToken);
        }

        var newRawToken = GenerateRawToken();
        var newExpiresAt = DateTimeOffset.UtcNow.AddDays(_options.RefreshTokenDays);
        var newHash = Hash(newRawToken);

        existing.Revoke(newHash);
        dbContext.RefreshTokens.Add(RefreshToken.Issue(existing.UserId, newHash, newExpiresAt));

        await dbContext.SaveChangesAsync(cancellationToken);

        return Result.Success(new RotatedRefreshToken(existing.UserId, newRawToken, newExpiresAt));
    }

    public async Task RevokeAsync(string rawToken, CancellationToken cancellationToken)
    {
        var hash = Hash(rawToken);
        var existing = await dbContext.RefreshTokens.SingleOrDefaultAsync(
            t => t.TokenHash == hash,
            cancellationToken
        );
        if (existing is null || existing.RevokedAt is not null)
            return;

        existing.Revoke();
        await dbContext.SaveChangesAsync(cancellationToken);
    }

    private async Task RevokeAllForUserAsync(Guid userId, CancellationToken cancellationToken)
    {
        var activeTokens = await dbContext
            .RefreshTokens.Where(t => t.UserId == userId && t.RevokedAt == null)
            .ToListAsync(cancellationToken);

        foreach (var token in activeTokens)
            token.Revoke();

        await dbContext.SaveChangesAsync(cancellationToken);
    }

    private static string GenerateRawToken() =>
        Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));

    private static string Hash(string rawToken) =>
        Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(rawToken)));
}
