using MaxLib.WebServer.Api;
using MaxLib.WebServer.Api.Rest;
using MaxLib.WebServer;
using System.Threading.Tasks;
using Fido2NetLib;
using System.Text.Json;
using System;
using System.IO;

namespace Dacryptero.Test.Fido2
{
    public static class Rest
    {
        private class Host : ApiRule
        {
            public Host(string key)
            {
                Key = key;
            }

            public string Key { get; set; } = "host";

            public override bool Check(RestQueryArgs args)
            {
                // var host = args.Host;
                // var ind = host.LastIndexOf(':');
                // if (ind >= 0)
                //     host = host.Remove(ind);
                args.ParsedArguments[Key] = args.Host;
                return true;
            }
        }

        private class PostArgs : ApiRule
        {
            public PostArgs(string key)
            {
                Key = key;
            }

            public string Key { get; set; }

            public override bool Check(RestQueryArgs args)
            {
                if (args.Post.Data is MaxLib.WebServer.Post.UrlEncodedData data &&
                    data.Parameter.TryGetValue(Key, out string? value)
                )
                {
                    args.ParsedArguments[Key] = value;
                    return true;
                }
                else return false;
            }
        }

        public static RestApiService Create()
        {
            var fact = new ApiRuleFactory();
            var service = new RestApiService("api");
            service.RestEndpoints.AddRange(new[]
            {
                RestActionEndpoint.Create<string>(NewRegister, "host")
                    .Add(fact.Location(
                        fact.UrlConstant("register"),
                        fact.UrlConstant("new"),
                        fact.MaxLength()
                    ))
                    .Add(new Host("host")),
                    // .Add(fact.Host("host", "localhost", true))
                RestActionEndpoint.Create<string, string, string>(FinishRegister, "host", "user", "data")
                    .Add(fact.Location(
                        fact.UrlConstant("register"),
                        fact.UrlConstant("finish"),
                        fact.MaxLength()
                    ))
                    .Add(new PostArgs("user"))
                    .Add(new PostArgs("data"))
                    .Add(new Host("host")),
                RestActionEndpoint.Create<string, string>(NewLogin, "host", "user")
                    .Add(fact.Location(
                        fact.UrlConstant("login"),
                        fact.UrlConstant("new"),
                        fact.MaxLength()
                    ))
                    .Add(new PostArgs("user"))
                    .Add(new Host("host")),
                RestActionEndpoint.Create<string, string, string>(FinishLogin, "host", "user", "data")
                    .Add(fact.Location(
                        fact.UrlConstant("login"),
                        fact.UrlConstant("finish"),
                        fact.MaxLength()
                    ))
                    .Add(new PostArgs("user"))
                    .Add(new PostArgs("data"))
                    .Add(new Host("host")),
            });
            return service;
        }

        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 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 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 Task<HttpDataSource> NewRegister(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 Task.FromResult<HttpDataSource>(new HttpStringDataSource(
                $"{{\"user\":\"{user.Id}\",\"publicKeyCredentialCreationOptions\":{opts.ToJson()}}}"
            )
            {
                MimeType = MimeType.ApplicationJson,
            });
        }
    
        private static async Task<HttpDataSource> FinishRegister(string host, string userId, 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 new HttpStringDataSource("{\"failed\":true}")
                {
                    MimeType = MimeType.ApplicationJson,
                };
            }
            
            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 new HttpStringDataSource("{\"failed\":false}")
            {
                MimeType = MimeType.ApplicationJson,
            };
        }
    
        private static Task<HttpDataSource> NewLogin(string host, 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 Task.FromResult<HttpDataSource>(new HttpStringDataSource("{\"failed\":true}")
                {
                    MimeType = MimeType.ApplicationJson,
                });
            }            

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

            return Task.FromResult<HttpDataSource>(new HttpStringDataSource(opt.ToJson())
            {
                MimeType = MimeType.ApplicationJson,
            });
        }

        private static async Task<HttpDataSource> FinishLogin(string host, string userId, 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 new HttpStringDataSource("{\"failed\":true}")
                {
                    MimeType = MimeType.ApplicationJson,
                };
            }

            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;
                Serilog.Log.Debug("Response: {data}", ToJson(resp));
                Serilog.Log.Debug("Data: {data}", ToJson(new {
                    Options = opt,
                    PublicKey = user.PublicKey,
                    Counter = user.Counter
                }));

                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
                );
                // success = await fido.MakeAssertionAsync(
                //     assertionResponse: GetAuthenticatorAssertionRawResponse(data),
                //     originalOptions: opt,
                //     storedPublicKey: user.PublicKey,
                //     storedSignatureCounter: user.Counter,
                //     isUserHandleOwnerOfCredentialIdCallback: args =>
                //     {
                //         return Task.FromResult(true);
                //     }
                // );
            }
            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 new HttpStringDataSource("{\"failed\":false}")
            {
                MimeType = MimeType.ApplicationJson,
            };
        }

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