using Fido2NetLib;
using Fido2NetLib.Objects;
using Forge.Core.Models;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;

namespace Forge.Data.Services;

public interface IFido2Service
{
    /// <summary>
    /// Get options to start passkey registration
    /// </summary>
    Task<CredentialCreateOptions> StartRegistrationAsync(string username);
    
    /// <summary>
    /// Complete passkey registration
    /// </summary>
    Task<PasskeyCredential> CompleteRegistrationAsync(string username, AuthenticatorAttestationRawResponse response, string? deviceName = null);
    
    /// <summary>
    /// Get options to start passkey authentication
    /// </summary>
    Task<AssertionOptions> StartAuthenticationAsync();
    
    /// <summary>
    /// Complete passkey authentication
    /// </summary>
    Task<string?> CompleteAuthenticationAsync(AuthenticatorAssertionRawResponse response);
    
    /// <summary>
    /// Get all passkeys for a user
    /// </summary>
    Task<IEnumerable<PasskeyCredential>> GetCredentialsAsync(string username);
    
    /// <summary>
    /// Delete a passkey
    /// </summary>
    Task<bool> DeleteCredentialAsync(Guid credentialId, string username);
}

public class Fido2Service : IFido2Service
{
    private readonly IFido2 _fido2;
    private readonly ForgeDbContext _db;
    
    // Store pending challenges in memory (in production, use distributed cache)
    private static readonly Dictionary<string, CredentialCreateOptions> _pendingRegistrations = new();
    private static readonly Dictionary<string, AssertionOptions> _pendingAssertions = new();

    public Fido2Service(IFido2 fido2, ForgeDbContext db)
    {
        _fido2 = fido2;
        _db = db;
    }

    public async Task<CredentialCreateOptions> StartRegistrationAsync(string username)
    {
        // Get existing credentials for this user
        var existingCredentials = await _db.PasskeyCredentials
            .Where(c => c.Username == username)
            .ToListAsync();
        
        var existingKeys = existingCredentials
            .Select(c => new PublicKeyCredentialDescriptor(c.CredentialId))
            .ToList();
        
        var user = new Fido2User
        {
            Id = System.Text.Encoding.UTF8.GetBytes(username),
            Name = username,
            DisplayName = username
        };
        
        var options = _fido2.RequestNewCredential(new RequestNewCredentialParams
        {
            User = user,
            ExcludeCredentials = existingKeys,
            AuthenticatorSelection = new AuthenticatorSelection
            {
                AuthenticatorAttachment = AuthenticatorAttachment.Platform,
                ResidentKey = ResidentKeyRequirement.Required,
                UserVerification = UserVerificationRequirement.Required
            },
            AttestationPreference = AttestationConveyancePreference.None
        });
        
        // Store options for verification
        _pendingRegistrations[username] = options;
        
        return options;
    }

    public async Task<PasskeyCredential> CompleteRegistrationAsync(
        string username, 
        AuthenticatorAttestationRawResponse response,
        string? deviceName = null)
    {
        if (!_pendingRegistrations.TryGetValue(username, out var options))
        {
            throw new InvalidOperationException("No pending registration found. Please start registration first.");
        }
        
        var result = await _fido2.MakeNewCredentialAsync(new MakeNewCredentialParams
        {
            AttestationResponse = response,
            OriginalOptions = options,
            IsCredentialIdUniqueToUserCallback = async (args, cancellationToken) =>
            {
                // Verify this credential isn't already registered
                return !await _db.PasskeyCredentials.AnyAsync(c => c.CredentialId.SequenceEqual(args.CredentialId), cancellationToken);
            }
        });
        
        var credential = new PasskeyCredential
        {
            Id = Guid.NewGuid(),
            Username = username,
            CredentialId = result.Id,
            PublicKey = result.PublicKey,
            SignCount = result.SignCount,
            AaGuid = result.AaGuid,
            Name = deviceName,
            CreatedAt = DateTime.UtcNow
        };
        
        _db.PasskeyCredentials.Add(credential);
        await _db.SaveChangesAsync();
        
        // Clean up pending registration
        _pendingRegistrations.Remove(username);
        
        return credential;
    }

    public Task<AssertionOptions> StartAuthenticationAsync()
    {
        // Allow any resident credential (passkey) to be used
        var options = _fido2.GetAssertionOptions(new GetAssertionOptionsParams
        {
            AllowedCredentials = [],
            UserVerification = UserVerificationRequirement.Required
        });
        
        // Store options for verification
        var key = Convert.ToBase64String(options.Challenge);
        _pendingAssertions[key] = options;
        
        return Task.FromResult(options);
    }

    public async Task<string?> CompleteAuthenticationAsync(AuthenticatorAssertionRawResponse response)
    {
        // Find the credential by ID
        var responseId = response.RawId;
        var allCredentials = await _db.PasskeyCredentials.ToListAsync();
        var credential = allCredentials.FirstOrDefault(c => c.CredentialId.AsSpan().SequenceEqual(responseId));
        
        if (credential == null)
        {
            return null;
        }
        
        // Find the pending assertion options
        AssertionOptions? options = null;
        string? optionsKey = null;
        
        foreach (var kvp in _pendingAssertions)
        {
            // Check if this challenge matches
            try
            {
                var clientData = System.Text.Json.JsonSerializer.Deserialize<JsonElement>(
                    System.Text.Encoding.UTF8.GetString(response.Response.ClientDataJson));
                if (clientData.TryGetProperty("challenge", out var challengeProp) &&
                    challengeProp.GetString() == kvp.Key)
                {
                    options = kvp.Value;
                    optionsKey = kvp.Key;
                    break;
                }
            }
            catch
            {
                // Continue searching
            }
        }
        
        if (options == null)
        {
            throw new InvalidOperationException("No pending authentication found.");
        }
        
        // Verify the assertion
        var result = await _fido2.MakeAssertionAsync(new MakeAssertionParams
        {
            AssertionResponse = response,
            OriginalOptions = options,
            StoredPublicKey = credential.PublicKey,
            StoredSignatureCounter = credential.SignCount,
            IsUserHandleOwnerOfCredentialIdCallback = (args, cancellationToken) =>
            {
                // Verify the credential belongs to the user handle
                return Task.FromResult(credential.Username == System.Text.Encoding.UTF8.GetString(args.UserHandle));
            }
        });
        
        // Update sign count and last used
        credential.SignCount = result.SignCount;
        credential.LastUsedAt = DateTime.UtcNow;
        await _db.SaveChangesAsync();
        
        // Clean up pending assertion
        if (optionsKey != null)
        {
            _pendingAssertions.Remove(optionsKey);
        }
        
        return credential.Username;
    }

    public async Task<IEnumerable<PasskeyCredential>> GetCredentialsAsync(string username)
    {
        return await _db.PasskeyCredentials
            .Where(c => c.Username == username)
            .OrderByDescending(c => c.LastUsedAt ?? c.CreatedAt)
            .ToListAsync();
    }

    public async Task<bool> DeleteCredentialAsync(Guid credentialId, string username)
    {
        var credential = await _db.PasskeyCredentials
            .FirstOrDefaultAsync(c => c.Id == credentialId && c.Username == username);
        
        if (credential == null)
        {
            return false;
        }
        
        _db.PasskeyCredentials.Remove(credential);
        await _db.SaveChangesAsync();
        
        return true;
    }
}
An unhandled error has occurred. Reload 🗙