// this is similar to Rest.cs but this use the newer build system. The goal is to reduce code!

using System.Text.Json;
using Fido2NetLib;
using MaxLib.WebServer;
using MaxLib.WebServer.Builder;

namespace Dacryptero.Test.Fido2;

public class Rest2
{
    
    private static Fido2Configuration GetConfiguration(string host)
    {
        var ind = host.LastIndexOf(':');
        var trimmedHost = ind >= 0 ? host.Remove(ind) : host;

        return new Fido2Configuration()
        {
            ServerDomain = trimmedHost,
            ServerName = trimmedHost,
            Origin = "http://" + host,
            Timeout = 60000,
        };
    }

    private static CredentialCreateOptions GetCredentialCreateOptions(User user, string host)
    {
        return CredentialCreateOptions.Create(
            config: GetConfiguration(host),
            challenge: user.Challenge,
            user: new Fido2User()
            {
                DisplayName = user.Id.ToString(),
                Id = user.Id.ToByteArray(),
                Name = user.Id.ToString(),
            },
            authenticatorSelection: new AuthenticatorSelection
            {
                AuthenticatorAttachment = Fido2NetLib.Objects.AuthenticatorAttachment.CrossPlatform,
                RequireResidentKey = false,
                UserVerification = Fido2NetLib.Objects.UserVerificationRequirement.Preferred,
            },
            attestationConveyancePreference: Fido2NetLib.Objects.AttestationConveyancePreference.Indirect,
            excludeCredentials: new List<Fido2NetLib.Objects.PublicKeyCredentialDescriptor>(),
            extensions: new Fido2NetLib.Objects.AuthenticationExtensionsClientInputs()
        );
    }
    
    private static AuthenticatorAttestationRawResponse? GetAuthenticatorAttestationRawResponse(string data)
    {
        var doc = JsonDocument.Parse(data);
        try
        {
            var response = new AuthenticatorAttestationRawResponse();
            response.Id = doc.RootElement.GetProperty("id").GetBytesFromBase64();
            response.RawId = doc.RootElement.GetProperty("rawId").GetBytesFromBase64();
            response.Type = doc.RootElement.GetProperty("type").GetString() switch
            {
                "public-key" => Fido2NetLib.Objects.PublicKeyCredentialType.PublicKey,
                _ => null,
            };
            response.Response = new AuthenticatorAttestationRawResponse.ResponseData();
            response.Response.AttestationObject =
                doc.RootElement.GetProperty("response").GetProperty("attestationObject").GetBytesFromBase64();
            response.Response.ClientDataJson =
                doc.RootElement.GetProperty("response").GetProperty("clientDataJSON").GetBytesFromBase64();
            return response;
        }
        catch (Exception e)
        {
            Serilog.Log.Error(e, "Cannot deserialize attestation response: {data}", data);
            return null;
        }
    }

    private static AuthenticatorAssertionRawResponse? GetAuthenticatorAssertionRawResponse(string data)
    {
        var doc = JsonDocument.Parse(data);
        try
        {
            var response = new AuthenticatorAssertionRawResponse();
            response.Id = doc.RootElement.GetProperty("id").GetBytesFromBase64();
            response.RawId = doc.RootElement.GetProperty("rawId").GetBytesFromBase64();
            response.Type = doc.RootElement.GetProperty("type").GetString() switch
            {
                "public-key" => Fido2NetLib.Objects.PublicKeyCredentialType.PublicKey,
                _ => null,
            };
            response.Response = new AuthenticatorAssertionRawResponse.AssertionResponse();
            response.Response.AuthenticatorData =
                doc.RootElement.GetProperty("response").GetProperty("authenticatorData").GetBytesFromBase64();
            response.Response.ClientDataJson =
                doc.RootElement.GetProperty("response").GetProperty("clientDataJSON").GetBytesFromBase64();
            response.Response.Signature =
                doc.RootElement.GetProperty("response").GetProperty("signature").GetBytesFromBase64();
            response.Response.UserHandle =
                doc.RootElement.GetProperty("response").GetProperty("userHandle").GetBytesFromBase64();
            return response;
        }
        catch (Exception e)
        {
            Serilog.Log.Error(e, "Cannot deserialize attestation response: {data}", data);
            return null;
        }
    }

    private static string ToJson<T>(T data)
    {
        return JsonSerializer.Serialize(
            data,
            new JsonSerializerOptions()
            {
                IncludeFields = true,
                WriteIndented = true,
            }
        );
    }

    [Path("/api2/register/new")]
    [return: Mime(MimeType.ApplicationJson)]
    public string NewRegister([HostVar] string host)
    {
        var user = User.Create();

        var rng = new Random();
        var challenge = new byte[16];
        rng.NextBytes(challenge);
        user.Challenge = challenge;

        var opts = GetCredentialCreateOptions(user, host);

        return $"{{\"user\":\"{user.Id}\",\"publicKeyCredentialCreationOptions\":{opts.ToJson()}}}";
    }

    [Path("/api2/register/finish")]
    [return: Mime(MimeType.ApplicationJson)]
    public async Task<string> FinishRegister([HostVar] string host, [UrlEncodedPost("user")] string userId, [UrlEncodedPost] string data)
    {
        var fido = new Fido2NetLib.Fido2(GetConfiguration(host));

        if (!Guid.TryParse(userId, out Guid parsedUserId))
            parsedUserId = Guid.Empty;
        var user = User.Get(parsedUserId);

        var parsedAttestation = GetAuthenticatorAttestationRawResponse(data);
        if (user is null || parsedAttestation is null)
        {
            return "{\"failed\":true}";
        }
        
        Fido2NetLib.Fido2.CredentialMakeResult success;
        try
        {
            success = await fido.MakeNewCredentialAsync(
                attestationResponse: GetAuthenticatorAttestationRawResponse(data),
                origChallenge: GetCredentialCreateOptions(user, host),
                isCredentialIdUniqueToUser: args =>
                {
                    return Task.FromResult(true);
                }
            );
        }
        catch (Exception e)
        {
            Serilog.Log.Error(e, "Cannot verify attestation");
            throw;
        }

        Serilog.Log.Debug("register result: {data}", ToJson(success));
        user.Descriptor = new Fido2NetLib.Objects.PublicKeyCredentialDescriptor(success.Result.CredentialId);
        user.PublicKey = success.Result.PublicKey;
        user.UserHandle = success.Result.User.Id;
        user.Counter = success.Result.Counter;

        return "{\"failed\":false}";
    }

    [Path("/api2/login/new")]
    [return: Mime(MimeType.ApplicationJson)]
    public string NewLogin([HostVar] string host, [UrlEncodedPost("user")] string userId)
    {
        var fido = new Fido2NetLib.Fido2(GetConfiguration(host));

        if (!Guid.TryParse(userId, out Guid parsedUserId))
            parsedUserId = Guid.Empty;
        var user = User.Get(parsedUserId);

        if (user is null)
        {
            return "{\"failed\":true}";
        }            

        var opt = fido.GetAssertionOptions(
            new []
            {
                user.Descriptor,
            },
            Fido2NetLib.Objects.UserVerificationRequirement.Preferred
        );
        opt.Challenge = user.Id.ToByteArray();

        return opt.ToJson();
    }

    [Path("/api2/login/finish")]
    [return: Mime(MimeType.ApplicationJson)]
    public async Task<string> FinishLogin([HostVar] string host, [UrlEncodedPost("user")] string userId, [UrlEncodedPost] string data)
    {
        var fido = new Fido2NetLib.Fido2(GetConfiguration(host));

        if (!Guid.TryParse(userId, out Guid parsedUserId))
            parsedUserId = Guid.Empty;
        var user = User.Get(parsedUserId);

        var parsedAttestation = GetAuthenticatorAssertionRawResponse(data);
        if (user is null || parsedAttestation is null)
        {
            return "{\"failed\":true}";
        }

        var opt = fido.GetAssertionOptions(
            new []
            {
                user.Descriptor,
            },
            Fido2NetLib.Objects.UserVerificationRequirement.Preferred
        );
        opt.Challenge = user.Id.ToByteArray();
        
        Fido2NetLib.Objects.AssertionVerificationResult success;
        try
        {
            var resp = AuthenticatorAssertionResponse.Parse(GetAuthenticatorAssertionRawResponse(data));
            if (resp.UserHandle.Length == 0)
                resp.UserHandle = null;

            if (new Uri(resp.Origin).Host != opt.RpId)
                throw new InvalidDataException("wrong host name");
            success = await resp.VerifyAsync(
                options: opt,
                expectedOrigin: resp.Origin,
                storedPublicKey: user.PublicKey,
                storedSignatureCounter: user.Counter,
                isUserHandleOwnerOfCredId: args =>
                {
                    return Task.FromResult(true);
                },
                requestTokenBindingId: null
            );
        }
        catch (Exception e)
        {
            Serilog.Log.Error(e, "Cannot verify attestation");
            throw;
        }

        Serilog.Log.Debug("register result: {data}", JsonSerializer.Serialize(
            success,
            new JsonSerializerOptions()
            {
                IncludeFields = true,
                WriteIndented = true,
            }
        ));
        user.Counter = success.Counter;

        return "{\"failed\":false}";
    }
}