using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace ErsatzTV.Application.Auth;

public class VerifyLocalAdminLoginHandler(
    IDbContextFactory<TvContext> dbContextFactory,
    ILocalPasswordHasher passwordHasher)
    : IRequestHandler<VerifyLocalAdminLogin, Either<BaseError, LocalAdminPrincipal>>
{
    private static readonly BaseError InvalidCredentials = BaseError.New("Invalid username or password");

    public async Task<Either<BaseError, LocalAdminPrincipal>> Handle(
        VerifyLocalAdminLogin request,
        CancellationToken cancellationToken)
    {
        string username = (request.Username ?? string.Empty).Trim();

        await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        // Read the hash and stamp in ONE snapshot so they are consistent (issue: a login racing a password
        // change must not return a stamp newer than the hash it verified). A concurrent change is then either
        // wholly before this read (the old password fails to verify) or wholly after it (we return the
        // pre-change stamp, so the cookie AuthController issues is revoked on its very next request by
        // CookieSecurityStampValidator). No writes happen here, so there is nothing to clobber.
        Dictionary<string, string> config = await dbContext.ConfigElements
            .Where(c => c.Key == ConfigElementKey.AuthLocalAdminUsername.Key
                        || c.Key == ConfigElementKey.AuthLocalAdminPasswordHash.Key
                        || c.Key == ConfigElementKey.AuthSecurityStamp.Key)
            .ToDictionaryAsync(c => c.Key, c => c.Value, cancellationToken);

        config.TryGetValue(ConfigElementKey.AuthLocalAdminUsername.Key, out string storedUser);
        config.TryGetValue(ConfigElementKey.AuthLocalAdminPasswordHash.Key, out string storedHash);
        config.TryGetValue(ConfigElementKey.AuthSecurityStamp.Key, out string stamp);

        // Always run exactly one PBKDF2 verify — against a dummy hash when unconfigured/unknown — so response
        // timing does not reveal whether the account exists (no user enumeration).
        string candidateHash = storedHash ?? passwordHasher.DummyHash;
        LocalPasswordVerification result = passwordHasher.Verify(candidateHash, request.Password ?? string.Empty);

        bool userMatches = storedUser is not null
                           && string.Equals(storedUser, username, StringComparison.OrdinalIgnoreCase);

        if (storedHash is null || !userMatches || result == LocalPasswordVerification.Failed)
        {
            return InvalidCredentials;
        }

        return new LocalAdminPrincipal(storedUser, stamp ?? string.Empty);
    }
}
