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

namespace Tiku.Infrastructure.TenantAdmin;

public sealed partial class TenantAdminDirectService
{
    public async Task<TenantAdminClassList> GetClassesAsync(
        TenantAdminActor actor,
        TenantAdminClassFilter filter,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var regionIds = scope.RegionIds.ToArray();
        var classIds = scope.ClassIds.ToArray();
        var query = dbContext.TenantClasses.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId)
            .ApplyDataScope(
                scope,
                item => item.CreatedBy == actor.UserId,
                item => classIds.Contains(item.Id) ||
                        (item.RegionId.HasValue && regionIds.Contains(item.RegionId.Value)));
        if (filter.RegionId.HasValue) query = query.Where(item => item.RegionId == filter.RegionId.Value);

        if (!string.IsNullOrWhiteSpace(filter.Status))
            query = query.Where(item =>
                item.Status == ParseEnum<TenantRecordStatus>(filter.Status, "invalid_class_status"));
        else
            query = query.Where(item => item.Status != TenantRecordStatus.Archived);

        if (!string.IsNullOrWhiteSpace(filter.Keyword))
        {
            var keyword = filter.Keyword.Trim();
            query = query.Where(item =>
                item.Name.Contains(keyword) ||
                (item.Code != null && item.Code.Contains(keyword)));
        }

        var items = await query
            .OrderBy(item => item.SortOrder)
            .ThenByDescending(item => item.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(item => new
            {
                Class = item,
                RegionName = dbContext.Regions
                    .Where(region => region.TenantId == actor.TenantId && region.Id == item.RegionId)
                    .Select(region => region.Name)
                    .FirstOrDefault(),
                StudentCount = dbContext.TenantClassMembers.Count(member =>
                    member.TenantId == actor.TenantId &&
                    member.ClassId == item.Id &&
                    member.Status == TenantClassMemberStatus.Active &&
                    member.MemberType == TenantClassMemberType.Student),
                StaffCount = dbContext.TenantClassMembers.Count(member =>
                    member.TenantId == actor.TenantId &&
                    member.ClassId == item.Id &&
                    member.Status == TenantClassMemberStatus.Active &&
                    member.MemberType != TenantClassMemberType.Student)
            })
            .ToArrayAsync(cancellationToken);

        return new TenantAdminClassList(
            items.Select(item => ToClassItem(item.Class, item.RegionName, item.StudentCount, item.StaffCount))
                .ToArray(),
            scope.Mode != DataScopeMode.All);
    }

    public async Task<ContentManagementResult<TenantAdminClassItem>> UpsertClassAsync(
        TenantAdminActor actor,
        UpsertTenantAdminClassCommand command,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        ArgumentException.ThrowIfNullOrWhiteSpace(command.Name);
        await AssertReferenceAsync<Region>(actor.TenantId, command.RegionId, "region_not_found", cancellationToken);

        var item = await ResolveTenantEntityAsync(dbContext.TenantClasses, actor.TenantId, command.Id, command.LegacyId,
            cancellationToken);
        var isNew = item is null;
        if (item is not null && !scope.AllowsResource(actor.UserId, item.CreatedBy, item.RegionId, item.Id))
            throw new TenantAdminDirectException("Class was not found.", "class_not_found");

        if (item is null && !scope.AllowsResource(actor.UserId, actor.UserId, command.RegionId, command.Id))
            throw new TenantAdminDirectException("Class was not found.", "class_not_found");

        item ??= new TenantClass
            { Id = command.Id ?? Guid.NewGuid(), TenantId = actor.TenantId, CreatedBy = actor.UserId };
        item.RegionId = command.RegionId;
        item.LegacyId = Normalize(command.LegacyId);
        item.Code = Normalize(command.Code);
        item.Name = command.Name.Trim();
        item.Description = Normalize(command.Description);
        item.Status = ParseEnum(command.Status, TenantRecordStatus.Active, "invalid_class_status");
        item.SortOrder = command.Order ?? item.SortOrder;
        item.Metadata = JsonObjectOrDefault(command.Metadata);
        item.UpdatedBy = actor.UserId;

        if (isNew) dbContext.TenantClasses.Add(item);

        await AddAuditAsync(actor, "tenant.class.upserted", "tenant_classes", item.Id, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);

        var regionName = item.RegionId.HasValue
            ? await dbContext.Regions
                .Where(region => region.TenantId == actor.TenantId && region.Id == item.RegionId.Value)
                .Select(region => region.Name)
                .FirstOrDefaultAsync(cancellationToken)
            : null;
        return new ContentManagementResult<TenantAdminClassItem>(ToClassItem(item, regionName, 0, 0));
    }

    public async Task<ContentManagementResult<TenantAdminClassItem>> DisableClassAsync(
        TenantAdminActor actor,
        Guid classId,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var regionIds = scope.RegionIds.ToArray();
        var classIds = scope.ClassIds.ToArray();
        var item = await dbContext.TenantClasses
            .Where(entity => entity.TenantId == actor.TenantId && entity.Id == classId)
            .ApplyDataScope(
                scope,
                entity => entity.CreatedBy == actor.UserId,
                entity => classIds.Contains(entity.Id) ||
                          (entity.RegionId.HasValue && regionIds.Contains(entity.RegionId.Value)))
            .FirstOrDefaultAsync(cancellationToken);
        if (item is null) throw new TenantAdminDirectException("Class was not found.", "class_not_found");

        item.Status = TenantRecordStatus.Disabled;
        item.UpdatedBy = actor.UserId;
        await AddAuditAsync(actor, "tenant.class.disabled", "tenant_classes", item.Id, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return new ContentManagementResult<TenantAdminClassItem>(ToClassItem(item, null, 0, 0));
    }

    public async Task<CatalogList<TenantAdminClassMemberItem>> GetClassMembersAsync(
        TenantAdminActor actor,
        TenantAdminClassMemberFilter filter,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        await AssertClassAsync(actor, scope, filter.ClassId, cancellationToken);
        var classIds = scope.ClassIds.ToArray();
        var regionIds = scope.RegionIds.ToArray();
        var query = dbContext.TenantClassMembers.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId && item.ClassId == filter.ClassId)
            .ApplyDataScope(
                scope,
                item => item.UserId == actor.UserId || item.CreatedBy == actor.UserId,
                item => classIds.Contains(item.ClassId) || dbContext.TenantClasses.Any(tenantClass =>
                    tenantClass.TenantId == actor.TenantId &&
                    tenantClass.Id == item.ClassId &&
                    tenantClass.RegionId.HasValue &&
                    regionIds.Contains(tenantClass.RegionId.Value)));

        if (!string.IsNullOrWhiteSpace(filter.MemberType))
            query = query.Where(item =>
                item.MemberType == ParseEnum<TenantClassMemberType>(filter.MemberType, "invalid_class_member_type"));

        query = query.Where(item =>
            item.Status == ParseEnum(filter.Status, TenantClassMemberStatus.Active, "invalid_class_member_status"));

        var items = await query
            .OrderBy(item => item.MemberType == TenantClassMemberType.HeadTeacher ? 0 :
                item.MemberType == TenantClassMemberType.Teacher ? 1 :
                item.MemberType == TenantClassMemberType.Assistant ? 2 : 9)
            .ThenBy(item => item.JoinedAt)
            .Take(ResolveLimit(filter.Limit))
            .Join(
                dbContext.Users.AsNoTracking(),
                member => member.UserId,
                user => user.Id,
                (member, user) => ToClassMemberItem(member, ToUserSummary(user)))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<TenantAdminClassMemberItem>(items);
    }

    public async Task<ContentManagementResult<TenantAdminClassMemberItem>> UpsertClassMemberAsync(
        TenantAdminActor actor,
        UpsertTenantAdminClassMemberCommand command,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        await AssertClassAsync(actor, scope, command.ClassId, cancellationToken);
        await using var transaction = dbContext.Database.CurrentTransaction is null
            ? await dbContext.Database.BeginTransactionAsync(cancellationToken)
            : null;
        var memberType = ParseEnum(command.MemberType, TenantClassMemberType.Student, "invalid_class_member_type");
        var status = ParseEnum(command.Status, TenantClassMemberStatus.Active, "invalid_class_member_status");
        var user = await ResolveUserAsync(command.User,
            memberType == TenantClassMemberType.Student ? "student" : "teacher", cancellationToken);
        await EnsureMembershipAsync(actor.TenantId, user.Id,
            memberType == TenantClassMemberType.Student ? TenantRole.Student : TenantRole.Teacher, cancellationToken);
        if (memberType == TenantClassMemberType.Student)
            await EnsureStudentProfileAsync(actor.TenantId, user.Id, null, null, null, null, JsonDefaults.Object(),
                JsonDefaults.Object(), JsonDefaults.Object(), cancellationToken);

        var item = await dbContext.TenantClassMembers.FirstOrDefaultAsync(member =>
                member.TenantId == actor.TenantId &&
                member.ClassId == command.ClassId &&
                member.UserId == user.Id &&
                member.MemberType == memberType,
            cancellationToken);
        var isNew = item is null;
        item ??= new TenantClassMember
        {
            TenantId = actor.TenantId,
            ClassId = command.ClassId,
            UserId = user.Id,
            MemberType = memberType,
            CreatedBy = actor.UserId
        };
        item.Status = status;
        item.LeftAt = status is TenantClassMemberStatus.Active ? null : DateTimeOffset.UtcNow;
        item.Metadata = JsonObjectOrDefault(command.Metadata);
        item.UpdatedBy = actor.UserId;
        if (isNew) dbContext.TenantClassMembers.Add(item);

        await AddAuditAsync(actor, "tenant.class_member.upserted", "tenant_class_members", item.Id, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        if (transaction is not null) await transaction.CommitAsync(cancellationToken);
        return new ContentManagementResult<TenantAdminClassMemberItem>(ToClassMemberItem(item, ToUserSummary(user)));
    }

    public async Task<ContentManagementResult<TenantAdminClassMemberItem>> RemoveClassMemberAsync(
        TenantAdminActor actor,
        Guid classMemberId,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var classIds = scope.ClassIds.ToArray();
        var regionIds = scope.RegionIds.ToArray();
        var item = await dbContext.TenantClassMembers
            .Where(member => member.TenantId == actor.TenantId && member.Id == classMemberId)
            .ApplyDataScope(
                scope,
                member => member.UserId == actor.UserId || member.CreatedBy == actor.UserId,
                member => classIds.Contains(member.ClassId) || dbContext.TenantClasses.Any(tenantClass =>
                    tenantClass.TenantId == actor.TenantId &&
                    tenantClass.Id == member.ClassId &&
                    tenantClass.RegionId.HasValue &&
                    regionIds.Contains(tenantClass.RegionId.Value)))
            .FirstOrDefaultAsync(cancellationToken);
        if (item is null) throw new TenantAdminDirectException("Class member was not found.", "class_member_not_found");

        var user = await dbContext.Users.SingleAsync(user => user.Id == item.UserId, cancellationToken);
        item.Status = TenantClassMemberStatus.Removed;
        item.LeftAt = DateTimeOffset.UtcNow;
        item.UpdatedBy = actor.UserId;
        await AddAuditAsync(actor, "tenant.class_member.removed", "tenant_class_members", item.Id, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return new ContentManagementResult<TenantAdminClassMemberItem>(ToClassMemberItem(item, ToUserSummary(user)));
    }
}