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; namespace Tiku.Infrastructure.Content; internal abstract partial class DirectContentServiceBase { 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.QuestionPlacementId, 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 }; } }