using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Fido2NetLib;
using Fido2NetLib.Objects;

namespace Dacryptero.DB
{
    /// <summary>
    /// The access information that is stored in the db config file
    /// </summary>
    public class DBAccess
    {
        public string User { get; }

        public Guid UserId { get; }

        public ReadOnlyMemory<byte> PasswordSalt { get; }

        public ReadOnlyMemory<byte> PasswordHash { get; }

        public PublicKeyCredentialDescriptor? FidoDescriptor { get; }

        public ReadOnlyMemory<byte> FidoPublicKey { get; }

        public ReadOnlyMemory<byte> FidoUserHandle { get; }

        public uint FidoCounter { get; set; }

        public ReadOnlyMemory<byte> EncSalt { get; }

        public ReadOnlyMemory<byte> EncKey { get; }

        public DateTime Created { get; }

        public DBAccess(string user, Guid userId, ReadOnlyMemory<byte> passwordSalt,
            ReadOnlyMemory<byte> passwordHash, PublicKeyCredentialDescriptor? fidoDescriptor,
            ReadOnlyMemory<byte> fidoPublicKey, ReadOnlyMemory<byte> fidoUserHandle,
            uint fidoCounter, ReadOnlyMemory<byte> encSalt, ReadOnlyMemory<byte> encKey,
            DateTime created
        )
        {
            User = user;
            UserId = userId;
            PasswordSalt = passwordSalt;
            PasswordHash = passwordHash;
            FidoDescriptor = fidoDescriptor;
            FidoPublicKey = fidoPublicKey;
            FidoUserHandle = fidoUserHandle;
            FidoCounter = fidoCounter;
            EncSalt = encSalt;
            EncKey = encKey;
            Created = created;
        }

        public static bool TryParse(JsonElement json, [NotNullWhen(true)] out DBAccess? access)
        {
            access = null;
            if (json.ValueKind != JsonValueKind.Object)
                return false;
            // user
            if (!json.TryGetProperty("user", out JsonElement node))
                return false;
            var user = node.GetString();
            if (user is null)
                return false;
            // userId
            if (!json.TryGetProperty("user-id", out node) || !node.TryGetGuid(out Guid userId))
                return false;
            // password-salt
            if (!json.TryGetProperty("password-salt", out node) 
                || !node.TryGetBytesFromBase64(out byte[]? passwordSalt))
                return false;
            // password hash
            if (!json.TryGetProperty("password-hash", out node) 
                || !node.TryGetBytesFromBase64(out byte[]? passwordHash))
                return false;
            // Fido Descriptor
            PublicKeyCredentialDescriptor? descriptor;
            try
            {
                var field = json.GetProperty("fido-descriptor");
                if (field.ValueKind == JsonValueKind.Null)
                    descriptor = null;
                else descriptor = field
                    .Deserialize<PublicKeyCredentialDescriptor>()
                    ?? throw new NullReferenceException("cannot parse descriptor");
            }
            catch (Exception e)
            {
                Serilog.Log.Warning(e, "cannot parse fido descriptor");
                return false;
            }
            // Fido Public Key
            if (!json.TryGetProperty("fido-public-key", out node)
                || !node.TryGetBytesFromBase64(out byte[]? fidoPublicKey))
                return false;
            // Fido user handle
            if (!json.TryGetProperty("fido-user-handle", out node)
                || !node.TryGetBytesFromBase64(out byte[]? fidoUserHandle))
                return false;
            // Fido counter
            if (!json.TryGetProperty("fido-counter", out node)
                || !node.TryGetUInt32(out uint fidoCounter))
                return false;
            // enc salt
            if (!json.TryGetProperty("enc-salt", out node)
                || !node.TryGetBytesFromBase64(out byte[]? encSalt))
                return false;
            // enc key
            if (!json.TryGetProperty("enc-key", out node) 
                || !node.TryGetBytesFromBase64(out byte[]? encKey))
                return false;
            // created
            if (!json.TryGetProperty("created", out node)
                || !node.TryGetDateTime(out DateTime created))
                return false;
            // finish
            access = new DBAccess(user, userId, passwordSalt, passwordHash, descriptor, fidoPublicKey,
                fidoUserHandle, fidoCounter, encSalt, encKey, created
            );
            return true;
        }

        public void WriteTo(Utf8JsonWriter writer)
        {
            writer.WriteStartObject();
            writer.WriteString("user", User);
            writer.WriteString("user-id", UserId);
            writer.WriteBase64String("password-salt", PasswordSalt.Span);
            writer.WriteBase64String("password-hash", PasswordHash.Span);
            writer.WritePropertyName("fido-descriptor");
            if (FidoDescriptor is null)
                writer.WriteNullValue();
            else JsonSerializer.SerializeToElement(FidoDescriptor)
                .WriteTo(writer);
            writer.WriteBase64String("fido-public-key", FidoPublicKey.Span);
            writer.WriteBase64String("fido-user-handle", FidoUserHandle.Span);
            writer.WriteNumber("fido-counter", FidoCounter);
            writer.WriteBase64String("enc-salt", EncSalt.Span);
            writer.WriteBase64String("enc-key", EncKey.Span);
            writer.WriteString("created", Created);
            writer.WriteEndObject();
        }

        public void WriteWebTo(Utf8JsonWriter writer)
        {
            writer.WriteStartObject();
            writer.WriteString("user", User);
            writer.WriteString("user-id", UserId);
            writer.WriteBoolean("fido", FidoDescriptor is not null);
            writer.WriteString("created", Created);
            writer.WriteEndObject();
        }
    }
}