using MemberCenter.Application.Abstractions;
using MemberCenter.Application.Constants;
using MemberCenter.Application.Models.Admin;
using MemberCenter.Domain.Entities;
using MemberCenter.Infrastructure.Identity;
using MemberCenter.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace MemberCenter.Infrastructure.Services;

public sealed class TenantManagerService : ITenantManagerService
{
    private readonly MemberCenterDbContext _dbContext;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly RoleManager<ApplicationRole> _roleManager;
    private readonly IAuditLogWriter _auditLogWriter;

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

    public async Task<IReadOnlyList<TenantManagerDto>> ListManagersAsync(
        Guid tenantId,
        CancellationToken cancellationToken = default)
    {
        var managers = await _dbContext.TenantManagers
            .AsNoTracking()
            .Where(manager => manager.TenantId == tenantId)
            .Join(
                _dbContext.Users.AsNoTracking(),
                manager => manager.UserId,
                user => user.Id,
                (manager, user) => new
                {
                    manager.TenantId,
                    manager.UserId,
                    user.Email,
                    CreatedAt = manager.CreatedAt
                })
            .GroupJoin(
                _dbContext.UserProfiles.AsNoTracking(),
                manager => manager.UserId,
                profile => profile.UserId,
                (manager, profiles) => new { manager, profiles })
            .SelectMany(
                item => item.profiles.DefaultIfEmpty(),
                (item, profile) => new
                {
                    item.manager.TenantId,
                    item.manager.UserId,
                    Email = item.manager.Email ?? string.Empty,
                    FirstName = profile == null ? string.Empty : profile.FirstName,
                    LastName = profile == null ? string.Empty : profile.LastName,
                    item.manager.CreatedAt
                })
            .OrderBy(manager => manager.Email)
            .ToListAsync(cancellationToken);

        return managers
            .Select(manager =>
            {
                var displayName = string.Join(" ", new[] { manager.FirstName, manager.LastName }
                    .Where(part => !string.IsNullOrWhiteSpace(part)));
                if (string.IsNullOrWhiteSpace(displayName))
                {
                    displayName = manager.Email;
                }

                return new TenantManagerDto(
                    manager.TenantId,
                    manager.UserId,
                    manager.Email,
                    displayName,
                    manager.CreatedAt);
            })
            .ToList();
    }

    public async Task<IReadOnlySet<Guid>> ListTenantIdsForUserAsync(
        Guid userId,
        CancellationToken cancellationToken = default)
    {
        var tenantIds = await _dbContext.TenantManagers
            .AsNoTracking()
            .Where(manager => manager.UserId == userId)
            .Select(manager => manager.TenantId)
            .Distinct()
            .ToListAsync(cancellationToken);

        return tenantIds.ToHashSet();
    }

    public Task<bool> CanManageTenantAsync(Guid userId, Guid tenantId, CancellationToken cancellationToken = default)
    {
        return _dbContext.TenantManagers
            .AsNoTracking()
            .AnyAsync(manager => manager.UserId == userId && manager.TenantId == tenantId, cancellationToken);
    }

    public async Task<bool> AssignAsync(
        Guid tenantId,
        Guid userId,
        Guid? actorId,
        CancellationToken cancellationToken = default)
    {
        var tenantExists = await _dbContext.Tenants
            .AsNoTracking()
            .AnyAsync(tenant => tenant.Id == tenantId, cancellationToken);
        var user = await _userManager.FindByIdAsync(userId.ToString());
        if (!tenantExists || user is null || user.DisabledAt.HasValue)
        {
            return false;
        }

        var exists = await _dbContext.TenantManagers
            .AnyAsync(manager => manager.TenantId == tenantId && manager.UserId == userId, cancellationToken);
        if (exists)
        {
            await EnsureTenantManagerRoleAsync(user);
            return true;
        }

        await using var transaction = await _dbContext.Database.BeginTransactionAsync(cancellationToken);
        await EnsureTenantManagerRoleAsync(user);
        _dbContext.TenantManagers.Add(new TenantManager
        {
            TenantId = tenantId,
            UserId = userId,
            CreatedBy = actorId,
            CreatedAt = DateTimeOffset.UtcNow
        });
        await _dbContext.SaveChangesAsync(cancellationToken);

        await _auditLogWriter.WriteAsync("user", actorId, "tenant_manager.assigned", new
        {
            tenant_id = tenantId,
            user_id = userId,
            email = user.Email
        });
        await transaction.CommitAsync(cancellationToken);
        return true;
    }

    public async Task<bool> RemoveAsync(
        Guid tenantId,
        Guid userId,
        Guid? actorId,
        CancellationToken cancellationToken = default)
    {
        var manager = await _dbContext.TenantManagers
            .FirstOrDefaultAsync(item => item.TenantId == tenantId && item.UserId == userId, cancellationToken);
        if (manager is null)
        {
            return false;
        }

        await using var transaction = await _dbContext.Database.BeginTransactionAsync(cancellationToken);
        _dbContext.TenantManagers.Remove(manager);
        await _dbContext.SaveChangesAsync(cancellationToken);

        var user = await _userManager.FindByIdAsync(userId.ToString());
        if (user is not null)
        {
            var hasAnyTenant = await _dbContext.TenantManagers
                .AsNoTracking()
                .AnyAsync(item => item.UserId == userId, cancellationToken);
            if (!hasAnyTenant && await _userManager.IsInRoleAsync(user, AdminPermissions.TenantManagerRole))
            {
                EnsureSucceeded(await _userManager.RemoveFromRoleAsync(user, AdminPermissions.TenantManagerRole));
            }
        }

        await _auditLogWriter.WriteAsync("user", actorId, "tenant_manager.removed", new
        {
            tenant_id = tenantId,
            user_id = userId,
            email = user?.Email
        });
        await transaction.CommitAsync(cancellationToken);
        return true;
    }

    private async Task EnsureTenantManagerRoleAsync(ApplicationUser user)
    {
        if (!await _roleManager.RoleExistsAsync(AdminPermissions.TenantManagerRole))
        {
            EnsureSucceeded(await _roleManager.CreateAsync(new ApplicationRole
            {
                Id = Guid.NewGuid(),
                Name = AdminPermissions.TenantManagerRole,
                NormalizedName = AdminPermissions.TenantManagerRole.ToUpperInvariant()
            }));
        }

        if (!await _userManager.IsInRoleAsync(user, AdminPermissions.TenantManagerRole))
        {
            EnsureSucceeded(await _userManager.AddToRoleAsync(user, AdminPermissions.TenantManagerRole));
        }
    }

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

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