using System.Security.Claims;
using MemberCenter.Application.Abstractions;
using MemberCenter.Application.Constants;
using MemberCenter.Infrastructure.Configuration;
using MemberCenter.Infrastructure.Identity;
using MemberCenter.Web.Models.Account;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;

namespace MemberCenter.Web.Controllers;

public class AccountController : Controller
{
    private readonly IAccountProvisioningService _accountProvisioningService;
    private readonly IAccountEmailService _accountEmailService;
    private readonly IAuditLogWriter _auditLogWriter;
    private readonly IConfiguration _configuration;
    private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
    private readonly bool _allowInsecureReturnUrls;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;

    public AccountController(
        IAccountProvisioningService accountProvisioningService,
        IAccountEmailService accountEmailService,
        IAuditLogWriter auditLogWriter,
        IConfiguration configuration,
        IAuthenticationSchemeProvider authenticationSchemeProvider,
        IWebHostEnvironment environment,
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager)
    {
        _accountProvisioningService = accountProvisioningService;
        _accountEmailService = accountEmailService;
        _auditLogWriter = auditLogWriter;
        _configuration = configuration;
        _authenticationSchemeProvider = authenticationSchemeProvider;
        _allowInsecureReturnUrls = environment.IsDevelopment();
        _userManager = userManager;
        _signInManager = signInManager;
    }

    [HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> Login(string? returnUrl = null)
    {
        await SetExternalLoginAvailabilityAsync();
        return View(new LoginViewModel { ReturnUrl = returnUrl });
    }

    [HttpPost]
    [AllowAnonymous]
    [EnableRateLimiting(RateLimitPolicyNames.PublicAuthLogin)]
    public async Task<IActionResult> Login(LoginViewModel model)
    {
        if (!ModelState.IsValid)
        {
            await SetExternalLoginAvailabilityAsync();
            return View(model);
        }

        var loginUser = await _userManager.FindByEmailAsync(model.Email);
        if (loginUser?.DisabledAt.HasValue == true)
        {
            ModelState.AddModelError(string.Empty, "Account is disabled.");
            await SetExternalLoginAvailabilityAsync();
            return View(model);
        }

        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, true);
        if (!result.Succeeded)
        {
            if (result.IsLockedOut)
            {
                ModelState.AddModelError(string.Empty, "Account is temporarily locked. Please try again later.");
                await SetExternalLoginAvailabilityAsync();
                return View(model);
            }

            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            await SetExternalLoginAvailabilityAsync();
            return View(model);
        }

        if (loginUser is not null)
        {
            await UpdateSignInMetadataAsync(loginUser);
        }

        if (IsAllowedReturnUrl(model.ReturnUrl, ReturnUrlPurpose.Login))
        {
            return Redirect(model.ReturnUrl!);
        }

        return RedirectToAction("Index", "Home", new { area = string.Empty });
    }

    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> ExternalLogin(string provider, string? returnUrl = null, bool rememberMe = false)
    {
        if (await _authenticationSchemeProvider.GetSchemeAsync(provider) is null)
        {
            ModelState.AddModelError(string.Empty, $"{provider} login is not configured.");
            await SetExternalLoginAvailabilityAsync();
            return View("Login", new LoginViewModel { ReturnUrl = returnUrl, RememberMe = rememberMe });
        }

        var redirectUrl = Url.Action(
            nameof(ExternalLoginCallback),
            "Account",
            new { area = string.Empty, returnUrl, rememberMe });
        var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
        return Challenge(properties, provider);
    }

    [HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> ExternalLoginCallback(string? returnUrl = null, bool rememberMe = false, string? remoteError = null)
    {
        if (!string.IsNullOrWhiteSpace(remoteError))
        {
            ModelState.AddModelError(string.Empty, $"External login failed: {remoteError}");
            await SetExternalLoginAvailabilityAsync();
            return View("Login", new LoginViewModel { ReturnUrl = returnUrl });
        }

        var info = await _signInManager.GetExternalLoginInfoAsync();
        if (info is null)
        {
            ModelState.AddModelError(string.Empty, "Unable to load external login information.");
            await SetExternalLoginAvailabilityAsync();
            return View("Login", new LoginViewModel { ReturnUrl = returnUrl });
        }

        var email = info.Principal.FindFirstValue(ClaimTypes.Email) ?? info.Principal.FindFirstValue("email");
        var emailVerified = bool.TryParse(info.Principal.FindFirstValue("email_verified"), out var parsed) && parsed;
        var result = await _accountProvisioningService.ProvisionExternalLoginAsync(
            info.LoginProvider,
            info.ProviderKey,
            email,
            emailVerified);

        if (!result.Succeeded || result.UserId is null)
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error);
            }

            await SetExternalLoginAvailabilityAsync();
            return View("Login", new LoginViewModel { ReturnUrl = returnUrl });
        }

        var user = await _userManager.FindByIdAsync(result.UserId.Value.ToString());
        if (user is null)
        {
            ModelState.AddModelError(string.Empty, "Unable to locate the linked account.");
            await SetExternalLoginAvailabilityAsync();
            return View("Login", new LoginViewModel { ReturnUrl = returnUrl });
        }

        if (user.DisabledAt.HasValue)
        {
            ModelState.AddModelError(string.Empty, "Account is disabled.");
            await SetExternalLoginAvailabilityAsync();
            return View("Login", new LoginViewModel { ReturnUrl = returnUrl });
        }

        await _signInManager.SignInAsync(user, rememberMe, info.LoginProvider);
        await UpdateSignInMetadataAsync(user);

        if (IsAllowedReturnUrl(returnUrl, ReturnUrlPurpose.Login))
        {
            return Redirect(returnUrl!);
        }

        return RedirectToAction("Index", "Home", new { area = string.Empty });
    }

    [HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> Logout(string? returnUrl = null)
    {
        if (User.Identity?.IsAuthenticated == true)
        {
            await _signInManager.SignOutAsync();
        }

        if (IsAllowedReturnUrl(returnUrl, ReturnUrlPurpose.Logout))
        {
            return Redirect(returnUrl!);
        }

        return LocalRedirect("~/");
    }

    [HttpPost]
    [ActionName("Logout")]
    [Authorize]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> LogoutPost(string? returnUrl = null)
    {
        await _signInManager.SignOutAsync();

        if (IsAllowedReturnUrl(returnUrl, ReturnUrlPurpose.Logout))
        {
            return Redirect(returnUrl!);
        }

        return LocalRedirect("~/");
    }

    [HttpGet]
    [Authorize]
    public IActionResult ChangePassword()
    {
        return View(new ChangePasswordViewModel());
    }

    [HttpPost]
    [Authorize]
    public async Task<IActionResult> ChangePassword(ChangePasswordViewModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        var user = await _userManager.GetUserAsync(User);
        if (user is null)
        {
            return RedirectToAction(nameof(Login), new { area = string.Empty });
        }

        var result = await _userManager.ChangePasswordAsync(user, model.CurrentPassword, model.NewPassword);
        if (!result.Succeeded)
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }

            return View(model);
        }

        await _signInManager.RefreshSignInAsync(user);
        await _auditLogWriter.WriteAsync("user", user.Id, "account.password_changed", new
        {
            user_id = user.Id,
            email = user.Email
        });
        ViewData["Result"] = "Password updated.";
        ModelState.Clear();
        return View(new ChangePasswordViewModel());
    }

    [HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> Register()
    {
        await SetExternalLoginAvailabilityAsync();
        return View(new RegisterViewModel());
    }

    [HttpPost]
    [AllowAnonymous]
    [EnableRateLimiting(RateLimitPolicyNames.PublicAuthRegister)]
    public async Task<IActionResult> Register(RegisterViewModel model)
    {
        if (!ModelState.IsValid)
        {
            await SetExternalLoginAvailabilityAsync();
            return View(model);
        }

        var result = await _accountProvisioningService.RegisterLocalAsync(model.Email, model.Password);
        if (!result.Succeeded)
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error);
            }
            await SetExternalLoginAvailabilityAsync();
            return View(model);
        }

        var user = await _userManager.FindByEmailAsync(model.Email);
        if (user is not null)
        {
            await _accountEmailService.SendVerificationEmailAsync(user.Id, GetBaseUrl());
        }

        return View("RegisterConfirmation");
    }

    [HttpGet]
    [AllowAnonymous]
    public IActionResult ForgotPassword()
    {
        return View(new ForgotPasswordViewModel());
    }

    [HttpPost]
    [AllowAnonymous]
    [EnableRateLimiting(RateLimitPolicyNames.PublicAuthRecovery)]
    public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        var user = await _userManager.FindByEmailAsync(model.Email);
        if (user is null)
        {
            return View("ForgotPasswordConfirmation");
        }

        await _accountEmailService.SendPasswordResetEmailAsync(user.Id, GetBaseUrl());
        return View("ForgotPasswordConfirmation");
    }

    [HttpGet]
    [AllowAnonymous]
    public IActionResult ResetPassword(string email, string token)
    {
        return View(new ResetPasswordViewModel { Email = email, Token = token });
    }

    [HttpPost]
    [AllowAnonymous]
    [EnableRateLimiting(RateLimitPolicyNames.PublicAuthTokenConsumption)]
    public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        var user = await _userManager.FindByEmailAsync(model.Email);
        if (user is null)
        {
            return View("ResetPasswordConfirmation");
        }

        var result = await _userManager.ResetPasswordAsync(user, model.Token, model.NewPassword);
        if (!result.Succeeded)
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }
            return View(model);
        }

        await _auditLogWriter.WriteAsync("user", user.Id, "account.password_reset_completed", new
        {
            user_id = user.Id,
            email = user.Email
        });
        return View("ResetPasswordConfirmation");
    }

    [HttpGet]
    [AllowAnonymous]
    [EnableRateLimiting(RateLimitPolicyNames.PublicAuthTokenConsumption)]
    public async Task<IActionResult> VerifyEmail(string email, string token)
    {
        var user = await _userManager.FindByEmailAsync(email);
        if (user is null)
        {
            return View("VerifyEmailResult", false);
        }

        var result = await _userManager.ConfirmEmailAsync(user, token);
        if (result.Succeeded)
        {
            await _auditLogWriter.WriteAsync("user", user.Id, "account.email_verified", new
            {
                user_id = user.Id,
                email = user.Email
            });
        }
        return View("VerifyEmailResult", result.Succeeded);
    }

    [Authorize]
    [HttpPost]
    [ValidateAntiForgeryToken]
    [EnableRateLimiting(RateLimitPolicyNames.PublicAuthRecovery)]
    public async Task<IActionResult> ResendVerification()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user is null)
        {
            return RedirectToAction(nameof(Login), new { area = string.Empty });
        }

        if (!user.EmailConfirmed)
        {
            await _accountEmailService.SendVerificationEmailAsync(user.Id, GetBaseUrl());
            TempData["Result"] = "Verification email sent.";
        }

        return RedirectToAction("Index", "Profile", new { area = string.Empty });
    }

    private string GetBaseUrl() => $"{Request.Scheme}://{Request.Host}{Request.PathBase}";

    private async Task SetExternalLoginAvailabilityAsync()
    {
        ViewData["GoogleLoginEnabled"] = await _authenticationSchemeProvider.GetSchemeAsync("Google") is not null;
    }

    private async Task UpdateSignInMetadataAsync(ApplicationUser user)
    {
        user.LastLoginAt = DateTimeOffset.UtcNow;
        user.LastSeenAt = user.LastLoginAt;
        await _userManager.UpdateAsync(user);
    }

    private bool IsAllowedReturnUrl(string? returnUrl, ReturnUrlPurpose purpose)
    {
        if (string.IsNullOrWhiteSpace(returnUrl))
        {
            return false;
        }

        if (Url.IsLocalUrl(returnUrl))
        {
            return true;
        }

        if (!Uri.TryCreate(returnUrl, UriKind.Absolute, out var parsed))
        {
            return false;
        }

        var allowedPrefixes = purpose == ReturnUrlPurpose.Login
            ? new[] { _configuration["Auth:Issuer"] }
                .Concat(GetConfiguredReturnUrls("Auth:AllowedLoginReturnUrlPrefixes"))
            : GetConfiguredReturnUrls("Auth:AllowedLogoutReturnUrlPrefixes");

        return ReturnUrlValidator.IsAllowedExternal(parsed, allowedPrefixes, _allowInsecureReturnUrls);
    }

    private IEnumerable<string> GetConfiguredReturnUrls(string key) =>
        (_configuration[key] ?? string.Empty)
            .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

    private enum ReturnUrlPurpose
    {
        Login,
        Logout
    }
}
