using MemberCenter.Application.Abstractions;
using MemberCenter.Application.Models.Admin;
using MemberCenter.Infrastructure.Identity;
using MemberCenter.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using OpenIddict.Abstractions;
using OpenIddict.EntityFrameworkCore.Models;

namespace MemberCenter.Infrastructure.Services;

public sealed class AccountGovernanceService : IAccountGovernanceService
{
    private const string AdminRole = "admin";
    private const string SuperuserRole = "superuser";

    private readonly MemberCenterDbContext _dbContext;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly RoleManager<ApplicationRole> _roleManager;
    private readonly IAuditLogWriter _auditLogWriter;

    public AccountGovernanceService(
        MemberCenterDbContext dbContext,
        UserManager<ApplicationUser> userManager,
        RoleManager<ApplicationRole> roleManager,
        IAuditLogWriter auditLogWriter)
    {
        _dbContext = dbContext;
        _userManager = userManager;
        _roleManager = roleManager;
        _auditLogWriter = auditLogWriter;
    }

    public async Task<IReadOnlyList<UserGovernanceSummaryDto>> ListUsersAsync(string? search = null, int take = 200)
    {
        var query = _dbContext.Users
            .AsNoTracking()
            .GroupJoin(
                _dbContext.UserProfiles.AsNoTracking(),
                user => user.Id,
                profile => profile.UserId,
                (user, profiles) => new
                {
                    User = user,
                    Profile = profiles.FirstOrDefault()
                });

        if (!string.IsNullOrWhiteSpace(search))
        {
            var term = search.Trim().ToLower();
            query = query.Where(x =>
                (x.User.Email ?? string.Empty).ToLower().Contains(term)
                || (x.Profile != null && (
                    x.Profile.LastName.ToLower().Contains(term)
                    || x.Profile.FirstName.ToLower().Contains(term)
                    || (x.Profile.NickName != null && x.Profile.NickName.ToLower().Contains(term)))));
        }

        var users = await query
            .OrderByDescending(x => x.User.CreatedAt)
            .Take(Math.Clamp(take, 1, 500))
            .ToListAsync();

        var result = new List<UserGovernanceSummaryDto>(users.Count);
        foreach (var item in users)
        {
            var roles = await _userManager.GetRolesAsync(item.User);
            result.Add(new UserGovernanceSummaryDto(
                item.User.Id,
                item.User.Email ?? string.Empty,
                item.Profile?.LastName,
                item.Profile?.FirstName,
                item.Profile?.NickName,
                item.User.EmailConfirmed,
                roles.Contains(AdminRole, StringComparer.OrdinalIgnoreCase),
                roles.Contains(SuperuserRole, StringComparer.OrdinalIgnoreCase),
                item.User.DisabledAt.HasValue,
                item.User.IsBlacklisted,
                item.User.CreatedAt,
                item.User.LastLoginAt,
                item.User.LastSeenAt,
                item.User.DisabledAt,
                item.User.DisabledBy));
        }

        return result;
    }

    public async Task SetAdminAsync(Guid actorUserId, Guid targetUserId, bool enabled)
    {
        await EnsureSuperuserAsync(actorUserId);
        await EnsureRoleExistsAsync(AdminRole);

        var targetUser = await _userManager.FindByIdAsync(targetUserId.ToString())
            ?? throw new InvalidOperationException("Target user not found.");
        await EnsureTargetIsMutableAsync(targetUser);
        if (enabled && targetUser.DisabledAt.HasValue)
        {
            throw new InvalidOperationException("Disabled accounts cannot be granted admin permissions.");
        }

        var inRole = await _userManager.IsInRoleAsync(targetUser, AdminRole);
        if (enabled && !inRole)
        {
            EnsureSucceeded(await _userManager.AddToRoleAsync(targetUser, AdminRole));
        }
        else if (!enabled && inRole)
        {
            EnsureSucceeded(await _userManager.RemoveFromRoleAsync(targetUser, AdminRole));
        }

        await _auditLogWriter.WriteAsync("user", actorUserId, "account.role_changed", new
        {
            target_user_id = targetUser.Id,
            email = targetUser.Email,
            role = AdminRole,
            enabled
        });
    }

    public async Task SetDisabledAsync(Guid actorUserId, Guid targetUserId, bool disabled)
    {
        await EnsureAdminAsync(actorUserId);
        if (actorUserId == targetUserId)
        {
            throw new InvalidOperationException("You cannot disable your own account.");
        }

        var targetUser = await _userManager.FindByIdAsync(targetUserId.ToString())
            ?? throw new InvalidOperationException("Target user not found.");
        await EnsureTargetIsMutableAsync(targetUser);
        await EnsureTargetIsMemberAsync(targetUser);

        targetUser.DisabledAt = disabled ? DateTimeOffset.UtcNow : null;
        targetUser.DisabledBy = disabled ? actorUserId.ToString() : null;
        EnsureSucceeded(await _userManager.UpdateAsync(targetUser));

        await _auditLogWriter.WriteAsync("user", actorUserId, disabled ? "account.disabled" : "account.enabled", new
        {
            target_user_id = targetUser.Id,
            email = targetUser.Email
        });
    }

    public async Task ResetPasswordAsync(Guid actorUserId, Guid targetUserId, string newPassword)
    {
        var actorIsSuperuser = await EnsureAdminAsync(actorUserId);
        var targetUser = await _userManager.FindByIdAsync(targetUserId.ToString())
            ?? throw new InvalidOperationException("Target user not found.");
        await EnsureTargetIsMutableAsync(targetUser);
        var targetIsAdmin = await _userManager.IsInRoleAsync(targetUser, AdminRole);
        if (targetIsAdmin && !actorIsSuperuser)
        {
            throw new InvalidOperationException("Only superuser can reset admin passwords.");
        }

        if (string.IsNullOrWhiteSpace(newPassword))
        {
            throw new InvalidOperationException("New password is required.");
        }

        var resetToken = await _userManager.GeneratePasswordResetTokenAsync(targetUser);
        EnsureSucceeded(await _userManager.ResetPasswordAsync(targetUser, resetToken, newPassword));
        EnsureSucceeded(await _userManager.UpdateSecurityStampAsync(targetUser));
        await RevokeUserAuthorizationsAsync(targetUser.Id);

        await _auditLogWriter.WriteAsync("user", actorUserId, "account.password_reset_by_admin", new
        {
            target_user_id = targetUser.Id,
            email = targetUser.Email,
            actor_is_superuser = actorIsSuperuser,
            revoke_existing_sessions = true
        });
    }

    private async Task EnsureSuperuserAsync(Guid actorUserId)
    {
        await EnsureRoleExistsAsync(SuperuserRole);

        var actor = await _userManager.FindByIdAsync(actorUserId.ToString())
            ?? throw new InvalidOperationException("Actor user not found.");

        if (!await _userManager.IsInRoleAsync(actor, SuperuserRole))
        {
            throw new InvalidOperationException("Only superuser can modify account governance.");
        }
    }

    private async Task<bool> EnsureAdminAsync(Guid actorUserId)
    {
        await EnsureRoleExistsAsync(AdminRole);
        await EnsureRoleExistsAsync(SuperuserRole);

        var actor = await _userManager.FindByIdAsync(actorUserId.ToString())
            ?? throw new InvalidOperationException("Actor user not found.");

        var isSuperuser = await _userManager.IsInRoleAsync(actor, SuperuserRole);
        var isAdmin = await _userManager.IsInRoleAsync(actor, AdminRole);
        if (!isAdmin && !isSuperuser)
        {
            throw new InvalidOperationException("Only admin can modify account governance.");
        }

        return isSuperuser;
    }

    private async Task EnsureTargetIsMutableAsync(ApplicationUser targetUser)
    {
        if (await _userManager.IsInRoleAsync(targetUser, SuperuserRole))
        {
            throw new InvalidOperationException("Superuser accounts cannot be modified from the management UI.");
        }
    }

    private async Task EnsureTargetIsMemberAsync(ApplicationUser targetUser)
    {
        if (await _userManager.IsInRoleAsync(targetUser, AdminRole))
        {
            throw new InvalidOperationException("Admin accounts must have admin permissions removed before they can be disabled.");
        }
    }

    private async Task EnsureRoleExistsAsync(string roleName)
    {
        if (await _roleManager.RoleExistsAsync(roleName))
        {
            return;
        }

        EnsureSucceeded(await _roleManager.CreateAsync(new ApplicationRole
        {
            Id = Guid.NewGuid(),
            Name = roleName,
            NormalizedName = roleName.ToUpperInvariant()
        }));
    }

    private static void EnsureSucceeded(IdentityResult result)
    {
        if (result.Succeeded)
        {
            return;
        }

        throw new InvalidOperationException(string.Join("; ", result.Errors.Select(x => x.Description)));
    }

    private async Task RevokeUserAuthorizationsAsync(Guid userId)
    {
        var subject = userId.ToString();

        var tokens = await _dbContext.Set<OpenIddictEntityFrameworkCoreToken>()
            .Where(x => x.Subject == subject && x.Status != OpenIddictConstants.Statuses.Revoked)
            .ToListAsync();

        foreach (var token in tokens)
        {
            token.Status = OpenIddictConstants.Statuses.Revoked;
            token.RedemptionDate = DateTime.UtcNow;
        }

        var authorizations = await _dbContext.Set<OpenIddictEntityFrameworkCoreAuthorization>()
            .Where(x => x.Subject == subject && x.Status != OpenIddictConstants.Statuses.Revoked)
            .ToListAsync();

        foreach (var authorization in authorizations)
        {
            authorization.Status = OpenIddictConstants.Statuses.Revoked;
        }

        await _dbContext.SaveChangesAsync();
    }
}
