using System.Text.Json; using Microsoft.EntityFrameworkCore; using Tiku.Application.Assets; using Tiku.Application.Content; using Tiku.Application.Security; using Tiku.Domain.Common; using Tiku.Domain.Content; using Tiku.Domain.Learning; using Tiku.Domain.Operations; using Tiku.Domain.QuestionBanks; namespace Tiku.Infrastructure.Content; internal abstract partial class DirectContentServiceBase { protected static QuestionManagementItem ToQuestionItem(Question question, QuestionVersion? version) { return new QuestionManagementItem( question.Id, version?.Id, question.QuestionBankId, question.SubjectId, question.CategoryId, question.NodeId, question.EntryId, question.ContentNodeId, question.PrimaryCollectionId, question.LegacyId, question.Type, question.TypeLabel, question.Difficulty, question.Tags, version?.Content, version?.Options ?? JsonDefaults.Array(), version?.CorrectOptionIndex, version?.CorrectOptionIndices ?? JsonDefaults.Array(), version?.AnswerText, version?.Explanation, version?.SubQuestions ?? JsonDefaults.Array(), version?.CodeLang, version?.CodeTemplate, question.MediaUrl, question.HasVideoExplanation, question.Status); } protected static VideoManagementItem ToVideoItem(VideoExplanation item) { return new VideoManagementItem( item.Id, item.SubjectId, item.LegacyId, item.Title, item.Description, item.VideoUrl, item.ThumbnailUrl, item.DurationSeconds, item.KnowledgeTags, item.IsGeneral, item.Difficulty, item.SortOrder, item.IsActive, item.Metadata); } protected static QuestionVideoManagementItem ToQuestionVideoItem(QuestionVideo item) { return new QuestionVideoManagementItem( item.Id, item.QuestionId, item.VideoId, item.LegacyId, item.VideoType, item.SortOrder, item.Metadata); } protected static OperationContentItem ToOperationItem(Banner item) { return new OperationContentItem( item.Id, "banners", item.RegionId, null, item.LegacyId, item.Title, item.Content, null, null, null, null, item.SortOrder, item.IsActive, JsonSerializer.SerializeToElement(new { item.Subtitle, item.ButtonText, item.ButtonLink, item.BackgroundColor, item.BorderColor })); } protected static OperationContentItem ToOperationItem(Faq item) { return new OperationContentItem( item.Id, "faqs", item.RegionId, null, item.LegacyId, null, null, item.Question, item.Answer, null, null, item.SortOrder, item.IsActive, JsonDefaults.Object()); } protected static OperationContentItem ToOperationItem(Announcement item) { return new OperationContentItem( item.Id, "announcements", null, null, item.LegacyId, null, item.Content, null, null, null, null, item.SortOrder, item.IsActive, JsonSerializer.SerializeToElement(new { item.Link, item.BackgroundColor })); } protected static OperationContentItem ToOperationItem(ExamDate item) { return new OperationContentItem( item.Id, "exam-dates", item.RegionId, item.SchoolId, item.LegacyId, item.ExamName, item.Description, null, null, item.ExamAt, item.ExamType, item.SortOrder, item.IsActive, item.Metadata); } protected static ContentImportJobItem ToJobItem(ContentImportJob job) { return new ContentImportJobItem( job.Id, job.TargetRegionId, job.TargetSubjectId, job.TargetCategoryId, job.TargetContentNodeId, job.TargetQuestionBankId, job.ImportType, job.SourceFormat, job.Status, job.SourceName, job.SourceHash, job.DryRun, job.TotalCount, job.ValidCount, job.ErrorCount, job.WarningCount, job.InsertedCount, job.UpdatedCount, job.SkippedCount, job.Summary, job.ErrorMessage, job.StartedAt, job.FinishedAt, job.CreatedAt, job.UpdatedAt); } protected static ContentImportItemModel ToImportItem(ContentImportItem item) { return new ContentImportItemModel( item.Id, item.JobId, item.RowNo, item.ExternalId, item.Status, item.TargetType, item.TargetId, item.SourcePayload, item.NormalizedPayload, item.ContentHash, item.IssuesCount); } protected async Task RequireDataScopeAsync( DirectContentActor actor, CancellationToken cancellationToken) { var access = await currentAccessContext.GetAsync(cancellationToken); if (!access.IsCurrentTenantMember || access.UserId != actor.UserId || access.TenantId != actor.TenantId || !ContentPermissions.Any(access.HasTenantPermission)) throw new ContentManagementException("Tenant content access was denied.", "content_access_denied"); return access.DataScope; } protected static void EnsureRegionWriteAllowed( CurrentDataScope scope, DirectContentActor actor, Guid? currentRegionId, Guid? targetRegionId, bool isNew, string notFoundCode) { var canAccessCurrent = isNew || scope.AllowsResource(actor.UserId, regionId: currentRegionId); var canAccessTarget = scope.AllowsResource(actor.UserId, regionId: targetRegionId); if (!canAccessCurrent || !canAccessTarget) throw new ContentManagementException("Content resource was not found.", notFoundCode); } protected async Task ResolveByIdOrLegacyAsync( DbSet set, Guid tenantId, Guid? id, string? legacyId, CancellationToken cancellationToken) where TEntity : AuditableTenantEntity { if (id.HasValue) return await set.SingleOrDefaultAsync(item => item.TenantId == tenantId && item.Id == id.Value, cancellationToken); var normalizedLegacyId = Normalize(legacyId); return normalizedLegacyId is null ? null : await set.SingleOrDefaultAsync( item => item.TenantId == tenantId && EF.Property(item, "LegacyId") == normalizedLegacyId, cancellationToken); } protected async Task AssertReferenceAsync( Guid tenantId, Guid? id, string code, CancellationToken cancellationToken) where TEntity : class { if (!id.HasValue) return; var exists = await unitOfWork.Set() .AnyAsync( item => EF.Property(item, "TenantId") == tenantId && EF.Property(item, "Id") == id.Value, cancellationToken); if (!exists) throw new ContentManagementException("Referenced entity was not found.", code); } protected async Task AssertImportJobAsync(Guid tenantId, Guid jobId, CancellationToken cancellationToken) { var exists = await questionBankPersistence.ContentImportJobs.AnyAsync( item => item.TenantId == tenantId && item.Id == jobId, cancellationToken); if (!exists) throw new ContentManagementException("Import job was not found.", "import_job_not_found"); } protected static string NormalizeOperationKind(string kind) { var normalized = Normalize(kind)?.ToLowerInvariant(); return normalized switch { "banner" or "banners" => "banners", "faq" or "faqs" => "faqs", "announcement" or "announcements" => "announcements", "exam-date" or "exam-dates" or "examdates" => "exam-dates", _ => normalized ?? string.Empty }; } protected static ContentImportType ParseImportType(string value) { return value.ToLowerInvariant() switch { "questions" => ContentImportType.Questions, "vocabulary" => ContentImportType.Vocabulary, "handbook" => ContentImportType.Handbook, "scoreline" => ContentImportType.Scoreline, "videos" => ContentImportType.Videos, _ => throw new ContentManagementException("Import type is invalid.", "import_type_invalid") }; } protected static TEnum Parse(string? value, TEnum fallback, string code) where TEnum : struct { if (string.IsNullOrWhiteSpace(value)) return fallback; if (Enum.TryParse(value.Trim(), true, out var parsed)) return parsed; throw new ContentManagementException("Enum value is invalid.", code); } protected static TEnum? ParseNullable(string? value, string code) where TEnum : struct { if (string.IsNullOrWhiteSpace(value)) return null; if (Enum.TryParse(value.Trim(), true, out var parsed)) return parsed; throw new ContentManagementException("Enum value is invalid.", code); } protected static string? Normalize(string? value) { return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); } protected static int ResolveLimit(int? limit) { return !limit.HasValue || limit <= 0 ? DefaultLimit : Math.Min(limit.Value, MaxLimit); } protected static JsonElement JsonObjectOrDefault(JsonElement value) { return value.ValueKind is JsonValueKind.Object ? value : JsonDefaults.Object(); } protected static JsonElement JsonArrayOrDefault(JsonElement value) { return value.ValueKind is JsonValueKind.Array ? value : JsonDefaults.Array(); } protected static JsonElement GetElement(JsonElement payload, string name, JsonElement fallback) { return payload.ValueKind == JsonValueKind.Object && payload.TryGetProperty(name, out var value) ? value : fallback; } protected static string? GetString(JsonElement payload, string name) { if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null; return value.ValueKind == JsonValueKind.String ? Normalize(value.GetString()) : value.ToString(); } protected static int? GetInt(JsonElement payload, string name) { if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null; return value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var number) ? number : int.TryParse(value.ToString(), out number) ? number : null; } protected static Guid? GetGuid(JsonElement payload, string name) { if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null; return value.ValueKind == JsonValueKind.String && Guid.TryParse(value.GetString(), out var guid) ? guid : null; } protected static bool? GetBool(JsonElement payload, string name) { if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null; return value.ValueKind switch { JsonValueKind.True => true, JsonValueKind.False => false, JsonValueKind.String when bool.TryParse(value.GetString(), out var parsed) => parsed, _ => null }; } }