using System.Text;
using Fido2NetLib;
using Fido2NetLib.Objects;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using PowderCoating.Core.Entities;
using PowderCoating.Infrastructure.Data;

namespace PowderCoating.Web.Controllers;

/// <summary>
/// Handles WebAuthn / FIDO2 passkey registration and authentication.
/// Registration requires an authenticated session (user logs in once with password,
/// then enrolls a passkey for future logins). Authentication is anonymous — the
/// browser sends the credential before any session exists.
///
/// Fido2 is constructed per-request from the incoming Host header so the RPID
/// matches automatically on localhost, dev, staging, and production without any
/// environment-specific configuration.
/// </summary>
// Intentional exception: WebAuthn/FIDO2 identity infrastructure. UserPasskeys is an ASP.NET Identity concern not exposed through IUnitOfWork; the anonymous login path has no tenant context; FIDO2 async callbacks capture _db by closure. See docs/DATA_ACCESS_ARCHITECTURE.md — Permanent Exceptions.
[Route("[controller]/[action]")]
public class PasskeyController : Controller
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly ApplicationDbContext _db;
    private readonly ILogger<PasskeyController> _logger;

    private const string RegChallengeKey = "passkey:reg:challenge";
    private const string AuthChallengeKey = "passkey:auth:challenge";

    public PasskeyController(
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager,
        ApplicationDbContext db,
        ILogger<PasskeyController> logger)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _db = db;
        _logger = logger;
    }

    /// <summary>
    /// Builds a Fido2 instance whose RPID and origin are derived from the current
    /// request, so the same code works on localhost, dev, and production unchanged.
    /// </summary>
    private IFido2 BuildFido2()
    {
        var req = HttpContext.Request;
        var host = req.Host.Host;                          // "localhost" or "myapp.azurewebsites.net"
        var port = req.Host.Port;
        var origin = port.HasValue
            ? $"{req.Scheme}://{host}:{port}"
            : $"{req.Scheme}://{host}";

        var config = new Fido2Configuration
        {
            ServerDomain = host,
            ServerName = "Powder Coating Logix",
            Origins = new HashSet<string> { origin },
            TimestampDriftTolerance = 300
        };

        return new Fido2(config);
    }

    // ─── Registration ────────────────────────────────────────────────────────

    /// <summary>
    /// Returns a WebAuthn creation options object for the currently signed-in user.
    /// Stores the challenge in session so Register can verify it.
    /// </summary>
    [HttpPost]
    [Authorize]
    public async Task<IActionResult> RegisterOptions()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null) return Unauthorized();

        var existingKeys = await _db.UserPasskeys
            .Where(p => p.UserId == user.Id)
            .Select(p => p.CredentialId)
            .ToListAsync();

        var fidoUser = new Fido2User
        {
            Id = Encoding.UTF8.GetBytes(user.Id),
            Name = user.Email!,
            DisplayName = user.FullName ?? user.Email!
        };

        var excludeCredentials = existingKeys
            .Select(k => new PublicKeyCredentialDescriptor(k))
            .ToList();

        var authenticatorSelection = new AuthenticatorSelection
        {
            ResidentKey = ResidentKeyRequirement.Required,
            UserVerification = UserVerificationRequirement.Required
        };

        var options = BuildFido2().RequestNewCredential(new RequestNewCredentialParams
        {
            User = fidoUser,
            ExcludeCredentials = excludeCredentials,
            AuthenticatorSelection = authenticatorSelection,
            AttestationPreference = AttestationConveyancePreference.None
        });

        HttpContext.Session.SetString(RegChallengeKey, options.ToJson());

        return Ok(options);
    }

    /// <summary>
    /// Verifies the authenticator response and persists the new passkey credential.
    /// </summary>
    [HttpPost]
    [Authorize]
    public async Task<IActionResult> Register(
        [FromBody] AuthenticatorAttestationRawResponse attestationResponse,
        [FromQuery] string? deviceName)
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null) return Unauthorized();

        var optionsJson = HttpContext.Session.GetString(RegChallengeKey);
        if (string.IsNullOrEmpty(optionsJson))
            return BadRequest(new { error = "Session expired — please try again." });

        HttpContext.Session.Remove(RegChallengeKey);

        RegisteredPublicKeyCredential credential;
        try
        {
            var options = CredentialCreateOptions.FromJson(optionsJson);
            credential = await BuildFido2().MakeNewCredentialAsync(new MakeNewCredentialParams
            {
                AttestationResponse = attestationResponse,
                OriginalOptions = options,
                IsCredentialIdUniqueToUserCallback = async (args, _) =>
                    !await _db.UserPasskeys.AnyAsync(p => p.CredentialId == args.CredentialId)
            });
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Passkey registration failed for user {UserId}", user.Id);
            return BadRequest(new { error = ex.Message });
        }

        var passkey = new UserPasskey
        {
            UserId = user.Id,
            CompanyId = user.CompanyId,
            CredentialId = credential.Id,
            PublicKey = credential.PublicKey,
            UserHandle = credential.User.Id,
            SignCount = credential.SignCount,
            DeviceFriendlyName = string.IsNullOrWhiteSpace(deviceName) ? null : deviceName.Trim()
        };

        _db.UserPasskeys.Add(passkey);
        await _db.SaveChangesAsync();

        _logger.LogInformation("Passkey registered for user {UserId} ({DeviceName})",
            user.Id, passkey.DeviceFriendlyName ?? "(unnamed)");

        return Ok(new { message = "Passkey registered successfully." });
    }

    // ─── Authentication ───────────────────────────────────────────────────────

    /// <summary>
    /// Returns a WebAuthn assertion options object. No session required — called before login.
    /// </summary>
    [HttpPost]
    [AllowAnonymous]
    public IActionResult LoginOptions()
    {
        var options = BuildFido2().GetAssertionOptions(new GetAssertionOptionsParams
        {
            AllowedCredentials = [],
            UserVerification = UserVerificationRequirement.Required
        });

        HttpContext.Session.SetString(AuthChallengeKey, options.ToJson());

        return Ok(options);
    }

    /// <summary>
    /// Verifies the assertion response against stored credentials and signs the user in.
    /// </summary>
    [HttpPost]
    [AllowAnonymous]
    public async Task<IActionResult> Login([FromBody] AuthenticatorAssertionRawResponse assertionResponse)
    {
        var optionsJson = HttpContext.Session.GetString(AuthChallengeKey);
        if (string.IsNullOrEmpty(optionsJson))
            return BadRequest(new { error = "Session expired — please try again." });

        HttpContext.Session.Remove(AuthChallengeKey);

        // Look up passkey by credential ID (RawId is byte[], Id is base64url string)
        var credentialId = assertionResponse.RawId;
        var passkey = await _db.UserPasskeys
            .FirstOrDefaultAsync(p => p.CredentialId == credentialId);

        if (passkey == null)
            return BadRequest(new { error = "Passkey not recognised." });

        // Load the user — verify account is still active
        var user = await _userManager.FindByIdAsync(passkey.UserId);
        if (user == null || !user.IsActive)
            return BadRequest(new { error = "Account not found or deactivated." });

        if (await _userManager.IsLockedOutAsync(user))
            return BadRequest(new { error = "Account is locked. Please contact your administrator." });

        VerifyAssertionResult verifyResult;
        try
        {
            var options = AssertionOptions.FromJson(optionsJson);
            verifyResult = await BuildFido2().MakeAssertionAsync(new MakeAssertionParams
            {
                AssertionResponse = assertionResponse,
                OriginalOptions = options,
                StoredPublicKey = passkey.PublicKey,
                StoredSignatureCounter = (uint)passkey.SignCount,
                IsUserHandleOwnerOfCredentialIdCallback = (args, _) =>
                    Task.FromResult(args.UserHandle.SequenceEqual(passkey.UserHandle))
            });
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Passkey assertion failed for user {UserId}", passkey.UserId);
            return BadRequest(new { error = "Passkey verification failed." });
        }

        // Update sign count and last-used timestamp
        passkey.SignCount = verifyResult.SignCount;
        passkey.LastUsedAt = DateTime.UtcNow;
        await _db.SaveChangesAsync();

        // Sign in — passkey satisfies both factors; no further 2FA required
        await _signInManager.SignInAsync(user, isPersistent: false);

        // Track login date so CompanyHealth and audit pages show accurate last-login times
        user.LastLoginDate = DateTime.UtcNow;
        await _userManager.UpdateAsync(user);

        _logger.LogInformation("User {UserId} signed in via passkey", user.Id);

        return Ok(new { redirectUrl = Url.Action("Index", "Dashboard") });
    }

    // ─── Management ───────────────────────────────────────────────────────────

    // ─── Post-login enrollment prompt ─────────────────────────────────────────

    /// <summary>
    /// Shown immediately after password login. Skips to returnUrl if the user already
    /// has a passkey or has previously dismissed the prompt.
    /// </summary>
    [Authorize]
    [HttpGet("/Passkey/EnrollPrompt")]
    public async Task<IActionResult> EnrollPrompt(string? returnUrl)
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null) return Unauthorized();

        var hasPasskey = await _db.UserPasskeys.AnyAsync(p => p.UserId == user.Id);
        if (hasPasskey || user.PasskeyPromptDismissed)
            return Redirect(returnUrl ?? "/");

        ViewBag.ReturnUrl = returnUrl ?? "/";
        return View();
    }

    /// <summary>
    /// Permanently dismisses the passkey enrollment prompt for this user. They can
    /// re-enable it from Profile → Security at any time.
    /// </summary>
    [Authorize]
    [HttpPost("/Passkey/DismissPrompt")]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DismissPrompt(string? returnUrl)
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null) return Unauthorized();

        user.PasskeyPromptDismissed = true;
        await _userManager.UpdateAsync(user);

        return Redirect(returnUrl ?? "/");
    }

    /// <summary>Shows all passkeys registered by the current user.</summary>
    [Authorize]
    [HttpGet("/Passkey/Manage")]
    public async Task<IActionResult> Manage()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null) return Unauthorized();

        var passkeys = await _db.UserPasskeys
            .Where(p => p.UserId == user.Id)
            .OrderByDescending(p => p.CreatedAt)
            .ToListAsync();

        return View(passkeys);
    }

    /// <summary>Removes a specific passkey for the current user.</summary>
    [HttpPost]
    [Authorize]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Remove(int id)
    {
        var user = await _userManager.GetUserAsync(User);
        if (user == null) return Unauthorized();

        var passkey = await _db.UserPasskeys
            .FirstOrDefaultAsync(p => p.Id == id && p.UserId == user.Id);

        if (passkey == null)
            return NotFound();

        _db.UserPasskeys.Remove(passkey);
        await _db.SaveChangesAsync();

        _logger.LogInformation("Passkey {PasskeyId} removed for user {UserId}", id, user.Id);

        TempData["Success"] = "Passkey removed.";
        return RedirectToAction(nameof(Manage));
    }
}
