using System.Security.Cryptography;
using System.Text;

namespace NATS.Server.Auth;

/// <summary>
/// Authenticates clients by looking up username in a dictionary and comparing
/// the password using bcrypt (for $2-prefixed hashes) or constant-time comparison
/// (for plain text passwords).
/// Reference: golang/nats-server/server/auth.go checkClientPassword.
/// </summary>
public sealed class UserPasswordAuthenticator : IAuthenticator
{
    private readonly Dictionary<string, User> _users;

    public UserPasswordAuthenticator(IEnumerable<User> users)
    {
        _users = new Dictionary<string, User>(StringComparer.Ordinal);
        foreach (var user in users)
            _users[user.Username] = user;
    }

    public AuthResult? Authenticate(ClientAuthContext context)
    {
        var username = context.Opts.Username;
        if (string.IsNullOrEmpty(username))
            return null;

        if (!_users.TryGetValue(username, out var user))
            return null;

        var clientPassword = context.Opts.Password ?? string.Empty;

        if (!ComparePasswords(user.Password, clientPassword))
            return null;

        return new AuthResult
        {
            Identity = user.Username,
            AccountName = user.Account,
            Permissions = user.Permissions,
            Expiry = user.ConnectionDeadline,
        };
    }

    private static bool ComparePasswords(string serverPassword, string clientPassword)
    {
        if (IsBcrypt(serverPassword))
        {
            try
            {
                return BCrypt.Net.BCrypt.Verify(clientPassword, serverPassword);
            }
            catch
            {
                return false;
            }
        }

        var serverBytes = Encoding.UTF8.GetBytes(serverPassword);
        var clientBytes = Encoding.UTF8.GetBytes(clientPassword);
        return CryptographicOperations.FixedTimeEquals(serverBytes, clientBytes);
    }

    private static bool IsBcrypt(string password) => password.StartsWith("$2");
}
