
// UserService.cs

using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Abyss.Components.Services.Misc;
using Abyss.Model.Security;
using Microsoft.Extensions.Caching.Memory;
using NSec.Cryptography;
using SQLite;
using Task = System.Threading.Tasks.Task;

namespace Abyss.Components.Services.Security;

public class UserService
{
    private readonly ILogger<UserService> _logger;
    private readonly IMemoryCache _cache;
    private readonly SQLiteAsyncConnection _database;
    private readonly Dictionary<int, string> _userAnnounces = new();
    
    public UserService(ILogger<UserService> logger, ConfigureService config, IMemoryCache cache)
    {
        _logger = logger;
        _cache = cache;
        
        _database = new SQLiteAsyncConnection(config.UserDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
        _database.CreateTableAsync<User>().Wait();
        
        if (config.DebugMode == "Debug")
            _cache.Set("abyss", $"1@127.0.0.1", DateTimeOffset.Now.AddHours(1));
            // Test token, can only be used locally. Will be destroyed in one hour.
            
    }

    public string? GetAnnounce(int id)
    {
        return _userAnnounces.GetValueOrDefault(id);
    }

    public bool SetAnnounce(int id, string? value, string token, string ip)
    {
        if (Validate(token, ip) == -1)
        {
            return false;
        }
        
        if (value == null)
        {
            _userAnnounces.Remove(id);
            return true;
        }
        
        _userAnnounces[id] = value;
        return true;
    }

    public async Task<bool> IsEmptyUser()
    {
        return await _database.Table<User>().CountAsync() == 0;
    }

    public async Task<string?> OpenUserAsync(string user, string token, string? bindIp, string ip)
    {
        var caller = Validate(token, ip);
        if (caller != 1)
        {
            return null;
        }

        var target = await QueryUser(user);
        if (target == null)
        {
            return null;
        }

        var ipToBind = string.IsNullOrWhiteSpace(bindIp) ? ip : bindIp;

        var t = CreateToken(target.Uuid!.Value, ipToBind, TimeSpan.FromHours(1));

        _logger.LogInformation("Root created 1h token for {User}, bound to {BindIp}, request from {ReqIp}", user,
            ipToBind, ip);
        return t;
    }
    
    public async Task<bool> CreateUserAsync(string user, UserCreating creating, string ip)
    {
        // Valid token
        var r = await Verify(user, creating.Response, ip);
        if (r == null)
            return false;

        // User exists ?
        var cu = await QueryUser(creating.Name);
        if (cu != null)
            return false;

        // Valid username string
        if (!IsAlphanumeric(creating.Name))
            return false;

        // Valid parent && Privilege
        var ou = await QueryUser(Validate(r, ip));
        if (creating.Privilege > ou?.Privilege || ou == null)
            return false;

        await AddUserAsync(new User
        {
            Username = creating.Name,
            ParentId = ou.Uuid!.Value,
            Privilege = creating.Privilege,
            PublicKey = creating.PublicKey,
        });

        Destroy(r);
        return true;
    }
    public async Task<string?> Challenge(string user)
    {
        var u = await _database.Table<User>().Where(x => x.Username == user).FirstOrDefaultAsync();
        
        if (u == null) // Error: User not exists
            return null;
        
        if (_cache.TryGetValue(u.Uuid!.Value, out _)) // The previous challenge has not yet expired
            _cache.Remove(u.Uuid);

        var c = Convert.ToBase64String(Encoding.UTF8.GetBytes(GenerateRandomAsciiString(32)));
        _cache.Set(u.Uuid, c, DateTimeOffset.Now.AddMinutes(1));
        return c;
    }

    // The challenge source and response source are not necessarily required to be the same,
    // but the source that obtains the token must be the same as the source that uses the token in the future
    public async Task<string?> Verify(string user, string response, string ip)
    {
        var u = await _database.Table<User>().Where(x => x.Username == user).FirstOrDefaultAsync();
        if (u == null) // Error: User not exists
        {
            return null;
        }
        
        if (_cache.TryGetValue(u.Uuid!.Value, out string? challenge))
        {
            bool isVerified = VerifySignature(
                PublicKey.Import(
                    SignatureAlgorithm.Ed25519, 
                    Convert.FromBase64String(u.PublicKey),
                    KeyBlobFormat.RawPublicKey),
                Convert.FromBase64String(challenge ?? ""), 
                Convert.FromBase64String(response));

            if (!isVerified)
            {
                // Verification failed, set the challenge string to random to prevent duplicate verification
                _cache.Set(u.Uuid, $"failed : {GenerateRandomAsciiString(32)}", DateTimeOffset.Now.AddMinutes(1));
                return null;
            }
            else
            {
                // Remove the challenge string and create a session
                _cache.Remove(u.Uuid);
                var s = GenerateRandomAsciiString(64);
                _cache.Set(s, $"{u.Uuid}@{ip}", DateTimeOffset.Now.AddDays(1));
                _logger.LogInformation($"Verified {u.Uuid}@{ip}, Name: {u.Username}");
                return s;
            }
        }

        return null;
    }

    // Id >= 1 : Success, Uid
    // Id == -1: Failed
    public int Validate(string token, string ip)
    {
        if (_cache.TryGetValue(token, out string? userAndIp))
        {
            if (ip != userAndIp?.Split('@')[1] && ip != "127.0.0.1" && token != "abyss")
            {
                _logger.LogError($"Token used from another Host: {token}");
                Destroy(token);
                return -1;
            }
            // _logger.LogInformation($"Validated {userAndIp}");
            return Convert.ToInt32(userAndIp?.Split('@')[0]);
        }
        _logger.LogWarning($"Validation failed {token}");
        return -1;
    }
    
    public void Destroy(string token)
    {
        _cache.Remove(token);
    }

    public async Task<User?> QueryUser(int uid)
    {
        if (uid == -1) 
            return null;
        var u = await _database.Table<User>().Where(x => x.Uuid == uid).FirstOrDefaultAsync();
        return u;
    }
    
    public async Task<User?> QueryUser(string username)
    {
        var u = await _database.Table<User>().Where(x => x.Username == username).FirstOrDefaultAsync();
        return u;
    }

    public async Task AddUserAsync(User user)
    {
        await _database.InsertAsync(user);
        _logger.LogInformation($"Created user: {user.Username}, Uid: {user.Uuid}, Parent: {user.ParentId}, Privilege: {user.Privilege}");
    }
    
    public static Key GenerateKeyPair()
    {
        var algorithm = SignatureAlgorithm.Ed25519;
        var creationParameters = new KeyCreationParameters
        {
            ExportPolicy = KeyExportPolicies.AllowPlaintextExport
        };
        return Key.Create(algorithm, creationParameters);
    }
    
    public static string GenerateRandomAsciiString(int length)
    {
        const string asciiChars = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
        
        using (var rng = RandomNumberGenerator.Create())
        {
            byte[] randomBytes = new byte[length];
            rng.GetBytes(randomBytes);
            
            char[] result = new char[length];
            for (int i = 0; i < length; i++)
            {
                result[i] = asciiChars[randomBytes[i] % asciiChars.Length];
            }
            return new string(result);
        }
    }
    
    public static bool VerifySignature(PublicKey publicKey, byte[] data, byte[] signature)
    {
        var algorithm = SignatureAlgorithm.Ed25519;
        return algorithm.Verify(publicKey, data, signature);
    }

    public async Task<bool> VerifyAny(byte[] data, byte[] signature)
    {
        var users = await _database.Table<User>().ToListAsync();
        foreach (var u in users)
        {
            try
            {
                var pubKeyBytes = Convert.FromBase64String(u.PublicKey);
                var pubKey = PublicKey.Import(
                    SignatureAlgorithm.Ed25519,
                    pubKeyBytes,
                    KeyBlobFormat.RawPublicKey);

                if (VerifySignature(pubKey, data, signature))
                {
                    _logger.LogInformation($"Signature verified using user {u.Username}");
                    return true;
                }
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, $"Failed to import public key for {u.Username}");
            }
        }
        return false;
    }
    
    public string CreateToken(int uid, string ip, TimeSpan lifetime)
    {
        var token = GenerateRandomAsciiString(64);
        _cache.Set(token, $"{uid}@{ip}", DateTimeOffset.Now.Add(lifetime));
        _logger.LogInformation($"Created token for {uid}@{ip}, valid {lifetime.TotalMinutes} minutes");
        return token;
    }
    
    public static bool IsAlphanumeric(string input)
    {
        if (string.IsNullOrEmpty(input))
            return false;
        return Regex.IsMatch(input, @"^[a-zA-Z0-9]+$");
    }
}