using Microsoft.EntityFrameworkCore; using Tiku.Application.Catalog; using Tiku.Application.Content; using Tiku.Domain.Catalog; using Tiku.Domain.Content; using Tiku.Domain.QuestionBanks; namespace Tiku.Infrastructure.Content; internal sealed class VideoManagementService(DirectContentServiceDependencies dependencies) : DirectContentServiceBase(dependencies), IVideoManagementService { public async Task> GetVideosAsync( DirectContentActor actor, AdminLimitFilter filter, CancellationToken cancellationToken = default) { var query = contentAssetPersistence.VideoExplanations.AsNoTracking().Where(item => item.TenantId == actor.TenantId); if (filter.SubjectId.HasValue) query = query.Where(item => item.SubjectId == filter.SubjectId.Value); if (!string.Equals(filter.Status, "all", StringComparison.OrdinalIgnoreCase)) query = query.Where(item => item.IsActive); if (!string.IsNullOrWhiteSpace(filter.Keyword)) { var keyword = filter.Keyword.Trim(); query = query.Where(item => item.Title.Contains(keyword) || (item.Description != null && item.Description.Contains(keyword))); } var items = await query .OrderBy(item => item.SortOrder) .ThenByDescending(item => item.CreatedAt) .Take(ResolveLimit(filter.Limit)) .Select(item => ToVideoItem(item)) .ToArrayAsync(cancellationToken); return new CatalogList(items); } public async Task> UpsertVideoAsync( DirectContentActor actor, VideoExplanationCommand command, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(command.Title); await AssertReferenceAsync(actor.TenantId, command.SubjectId, "subject_not_found", cancellationToken); var item = await ResolveByIdOrLegacyAsync(contentAssetPersistence.VideoExplanations, actor.TenantId, command.Id, command.LegacyId, cancellationToken); var isNew = item is null; item ??= new VideoExplanation { Id = command.Id ?? Guid.NewGuid(), TenantId = actor.TenantId }; item.SubjectId = command.SubjectId; item.LegacyId = Normalize(command.LegacyId); item.Title = command.Title.Trim(); item.Description = Normalize(command.Description); item.VideoUrl = Normalize(command.VideoUrl); item.ThumbnailUrl = Normalize(command.ThumbnailUrl); item.DurationSeconds = command.DurationSeconds; item.KnowledgeTags = JsonArrayOrDefault(command.KnowledgeTags); item.IsGeneral = command.IsGeneral ?? item.IsGeneral; item.Difficulty = command.Difficulty; item.SortOrder = command.Order ?? item.SortOrder; item.IsActive = command.IsActive ?? item.IsActive; item.Metadata = JsonObjectOrDefault(command.Metadata); if (isNew) contentAssetPersistence.VideoExplanations.Add(item); await unitOfWork.SaveChangesAsync(cancellationToken); return new ContentManagementResult(ToVideoItem(item)); } public async Task> BindQuestionVideoAsync( DirectContentActor actor, QuestionVideoCommand command, CancellationToken cancellationToken = default) { await AssertReferenceAsync(actor.TenantId, command.QuestionId, "question_not_found", cancellationToken); await AssertReferenceAsync(actor.TenantId, command.VideoId, "video_not_found", cancellationToken); var item = await contentAssetPersistence.QuestionVideos.SingleOrDefaultAsync( link => link.TenantId == actor.TenantId && link.QuestionId == command.QuestionId && link.VideoId == command.VideoId, cancellationToken); var isNew = item is null; item ??= new QuestionVideo { TenantId = actor.TenantId }; item.QuestionId = command.QuestionId; item.VideoId = command.VideoId; item.LegacyId = Normalize(command.LegacyId); item.VideoType = Parse(command.VideoType, QuestionVideoType.Specific, "question_video_type_invalid"); item.SortOrder = command.Order ?? item.SortOrder; item.Metadata = JsonObjectOrDefault(command.Metadata); if (isNew) contentAssetPersistence.QuestionVideos.Add(item); var question = await questionBankPersistence.Questions.SingleAsync( question => question.TenantId == actor.TenantId && question.Id == command.QuestionId, cancellationToken); question.HasVideoExplanation = true; await unitOfWork.SaveChangesAsync(cancellationToken); return new ContentManagementResult(ToQuestionVideoItem(item)); } }