using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Application.Notifications;
using Tiku.Application.Tenancy;
using Tiku.Application.TenantAdmin;
using Tiku.Domain.Learning;
using Tiku.Domain.Operations;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Security;
using Tiku.Infrastructure.Tenancy;

namespace Tiku.Infrastructure.TenantAdmin;

public sealed partial class TenantAdminDirectService
{
    public async Task<CatalogList<TenantDomainItem>> GetDomainsAsync(
        TenantAdminActor actor,
        CancellationToken cancellationToken = default)
    {
        await RequireAllDataScopeAsync(actor, cancellationToken);
        var items = await dbContext.TenantDomains.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId)
            .OrderByDescending(item => item.IsPrimary)
            .ThenBy(item => item.CreatedAt)
            .Select(item => ToDomainItem(item))
            .ToArrayAsync(cancellationToken);
        return new CatalogList<TenantDomainItem>(items);
    }

    public async Task<ContentManagementResult<TenantDomainItem>> CreateDomainAsync(
        TenantAdminActor actor,
        CreateTenantDomainCommand command,
        CancellationToken cancellationToken = default)
    {
        await RequireAllDataScopeAsync(actor, cancellationToken);
        TenantDomain generated;
        try
        {
            generated = TenantDomainProvisioning.CreatePrimary(actor.TenantId, command.Host);
        }
        catch (ArgumentException exception)
        {
            throw new TenantAdminDirectException(exception.Message, "invalid_domain_host");
        }

        var host = generated.Host;
        if (command.IsPrimary)
        {
            var primaryDomains = await dbContext.TenantDomains
                .Where(item => item.TenantId == actor.TenantId && item.IsPrimary)
                .ToArrayAsync(cancellationToken);
            foreach (var domain in primaryDomains) domain.IsPrimary = false;
        }

        var item = new TenantDomain
        {
            TenantId = actor.TenantId,
            Host = host,
            DomainType = ParseEnum(command.DomainType, TenantDomainType.Custom, "invalid_domain_type"),
            Status = TenantDomainStatus.Pending,
            IsPrimary = command.IsPrimary,
            VerificationToken = generated.VerificationToken
        };
        dbContext.TenantDomains.Add(item);
        await AddAuditAsync(actor, "tenant.domain.created", "tenant_domains", item.Id, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return new ContentManagementResult<TenantDomainItem>(ToDomainItem(item));
    }

    public async Task<CatalogList<TenantIdentityProviderItem>> GetAuthProvidersAsync(
        TenantAdminActor actor,
        CancellationToken cancellationToken = default)
    {
        await RequireAllDataScopeAsync(actor, cancellationToken);
        var items = await providerConfigService.GetProvidersAsync(
            actor.TenantId,
            TenantExternalProviderCapability.Identity,
            cancellationToken: cancellationToken);
        return new CatalogList<TenantIdentityProviderItem>(items.Select(ToAuthProviderItem).ToArray());
    }

    public async Task<ContentManagementResult<TenantIdentityProviderItem>> UpsertAuthProviderAsync(
        TenantAdminActor actor,
        UpsertTenantIdentityProviderCommand command,
        CancellationToken cancellationToken = default)
    {
        await RequireAllDataScopeAsync(actor, cancellationToken);
        ArgumentException.ThrowIfNullOrWhiteSpace(command.Provider);
        var item = await providerConfigService.UpsertProviderAsync(
            actor.TenantId,
            new UpsertTenantExternalProviderCommand(
                TenantExternalProviderCapability.Identity,
                command.Provider,
                ParseEnum(command.Status, TenantExternalProviderStatus.Disabled, "invalid_auth_provider_status"),
                command.DisplayName,
                command.SecretRef,
                command.Priority,
                command.ConfigPublic,
                JsonObjectOrDefault(default)),
            cancellationToken);

        await AddAuditAsync(actor, "tenant.auth_provider.upserted", "tenant_external_providers", item.Id,
            cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return new ContentManagementResult<TenantIdentityProviderItem>(ToAuthProviderItem(item));
    }

    public async Task<CatalogList<TenantAdminBadgeItem>> GetBadgesAsync(
        TenantAdminActor actor,
        TenantAdminBadgeFilter filter,
        CancellationToken cancellationToken = default)
    {
        await RequireAllDataScopeAsync(actor, cancellationToken);
        var query = dbContext.Badges.AsNoTracking().Where(item => item.TenantId == actor.TenantId);
        if (!string.IsNullOrWhiteSpace(filter.Category))
            query = query.Where(item => item.Category == filter.Category.Trim());

        if (!filter.IncludeInactive) query = query.Where(item => item.IsActive);

        var items = await query
            .OrderBy(item => item.SortOrder)
            .ThenBy(item => item.Level ?? int.MaxValue)
            .ThenByDescending(item => item.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(item => ToBadgeItem(item))
            .ToArrayAsync(cancellationToken);
        return new CatalogList<TenantAdminBadgeItem>(items);
    }

    public async Task<ContentManagementResult<TenantAdminBadgeItem>> UpsertBadgeAsync(
        TenantAdminActor actor,
        UpsertTenantAdminBadgeCommand command,
        CancellationToken cancellationToken = default)
    {
        await RequireAllDataScopeAsync(actor, cancellationToken);
        ArgumentException.ThrowIfNullOrWhiteSpace(command.Name);
        var item = await ResolveTenantEntityAsync(dbContext.Badges, actor.TenantId, command.Id, command.LegacyId,
            cancellationToken);
        var isNew = item is null;
        item ??= new Badge { Id = command.Id ?? Guid.NewGuid(), TenantId = actor.TenantId };
        item.LegacyId = Normalize(command.LegacyId);
        item.Name = command.Name.Trim();
        item.Description = Normalize(command.Description);
        item.Category = Normalize(command.Category) ?? "custom";
        item.IconUrl = Normalize(command.IconUrl);
        item.Level = command.Level;
        item.UnlockType = Normalize(command.UnlockType) ?? "manual";
        item.ConditionField = Normalize(command.ConditionField);
        item.ConditionOperator = Normalize(command.ConditionOperator);
        item.ConditionValue = command.ConditionValue;
        item.ConditionExtra = JsonObjectOrDefault(command.ConditionExtra);
        item.SortOrder = command.Order ?? item.SortOrder;
        item.IsActive = command.IsActive ?? item.IsActive;
        if (isNew) dbContext.Badges.Add(item);

        await AddAuditAsync(actor, "tenant.badge.upserted", "badges", item.Id, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return new ContentManagementResult<TenantAdminBadgeItem>(ToBadgeItem(item));
    }

    public async Task<CatalogList<TenantAdminBadgeGrantItem>> GetBadgeGrantsAsync(
        TenantAdminActor actor,
        TenantAdminBadgeGrantFilter filter,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var regionIds = scope.RegionIds.ToArray();
        var classIds = scope.ClassIds.ToArray();
        var query = dbContext.UserBadges.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId)
            .ApplyDataScope(
                scope,
                item => item.UserId == actor.UserId || item.GrantedBy == actor.UserId,
                item => item.UserId.HasValue &&
                        (dbContext.StudentProfiles.Any(profile =>
                             profile.TenantId == actor.TenantId &&
                             profile.UserId == item.UserId.Value &&
                             profile.RegionId.HasValue &&
                             regionIds.Contains(profile.RegionId.Value)) ||
                         dbContext.TenantClassMembers.Any(member =>
                             member.TenantId == actor.TenantId &&
                             member.UserId == item.UserId.Value &&
                             member.Status == TenantClassMemberStatus.Active &&
                             classIds.Contains(member.ClassId))));
        if (filter.UserId.HasValue) query = query.Where(item => item.UserId == filter.UserId.Value);

        if (filter.BadgeId.HasValue) query = query.Where(item => item.BadgeId == filter.BadgeId.Value);

        var grants = await query
            .OrderByDescending(item => item.GrantedAt ?? item.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .ToArrayAsync(cancellationToken);
        var userIds = grants.Select(item => item.UserId).OfType<Guid>()
            .Concat(grants.Select(item => item.GrantedBy).OfType<Guid>()).Distinct().ToArray();
        var badgeIds = grants.Select(item => item.BadgeId).OfType<Guid>().Distinct().ToArray();
        var users = await dbContext.Users.AsNoTracking()
            .Where(user => userIds.Contains(user.Id))
            .ToDictionaryAsync(user => user.Id, cancellationToken);
        var badges = await dbContext.Badges.AsNoTracking()
            .Where(badge => badge.TenantId == actor.TenantId && badgeIds.Contains(badge.Id))
            .ToDictionaryAsync(badge => badge.Id, cancellationToken);

        return new CatalogList<TenantAdminBadgeGrantItem>(grants.Select(grant =>
                ToBadgeGrantItem(
                    grant,
                    grant.UserId.HasValue && users.TryGetValue(grant.UserId.Value, out var user) ? user : null,
                    grant.GrantedBy.HasValue && users.TryGetValue(grant.GrantedBy.Value, out var grantedBy)
                        ? grantedBy
                        : null,
                    grant.BadgeId.HasValue && badges.TryGetValue(grant.BadgeId.Value, out var badge) ? badge : null))
            .ToArray());
    }

    public async Task<ContentManagementResult<TenantAdminBadgeGrantItem>> GrantBadgeAsync(
        TenantAdminActor actor,
        GrantTenantAdminBadgeCommand command,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var badge = await dbContext.Badges.FirstOrDefaultAsync(
            item => item.TenantId == actor.TenantId && item.Id == command.BadgeId,
            cancellationToken);
        if (badge is null) throw new TenantAdminDirectException("Badge was not found.", "badge_not_found");

        if (!badge.IsActive) throw new TenantAdminDirectException("Cannot grant inactive badge.", "badge_inactive");

        await AssertStudentAsync(actor, scope, command.UserId, cancellationToken);
        var grant = await dbContext.UserBadges.FirstOrDefaultAsync(
            item => item.TenantId == actor.TenantId && item.UserId == command.UserId && item.BadgeId == command.BadgeId,
            cancellationToken);
        var isNew = grant is null;
        grant ??= new UserBadge
        {
            TenantId = actor.TenantId,
            UserId = command.UserId,
            BadgeId = command.BadgeId,
            GrantedBy = actor.UserId,
            GrantedAt = command.GrantedAt ?? DateTimeOffset.UtcNow
        };
        grant.LegacyId = Normalize(command.LegacyId) ?? grant.LegacyId;
        grant.Note = Normalize(command.Note) ?? grant.Note;
        grant.GrantedBy ??= actor.UserId;
        grant.GrantedAt ??= command.GrantedAt ?? DateTimeOffset.UtcNow;
        if (isNew) dbContext.UserBadges.Add(grant);

        var dedupeKey = $"badge:{grant.Id:N}";
        await notificationProvider.UpsertInAppAsync(
            new InAppNotificationRequest(
                actor.TenantId,
                command.UserId,
                "badge_granted",
                NotificationSeverity.Success,
                $"获得勋章：{badge.Name}",
                Normalize(command.Note) ?? "管理员为你发放了一枚新的学习勋章。",
                actor.UserId,
                "查看勋章",
                "/profile?tab=badges",
                "user_badges",
                grant.Id,
                dedupeKey,
                JsonSerializer.SerializeToElement(new { badgeId = badge.Id, badgeName = badge.Name })),
            cancellationToken);

        await AddAuditAsync(actor, "tenant.badge.granted", "user_badges", grant.Id, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        var user = await dbContext.Users.AsNoTracking()
            .FirstOrDefaultAsync(item => item.Id == command.UserId, cancellationToken);
        return new ContentManagementResult<TenantAdminBadgeGrantItem>(ToBadgeGrantItem(grant, user, null, badge));
    }

    public async Task<CatalogList<TenantAdminNotificationItem>> GetNotificationsAsync(
        TenantAdminActor actor,
        TenantAdminNotificationFilter filter,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var regionIds = scope.RegionIds.ToArray();
        var classIds = scope.ClassIds.ToArray();
        var query = dbContext.UserNotifications.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId)
            .ApplyDataScope(
                scope,
                item => item.UserId == actor.UserId || item.CreatedBy == 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.UserId.HasValue) query = query.Where(item => item.UserId == filter.UserId.Value);

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

        if (!string.IsNullOrWhiteSpace(filter.NotificationType))
            query = query.Where(item => item.NotificationType == filter.NotificationType.Trim());

        var items = await query
            .OrderByDescending(item => item.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .Select(item => ToNotificationItem(item))
            .ToArrayAsync(cancellationToken);
        return new CatalogList<TenantAdminNotificationItem>(items);
    }

    public async Task<ContentManagementResult<TenantAdminNotificationItem>> UpsertNotificationAsync(
        TenantAdminActor actor,
        UpsertTenantAdminNotificationCommand command,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        ArgumentException.ThrowIfNullOrWhiteSpace(command.NotificationType);
        ArgumentException.ThrowIfNullOrWhiteSpace(command.Title);
        ArgumentException.ThrowIfNullOrWhiteSpace(command.Message);
        await AssertStudentAsync(actor, scope, command.UserId, cancellationToken);

        var item = await notificationProvider.UpsertInAppAsync(
            new InAppNotificationRequest(
                actor.TenantId,
                command.UserId,
                command.NotificationType,
                ParseEnum(command.Severity, NotificationSeverity.Info, "invalid_notification_severity"),
                command.Title,
                command.Message,
                actor.UserId,
                command.ActionLabel,
                command.ActionPath,
                command.SourceType,
                command.SourceId,
                command.DedupeKey,
                JsonObjectOrDefault(command.Metadata)),
            cancellationToken);

        await AddAuditAsync(actor, "tenant.notification.upserted", "user_notifications", item.Id, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        return new ContentManagementResult<TenantAdminNotificationItem>(ToNotificationItem(item));
    }

    public async Task<CatalogList<TenantAdminFeedbackItem>> GetFeedbacksAsync(
        TenantAdminActor actor,
        TenantAdminFeedbackFilter filter,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var regionIds = scope.RegionIds.ToArray();
        var classIds = scope.ClassIds.ToArray();
        var query = dbContext.Reports.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId)
            .ApplyDataScope(
                scope,
                item => item.UserId == actor.UserId || item.HandledBy == actor.UserId,
                item => item.UserId.HasValue &&
                        (dbContext.StudentProfiles.Any(profile =>
                             profile.TenantId == actor.TenantId &&
                             profile.UserId == item.UserId.Value &&
                             profile.RegionId.HasValue &&
                             regionIds.Contains(profile.RegionId.Value)) ||
                         dbContext.TenantClassMembers.Any(member =>
                             member.TenantId == actor.TenantId &&
                             member.UserId == item.UserId.Value &&
                             member.Status == TenantClassMemberStatus.Active &&
                             classIds.Contains(member.ClassId))));
        if (filter.UserId.HasValue) query = query.Where(item => item.UserId == filter.UserId.Value);

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

        if (!string.IsNullOrWhiteSpace(filter.Type))
            query = query.Where(item => item.Type == ParseEnum<ReportType>(filter.Type, "invalid_feedback_type"));

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

        var reports = await query
            .OrderByDescending(item => item.CreatedAt)
            .Take(ResolveLimit(filter.Limit))
            .ToArrayAsync(cancellationToken);
        var userIds = reports.Select(item => item.UserId).OfType<Guid>().Distinct().ToArray();
        var users = await dbContext.Users.AsNoTracking()
            .Where(user => userIds.Contains(user.Id))
            .ToDictionaryAsync(user => user.Id, cancellationToken);
        return new CatalogList<TenantAdminFeedbackItem>(reports.Select(report =>
                ToFeedbackItem(report,
                    report.UserId.HasValue && users.TryGetValue(report.UserId.Value, out var user) ? user : null))
            .ToArray());
    }

    public async Task<ContentManagementResult<TenantAdminFeedbackItem>> UpdateFeedbackAsync(
        TenantAdminActor actor,
        UpdateTenantAdminFeedbackCommand command,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        var regionIds = scope.RegionIds.ToArray();
        var classIds = scope.ClassIds.ToArray();
        var report = await dbContext.Reports
            .Where(item => item.TenantId == actor.TenantId && item.Id == command.FeedbackId)
            .ApplyDataScope(
                scope,
                item => item.UserId == actor.UserId || item.HandledBy == actor.UserId,
                item => item.UserId.HasValue &&
                        (dbContext.StudentProfiles.Any(profile =>
                             profile.TenantId == actor.TenantId &&
                             profile.UserId == item.UserId.Value &&
                             profile.RegionId.HasValue &&
                             regionIds.Contains(profile.RegionId.Value)) ||
                         dbContext.TenantClassMembers.Any(member =>
                             member.TenantId == actor.TenantId &&
                             member.UserId == item.UserId.Value &&
                             member.Status == TenantClassMemberStatus.Active &&
                             classIds.Contains(member.ClassId))))
            .FirstOrDefaultAsync(cancellationToken);
        if (report is null) throw new TenantAdminDirectException("Feedback was not found.", "feedback_not_found");

        var fromStatus = report.Status;
        report.Status = ParseEnum(command.Status, report.Status, "invalid_feedback_status");
        report.Priority = ParseEnum(command.Priority, report.Priority, "invalid_feedback_priority");
        report.Resolution = Normalize(command.Resolution) ?? report.Resolution;
        if (report.Status is ReportStatus.Accepted or ReportStatus.Rejected or ReportStatus.Resolved
            or ReportStatus.Closed)
        {
            report.HandledBy = actor.UserId;
            report.HandledAt = DateTimeOffset.UtcNow;
        }

        dbContext.ReportStatusEvents.Add(new ReportStatusEvent
        {
            TenantId = actor.TenantId,
            ReportId = report.Id,
            FromStatus = fromStatus,
            ToStatus = report.Status,
            Note = Normalize(command.Note),
            ActorUserId = actor.UserId,
            Metadata = JsonObjectOrDefault(command.Metadata)
        });
        await AddAuditAsync(actor, "tenant.feedback.updated", "reports", report.Id, cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);
        var user = report.UserId.HasValue
            ? await dbContext.Users.AsNoTracking()
                .FirstOrDefaultAsync(item => item.Id == report.UserId.Value, cancellationToken)
            : null;
        return new ContentManagementResult<TenantAdminFeedbackItem>(ToFeedbackItem(report, user));
    }
}