using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Ikar.Server.Data;
using Ikar.Server.Data.Entities;
using Ikar.Shared;

namespace Ikar.Server.Infrastructure.Auth;

public sealed class TokenService(
    IkarDbContext dbContext,
    IOptions<JwtOptions> jwtOptions)
{
    private readonly JwtOptions _jwtOptions = jwtOptions.Value;

    public Task<AuthSessionDto> CreateSessionAsync(User user, CancellationToken cancellationToken = default) =>
        CreateSessionAsync(user, null, cancellationToken);

    public async Task<AuthSessionDto> CreateSessionAsync(
        User user,
        ClientVersionSnapshot? clientVersion,
        CancellationToken cancellationToken = default)
    {
        var now = DateTimeOffset.UtcNow;
        var session = new UserSession
        {
            UserId = user.Id,
            LastSeenAt = now
        };
        if (clientVersion is not null)
        {
            ClientVersionRequestReader.Apply(session, clientVersion);
        }

        var refreshToken = CreateOpaqueToken();
        session.TokenHash = HashToken(refreshToken);
        dbContext.UserSessions.Add(session);

        await dbContext.SaveChangesAsync(cancellationToken);
        return CreateAuthSession(user, session.Id, refreshToken, now);
    }

    public Task<AuthSessionDto?> RefreshSessionAsync(string rawRefreshToken, CancellationToken cancellationToken = default) =>
        RefreshSessionAsync(rawRefreshToken, null, cancellationToken);

    public async Task<AuthSessionDto?> RefreshSessionAsync(
        string rawRefreshToken,
        ClientVersionSnapshot? clientVersion,
        CancellationToken cancellationToken = default)
    {
        var hash = HashToken(rawRefreshToken);
        var session = await dbContext.UserSessions.SingleOrDefaultAsync(x => x.TokenHash == hash, cancellationToken);
        if (session is null)
        {
            return null;
        }

        if (session.CreatedAt.AddDays(_jwtOptions.RefreshTokenLifetimeDays) <= DateTimeOffset.UtcNow)
        {
            dbContext.UserSessions.Remove(session);
            await dbContext.SaveChangesAsync(cancellationToken);
            return null;
        }

        var user = await dbContext.Users.SingleAsync(x => x.Id == session.UserId, cancellationToken);
        if (user.IsBlocked)
        {
            dbContext.UserSessions.Remove(session);
            await dbContext.SaveChangesAsync(cancellationToken);
            return null;
        }

        var now = DateTimeOffset.UtcNow;
        var refreshToken = CreateOpaqueToken();
        session.TokenHash = HashToken(refreshToken);
        session.CreatedAt = now;
        session.LastSeenAt = now;
        if (clientVersion is not null)
        {
            ClientVersionRequestReader.Apply(session, clientVersion);
        }

        await dbContext.SaveChangesAsync(cancellationToken);

        return CreateAuthSession(user, session.Id, refreshToken, now);
    }

    public async Task<bool> RevokeSessionAsync(Guid sessionId, CancellationToken cancellationToken = default)
    {
        var session = await dbContext.UserSessions.SingleOrDefaultAsync(x => x.Id == sessionId, cancellationToken);
        if (session is null)
        {
            return false;
        }

        dbContext.UserSessions.Remove(session);
        await dbContext.SaveChangesAsync(cancellationToken);
        return true;
    }

    public static string HashToken(string token)
    {
        var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
        return Convert.ToHexString(hashBytes);
    }

    private AuthSessionDto CreateAuthSession(User user, Guid sessionId, string refreshToken, DateTimeOffset now)
    {
        var accessTokenExpiresAt = now.AddMinutes(_jwtOptions.AccessTokenLifetimeMinutes);
        var credentials = new SigningCredentials(
            new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SigningKey)),
            SecurityAlgorithms.HmacSha256);

        var claims = new List<Claim>
        {
            new(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new(ClaimTypes.Name, user.DisplayName),
            new("username", user.Username),
            new("sid", sessionId.ToString()),
            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N"))
        };

        var jwt = new JwtSecurityToken(
            issuer: _jwtOptions.Issuer,
            audience: _jwtOptions.Audience,
            claims: claims,
            notBefore: now.UtcDateTime,
            expires: accessTokenExpiresAt.UtcDateTime,
            signingCredentials: credentials);

        var accessToken = new JwtSecurityTokenHandler().WriteToken(jwt);

        return new AuthSessionDto(
            accessToken,
            refreshToken,
            accessTokenExpiresAt,
            new(
                user.Id,
                user.Username,
                user.DisplayName,
                false,
                user.PhoneNumber,
                now,
                user.About,
                string.IsNullOrWhiteSpace(user.AvatarStoragePath)
                    ? null
                    : $"/api/users/{user.Id}/avatar/{Uri.EscapeDataString(Path.GetFileName(user.AvatarStoragePath))}",
                user.IsBot));
    }

    private static string CreateOpaqueToken()
    {
        var tokenBytes = RandomNumberGenerator.GetBytes(32);
        return Convert.ToBase64String(tokenBytes)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }
}
