using Insight.Infrastructure;
using Insight.Infrastructure.Entities;
using Insight.Web.Constants;
using Insight.Web.Models.Account;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using MongoDB.Driver;
using System.Collections.Concurrent;

namespace Insight.Web.Middleware;

public class IdentityMiddleware
{
    public static IDictionary<Guid, LoginModel> Logins { get; private set; } = new ConcurrentDictionary<Guid, LoginModel>();
    public static IDictionary<Guid, ChangePasswordModel> Passwords { get; private set; } = new ConcurrentDictionary<Guid, ChangePasswordModel>();

    private RequestDelegate Next { get; }
    private ILogger<IdentityMiddleware> Logger { get; }

    public IdentityMiddleware(RequestDelegate next, ILogger<IdentityMiddleware> logger)
    {
        Next = next;
        Logger = logger;
    }

    public async Task Invoke(HttpContext context, IServiceProvider serviceProvider)
    {
        try
        {
            // skip blazor / internal requests
            if (context.Request.Path.Value.StartsWith("/_blazor")) return;
            if (context.Request.Path.Value.StartsWith("/_framework")) return;
            if (context.Request.Path.Value.StartsWith("/_content")) return;
            if (context.Request.Path.Value.StartsWith("/css")) return;
            if (context.Request.Path.Value.StartsWith("/fonts")) return;
            if (context.Request.Path.Value.StartsWith("/media")) return;
            if (context.Request.Path.Value.StartsWith("/js")) return;
            if (context.Request.Path.Value.StartsWith("/favicon")) return;

            //Logger.LogWarning($"{context.Request.Path}");

            // skip hub request (test)
            if (context.Request.Path.Value.StartsWith("/hub")) return;


            // ignore 2fa login request
            if (context.Request.Path.Value.StartsWith($"/{Navigation.Account.LoginTFA}")) return;

            // ignore login request
            if (context.Request.Path.Value.StartsWith($"/{Navigation.Account.Login}")) return;

            // 2fa signin request
            if (context.Request.Path.Value.StartsWith($"/{Navigation.Account.SignInTFA}"))
            {
                await OnSignInTFAAsync(context, serviceProvider).ConfigureAwait(false);
                return;
            }

            // signin request
            if (context.Request.Path.Value.StartsWith($"/{Navigation.Account.SignIn}"))
            {
                await OnSignInAsync(context, serviceProvider).ConfigureAwait(false);
                return;
            }

            // logout request
            if (context.Request.Path.Value.StartsWith($"/{Navigation.Account.Logout}"))
            {
                await OnLogoutAsync(context, serviceProvider).ConfigureAwait(false);
                return;
            }

            //password change request
            if (context.Request.Path.Value.StartsWith($"/{Navigation.Account.ChangePassword}"))
            {
                await OnChangePasswordAsync(context, serviceProvider).ConfigureAwait(false);
                return;
            }

            await OnCatchRoute(context, serviceProvider).ConfigureAwait(false);
        }
        finally
        {
            await Next.Invoke(context).ConfigureAwait(false);
        }
    }

    private async ValueTask OnSignInAsync(HttpContext context, IServiceProvider serviceProvider)
    {
        if (context.Request.Query.ContainsKey("key") is false) return;
        var key = Guid.Parse(context.Request.Query["key"]);

        try
        {
            using var scope = serviceProvider.CreateScope();
            var database = scope.ServiceProvider.GetRequiredService<IMongoDatabase>();
            var userManager = scope.ServiceProvider.GetRequiredService<UserManager<InsightUser>>();
            var signInManager = scope.ServiceProvider.GetRequiredService<SignInManager<InsightUser>>();

            if (await userManager.FindByEmailAsync(Logins[key].Email) is not InsightUser user)
            {
                context.Response.Redirect($"/{Navigation.Account.LoginHref(Logins[key].Redirect)}");
                return;
            }

            if (await userManager.CheckPasswordAsync(user, Logins[key].Password) is false)
            {
                context.Response.Redirect($"/{Navigation.Account.LoginHref(Logins[key].Redirect)}");
                return;
            }

            if (await userManager.GetTwoFactorEnabledAsync(user))
            {
                var result = await signInManager.PasswordSignInAsync(user.UserName, Logins[key].Password, Logins[key].RememberMe, lockoutOnFailure: false).ConfigureAwait(false);
                context.Response.Redirect($"/{Navigation.Account.LoginTFAHref(key)}");
                return;
            }

            if (string.IsNullOrWhiteSpace(Logins[key].Redirect) is false)
            {
                context.Response.Redirect($"/{Logins[key].Redirect}");
            }
            else
            {
                context.Response.Redirect($"/{Navigation.Home}");
            }

            await signInManager.SignInAsync(user, new AuthenticationProperties
            {
                AllowRefresh = true,
                IsPersistent = Logins[key].RememberMe,
                IssuedUtc = DateTimeOffset.UtcNow,
                ExpiresUtc = DateTimeOffset.UtcNow.AddHours(8)
            }).ConfigureAwait(false);

            await database.UserLog()
            .InsertOneAsync(new InsightUserLogEntity
            {
                Insert = DateTime.Now,
                User = user.Id.ToString(),
                Timestamp = DateTime.Now,
                Message = $"Login ({context.Request.Host})",
            }, cancellationToken: default)
            .ConfigureAwait(false);

            Logins.Remove(key);
        }
        catch (Exception ex)
        {
            Logger.LogError(ex.ToString());
        }
        finally
        {
            //Logger.LogInformation("redirect {0}", string.Concat(context.Response.Headers));
        }
    }

    private async ValueTask OnSignInTFAAsync(HttpContext context, IServiceProvider serviceProvider)
    {
        Logger.LogInformation($"OnSignInTFAAsync ({context.Request.Path})");

        if (context.Request.Query.ContainsKey("key") is false) return;

        var key = Guid.Parse(context.Request.Query["key"]);

        try
        {
            using var scope = serviceProvider.CreateScope();
            var database = scope.ServiceProvider.GetRequiredService<IMongoDatabase>();
            var userManager = scope.ServiceProvider.GetRequiredService<UserManager<InsightUser>>();
            var signInManager = scope.ServiceProvider.GetRequiredService<SignInManager<InsightUser>>();

            var user = await userManager.FindByEmailAsync(Logins[key].Email).ConfigureAwait(false);

            var authenticatorCode = Logins[key].TwoFactorToken.Replace(" ", string.Empty).Replace("-", string.Empty);

            var valid = await userManager.VerifyTwoFactorTokenAsync(user, userManager.Options.Tokens.AuthenticatorTokenProvider, authenticatorCode).ConfigureAwait(false);
            if (valid is false)
            {
                context.Response.Redirect($"/{Navigation.Account.LoginTFAHref(key)}");
                return;
            }

            var result = await signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, true, Logins[key].RememberMe).ConfigureAwait(false);

            if (result.IsNotAllowed) return;
            if (result.IsLockedOut) return;

            if (result.Succeeded)
            {
                if (string.IsNullOrWhiteSpace(Logins[key].Redirect) is false)
                {
                    context.Response.Redirect($"/{Logins[key].Redirect}");
                }
                else
                {
                    context.Response.Redirect($"/{Navigation.Home}");
                }

                await database.UserLog()
                    .InsertOneAsync(new InsightUserLogEntity
                    {
                        Insert = DateTime.Now,
                        User = user.Id.ToString(),
                        Timestamp = DateTime.Now,
                        Message = $"Login 2FA ({context.Request.Host})",
                    }, cancellationToken: default)
                    .ConfigureAwait(false);
            }
            else
            {
                context.Response.Redirect($"/{Navigation.Account.LoginHref(Logins[key].Redirect)}");
            }
        }
        catch (Exception ex)
        {
            Logger.LogError(ex.ToString());
        }
        finally
        {
            Logins.Remove(key);
        }
    }

    private async ValueTask OnLogoutAsync(HttpContext context, IServiceProvider serviceProvider)
    {
        Logger.LogInformation($"OnLogoutAsync ({context.Request.Path})");

        using var scope = serviceProvider.CreateScope();
        var signInManager = scope.ServiceProvider.GetRequiredService<SignInManager<InsightUser>>();
        await signInManager.SignOutAsync().ConfigureAwait(false);

        context.Response.Redirect($"/");
    }

    private async ValueTask OnDisconnectAsync(HttpContext context, IServiceProvider serviceProvider)
    {
        Logger.LogInformation($"OnDisconnectAsync ({context.Request.Path})");

        using var scope = serviceProvider.CreateScope();
        var signInManager = scope.ServiceProvider.GetRequiredService<SignInManager<InsightUser>>();
        await signInManager.SignOutAsync().ConfigureAwait(false);

        context.Abort();
    }

    private async ValueTask OnChangePasswordAsync(HttpContext context, IServiceProvider serviceProvider)
    {
        Logger.LogInformation($"OnChangePasswordAsync ({context.Request.Path})");

        if (context.Request.Query.ContainsKey("key") is false) return;

        var key = Guid.Parse(context.Request.Query["key"]);

        try
        {
            using var scope = serviceProvider.CreateScope();
            var userManager = scope.ServiceProvider.GetRequiredService<UserManager<InsightUser>>();

            var user = await userManager.GetUserAsync(context.User).ConfigureAwait(false);
            var result = await userManager.ChangePasswordAsync(user, Passwords[key]?.OldPassword, Passwords[key]?.NewPassword).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Logger.LogError(ex.ToString());
        }
        finally
        {
            Passwords.Remove(key);
            context.Response.Redirect($"/{Navigation.Account.Profile}");
        }
    }

    private ValueTask OnCatchRoute(HttpContext context, IServiceProvider serviceProvider)
    {
        using var scope = serviceProvider.CreateScope();
        var signInManager = scope.ServiceProvider.GetRequiredService<SignInManager<InsightUser>>();

        //check authentication
        var authenticated = signInManager.IsSignedIn(context.User);
        if (authenticated) return default;

        Logger.LogCritical("non auth - redirect");

        var returnPath = context.Request.Path.Value ?? string.Empty;
        if (returnPath.StartsWith("/"))
        {
            returnPath = returnPath.Substring(1);
        }

        context.Response.Redirect($"/{Navigation.Account.LoginHref(returnPath)}");

        return default;
    }
}