using System.Security.Cryptography;
using System.Text.Json;
using Backend.Common;
using Backend.Contracts;
using Backend.Data;
using Backend.Domain;
using Backend.Security;
using Backend.Services;
using Microsoft.EntityFrameworkCore;

namespace Backend.Endpoints;

public static class AdminTeamEndpoints
{
    private const int MinPasswordLength = 10;
    private static readonly TimeSpan OnlineActivityWindow = TimeSpan.FromMinutes(5);
    private const string PersonalOwnerLogin = "jayuhime";
    private static readonly string[] PersonalCreatorLogins = ["sleepy_bao"];

    private static readonly AdminTeamPermissionDto[] PermissionCatalog =
    [
        new(AdminPermissionCatalog.Dashboard, "Dashboard", "Live-Lage, Aufgaben und Checks sehen.", "Betrieb", "/admin/dashboard", true),
        new(AdminPermissionCatalog.Nominations, "Nominierungen", "Nominierungen prüfen und entscheiden.", "Betrieb", "/admin/nominations", false),
        new(AdminPermissionCatalog.Years, "Jahre", "Award-Jahre anlegen und pflegen.", "Awards", "/admin/years", false),
        new(AdminPermissionCatalog.Categories, "Kategorien", "Hauptkategorien, Unterkategorien und Limits verwalten.", "Awards", "/admin/categories", false),
        new(AdminPermissionCatalog.Candidates, "Kandidaten", "Kandidatenbasis, Clips und Annahmestatus pflegen.", "Awards", "/admin/candidates", false),
        new(AdminPermissionCatalog.Clips, "Clips", "Optionale Clip-Einreichungen prüfen.", "Awards", "/admin/clips", false),
        new(AdminPermissionCatalog.Content, "Landingpage", "FAQ, Links, Footer, Showacts und öffentliche Inhalte pflegen.", "Landingpage", "/admin/content", false),
        new(AdminPermissionCatalog.Risk, "Risiko", "Flags, Regeln und Moderationsrisiken sehen.", "Kontrolle", "/admin/risk", true),
        new(AdminPermissionCatalog.Audit, "Audit-Log", "Admin-Aktionen nachvollziehen.", "Kontrolle", "/admin/users-logs", true),
        new(AdminPermissionCatalog.Analytics, "Analytics", "Jahresmetriken und Überblick lesen.", "Auswertung", "/admin/analytics", true),
        new(AdminPermissionCatalog.Voting, "Voting", "Stimmenlage und Gewinner-Vorbereitung sehen.", "Auswertung", "/admin/voting", true),
        new(AdminPermissionCatalog.Winners, "Gewinner", "Finale Ergebnisse pflegen und freigeben.", "Auswertung", "/admin/winners", false),
        new(AdminPermissionCatalog.Settings, "Einstellungen", "Systemchecks, Demo-Zugang, Wartung und Workflow-Steuerung sehen.", "Einstellungen", "/admin/settings", true),
        new(AdminPermissionCatalog.Team, "Team", "Mitglieder, Rollen und Berechtigungen verwalten.", "Einstellungen", "/admin/team", false),
    ];

    private static readonly AdminTeamRoleDto[] DefaultRoles =
    [
        new(AdminRoles.Owner, "Owner", "Jayuhime: vollständige Kontrolle inklusive Passwortreset und Betriebseinstellungen.", true, AdminPermissionCatalog.DefaultPermissionKeys(AdminRoles.Owner)),
        new(AdminRoles.Creator, "Creator", "Persönliche Jayuhime-Rolle mit denselben Rechten wie Owner.", true, AdminPermissionCatalog.DefaultPermissionKeys(AdminRoles.Creator)),
        new(AdminRoles.Admin, "Admins", "Operatives Kernteam mit Schreibrechten in allen Award-Bereichen.", true, AdminPermissionCatalog.DefaultPermissionKeys(AdminRoles.Admin)),
        new(AdminRoles.Member, "Mitglied", "Internes Team für Pflege, Nominierungen und Clip-Arbeit.", true, AdminPermissionCatalog.DefaultPermissionKeys(AdminRoles.Member)),
        new(AdminRoles.Reviewer, "Reviewer", "Review-Fokus für Nominierungen, Clips und Risiko-Hinweise.", true, AdminPermissionCatalog.DefaultPermissionKeys(AdminRoles.Reviewer)),
        new(AdminRoles.OrganizationTeam, "Organisation Team", "Externe Personen mit Read-only-Sicht auf ausgewählte Bereiche.", true, AdminPermissionCatalog.DefaultPermissionKeys(AdminRoles.OrganizationTeam)),
    ];

    public static RouteGroupBuilder MapAdminTeamEndpoints(this RouteGroupBuilder group)
    {
        group.MapGet("/team", GetTeam)
            .AddEndpointFilter((context, next) => AdminEndpointConventions.RequirePermission(context, next, AdminPermissionCatalog.Team))
            .WithName("GetAdminTeam")
            .WithOpenApi();
        group.MapPost("/team/members", CreateMember)
            .AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, AdminPermissionCatalog.Team))
            .WithName("CreateAdminTeamMember")
            .WithOpenApi();
        group.MapPut("/team/members/{memberId:int}", UpdateMember)
            .AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, AdminPermissionCatalog.Team))
            .WithName("UpdateAdminTeamMember")
            .WithOpenApi();
        group.MapPost("/team/members/{memberId:int}/reset-password", ResetPassword)
            .AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, AdminPermissionCatalog.Team))
            .WithName("ResetAdminTeamMemberPassword")
            .WithOpenApi();
        group.MapDelete("/team/members/{memberId:int}", DeleteMember)
            .AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, AdminPermissionCatalog.Team))
            .WithName("DeleteAdminTeamMember")
            .WithOpenApi();
        group.MapPut("/team/roles", UpdateRoles)
            .AddEndpointFilter((context, next) => AdminEndpointConventions.RequireWritePermission(context, next, AdminPermissionCatalog.Team))
            .WithName("UpdateAdminTeamRoles")
            .WithOpenApi();
        return group;
    }

    private static async Task<IResult> GetTeam(AwardsDbContext db)
    {
        var members = await db.TeamMembers
            .AsNoTracking()
            .OrderByDescending(item => item.Role == AdminRoles.Owner)
            .ThenBy(item => item.Role)
            .ThenBy(item => item.DisplayName)
            .ToArrayAsync();
        var sessionKeys = members
            .SelectMany(MemberSessionKeys)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();
        var sessions = sessionKeys.Length == 0
            ? []
            : await db.UserSessions
                .AsNoTracking()
                .Where(item => sessionKeys.Contains(item.TwitchUserId))
                .ToArrayAsync();
        var now = DateTimeOffset.UtcNow;

        return Results.Ok(new AdminTeamResponse(
            members.Select(member => ToMemberDto(member, sessions, now)),
            await BuildRoleDtosAsync(db),
            PermissionCatalog));
    }

    private static async Task<IResult> CreateMember(
        HttpContext context,
        AwardsDbContext db,
        CreateTeamMemberRequest request,
        IAdminAuditService adminAuditService)
    {
        var session = AdminEndpointConventions.CurrentSession(context);

        var normalizedLogin = NormalizeLogin(request.Login);
        var normalizedRole = AdminRoles.Normalize(request.Role);
        var displayName = NormalizeDisplayName(request.DisplayName);
        var validationError = ValidateMemberFields(normalizedLogin, displayName, normalizedRole);
        if (validationError is not null)
        {
            return validationError;
        }

        if (normalizedRole == AdminRoles.Creator)
        {
            return Results.Json(new { message = "Creator-Konten werden fest vorkonfiguriert und können nicht manuell angelegt werden." }, statusCode: StatusCodes.Status403Forbidden);
        }

        if (AdminRoles.IsPrivilegedFullControlRole(normalizedRole) && !AdminRoles.CanResetTeamPasswords(session.Role))
        {
            return Results.Json(new { message = "Owner- und Creator-Konten können nur von Owner oder Creator angelegt werden." }, statusCode: StatusCodes.Status403Forbidden);
        }

        var privilegedValidationError = await ValidatePrivilegedMemberChangeAsync(db, null, normalizedLogin, normalizedRole, context.RequestAborted);
        if (privilegedValidationError is not null)
        {
            return privilegedValidationError;
        }

        if (await db.TeamMembers.AnyAsync(item => item.Login == normalizedLogin, context.RequestAborted))
        {
            return Results.Conflict(new { message = "Dieser Login existiert bereits." });
        }

        var generatedPassword = GeneratePassword();
        var credentials = DemoCredentialHasher.HashPassword(generatedPassword);
        var member = new TeamMember
        {
            Login = normalizedLogin,
            DisplayName = displayName,
            Role = normalizedRole,
            PasswordHash = credentials.Hash,
            PasswordSalt = credentials.Salt,
            MustChangePassword = true,
            IsActive = true,
            CreatedAt = DateTimeOffset.UtcNow,
            CreatedByTwitchId = session.TwitchUserId,
        };

        db.TeamMembers.Add(member);
        adminAuditService.AddEntry(
            session.TwitchUserId,
            "team.member.create",
            "team-member",
            normalizedLogin,
            $"Team-Login {displayName} wurde angelegt.",
            new { member.Login, member.DisplayName, member.Role },
            RequestMetadataReader.Read(context));

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new TeamMemberPasswordResponse(true, member.Id, generatedPassword, member.MustChangePassword));
    }

    private static async Task<IResult> UpdateMember(
        int memberId,
        HttpContext context,
        AwardsDbContext db,
        UpdateTeamMemberRequest request,
        IAdminAuditService adminAuditService)
    {
        var session = AdminEndpointConventions.CurrentSession(context);

        var member = await db.TeamMembers.FirstOrDefaultAsync(item => item.Id == memberId, context.RequestAborted);
        if (member is null)
        {
            return Results.NotFound();
        }

        var normalizedLogin = NormalizeLogin(request.Login);
        var normalizedRole = AdminRoles.Normalize(request.Role);
        var displayName = NormalizeDisplayName(request.DisplayName);
        var validationError = ValidateMemberFields(normalizedLogin, displayName, normalizedRole);
        if (validationError is not null)
        {
            return validationError;
        }

        var loginChanged = !string.Equals(member.Login, normalizedLogin, StringComparison.OrdinalIgnoreCase);
        if (loginChanged && !AdminRoles.CanDeleteTeamMembers(session.Role))
        {
            return Results.Json(new { message = "Logins können nur von Owner oder Creator geändert werden." }, statusCode: StatusCodes.Status403Forbidden);
        }

        if (loginChanged && await db.TeamMembers.AnyAsync(item => item.Id != member.Id && item.Login == normalizedLogin, context.RequestAborted))
        {
            return Results.Conflict(new { message = "Dieser Login existiert bereits." });
        }

        if (normalizedRole == AdminRoles.Creator && member.Role != AdminRoles.Creator)
        {
            return Results.Json(new { message = "Creator-Konten werden fest vorkonfiguriert und können nicht manuell zugewiesen werden." }, statusCode: StatusCodes.Status403Forbidden);
        }

        if ((AdminRoles.IsPrivilegedFullControlRole(member.Role) || AdminRoles.IsPrivilegedFullControlRole(normalizedRole)) && !AdminRoles.CanResetTeamPasswords(session.Role))
        {
            return Results.Json(new { message = "Owner- und Creator-Konten können nur von Owner oder Creator bearbeitet werden." }, statusCode: StatusCodes.Status403Forbidden);
        }

        var previousLogin = member.Login;
        var privilegedValidationError = await ValidatePrivilegedMemberChangeAsync(db, member.Id, normalizedLogin, normalizedRole, context.RequestAborted);
        if (privilegedValidationError is not null)
        {
            return privilegedValidationError;
        }

        member.Login = normalizedLogin;
        member.DisplayName = displayName;
        member.Role = normalizedRole;
        member.IsActive = request.IsActive;
        member.UpdatedAt = DateTimeOffset.UtcNow;
        member.UpdatedByTwitchId = session.TwitchUserId;

        adminAuditService.AddEntry(
            session.TwitchUserId,
            "team.member.update",
            "team-member",
            member.Id.ToString(),
            $"Team-Login {member.DisplayName} wurde aktualisiert.",
            new { previousLogin, member.Login, member.DisplayName, member.Role, member.IsActive },
            RequestMetadataReader.Read(context));

        if (!member.IsActive)
        {
            DeactivateMemberSessions(db, member, previousLogin);
        }
        else
        {
            await SyncMemberSessionRolesAsync(db, member, previousLogin, context.RequestAborted);
        }

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new { saved = true, member = ToMemberDto(member) });
    }

    private static async Task<IResult> ResetPassword(
        int memberId,
        HttpContext context,
        AwardsDbContext db,
        IAdminAuditService adminAuditService)
    {
        var session = AdminEndpointConventions.CurrentSession(context);
        if (!AdminRoles.CanResetTeamPasswords(session.Role))
        {
            return Results.Json(new { message = "Passwort-Reset ist Owner und Creator vorbehalten." }, statusCode: StatusCodes.Status403Forbidden);
        }

        var member = await db.TeamMembers.FirstOrDefaultAsync(item => item.Id == memberId, context.RequestAborted);
        if (member is null)
        {
            return Results.NotFound();
        }

        var generatedPassword = GeneratePassword();
        var credentials = DemoCredentialHasher.HashPassword(generatedPassword);
        member.PasswordHash = credentials.Hash;
        member.PasswordSalt = credentials.Salt;
        member.MustChangePassword = true;
        member.PasswordResetAt = DateTimeOffset.UtcNow;
        member.UpdatedAt = DateTimeOffset.UtcNow;
        member.UpdatedByTwitchId = session.TwitchUserId;

        adminAuditService.AddEntry(
            session.TwitchUserId,
            "team.member.password-reset",
            "team-member",
            member.Id.ToString(),
            $"Passwort für {member.DisplayName} wurde zurückgesetzt.",
            new { member.Login, member.Role },
            RequestMetadataReader.Read(context));

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new TeamMemberPasswordResponse(true, member.Id, generatedPassword, member.MustChangePassword));
    }

    private static async Task<IResult> DeleteMember(
        int memberId,
        HttpContext context,
        AwardsDbContext db,
        IAdminAuditService adminAuditService)
    {
        var session = AdminEndpointConventions.CurrentSession(context);
        if (!AdminRoles.CanDeleteTeamMembers(session.Role))
        {
            return Results.Json(new { message = "Mitglieder löschen ist Owner und Creator vorbehalten." }, statusCode: StatusCodes.Status403Forbidden);
        }

        var member = await db.TeamMembers.FirstOrDefaultAsync(item => item.Id == memberId, context.RequestAborted);
        if (member is null)
        {
            return Results.NotFound();
        }

        if (AdminRoles.IsPrivilegedFullControlRole(member.Role) && !CanDeletePrivilegedMember(session, member))
        {
            return Results.Json(new { message = "Creator darf Owner-Konten löschen. Creator-Konten können nur vom eigenen Creator-Account gelöscht werden." }, statusCode: StatusCodes.Status403Forbidden);
        }

        DeactivateMemberSessions(db, member);
        db.TeamMembers.Remove(member);

        adminAuditService.AddEntry(
            session.TwitchUserId,
            "team.member.delete",
            "team-member",
            member.Id.ToString(),
            $"Team-Login {member.DisplayName} wurde gelöscht.",
            new { member.Login, member.DisplayName, member.Role },
            RequestMetadataReader.Read(context));

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new DeleteTeamMemberResponse(true, memberId));
    }

    private static async Task<IResult> UpdateRoles(
        HttpContext context,
        AwardsDbContext db,
        UpdateTeamRolesRequest request,
        IAdminAuditService adminAuditService)
    {
        var session = AdminEndpointConventions.CurrentSession(context);
        if (!AdminRoles.IsPrivilegedFullControlRole(session.Role))
        {
            return Results.Json(new { message = "Rollenberechtigungen können nur von Owner oder Creator geändert werden." }, statusCode: StatusCodes.Status403Forbidden);
        }

        var allowedPermissions = AdminPermissionCatalog.AllPermissionKeys.ToHashSet(StringComparer.OrdinalIgnoreCase);
        var allowedRoles = DefaultRoles.Select(item => item.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
        var existingRows = await db.TeamRolePermissions.ToDictionaryAsync(item => item.Role, StringComparer.OrdinalIgnoreCase, context.RequestAborted);
        var roleChanges = new List<object>();

        foreach (var roleUpdate in request.Roles ?? [])
        {
            var role = AdminRoles.Normalize(roleUpdate.Key);
            if (!allowedRoles.Contains(role))
            {
                return Results.BadRequest(new { message = $"Unbekannte Rolle: {roleUpdate.Key}" });
            }

            var permissionKeys = (roleUpdate.PermissionKeys ?? [])
                .Select(item => item.Trim())
                .Where(item => allowedPermissions.Contains(item))
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToArray();
            permissionKeys = AdminPermissionCatalog.NormalizePermissionKeys(role, permissionKeys);

            if (AdminRoles.IsPrivilegedFullControlRole(role) && permissionKeys.Length != AdminPermissionCatalog.AllPermissionKeys.Length)
            {
                return Results.BadRequest(new { message = "Owner und Creator müssen alle Berechtigungen behalten." });
            }

            var beforePermissions = existingRows.TryGetValue(role, out var existingRow)
                ? AdminPermissionCatalog.NormalizePermissionKeys(role, ReadPermissionKeys(existingRow.PermissionsJson, AdminPermissionCatalog.DefaultPermissionKeys(role)))
                : AdminPermissionCatalog.NormalizePermissionKeys(role, AdminPermissionCatalog.DefaultPermissionKeys(role));

            if (!existingRows.TryGetValue(role, out var row))
            {
                row = new TeamRolePermission { Role = role };
                db.TeamRolePermissions.Add(row);
            }

            row.PermissionsJson = JsonSerializer.Serialize(permissionKeys);
            row.UpdatedAt = DateTimeOffset.UtcNow;
            row.UpdatedByTwitchId = session.TwitchUserId;

            if (!beforePermissions.SequenceEqual(permissionKeys, StringComparer.OrdinalIgnoreCase))
            {
                roleChanges.Add(new
                {
                    role,
                    before = beforePermissions,
                    after = permissionKeys,
                    added = permissionKeys.Except(beforePermissions, StringComparer.OrdinalIgnoreCase).ToArray(),
                    removed = beforePermissions.Except(permissionKeys, StringComparer.OrdinalIgnoreCase).ToArray(),
                });
            }
        }

        adminAuditService.AddEntry(
            session.TwitchUserId,
            "team.roles.update",
            "team-role",
            "matrix",
            "Team-Rollenberechtigungen wurden gespeichert.",
            new { roleCount = request.Roles?.Length ?? 0, changes = roleChanges },
            RequestMetadataReader.Read(context));

        await db.SaveChangesAsync(context.RequestAborted);
        return Results.Ok(new { saved = true, roles = await BuildRoleDtosAsync(db) });
    }

    public static async Task<AdminTeamRoleDto[]> BuildRoleDtosAsync(AwardsDbContext db)
    {
        var overrides = await db.TeamRolePermissions
            .AsNoTracking()
            .ToDictionaryAsync(item => item.Role, item => item.PermissionsJson);

        return DefaultRoles
            .Select(role =>
            {
                var permissions = overrides.TryGetValue(role.Key, out var json)
                    ? ReadPermissionKeys(json, role.PermissionKeys)
                    : role.PermissionKeys;

                permissions = AdminPermissionCatalog.NormalizePermissionKeys(role.Key, permissions);

                return role with { PermissionKeys = permissions };
            })
            .ToArray();
    }

    private static AdminTeamMemberDto ToMemberDto(
        TeamMember member,
        IReadOnlyCollection<UserSession>? sessions = null,
        DateTimeOffset? now = null)
    {
        var memberSessions = sessions?
            .Where(session => MemberSessionKeys(member).Contains(session.TwitchUserId, StringComparer.OrdinalIgnoreCase))
            .ToArray() ?? [];
        var lastSeenAt = memberSessions
            .Select(session => (DateTimeOffset?)session.LastSeenAt)
            .Max() ?? member.LastLoginAt;
        var isOnline = member.IsActive
            && memberSessions.Any(session =>
                session.IsActive
                && session.LastSeenAt >= (now ?? DateTimeOffset.UtcNow).Subtract(OnlineActivityWindow));

        return new(
            member.Id,
            member.Login,
            member.DisplayName,
            member.Role,
            member.BoundTwitchUserId,
            member.BoundTwitchDisplayName,
            member.IsActive,
            member.MustChangePassword,
            member.CreatedAt,
            member.UpdatedAt,
            member.LastLoginAt,
            lastSeenAt,
            isOnline,
            member.TwitchBoundAt,
            member.PasswordResetAt);
    }

    private static IEnumerable<string> MemberSessionKeys(TeamMember member)
    {
        yield return $"team:{member.Login}";

        if (!string.IsNullOrWhiteSpace(member.BoundTwitchUserId))
        {
            yield return member.BoundTwitchUserId;
        }
    }

    private static IEnumerable<string> ReadPermissionKeys(string json, IEnumerable<string> fallback)
    {
        try
        {
            return JsonSerializer.Deserialize<string[]>(json) ?? fallback;
        }
        catch (JsonException)
        {
            return fallback;
        }
    }

    private static IResult? ValidateMemberFields(string login, string displayName, string role)
    {
        if (string.IsNullOrWhiteSpace(login) || login.Length > 80)
        {
            return Results.BadRequest(new { message = "Login ist erforderlich und darf maximal 80 Zeichen haben." });
        }

        if (!login.All(value => char.IsLetterOrDigit(value) || value is '_' or '-' or '.'))
        {
            return Results.BadRequest(new { message = "Login darf nur Buchstaben, Zahlen, Punkt, Unterstrich und Bindestrich enthalten." });
        }

        if (string.IsNullOrWhiteSpace(displayName) || displayName.Length > 120)
        {
            return Results.BadRequest(new { message = "Anzeigename ist erforderlich und darf maximal 120 Zeichen haben." });
        }

        if (!AdminRoles.IsKnownRole(role) || role == AdminRoles.Viewer || role == AdminRoles.ContentAdmin)
        {
            return Results.BadRequest(new { message = "Bitte wähle Owner, Creator, Admins, Mitglied, Reviewer oder Organisation Team." });
        }

        return null;
    }

    private static async Task<IResult?> ValidatePrivilegedMemberChangeAsync(
        AwardsDbContext db,
        int? memberId,
        string login,
        string role,
        CancellationToken cancellationToken)
    {
        if (role == AdminRoles.Owner)
        {
            if (!string.Equals(login, PersonalOwnerLogin, StringComparison.OrdinalIgnoreCase))
            {
                return Results.BadRequest(new { message = $"Die Owner-Rolle ist nur für @{PersonalOwnerLogin} vorgesehen." });
            }

            var ownerExists = await db.TeamMembers.AnyAsync(
                item => item.Role == AdminRoles.Owner && (!memberId.HasValue || item.Id != memberId.Value),
                cancellationToken);
            return ownerExists
                ? Results.Conflict(new { message = "Es darf nur einen Owner-Account geben." })
                : null;
        }

        if (role != AdminRoles.Creator)
        {
            return null;
        }

        if (!PersonalCreatorLogins.Contains(login, StringComparer.OrdinalIgnoreCase))
        {
            return Results.BadRequest(new { message = $"Die Creator-Rolle ist nur für @{string.Join(" oder @", PersonalCreatorLogins)} vorgesehen." });
        }

        var creatorExists = await db.TeamMembers.AnyAsync(
            item => item.Role == AdminRoles.Creator && (!memberId.HasValue || item.Id != memberId.Value),
            cancellationToken);
        return creatorExists
            ? Results.Conflict(new { message = "Es darf nur einen Creator-Account geben." })
            : null;
    }

    private static void DeactivateMemberSessions(AwardsDbContext db, TeamMember member, string? previousLogin = null)
    {
        var teamSessionId = $"team:{member.Login}";
        var previousTeamSessionId = string.IsNullOrWhiteSpace(previousLogin) ? string.Empty : $"team:{NormalizeLogin(previousLogin)}";
        var twitchUserId = member.BoundTwitchUserId;
        foreach (var session in db.UserSessions.Where(item =>
            item.TwitchUserId == teamSessionId
            || (!string.IsNullOrWhiteSpace(previousTeamSessionId) && item.TwitchUserId == previousTeamSessionId)
            || (!string.IsNullOrWhiteSpace(twitchUserId) && item.TwitchUserId == twitchUserId)))
        {
            session.IsActive = false;
        }
    }

    private static bool IsOwnTeamAccount(UserSession session, TeamMember member)
    {
        var teamLogin = AuthEndpoints.ReadTeamLoginFromSession(session.TwitchUserId);
        if (!string.IsNullOrWhiteSpace(teamLogin)
            && string.Equals(teamLogin, member.Login, StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        return !string.IsNullOrWhiteSpace(member.BoundTwitchUserId)
            && string.Equals(session.TwitchUserId, member.BoundTwitchUserId, StringComparison.OrdinalIgnoreCase);
    }

    private static bool CanDeletePrivilegedMember(UserSession session, TeamMember member)
    {
        if (IsOwnTeamAccount(session, member))
        {
            return true;
        }

        return AdminRoles.Normalize(session.Role) == AdminRoles.Creator
            && AdminRoles.Normalize(member.Role) == AdminRoles.Owner;
    }

    private static async Task SyncMemberSessionRolesAsync(AwardsDbContext db, TeamMember member, string? previousLogin, CancellationToken cancellationToken)
    {
        var teamSessionId = $"team:{member.Login}";
        var previousTeamSessionId = string.IsNullOrWhiteSpace(previousLogin) ? string.Empty : $"team:{NormalizeLogin(previousLogin)}";
        var twitchUserId = member.BoundTwitchUserId;
        var sessions = await db.UserSessions
            .Where(item => item.IsActive
                && (item.TwitchUserId == teamSessionId
                    || (!string.IsNullOrWhiteSpace(previousTeamSessionId) && item.TwitchUserId == previousTeamSessionId)
                    || (!string.IsNullOrWhiteSpace(twitchUserId) && item.TwitchUserId == twitchUserId)))
            .ToArrayAsync(cancellationToken);

        foreach (var session in sessions)
        {
            if (session.TwitchUserId == previousTeamSessionId)
            {
                session.TwitchUserId = teamSessionId;
            }

            session.DisplayName = member.DisplayName;
            session.Role = AdminRoles.Normalize(member.Role);
        }
    }

    private static string NormalizeLogin(string value) =>
        value.Trim().TrimStart('@').ToLowerInvariant();

    private static string NormalizeDisplayName(string value) =>
        value.Trim();

    private static string GeneratePassword()
    {
        const string alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!?#%";
        Span<char> chars = stackalloc char[MinPasswordLength];
        Span<byte> bytes = stackalloc byte[MinPasswordLength];
        RandomNumberGenerator.Fill(bytes);

        for (var index = 0; index < chars.Length; index++)
        {
            chars[index] = alphabet[bytes[index] % alphabet.Length];
        }

        return new string(chars);
    }
}
