using System.Security.Cryptography;
using CalledIt.DataAccess;
using CalledIt.DataAccess.Entities;
using CalledIt.Domain.Dtos;
using Microsoft.EntityFrameworkCore;

namespace CalledIt.Domain.Services;

public class RefreshTokenService : IRefreshTokenService
{
    private const int TokenByteLength = 32;

    private readonly CalledItDbContext _dbContext;

    public RefreshTokenService(CalledItDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<RefreshTokenIssueDto> IssueAsync(int userId, int lifetimeDays)
    {
        var (raw, hash) = GenerateToken();
        var now = DateTime.UtcNow;
        var expires = now.AddDays(lifetimeDays);

        _dbContext.RefreshTokens.Add(new RefreshTokenEntity
        {
            Id = Guid.NewGuid(),
            UserId = userId,
            TokenHash = hash,
            CreatedAtUtc = now,
            ExpiresAtUtc = expires
        });

        await _dbContext.SaveChangesAsync();

        return new RefreshTokenIssueDto(raw, expires);
    }

    public async Task<RefreshTokenRotationDto> RotateAsync(string rawToken, int lifetimeDays)
    {
        var hash = Hash(rawToken);

        var existing = await _dbContext.RefreshTokens
            .SingleOrDefaultAsync(t => t.TokenHash == hash);

        if (existing is null)
        {
            throw new UnauthorizedAccessException("Invalid refresh token.");
        }

        // Replay detection: a token that was already rotated (revoked AND has a
        // successor) being presented again means the attacker has an old copy.
        // Kill the whole family for that user.
        if (existing.RevokedAtUtc is not null)
        {
            if (existing.ReplacedByTokenId is not null)
            {
                await RevokeAllForUserAsync(existing.UserId);
            }

            throw new UnauthorizedAccessException("Refresh token reuse detected.");
        }

        if (existing.ExpiresAtUtc <= DateTime.UtcNow)
        {
            throw new UnauthorizedAccessException("Refresh token has expired.");
        }

        var (newRaw, newHash) = GenerateToken();
        var now = DateTime.UtcNow;
        var newExpires = now.AddDays(lifetimeDays);
        var newId = Guid.NewGuid();

        existing.RevokedAtUtc = now;
        existing.ReplacedByTokenId = newId;

        _dbContext.RefreshTokens.Add(new RefreshTokenEntity
        {
            Id = newId,
            UserId = existing.UserId,
            TokenHash = newHash,
            CreatedAtUtc = now,
            ExpiresAtUtc = newExpires
        });

        await _dbContext.SaveChangesAsync();

        return new RefreshTokenRotationDto(existing.UserId, newRaw, newExpires);
    }

    public async Task RevokeAsync(string rawToken)
    {
        var hash = Hash(rawToken);

        var existing = await _dbContext.RefreshTokens
            .SingleOrDefaultAsync(t => t.TokenHash == hash);

        if (existing is null || existing.RevokedAtUtc is not null)
        {
            return;
        }

        existing.RevokedAtUtc = DateTime.UtcNow;
        await _dbContext.SaveChangesAsync();
    }

    public async Task RevokeAllForUserAsync(int userId)
    {
        var now = DateTime.UtcNow;
        await _dbContext.RefreshTokens
            .Where(t => t.UserId == userId && t.RevokedAtUtc == null)
            .ExecuteUpdateAsync(s => s.SetProperty(t => t.RevokedAtUtc, now));
    }

    private static (string Raw, byte[] Hash) GenerateToken()
    {
        var bytes = RandomNumberGenerator.GetBytes(TokenByteLength);
        var raw = Convert.ToBase64String(bytes)
            .Replace('+', '-')
            .Replace('/', '_')
            .TrimEnd('=');
        return (raw, SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(raw)));
    }

    private static byte[] Hash(string rawToken) =>
        SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rawToken));
}
