using Microsoft.AspNetCore.Identity;
using MongoDB.Driver;

namespace Guestlist;

public sealed class UserService(MongoDbContext db)
{
    private readonly IMongoCollection<UserDocument> _users = db.Database.GetCollection<UserDocument>("users");
    private readonly PasswordHasher<UserDocument> _passwordHasher = new();

    private readonly IMongoCollection<UserBackupDocument> _userBackups = db.Database.GetCollection<UserBackupDocument>("user_backups");
    
    public Task CreateAdminAsync(string displayName, string username, string password)
    {
        return CreateUserAsync(displayName, username, password, UserRoles.Admin);
    }
    
    public Task CreateMaintainerAsync(string displayName, string username, string password)
    {
        return CreateUserAsync(displayName, username, password, UserRoles.Maintainer);
    }

    public async Task<List<UserDocument>> GetMaintainersAsync()
    {
        return await _users
            .Find(x => x.Role == UserRoles.Maintainer)
            .SortBy(x => x.Username)
            .ToListAsync();
    }

    public async Task DeleteMaintainerAsync(string id)
    {
        await _users.DeleteOneAsync(x => x.Id == id && x.Role == UserRoles.Maintainer);
    }

    public async Task DeleteUserByUsernameAsync(string username)
    {
        var normalizedUsername = InputRules.NormalizeUsername(username);
        await _users.DeleteOneAsync(x => x.Username == normalizedUsername);
    }
    
    private async Task CreateUserAsync(string displayName, string username, string password, string role)
    {
        var normalizedUsername = InputRules.NormalizeUsername(username);
        var cleanedDisplayName = string.IsNullOrWhiteSpace(displayName)
            ? normalizedUsername
            : InputRules.CleanRequiredText(displayName, "a name", InputRules.DisplayNameMaxLength);

        InputRules.ValidatePassword(password);

        if (await UsernameExistsAsync(normalizedUsername))
        {
            throw new InvalidOperationException("Username already exists.");
        }

        var user = new UserDocument
        {
            DisplayName = cleanedDisplayName,
            Username = normalizedUsername,
            Role = role,
            IsActive = true,
            CreatedAt = DateTime.UtcNow
        };

        user.PasswordHash = _passwordHasher.HashPassword(user, password);

        await _users.InsertOneAsync(user);
    }
    
    public async Task<bool> HasAnyUsersAsync()
    => await _users.CountDocumentsAsync(FilterDefinition<UserDocument>.Empty) > 0;
    
    public async Task<int> BackupAndClearUsersAsync(string reason)
    {
        var users = await _users
            .Find(FilterDefinition<UserDocument>.Empty)
            .ToListAsync();

        var backupBatchId = Guid.NewGuid().ToString("N");
        var backedUpAt = DateTime.UtcNow;
        
        if (users.Count > 0)
        {
            var backups = users.Select(user => new UserBackupDocument
            {
                BackupBatchId = backupBatchId,
                User = user,
                Reason = reason,
                BackedUpAt = backedUpAt
            });

            await _userBackups.InsertManyAsync(backups);
        }

        var deleteResult = await _users.DeleteManyAsync(FilterDefinition<UserDocument>.Empty);

        if (!deleteResult.IsAcknowledged)
        {
            throw new InvalidOperationException("MongoDB did not acknowledge user deletion.");
        }

        var remainingUsers = await _users.CountDocumentsAsync(FilterDefinition<UserDocument>.Empty);

        if (remainingUsers > 0)
        {
            throw new InvalidOperationException("Users could not be cleared.");
        }

        return users.Count;
    }
    
    public async Task<bool> HasAnyUserBackupsAsync()
    {
        var count = await _userBackups.CountDocumentsAsync(
            FilterDefinition<UserBackupDocument>.Empty);

        return count > 0;
    }
    
    public async Task<bool> RestoreLatestUserBackupAsync()
    {
        var latestBackup = await _userBackups
            .Find(x => x.BackupBatchId != "")
            .SortByDescending(x => x.BackedUpAt)
            .FirstOrDefaultAsync();

        if (latestBackup is null)
        {
            return false;
        }

        var existingUsersCount = await _users.CountDocumentsAsync(
            FilterDefinition<UserDocument>.Empty);

        if (existingUsersCount > 0)
        {
            return false;
        }

        var backupBatchId = latestBackup.BackupBatchId;

        var backups = await _userBackups
            .Find(x => x.BackupBatchId == backupBatchId)
            .ToListAsync();

        if (backups.Count == 0)
        {
            return false;
        }

        var users = backups.Select(backup =>
        {
            var user = backup.User;
            user.Id = null;
            return user;
        }).ToList();

        await _users.InsertManyAsync(users);

        return true;
    }
    
    public async Task<bool> UsernameExistsAsync(string username)
    {
        var normalizedUsername = InputRules.NormalizeUsername(username);
        return await _users.CountDocumentsAsync(x => x.Username == normalizedUsername) > 0;
    }
    
    public async Task EnsureIndexesAsync()
    {
        var usernameIndex = new CreateIndexModel<UserDocument>(
            Builders<UserDocument>.IndexKeys.Ascending(x => x.Username),
            new CreateIndexOptions { Unique = true });

        await _users.Indexes.CreateOneAsync(usernameIndex);
    }
    
    public async Task<UserDocument?> ValidateCredentialsAsync(string username, string password)
    {
        if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
        {
            return null;
        }

        if (password.Length > InputRules.PasswordMaxLength)
        {
            return null;
        }

        string normalizedUsername;

        try
        {
            normalizedUsername = InputRules.NormalizeUsername(username);
        }
        catch (InvalidOperationException)
        {
            return null;
        }

        var user = await _users
            .Find(x => x.Username == normalizedUsername && x.IsActive)
            .FirstOrDefaultAsync();

        if (user is null)
        {
            return null;
        }

        var result = _passwordHasher.VerifyHashedPassword(
            user,
            user.PasswordHash,
            password);

        return result == PasswordVerificationResult.Success ? user : null;
    }
}
