using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using PowderCoating.Core.Entities;

namespace PowderCoating.Web.Filters;

/// <summary>
/// Redirects SuperAdmin users to the 2FA setup page if they have not yet
/// configured two-factor authentication. Exempt routes are allowed through.
/// Disabled entirely when IHostEnvironment.IsProduction() is false, or when
/// AppSettings:Disable2FAEnforcement is true in appsettings (dev/staging only).
/// </summary>
public class EnforceSuperAdmin2FAFilter : IAsyncActionFilter
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly IHostEnvironment _env;
    private readonly IConfiguration _config;

    /// <summary>
    /// Controllers and areas excluded from 2FA enforcement.
    /// <c>TwoFactorSetup</c> must be exempt so users can reach the setup page
    /// itself.  <c>Home</c> is exempt to avoid an infinite redirect if the
    /// home page is the post-login landing.  The entire <c>Identity</c> area
    /// is exempt so that login, logout, and 2FA challenge flows remain reachable.
    /// <see cref="HashSet{T}"/> with <see cref="StringComparer.OrdinalIgnoreCase"/>
    /// is used for O(1) lookup with case-insensitive matching (route values are
    /// not guaranteed to be consistently cased).
    /// </summary>
    private static readonly HashSet<string> ExemptControllers = new(StringComparer.OrdinalIgnoreCase)
    {
        "TwoFactorSetup", "Home"
    };

    /// <summary>Areas excluded from 2FA enforcement (currently only <c>Identity</c>).</summary>
    private static readonly HashSet<string> ExemptAreas = new(StringComparer.OrdinalIgnoreCase)
    {
        "Identity"
    };

    /// <summary>
    /// Initialises the filter with its required dependencies.
    /// </summary>
    /// <param name="userManager">Used to load the ASP.NET Identity user record and
    /// check whether two-factor authentication is enabled.</param>
    /// <param name="env">Hosting environment; enforcement is skipped in Development.</param>
    /// <param name="config">Application configuration; allows disabling enforcement
    /// via <c>AppSettings:Disable2FAEnforcement</c> for staging environments.</param>
    public EnforceSuperAdmin2FAFilter(
        UserManager<ApplicationUser> userManager,
        IHostEnvironment env,
        IConfiguration config)
    {
        _userManager = userManager;
        _env         = env;
        _config      = config;
    }

    /// <summary>
    /// Checks whether the current SuperAdmin user has 2FA configured and, if not,
    /// redirects them to the setup page before the action executes.
    /// <para>
    /// AJAX/fetch requests (identified by the <c>Sec-Fetch-Mode</c> header being
    /// present and not equal to <c>navigate</c>) receive a <c>401 Unauthorized</c>
    /// response instead of a redirect.  This is intentional: a redirect followed
    /// by a fetch call would silently hit the 2FA setup GET endpoint, which calls
    /// <c>ResetAuthenticatorKeyAsync</c> and would invalidate any in-progress
    /// TOTP setup the user is performing in another tab.
    /// </para>
    /// </summary>
    /// <param name="context">The action-executing context for the current request.</param>
    /// <param name="next">The delegate to invoke to proceed with the action.</param>
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // Skip enforcement in Development only, or when explicitly disabled via config.
        // Staging and Production both enforce 2FA.
        if (_env.IsDevelopment() ||
            _config.GetValue<bool>("AppSettings:Disable2FAEnforcement"))
        {
            await next();
            return;
        }

        var user = context.HttpContext.User;

        // Only enforce for authenticated SuperAdmins
        if (!user.Identity?.IsAuthenticated ?? true)
        {
            await next();
            return;
        }

        if (!user.IsInRole("SuperAdmin"))
        {
            await next();
            return;
        }

        // Skip exempt areas/controllers
        var routeData = context.RouteData;
        var area = routeData.Values["area"]?.ToString() ?? string.Empty;
        var controller = routeData.Values["controller"]?.ToString() ?? string.Empty;

        if (ExemptAreas.Contains(area) || ExemptControllers.Contains(controller))
        {
            await next();
            return;
        }

        // Check if 2FA is enabled
        var appUser = await _userManager.GetUserAsync(user);
        if (appUser != null && !appUser.TwoFactorEnabled)
        {
            // For AJAX/fetch requests (e.g. notification polling, SignalR negotiate),
            // return 401 instead of redirecting — fetch follows redirects and would
            // silently call Setup GET, resetting the authenticator key mid-setup.
            var fetchMode = context.HttpContext.Request.Headers["Sec-Fetch-Mode"].ToString();
            var isNonNavigation = !string.IsNullOrEmpty(fetchMode) && fetchMode != "navigate";
            if (isNonNavigation)
            {
                context.Result = new UnauthorizedResult();
                return;
            }

            context.Result = new RedirectToActionResult("Setup", "TwoFactorSetup", null);
            return;
        }

        await next();
    }
}
