using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Application.TenantAdmin;
using Tiku.Domain.Catalog;
using Tiku.Domain.Common;
using Tiku.Domain.Identity;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Security;

namespace Tiku.Infrastructure.TenantAdmin;

public sealed partial class TenantAdminDirectService
{
    public async Task<TenantAdminStudentList> GetStudentsAsync(
        TenantAdminActor actor,
        TenantAdminStudentFilter filter,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var status = ParseEnum(filter.Status, MembershipStatus.Active, "invalid_student_status");
        var regionIds = scope.RegionIds.ToArray();
        var classIds = scope.ClassIds.ToArray();
        var query = dbContext.TenantMemberships.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId && item.Role == TenantRole.Student && item.Status == status)
            .ApplyDataScope(
                scope,
                item => item.UserId == actor.UserId,
                item => dbContext.StudentProfiles.Any(profile =>
                            profile.TenantId == actor.TenantId &&
                            profile.UserId == item.UserId &&
                            profile.RegionId.HasValue &&
                            regionIds.Contains(profile.RegionId.Value)) ||
                        dbContext.TenantClassMembers.Any(member =>
                            member.TenantId == actor.TenantId &&
                            member.UserId == item.UserId &&
                            member.Status == TenantClassMemberStatus.Active &&
                            classIds.Contains(member.ClassId)));

        if (filter.ClassId.HasValue)
        {
            await AssertClassAsync(actor, scope, filter.ClassId.Value, cancellationToken);
            query = query.Where(item => dbContext.TenantClassMembers.Any(member =>
                member.TenantId == actor.TenantId &&
                member.ClassId == filter.ClassId.Value &&
                member.UserId == item.UserId &&
                member.MemberType == TenantClassMemberType.Student &&
                member.Status == TenantClassMemberStatus.Active));
        }

        if (filter.RegionId.HasValue)
            query = query.Where(item => dbContext.StudentProfiles.Any(profile =>
                profile.TenantId == actor.TenantId &&
                profile.UserId == item.UserId &&
                profile.RegionId == filter.RegionId.Value));

        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
            .OrderByDescending(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);
        var profiles = await dbContext.StudentProfiles.AsNoTracking()
            .Where(profile => profile.TenantId == actor.TenantId && userIds.Contains(profile.UserId))
            .ToDictionaryAsync(profile => profile.UserId, cancellationToken);
        var regions = await dbContext.Regions.AsNoTracking()
            .Where(region => region.TenantId == actor.TenantId)
            .ToDictionaryAsync(region => region.Id, region => region.Name, cancellationToken);
        var schools = await dbContext.Schools.AsNoTracking()
            .Where(school => school.TenantId == actor.TenantId)
            .ToDictionaryAsync(school => school.Id, school => school.Name, cancellationToken);
        var majors = await dbContext.Majors.AsNoTracking()
            .Where(major => major.TenantId == actor.TenantId)
            .ToDictionaryAsync(major => major.Id, major => major.Name, cancellationToken);
        var classes = await (
                from member in dbContext.TenantClassMembers.AsNoTracking()
                join tenantClass in dbContext.TenantClasses.AsNoTracking() on member.ClassId equals tenantClass.Id
                where member.TenantId == actor.TenantId &&
                      tenantClass.TenantId == actor.TenantId &&
                      userIds.Contains(member.UserId) &&
                      member.Status == TenantClassMemberStatus.Active &&
                      member.MemberType == TenantClassMemberType.Student
                orderby tenantClass.SortOrder, tenantClass.CreatedAt descending
                select new
                {
                    member.UserId, member.MemberType, member.JoinedAt, ClassId = tenantClass.Id, tenantClass.Name,
                    tenantClass.Code
                })
            .ToArrayAsync(cancellationToken);
        var classLookup = classes
            .GroupBy(item => item.UserId)
            .ToDictionary(
                group => group.Key,
                group => group.Select(item => new TenantAdminStudentClassSummary(
                    item.ClassId,
                    item.Name,
                    item.Code,
                    item.MemberType,
                    item.JoinedAt)).ToArray() as IReadOnlyCollection<TenantAdminStudentClassSummary>);

        return new TenantAdminStudentList(
            memberships.Select(membership =>
            {
                users.TryGetValue(membership.UserId, out var user);
                profiles.TryGetValue(membership.UserId, out var profile);
                classLookup.TryGetValue(membership.UserId, out var studentClasses);
                return ToStudentItem(
                    membership,
                    user ?? new User { Id = membership.UserId },
                    profile,
                    regions,
                    schools,
                    majors,
                    studentClasses ?? []);
            }).ToArray(),
            scope.Mode != DataScopeMode.All);
    }

    public async Task<ContentManagementResult<TenantAdminStudentItem>> UpsertStudentAsync(
        TenantAdminActor actor,
        UpsertTenantAdminStudentCommand command,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        await AssertReferenceAsync<Region>(actor.TenantId, command.RegionId, "region_not_found", cancellationToken);
        await AssertReferenceAsync<School>(actor.TenantId, command.SelectedSchoolId, "school_not_found",
            cancellationToken);
        await AssertReferenceAsync<Major>(actor.TenantId, command.SelectedMajorId, "major_not_found",
            cancellationToken);
        await using var transaction = dbContext.Database.CurrentTransaction is null
            ? await dbContext.Database.BeginTransactionAsync(cancellationToken)
            : null;
        var user = await ResolveUserAsync(command.User, "student", cancellationToken);
        if (!scope.AllowsResource(actor.UserId, user.Id, command.RegionId))
            throw new TenantAdminDirectException("Student was not found.", "student_not_found");
        if (command.RawProfile.ValueKind == JsonValueKind.Object) user.RawProfile = command.RawProfile.Clone();

        var membership = await EnsureMembershipAsync(actor.TenantId, user.Id, TenantRole.Student, cancellationToken);
        var profile = await EnsureStudentProfileAsync(
            actor.TenantId,
            user.Id,
            command.RegionId,
            command.SelectedSchoolId,
            command.SelectedMajorId,
            Normalize(command.AvatarPreset),
            command.Stats,
            command.Progress,
            command.ModuleSelections,
            cancellationToken);

        await AddAuditAsync(actor, "tenant.student.upserted", "student_profiles", profile.Id, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        if (transaction is not null) await transaction.CommitAsync(cancellationToken);

        return new ContentManagementResult<TenantAdminStudentItem>(
            ToStudentItem(
                membership,
                user,
                profile,
                new Dictionary<Guid, string>(),
                new Dictionary<Guid, string>(),
                new Dictionary<Guid, string>(),
                []));
    }

    public async Task<ContentManagementResult<TenantAdminStudentStatusItem>> UpdateStudentStatusAsync(
        TenantAdminActor actor,
        UpdateTenantAdminStudentStatusCommand command,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var status = ParseEnum(command.Status, MembershipStatus.Active, "invalid_student_status");
        var regionIds = scope.RegionIds.ToArray();
        var classIds = scope.ClassIds.ToArray();
        var membership = await dbContext.TenantMemberships
            .Where(item =>
                item.TenantId == actor.TenantId &&
                item.UserId == command.UserId &&
                item.Role == TenantRole.Student)
            .ApplyDataScope(
                scope,
                item => item.UserId == actor.UserId,
                item => dbContext.StudentProfiles.Any(profile =>
                            profile.TenantId == actor.TenantId &&
                            profile.UserId == item.UserId &&
                            profile.RegionId.HasValue &&
                            regionIds.Contains(profile.RegionId.Value)) ||
                        dbContext.TenantClassMembers.Any(member =>
                            member.TenantId == actor.TenantId &&
                            member.UserId == item.UserId &&
                            member.Status == TenantClassMemberStatus.Active &&
                            classIds.Contains(member.ClassId)))
            .FirstOrDefaultAsync(cancellationToken);
        if (membership is null)
            throw new TenantAdminDirectException("Student membership was not found.", "student_not_found");

        await using var transaction = dbContext.Database.CurrentTransaction is null
            ? await dbContext.Database.BeginTransactionAsync(cancellationToken)
            : null;
        var wasCounted = await IsUserCountedForMetricAsync(
            actor.TenantId, membership.UserId, SaasQuotaMetricCatalog.StudentCount, cancellationToken);
        var otherMembershipCounted = await IsUserCountedForMetricAsync(
            actor.TenantId, membership.UserId, SaasQuotaMetricCatalog.StudentCount, cancellationToken, membership.Id);
        var willBeCounted = otherMembershipCounted || status == MembershipStatus.Active;
        if (!wasCounted && willBeCounted)
            await featureAccessService.ConsumeQuotaIfConfiguredAsync(
                actor.TenantId,
                SaasQuotaMetricCatalog.StudentCount,
                cancellationToken: cancellationToken);
        membership.Status = status;
        if (status != MembershipStatus.Active)
            await sessionStore.RevokeRealmAsync(
                command.UserId, AuthRealm.Tenant, actor.TenantId, "membership_disabled", cancellationToken);

        await AddAuditAsync(actor, "tenant.student.status_updated", "tenant_memberships", membership.Id,
            cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        if (wasCounted && !willBeCounted)
            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);
        return new ContentManagementResult<TenantAdminStudentStatusItem>(
            new TenantAdminStudentStatusItem(membership.Id, membership.UserId, membership.Role, membership.Status,
                membership.UpdatedAt));
    }

    public async Task<TenantAdminStudentImportPreview> PreviewStudentImportAsync(
        TenantAdminActor actor,
        TenantAdminStudentImportCommand command,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var items = await BuildStudentImportPreviewAsync(actor, scope, command, cancellationToken);
        return new TenantAdminStudentImportPreview(
            command.Rows.Count,
            items.Count(item => item.Valid),
            items.Count(item => !item.Valid),
            items);
    }

    public async Task<TenantAdminStudentImportResult> ImportStudentsAsync(
        TenantAdminActor actor,
        TenantAdminStudentImportCommand command,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var previewItems = await BuildStudentImportPreviewAsync(actor, scope, command, cancellationToken);
        var invalidItems = previewItems.Where(item => !item.Valid).ToArray();
        await using var transaction = dbContext.Database.CurrentTransaction is null
            ? await dbContext.Database.BeginTransactionAsync(cancellationToken)
            : null;
        var createdOrUpdated = 0;
        var classAssigned = 0;
        var rowNo = 0;
        foreach (var row in command.Rows)
        {
            rowNo++;
            if (invalidItems.Any(item => item.RowNo == rowNo)) continue;

            var user = await ResolveUserAsync(row.User, "student", cancellationToken);
            if (row.RawProfile.ValueKind == JsonValueKind.Object) user.RawProfile = row.RawProfile.Clone();

            await EnsureMembershipAsync(actor.TenantId, user.Id, TenantRole.Student, cancellationToken);
            await EnsureStudentProfileAsync(
                actor.TenantId,
                user.Id,
                row.RegionId,
                null,
                null,
                Normalize(row.AvatarPreset),
                JsonDefaults.Object(),
                JsonDefaults.Object(),
                JsonDefaults.Object(),
                cancellationToken);
            createdOrUpdated++;

            if (row.ClassId.HasValue)
            {
                await UpsertClassMemberCoreAsync(
                    actor,
                    row.ClassId.Value,
                    user.Id,
                    TenantClassMemberType.Student,
                    TenantClassMemberStatus.Active,
                    JsonSerializer.SerializeToElement(new { source = "student_import" }),
                    cancellationToken);
                classAssigned++;
            }
        }

        await AddAuditAsync(actor, "tenant.student.imported", "student_profiles", actor.TenantId, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        if (transaction is not null) await transaction.CommitAsync(cancellationToken);
        return new TenantAdminStudentImportResult(command.Rows.Count, createdOrUpdated, classAssigned, invalidItems);
    }

    public async Task<TenantAdminBulkOperationResult> BulkAssignClassAsync(
        TenantAdminActor actor,
        TenantAdminBulkAssignClassCommand command,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        await AssertClassAsync(actor, scope, command.ClassId, cancellationToken);
        var failed = new List<Guid>();
        var succeeded = 0;
        foreach (var userId in command.UserIds.Where(id => id != Guid.Empty).Distinct())
            try
            {
                await AssertStudentAsync(actor, scope, userId, cancellationToken);
                await UpsertClassMemberCoreAsync(
                    actor,
                    command.ClassId,
                    userId,
                    TenantClassMemberType.Student,
                    TenantClassMemberStatus.Active,
                    JsonSerializer.SerializeToElement(new { source = "bulk_assign_class" }),
                    cancellationToken);
                succeeded++;
            }
            catch (TenantAdminDirectException)
            {
                failed.Add(userId);
            }

        await AddAuditAsync(actor, "tenant.student.bulk_class_assigned", "tenant_classes", command.ClassId,
            cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return new TenantAdminBulkOperationResult(command.UserIds.Count, succeeded, failed.Count, failed);
    }

    public async Task<TenantAdminBulkOperationResult> BulkUpdateStudentStatusAsync(
        TenantAdminActor actor,
        TenantAdminBulkStatusCommand command,
        CancellationToken cancellationToken = default)
    {
        var failed = new List<Guid>();
        var succeeded = 0;
        foreach (var userId in command.UserIds.Where(id => id != Guid.Empty).Distinct())
            try
            {
                await UpdateStudentStatusAsync(
                    actor,
                    new UpdateTenantAdminStudentStatusCommand(userId, command.Status, command.Reason),
                    cancellationToken);
                succeeded++;
            }
            catch (TenantAdminDirectException)
            {
                failed.Add(userId);
            }

        await AddAuditAsync(actor, "tenant.student.bulk_status_updated", "tenant_memberships", actor.TenantId,
            cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return new TenantAdminBulkOperationResult(command.UserIds.Count, succeeded, failed.Count, failed);
    }
}