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

namespace Tiku.Infrastructure.TenantAdmin;

public sealed partial class TenantAdminDirectService
{
    private async Task<User> ResolveUserAsync(UserLookupCommand command, string primaryRole,
        CancellationToken cancellationToken)
    {
        User? user = null;
        if (command.UserId.HasValue)
        {
            user = await dbContext.Users.FirstOrDefaultAsync(item => item.Id == command.UserId.Value,
                cancellationToken);
            if (user is null) throw new TenantAdminDirectException("User was not found.", "user_not_found");
        }
        else
        {
            var phone = Normalize(command.Phone);
            var email = Normalize(command.Email);
            var username = Normalize(command.Username);
            user = await dbContext.Users.FirstOrDefaultAsync(item =>
                    (phone != null && item.Phone == phone) ||
                    (email != null && item.Email == email) ||
                    (username != null && item.UserName == username),
                cancellationToken);

            if (user is null)
            {
                if (phone is null && email is null && username is null && Normalize(command.Name) is null)
                    throw new TenantAdminDirectException("userId, phone, email, username or name is required.",
                        "user_required");

                user = new User
                {
                    UserName = username ?? phone ?? email,
                    Email = email,
                    Phone = phone,
                    Name = Normalize(command.Name) ?? username ?? phone ?? email,
                    PrimaryRole = primaryRole,
                    RawProfile = JsonDefaults.Object()
                };
                dbContext.Users.Add(user);
            }
        }

        user.UserName = Normalize(command.Username) ?? user.UserName;
        user.Email = Normalize(command.Email) ?? user.Email;
        user.Phone = Normalize(command.Phone) ?? user.Phone;
        user.Name = Normalize(command.Name) ?? user.Name;
        user.AvatarUrl = Normalize(command.AvatarUrl) ?? user.AvatarUrl;
        user.PrimaryRole = string.IsNullOrWhiteSpace(user.PrimaryRole) ? primaryRole : user.PrimaryRole;
        return user;
    }

    private async Task<TenantMembership> EnsureMembershipAsync(
        Guid tenantId,
        Guid userId,
        TenantRole role,
        CancellationToken cancellationToken)
    {
        var membership = await dbContext.TenantMemberships.FirstOrDefaultAsync(item =>
                item.TenantId == tenantId && item.UserId == userId && item.Role == role,
            cancellationToken);
        if (membership is null)
        {
            var metricCode = QuotaMetricForRole(role);
            if (!await IsUserCountedForMetricAsync(tenantId, userId, metricCode, cancellationToken))
                await featureAccessService.ConsumeQuotaIfConfiguredAsync(
                    tenantId,
                    metricCode,
                    cancellationToken: cancellationToken);
            membership = new TenantMembership
            {
                TenantId = tenantId,
                UserId = userId,
                Role = role,
                Status = MembershipStatus.Active
            };
            dbContext.TenantMemberships.Add(membership);
        }
        else
        {
            if (membership.Status != MembershipStatus.Active)
            {
                var metricCode = QuotaMetricForRole(role);
                if (!await IsUserCountedForMetricAsync(tenantId, userId, metricCode, cancellationToken))
                    await featureAccessService.ConsumeQuotaIfConfiguredAsync(
                        tenantId,
                        metricCode,
                        cancellationToken: cancellationToken);
            }

            membership.Status = MembershipStatus.Active;
        }

        return membership;
    }

    private static string QuotaMetricForRole(TenantRole role)
    {
        return role == TenantRole.Student
            ? SaasQuotaMetricCatalog.StudentCount
            : SaasQuotaMetricCatalog.StaffCount;
    }

    private Task<bool> IsUserCountedForMetricAsync(
        Guid tenantId,
        Guid userId,
        string metricCode,
        CancellationToken cancellationToken,
        Guid? excludedMembershipId = null)
    {
        var query = dbContext.TenantMemberships.AsNoTracking().Where(item =>
            item.TenantId == tenantId &&
            item.UserId == userId &&
            item.Status == MembershipStatus.Active);
        if (excludedMembershipId.HasValue) query = query.Where(item => item.Id != excludedMembershipId.Value);

        return metricCode == SaasQuotaMetricCatalog.StudentCount
            ? query.AnyAsync(item => item.Role == TenantRole.Student, cancellationToken)
            : query.AnyAsync(item => item.Role != TenantRole.Student, cancellationToken);
    }

    private async Task<StudentProfile> EnsureStudentProfileAsync(
        Guid tenantId,
        Guid userId,
        Guid? regionId,
        Guid? schoolId,
        Guid? majorId,
        string? avatarPreset,
        JsonElement stats,
        JsonElement progress,
        JsonElement moduleSelections,
        CancellationToken cancellationToken)
    {
        var profile = await dbContext.StudentProfiles.FirstOrDefaultAsync(item =>
                item.TenantId == tenantId && item.UserId == userId,
            cancellationToken);
        if (profile is null)
        {
            profile = new StudentProfile
            {
                TenantId = tenantId,
                UserId = userId,
                Stats = JsonDefaults.Object(),
                Progress = JsonDefaults.Object(),
                ModuleSelections = JsonDefaults.Object(),
                RecentActivities = JsonDefaults.Array()
            };
            dbContext.StudentProfiles.Add(profile);
        }

        profile.RegionId = regionId ?? profile.RegionId;
        profile.SelectedSchoolId = schoolId ?? profile.SelectedSchoolId;
        profile.SelectedMajorId = majorId ?? profile.SelectedMajorId;
        profile.AvatarPreset = avatarPreset ?? profile.AvatarPreset;
        profile.Stats = JsonObjectOrDefault(stats);
        profile.Progress = JsonObjectOrDefault(progress);
        profile.ModuleSelections = JsonObjectOrDefault(moduleSelections);
        return profile;
    }

    private async Task<IReadOnlyCollection<TenantAdminStudentImportPreviewItem>> BuildStudentImportPreviewAsync(
        TenantAdminActor actor,
        CurrentDataScope scope,
        TenantAdminStudentImportCommand command,
        CancellationToken cancellationToken)
    {
        var items = new List<TenantAdminStudentImportPreviewItem>();
        var rowNo = 0;
        foreach (var row in command.Rows.Take(1000))
        {
            rowNo++;
            string? reason = null;
            var phone = Normalize(row.User.Phone);
            var email = Normalize(row.User.Email);
            var name = Normalize(row.User.Name);
            if (row.User.UserId is null && phone is null && email is null && name is null)
                reason = "user_required";
            else if (!scope.AllowsResource(actor.UserId, actor.UserId, row.RegionId))
                reason = "data_scope_denied";
            else if (row.RegionId.HasValue && !await dbContext.Regions.AnyAsync(
                         item => item.TenantId == actor.TenantId && item.Id == row.RegionId.Value, cancellationToken))
                reason = "region_not_found";
            else if (row.ClassId.HasValue)
                try
                {
                    await AssertClassAsync(actor, scope, row.ClassId, cancellationToken);
                }
                catch (TenantAdminDirectException exception)
                {
                    reason = exception.Code;
                }

            items.Add(new TenantAdminStudentImportPreviewItem(
                rowNo,
                reason is null,
                reason,
                phone,
                email,
                name,
                row.RegionId,
                row.ClassId));
        }

        return items;
    }

    private async Task<TenantClassMember> UpsertClassMemberCoreAsync(
        TenantAdminActor actor,
        Guid classId,
        Guid userId,
        TenantClassMemberType memberType,
        TenantClassMemberStatus status,
        JsonElement metadata,
        CancellationToken cancellationToken)
    {
        var item = await dbContext.TenantClassMembers.FirstOrDefaultAsync(member =>
                member.TenantId == actor.TenantId &&
                member.ClassId == classId &&
                member.UserId == userId &&
                member.MemberType == memberType,
            cancellationToken);
        if (item is null)
        {
            item = new TenantClassMember
            {
                TenantId = actor.TenantId,
                ClassId = classId,
                UserId = userId,
                MemberType = memberType,
                JoinedAt = DateTimeOffset.UtcNow
            };
            dbContext.TenantClassMembers.Add(item);
        }

        item.Status = status;
        item.LeftAt = status == TenantClassMemberStatus.Removed ? DateTimeOffset.UtcNow : null;
        item.Metadata = JsonObjectOrDefault(metadata);
        return item;
    }

    private async Task<IReadOnlyCollection<TenantSupervisionRuleItem>> GetSupervisionRulesCoreAsync(
        Guid tenantId,
        CancellationToken cancellationToken)
    {
        var settings = await dbContext.TenantSettings.AsNoTracking()
            .SingleOrDefaultAsync(item => item.TenantId == tenantId, cancellationToken);
        if (settings is null ||
            settings.AdminFeatureFlags.ValueKind != JsonValueKind.Object ||
            !settings.AdminFeatureFlags.TryGetProperty("supervisionRules", out var rulesElement) ||
            rulesElement.ValueKind != JsonValueKind.Array)
            return [];

        return JsonSerializer.Deserialize<TenantSupervisionRuleItem[]>(rulesElement.GetRawText()) ?? [];
    }

    private async Task SaveSupervisionRulesCoreAsync(
        Guid tenantId,
        IReadOnlyCollection<TenantSupervisionRuleItem> rules,
        CancellationToken cancellationToken)
    {
        var settings =
            await dbContext.TenantSettings.SingleOrDefaultAsync(item => item.TenantId == tenantId, cancellationToken);
        if (settings is null)
        {
            settings = new TenantSettings { TenantId = tenantId };
            dbContext.TenantSettings.Add(settings);
        }

        var existing = settings.AdminFeatureFlags.ValueKind == JsonValueKind.Object
            ? JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(settings.AdminFeatureFlags.GetRawText()) ?? []
            : [];
        existing["supervisionRules"] = JsonSerializer.SerializeToElement(rules);
        settings.AdminFeatureFlags = JsonSerializer.SerializeToElement(existing);
    }

    private async Task<IReadOnlyCollection<TenantSupervisionRiskStudentItem>> BuildSupervisionRiskStudentsAsync(
        TenantAdminActor actor,
        CurrentDataScope scope,
        CancellationToken cancellationToken)
    {
        var rules = (await GetSupervisionRulesCoreAsync(actor.TenantId, cancellationToken))
            .Where(rule => rule.Enabled)
            .ToArray();
        if (rules.Length == 0) return [];

        var regionIds = scope.RegionIds.ToArray();
        var students = await dbContext.StudentProfiles.AsNoTracking()
            .Where(profile => profile.TenantId == actor.TenantId)
            .ApplyDataScope(
                scope,
                profile => profile.UserId == actor.UserId,
                profile => profile.RegionId.HasValue && regionIds.Contains(profile.RegionId.Value))
            .Select(profile => new
            {
                Profile = profile,
                User = dbContext.Users.Where(user => user.Id == profile.UserId).FirstOrDefault()
            })
            .ToArrayAsync(cancellationToken);
        var today = DateOnly.FromDateTime(DateTime.UtcNow);
        var result = new List<TenantSupervisionRiskStudentItem>();
        foreach (var row in students)
        {
            var hitRules = new List<string>();
            var reasons = new List<string>();
            foreach (var rule in rules)
            {
                if (rule.DaysWithoutCheckIn.HasValue)
                {
                    var days = row.Profile.LastCheckInDate.HasValue
                        ? today.DayNumber - row.Profile.LastCheckInDate.Value.DayNumber
                        : int.MaxValue;
                    if (days >= rule.DaysWithoutCheckIn.Value)
                    {
                        hitRules.Add(rule.Code);
                        reasons.Add($"{rule.Title}: {days} days without check-in");
                    }
                }

                if (rule.MaxQuestionsAnsweredToday.HasValue &&
                    row.Profile.QuestionsAnsweredToday <= rule.MaxQuestionsAnsweredToday.Value)
                {
                    hitRules.Add(rule.Code);
                    reasons.Add($"{rule.Title}: questions answered today <= {rule.MaxQuestionsAnsweredToday.Value}");
                }
            }

            if (hitRules.Count > 0)
                result.Add(new TenantSupervisionRiskStudentItem(
                    row.Profile.UserId,
                    row.User?.Name,
                    MaskPhone(row.User?.Phone),
                    row.Profile.RegionId,
                    hitRules.Distinct(StringComparer.Ordinal).ToArray(),
                    reasons.Distinct(StringComparer.Ordinal).ToArray()));
        }

        return result;
    }

    private async Task AssertStudentAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken)
    {
        var exists = await dbContext.TenantMemberships.AnyAsync(
            item => item.TenantId == tenantId && item.UserId == userId && item.Role == TenantRole.Student,
            cancellationToken);
        if (!exists) throw new TenantAdminDirectException("Student was not found.", "student_not_found");
    }

    private async Task AssertStudentAsync(
        TenantAdminActor actor,
        CurrentDataScope scope,
        Guid userId,
        CancellationToken cancellationToken)
    {
        var regionIds = scope.RegionIds.ToArray();
        var classIds = scope.ClassIds.ToArray();
        var exists = await dbContext.TenantMemberships
            .Where(item => item.TenantId == actor.TenantId && item.UserId == 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)))
            .AnyAsync(cancellationToken);
        if (!exists) throw new TenantAdminDirectException("Student was not found.", "student_not_found");
    }

    private async Task AssertTenantMemberAsync(Guid tenantId, Guid? userId, CancellationToken cancellationToken)
    {
        if (!userId.HasValue) return;

        var exists = await dbContext.TenantMemberships.AnyAsync(
            item => item.TenantId == tenantId && item.UserId == userId.Value && item.Status == MembershipStatus.Active,
            cancellationToken);
        if (!exists) throw new TenantAdminDirectException("Tenant member was not found.", "tenant_member_not_found");
    }

    private async Task RevokeSessionsAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken)
    {
        await sessionStore.RevokeRealmAsync(
            userId, AuthRealm.Tenant, tenantId, "membership_disabled", cancellationToken);
    }

    private async Task EnsureTenantOwnerBackendRoleAsync(
        Guid tenantId,
        Guid userId,
        CancellationToken cancellationToken)
    {
        const string roleCode = "tenant_owner";
        var role = await dbContext.TenantBackendRoles.FirstOrDefaultAsync(
            item => item.TenantId == tenantId && item.Code == roleCode,
            cancellationToken);
        if (role is null)
        {
            role = new TenantBackendRole
            {
                TenantId = tenantId,
                Code = roleCode,
                Name = "租户所有者",
                Status = BackendRoleStatus.Active,
                IsSystem = true,
                Description = "系统内置租户所有者角色",
                DataScope = JsonSerializer.SerializeToElement(new { mode = "All" })
            };
            dbContext.TenantBackendRoles.Add(role);
        }
        else
        {
            role.Status = BackendRoleStatus.Active;
            role.IsSystem = true;
            role.DataScope = JsonSerializer.SerializeToElement(new { mode = "All" });
        }

        var tenantPermissionCodes = BackendPermissions.Tenant.ToArray();
        var existingPermissionCodes = await dbContext.BackendPermissions
            .Where(permission => tenantPermissionCodes.Contains(permission.Code))
            .Select(permission => permission.Code)
            .ToArrayAsync(cancellationToken);
        foreach (var permissionCode in
                 BackendPermissions.Tenant.Except(existingPermissionCodes, StringComparer.Ordinal))
            dbContext.BackendPermissions.Add(new BackendPermission
            {
                Code = permissionCode,
                Name = permissionCode,
                Area = BackendPermissionArea.Tenant,
                PermissionModuleCode = PermissionModuleCatalog.ResolvePermissionModuleCode(permissionCode),
                IsSystem = true
            });

        var boundPermissionCodes = await dbContext.TenantBackendRolePermissions
            .Where(binding => binding.TenantId == tenantId && binding.RoleId == role.Id)
            .Select(binding => binding.PermissionCode)
            .ToArrayAsync(cancellationToken);
        dbContext.TenantBackendRolePermissions.AddRange(
            tenantPermissionCodes
                .Except(boundPermissionCodes, StringComparer.Ordinal)
                .Select(permissionCode => new TenantBackendRolePermission
                {
                    TenantId = tenantId,
                    RoleId = role.Id,
                    PermissionCode = permissionCode
                }));

        if (!await dbContext.TenantBackendUserRoles.AnyAsync(
                binding => binding.TenantId == tenantId && binding.UserId == userId && binding.RoleId == role.Id,
                cancellationToken))
            dbContext.TenantBackendUserRoles.Add(new TenantBackendUserRole
            {
                TenantId = tenantId,
                UserId = userId,
                RoleId = role.Id
            });
    }

    private async Task EnsureBrandingThemeAsync(
        Guid tenantId,
        JsonElement theme,
        JsonElement publicAssets,
        CancellationToken cancellationToken)
    {
        var branding =
            await dbContext.TenantBrandings.FirstOrDefaultAsync(item => item.TenantId == tenantId, cancellationToken);
        if (branding is null)
        {
            var tenantName = await dbContext.Tenants
                .Where(tenant => tenant.Id == tenantId)
                .Select(tenant => tenant.Name)
                .FirstOrDefaultAsync(cancellationToken);
            branding = new TenantBranding
            {
                TenantId = tenantId,
                BrandName = tenantName ?? "租户题库"
            };
            dbContext.TenantBrandings.Add(branding);
        }

        branding.Theme = theme.Clone();
        branding.PublicAssets = MergeJsonObjects(branding.PublicAssets, publicAssets);
    }

    private async Task AssertClassAsync(Guid tenantId, Guid? classId, CancellationToken cancellationToken)
    {
        if (!classId.HasValue) return;

        var exists = await dbContext.TenantClasses.AnyAsync(
            item => item.TenantId == tenantId && item.Id == classId.Value,
            cancellationToken);
        if (!exists) throw new TenantAdminDirectException("Class was not found.", "class_not_found");
    }

    private async Task AssertClassAsync(
        TenantAdminActor actor,
        CurrentDataScope scope,
        Guid? classId,
        CancellationToken cancellationToken)
    {
        if (!classId.HasValue) return;

        var regionIds = scope.RegionIds.ToArray();
        var classIds = scope.ClassIds.ToArray();
        var exists = await dbContext.TenantClasses
            .Where(item => item.TenantId == actor.TenantId && item.Id == classId.Value)
            .ApplyDataScope(
                scope,
                item => item.CreatedBy == actor.UserId,
                item => classIds.Contains(item.Id) ||
                        (item.RegionId.HasValue && regionIds.Contains(item.RegionId.Value)))
            .AnyAsync(cancellationToken);
        if (!exists) throw new TenantAdminDirectException("Class was not found.", "class_not_found");
    }

    private async Task<CurrentDataScope> RequireDataScopeAsync(
        TenantAdminActor actor,
        CancellationToken cancellationToken)
    {
        var access = await currentAccessContext.GetAsync(cancellationToken);
        if (!access.IsCurrentTenantMember || access.UserId != actor.UserId || access.TenantId != actor.TenantId)
            throw new TenantAdminDirectException("Tenant member was not found.", "tenant_member_not_found");

        return access.DataScope;
    }

    private async Task RequireAllDataScopeAsync(
        TenantAdminActor actor,
        CancellationToken cancellationToken)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        if (scope.Mode != DataScopeMode.All)
            throw new TenantAdminDirectException("Tenant-wide resource was not found.", "tenant_resource_not_found");
    }

    private async Task AssertReferenceAsync<TEntity>(
        Guid tenantId,
        Guid? id,
        string code,
        CancellationToken cancellationToken)
        where TEntity : TenantEntity
    {
        if (!id.HasValue) return;

        var exists = await dbContext.Set<TEntity>()
            .AnyAsync(entity => entity.TenantId == tenantId && entity.Id == id.Value, cancellationToken);
        if (!exists) throw new TenantAdminDirectException("Referenced entity was not found in this tenant.", code);
    }

    private async Task<TEntity?> ResolveTenantEntityAsync<TEntity>(
        DbSet<TEntity> set,
        Guid tenantId,
        Guid? id,
        string? legacyId,
        CancellationToken cancellationToken)
        where TEntity : AuditableTenantEntity
    {
        if (id.HasValue)
            return await set.FirstOrDefaultAsync(entity => entity.TenantId == tenantId && entity.Id == id.Value,
                cancellationToken);

        legacyId = Normalize(legacyId);
        if (legacyId is null) return null;

        return typeof(TEntity).GetProperty("LegacyId") is null
            ? null
            : await set.FirstOrDefaultAsync(
                entity => entity.TenantId == tenantId && EF.Property<string?>(entity, "LegacyId") == legacyId,
                cancellationToken);
    }

    private async Task AddAuditAsync(
        TenantAdminActor actor,
        string action,
        string targetType,
        Guid targetId,
        CancellationToken cancellationToken)
    {
        dbContext.AuditLogs.Add(new AuditLog
        {
            TenantId = actor.TenantId,
            ActorUserId = actor.UserId,
            Action = action,
            TargetType = targetType,
            TargetId = targetId.ToString(),
            Details = JsonDefaults.Object()
        });
        await Task.CompletedTask.WaitAsync(cancellationToken);
    }
}