using Fido2NetLib;
using Fido2NetLib.Objects;
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace Dacryptero.Session.Fido;

public abstract class RegisterUserBase : UserBase
{
    public Web.Events.Receive.AccountRegister Account { get; }

    public Fido2User User { get; }

    public Guid UserId { get; }

    public CredentialCreateOptions CredentialCreateOptions { get; }

    public RegisterUserBase(Web.WebSocketConnection connection, Fido2Configuration configuration,
        Web.Events.Receive.AccountRegister account
    )
        : base(connection, configuration)
    {
        Account = account;
        User = new Fido2User
        {
            DisplayName = Account.Username,
            Name = Account.Username,
            Id = (UserId = Guid.NewGuid()).ToByteArray(),
        };

        CredentialCreateOptions = CredentialCreateOptions.Create(
            config: Configuration,
            challenge: Challenge.ToArray(),
            user: User,
            authenticatorSelection: new AuthenticatorSelection
            {
                AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
                RequireResidentKey = false,
                UserVerification = UserVerificationRequirement.Preferred,
            },
            attestationConveyancePreference: AttestationConveyancePreference.Indirect,
            excludeCredentials: new List<PublicKeyCredentialDescriptor>(),
            extensions: new AuthenticationExtensionsClientInputs()
        );
    }

    public override (string method, JsonElement data) GetMessage()
    {
        var json = CredentialCreateOptions.ToJson();
        return ("register", JsonDocument.Parse(json).RootElement);
    }


    /// <summary>
    /// [WITHOUT TFA!] Adds access without TFA requirements.
    /// </summary>
    /// <returns>error message</returns>
    /// <exception cref="InvalidOperationException" />
    public async Task UnsecureExecute()
    {
        if (Config.Default.Security.Tfa)
            throw new InvalidOperationException("This method is only allowed to use with deactivated TFA");

        await AddAccount(CreateAccess(null));
    }

    public override async Task<string?> ValidateResult(JsonElement data)
    {
        var fido = new Fido2(Configuration);

        var parsedAttestation = Converter.GetAuthenticatorAttestationRawResponse(data);
        if (parsedAttestation is null)
            return "cannot parse attestation";
        
        Fido2.CredentialMakeResult success;
        try
        {
            success = await fido.MakeNewCredentialAsync(
                attestationResponse: parsedAttestation,
                origChallenge: CredentialCreateOptions,
                isCredentialIdUniqueToUser: args =>
                {
                    return Task.FromResult(true);
                }
            );
        }
        catch (Exception e)
        {
            Serilog.Log.Error(e, "Cannot verify attestation");
            return $"cannot verify attestation\n{e.GetType()}: {e.Message}";
        }

        await AddAccount(CreateAccess(success));
        return null;
    }

    protected abstract Task AddAccount(DB.DBAccess access);

    protected abstract ReadOnlyMemory<byte> GetDatabaseKey();

    protected virtual DB.DBAccess CreateAccess(Fido2.CredentialMakeResult? result)
    {
        Memory<byte> salt = new byte[64];
        RandomNumberGenerator.Fill(salt.Span);
        Memory<byte> pwPreData = Encoding.UTF8.GetBytes(Account.Password);
        Span<byte> pwData = stackalloc byte[pwPreData.Length + salt.Length];
        pwPreData.Span.CopyTo(pwData[..pwPreData.Length]);
        salt.Span.CopyTo(pwData[pwPreData.Length..]);
        Memory<byte> pwHash = SHA512.HashData(pwData);

        Memory<byte> encSalt = new byte[64];
        RandomNumberGenerator.Fill(encSalt.Span);
        var pdb = new Rfc2898DeriveBytes(Account.Password, encSalt.ToArray(), 10000, HashAlgorithmName.SHA256);
        using var aes = Aes.Create();
        aes.Key = pdb.GetBytes(32);
        aes.IV = pdb.GetBytes(16);
        aes.Padding = PaddingMode.Zeros;
        using var m = new MemoryStream();
        using var c = new CryptoStream(m, aes.CreateEncryptor(aes.Key, aes.IV), CryptoStreamMode.Write);
        c.Write(GetDatabaseKey().Span);
        c.FlushFinalBlock();
        var encKey = m.ToArray();

        return new DB.DBAccess(
            user: Account.Username,
            userId: UserId,
            passwordSalt: salt,
            passwordHash: pwHash,
            fidoDescriptor: result is null ? null : new PublicKeyCredentialDescriptor(result.Result.CredentialId),
            fidoPublicKey: result?.Result.PublicKey ?? ReadOnlyMemory<byte>.Empty,
            fidoUserHandle: result?.Result.User.Id ?? ReadOnlyMemory<byte>.Empty,
            fidoCounter: result?.Result.Counter ?? 0,
            encSalt: encSalt,
            encKey: encKey,
            created: DateTime.UtcNow
        );
    }
}