using Microsoft.EntityFrameworkCore;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Application.TenantAdmin;
using Tiku.Domain.Identity;
using Tiku.Domain.Tenancy;

namespace Tiku.Infrastructure.TenantAdmin;

public sealed partial class TenantAdminDirectService
{
    public async Task<CatalogList<TenantAdminMemberItem>> GetMembersAsync(
        TenantAdminActor actor,
        TenantAdminMemberFilter filter,
        CancellationToken cancellationToken = default)
    {
        await RequireAllDataScopeAsync(actor, cancellationToken);
        var query = dbContext.TenantMemberships.AsNoTracking().Where(item => item.TenantId == actor.TenantId);
        if (!string.IsNullOrWhiteSpace(filter.Role))
            query = query.Where(item => item.Role == ParseEnum<TenantRole>(filter.Role, "invalid_member_role"));

        if (!string.IsNullOrWhiteSpace(filter.Status))
            query = query.Where(item =>
                item.Status == ParseEnum<MembershipStatus>(filter.Status, "invalid_member_status"));

        if (!string.IsNullOrWhiteSpace(filter.Keyword))
        {
            var keyword = filter.Keyword.Trim();
            query = query.Where(item => dbContext.Users.Any(user =>
                user.Id == item.UserId &&
                ((user.UserName != null && user.UserName.Contains(keyword)) ||
                 (user.Phone != null && user.Phone.Contains(keyword)) ||
                 (user.Email != null && user.Email.Contains(keyword)) ||
                 (user.Name != null && user.Name.Contains(keyword)))));
        }

        var memberships = await query
            .OrderBy(item => item.Role == TenantRole.TenantOwner ? 0 :
                item.Role == TenantRole.TenantAdmin ? 1 :
                item.Role == TenantRole.TenantOperator ? 2 :
                item.Role == TenantRole.Teacher ? 3 : 9)
            .ThenBy(item => item.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .ToArrayAsync(cancellationToken);
        var userIds = memberships.Select(item => item.UserId).ToArray();
        var users = await dbContext.Users.AsNoTracking()
            .Where(user => userIds.Contains(user.Id))
            .ToDictionaryAsync(user => user.Id, cancellationToken);

        return new CatalogList<TenantAdminMemberItem>(memberships.Select(item =>
        {
            users.TryGetValue(item.UserId, out var user);
            return ToMemberItem(item, user ?? new User { Id = item.UserId });
        }).ToArray());
    }

    public async Task<ContentManagementResult<TenantAdminMemberItem>> UpsertMemberAsync(
        TenantAdminActor actor,
        UpsertTenantAdminMemberCommand command,
        CancellationToken cancellationToken = default)
    {
        await RequireAllDataScopeAsync(actor, cancellationToken);
        await using var transaction = dbContext.Database.CurrentTransaction is null
            ? await dbContext.Database.BeginTransactionAsync(cancellationToken)
            : null;
        var role = ParseEnum(command.Role, TenantRole.Student, "invalid_member_role");
        var status = ParseEnum(command.Status, MembershipStatus.Active, "invalid_member_status");
        await AssertGrantableAsync(actor, role, cancellationToken);
        var primaryRole = Normalize(command.PrimaryRole) ?? RoleToPrimaryRole(role);
        var user = await ResolveUserAsync(command.User, primaryRole, cancellationToken);
        if (user.Id == actor.UserId && status == MembershipStatus.Disabled)
            throw new TenantAdminDirectException("Cannot disable your own tenant membership.", "cannot_disable_self");

        TenantMembership? membership = null;
        if (command.MembershipId.HasValue)
        {
            membership = await dbContext.TenantMemberships.FirstOrDefaultAsync(
                item => item.TenantId == actor.TenantId && item.Id == command.MembershipId.Value,
                cancellationToken);
            if (membership is null)
                throw new TenantAdminDirectException("Tenant member was not found.", "tenant_member_not_found");
        }
        else
        {
            membership = await dbContext.TenantMemberships.FirstOrDefaultAsync(
                item => item.TenantId == actor.TenantId && item.UserId == user.Id && item.Role == role,
                cancellationToken);
        }

        var isNew = membership is null;
        var previousStatus = membership?.Status.ToString() ?? "none";
        var wasStaffCounted = await IsUserCountedForMetricAsync(
            actor.TenantId, user.Id, SaasQuotaMetricCatalog.StaffCount, cancellationToken);
        var wasStudentCounted = await IsUserCountedForMetricAsync(
            actor.TenantId, user.Id, SaasQuotaMetricCatalog.StudentCount, cancellationToken);
        Guid? excludedMembershipId = isNew ? null : membership!.Id;
        var otherStaffCounted = await IsUserCountedForMetricAsync(
            actor.TenantId, user.Id, SaasQuotaMetricCatalog.StaffCount, cancellationToken, excludedMembershipId);
        var otherStudentCounted = await IsUserCountedForMetricAsync(
            actor.TenantId, user.Id, SaasQuotaMetricCatalog.StudentCount, cancellationToken, excludedMembershipId);
        var willStaffBeCounted = otherStaffCounted || (status == MembershipStatus.Active && role != TenantRole.Student);
        var willStudentBeCounted =
            otherStudentCounted || (status == MembershipStatus.Active && role == TenantRole.Student);
        if (membership?.Role == TenantRole.TenantOwner && role != TenantRole.TenantOwner)
            throw new TenantAdminDirectException("Tenant owner membership cannot be downgraded.",
                "tenant_owner_required");

        if (!wasStaffCounted && willStaffBeCounted)
            await featureAccessService.ConsumeQuotaIfConfiguredAsync(
                actor.TenantId,
                SaasQuotaMetricCatalog.StaffCount,
                cancellationToken: cancellationToken);
        if (!wasStudentCounted && willStudentBeCounted)
            await featureAccessService.ConsumeQuotaIfConfiguredAsync(
                actor.TenantId,
                SaasQuotaMetricCatalog.StudentCount,
                cancellationToken: cancellationToken);

        membership ??= new TenantMembership
        {
            TenantId = actor.TenantId,
            UserId = user.Id
        };
        membership.UserId = user.Id;
        membership.Role = role;
        membership.Status = status;
        if (isNew) dbContext.TenantMemberships.Add(membership);

        if (status != MembershipStatus.Active)
            await RevokeSessionsAsync(actor.TenantId, user.Id, cancellationToken);
        else if (role == TenantRole.TenantOwner)
            await EnsureTenantOwnerBackendRoleAsync(actor.TenantId, user.Id, cancellationToken);

        await AddAuditAsync(actor, "tenant.member.upserted", "tenant_memberships", membership.Id, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        if (wasStaffCounted && !willStaffBeCounted)
            await featureAccessService.ReleaseQuotaAsync(
                actor.TenantId,
                SaasQuotaMetricCatalog.StaffCount,
                1,
                cancellationToken);
        if (wasStudentCounted && !willStudentBeCounted)
            await featureAccessService.ReleaseQuotaAsync(
                actor.TenantId,
                SaasQuotaMetricCatalog.StudentCount,
                1,
                cancellationToken);
        if (transaction is not null) await transaction.CommitAsync(cancellationToken);
        await authorizationStateInvalidator.InvalidateMembershipAsync(
            actor.TenantId, membership.UserId, cancellationToken);
        await authorizationStateInvalidator.BumpScopeAsync(AuthRealm.Tenant, actor.TenantId, cancellationToken);
        return new ContentManagementResult<TenantAdminMemberItem>(ToMemberItem(membership, user));
    }

    public async Task<ContentManagementResult<TenantAdminMemberItem>> DisableMemberAsync(
        TenantAdminActor actor,
        Guid membershipId,
        CancellationToken cancellationToken = default)
    {
        await RequireAllDataScopeAsync(actor, cancellationToken);
        await using var transaction = dbContext.Database.CurrentTransaction is null
            ? await dbContext.Database.BeginTransactionAsync(cancellationToken)
            : null;
        var membership = await dbContext.TenantMemberships.FirstOrDefaultAsync(
            item => item.TenantId == actor.TenantId && item.Id == membershipId,
            cancellationToken);
        if (membership is null)
            throw new TenantAdminDirectException("Tenant member was not found.", "tenant_member_not_found");

        if (membership.UserId == actor.UserId)
            throw new TenantAdminDirectException("Cannot disable your own tenant membership.", "cannot_disable_self");

        await AssertGrantableAsync(actor, membership.Role, cancellationToken);
        var previousStatus = membership.Status;
        var metricCode = QuotaMetricForRole(membership.Role);
        var wasCounted = await IsUserCountedForMetricAsync(
            actor.TenantId, membership.UserId, metricCode, cancellationToken);
        var otherMembershipCounted = await IsUserCountedForMetricAsync(
            actor.TenantId, membership.UserId, metricCode, cancellationToken, membership.Id);
        membership.Status = MembershipStatus.Disabled;
        await RevokeSessionsAsync(actor.TenantId, membership.UserId, cancellationToken);
        await AddAuditAsync(actor, "tenant.member.disabled", "tenant_memberships", membership.Id, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        if (previousStatus == MembershipStatus.Active && wasCounted && !otherMembershipCounted)
            await featureAccessService.ReleaseQuotaAsync(
                actor.TenantId,
                metricCode,
                1,
                cancellationToken);
        if (transaction is not null) await transaction.CommitAsync(cancellationToken);
        await authorizationStateInvalidator.InvalidateMembershipAsync(
            actor.TenantId, membership.UserId, cancellationToken);
        var user = await dbContext.Users.AsNoTracking()
            .SingleAsync(item => item.Id == membership.UserId, cancellationToken);
        return new ContentManagementResult<TenantAdminMemberItem>(ToMemberItem(membership, user));
    }
}