using Microsoft.EntityFrameworkCore; using Tiku.Application.Assets; using Tiku.Application.Catalog; using Tiku.Domain.Content; using Tiku.Infrastructure.Persistence; namespace Tiku.Infrastructure.Assets; public sealed class AssetQueryService(TikuDbContext dbContext) : IAssetQueryService { private const int DefaultLimit = 100; private const int MaxLimit = 500; public async Task> GetContentAssetsAsync( AssetFilter filter, CancellationToken cancellationToken = default) { var query = dbContext.ContentAssets .AsNoTracking() .Where(asset => asset.TenantId == filter.TenantId); if (!filter.IncludeInactive) { query = query.Where(asset => asset.Status == ContentStatus.Active); } if (!filter.IncludeLocked) { query = query.Where(asset => asset.Visibility == ContentVisibility.Public || asset.IsPublic); } if (filter.RegionId.HasValue) { query = query.Where(asset => asset.RegionId == filter.RegionId.Value || asset.RegionId == null); } if (filter.SubjectId.HasValue) { query = query.Where(asset => asset.SubjectId == filter.SubjectId.Value || asset.SubjectId == null); } if (filter.CategoryId.HasValue) { query = query.Where(asset => asset.CategoryId == filter.CategoryId.Value || asset.CategoryId == null); } if (filter.ContentNodeId.HasValue) { query = query.Where(asset => asset.ContentNodeId == filter.ContentNodeId.Value || asset.ContentNodeId == null); } if (filter.AssetId.HasValue) { query = query.Where(asset => asset.Id == filter.AssetId.Value); } if (!string.IsNullOrWhiteSpace(filter.AssetType) && Enum.TryParse(filter.AssetType, ignoreCase: true, out var assetType)) { query = query.Where(asset => asset.AssetType == assetType); } if (!string.IsNullOrWhiteSpace(filter.Category)) { var category = filter.Category.Trim(); query = query.Where(asset => asset.Category == category); } if (!string.IsNullOrWhiteSpace(filter.AssetKey)) { var assetKey = filter.AssetKey.Trim(); query = query.Where(asset => asset.AssetKey == assetKey); } if (!string.IsNullOrWhiteSpace(filter.Keyword)) { var keyword = filter.Keyword.Trim(); query = query.Where(asset => (asset.Title != null && asset.Title.Contains(keyword)) || (asset.FileName != null && asset.FileName.Contains(keyword)) || (asset.Description != null && asset.Description.Contains(keyword)) || (asset.AssetKey != null && asset.AssetKey.Contains(keyword))); } var items = await query .OrderBy(asset => asset.SortOrder) .ThenByDescending(asset => asset.CreatedAt) .Take(ResolveLimit(filter.Limit)) .Select(asset => new ContentAssetCatalogItem( asset.Id, asset.LegacyId, asset.RegionId, asset.SubjectId, asset.CategoryId, asset.ContentNodeId, asset.AssetKey, asset.Title, asset.Category, asset.Description, asset.FileName, asset.CdnUrl, asset.IsPublic, asset.AssetType, asset.StorageProvider, asset.Bucket, asset.ObjectKey, asset.MimeType, asset.FileSizeBytes, asset.ChecksumSha256, asset.Visibility, asset.PreviewUrl, asset.Status, asset.SortOrder, asset.UploadStatus, asset.VerifiedAt, asset.PreviewStatus, asset.SecurityScanStatus, asset.Metadata)) .ToArrayAsync(cancellationToken); return new CatalogList(items); } public async Task> GetImagesAsync( AssetFilter filter, CancellationToken cancellationToken = default) { var query = dbContext.Images .AsNoTracking() .Where(image => image.TenantId == filter.TenantId); if (!filter.IncludeLocked) { query = query.Where(image => image.IsPublic); } if (!string.IsNullOrWhiteSpace(filter.Category)) { var category = filter.Category.Trim(); query = query.Where(image => image.Category == category); } if (!string.IsNullOrWhiteSpace(filter.Keyword)) { var keyword = filter.Keyword.Trim(); query = query.Where(image => (image.Title != null && image.Title.Contains(keyword)) || (image.FileName != null && image.FileName.Contains(keyword))); } var items = await query .OrderBy(image => image.Category) .ThenBy(image => image.Title) .Take(ResolveLimit(filter.Limit)) .Select(image => new ImageAssetCatalogItem( image.Id, image.LegacyId, image.Title, image.Category, image.FileName, image.IsPublic, image.CdnUrl, image.Metadata)) .ToArrayAsync(cancellationToken); return new CatalogList(items); } public async Task> GetAppAssetsAsync( AssetFilter filter, CancellationToken cancellationToken = default) { var query = dbContext.AppAssets .AsNoTracking() .Where(asset => asset.TenantId == filter.TenantId); if (!string.IsNullOrWhiteSpace(filter.AssetKey)) { var assetKey = filter.AssetKey.Trim(); query = query.Where(asset => asset.AssetKey == assetKey); } if (!string.IsNullOrWhiteSpace(filter.Keyword)) { var keyword = filter.Keyword.Trim(); query = query.Where(asset => asset.AssetKey.Contains(keyword) || (asset.FileName != null && asset.FileName.Contains(keyword)) || (asset.Description != null && asset.Description.Contains(keyword))); } var items = await query .OrderBy(asset => asset.AssetKey) .Take(ResolveLimit(filter.Limit)) .Select(asset => new AppAssetCatalogItem( asset.Id, asset.LegacyId, asset.AssetKey, asset.FileName, asset.Description, asset.Metadata)) .ToArrayAsync(cancellationToken); return new CatalogList(items); } public async Task> GetVideoExplanationsAsync( AssetFilter filter, CancellationToken cancellationToken = default) { var query = dbContext.VideoExplanations .AsNoTracking() .Where(video => video.TenantId == filter.TenantId); if (!filter.IncludeInactive) { query = query.Where(video => video.IsActive); } if (filter.SubjectId.HasValue) { query = query.Where(video => video.SubjectId == filter.SubjectId.Value || video.SubjectId == null); } if (!string.IsNullOrWhiteSpace(filter.Keyword)) { var keyword = filter.Keyword.Trim(); query = query.Where(video => video.Title.Contains(keyword) || (video.Description != null && video.Description.Contains(keyword))); } var items = await query .OrderBy(video => video.SortOrder) .ThenBy(video => video.Title) .Take(ResolveLimit(filter.Limit)) .Select(video => new VideoExplanationCatalogItem( video.Id, video.LegacyId, video.SubjectId, video.Title, video.Description, video.VideoUrl, video.ThumbnailUrl, video.DurationSeconds, video.KnowledgeTags, video.IsGeneral, video.Difficulty, video.SortOrder, video.IsActive, video.Metadata)) .ToArrayAsync(cancellationToken); return new CatalogList(items); } public async Task> GetQuestionVideosAsync( AssetFilter filter, CancellationToken cancellationToken = default) { var query = dbContext.QuestionVideos .AsNoTracking() .Where(video => video.TenantId == filter.TenantId); if (filter.QuestionId.HasValue) { query = query.Where(video => video.QuestionId == filter.QuestionId.Value); } if (filter.AssetId.HasValue) { query = query.Where(video => video.VideoId == filter.AssetId.Value); } var videoExplanations = dbContext.VideoExplanations.AsNoTracking(); var items = await query .OrderBy(video => video.SortOrder) .ThenBy(video => video.CreatedAt) .Take(ResolveLimit(filter.Limit)) .GroupJoin( videoExplanations, questionVideo => new { questionVideo.TenantId, Id = questionVideo.VideoId }, video => new { video.TenantId, Id = (Guid?)video.Id }, (questionVideo, videos) => new { QuestionVideo = questionVideo, Video = videos.FirstOrDefault() }) .Select(row => new QuestionVideoCatalogItem( row.QuestionVideo.Id, row.QuestionVideo.LegacyId, row.QuestionVideo.QuestionId, row.QuestionVideo.VideoId, row.QuestionVideo.VideoType, row.QuestionVideo.SortOrder, row.QuestionVideo.Metadata, row.Video == null ? null : new VideoExplanationCatalogItem( row.Video.Id, row.Video.LegacyId, row.Video.SubjectId, row.Video.Title, row.Video.Description, row.Video.VideoUrl, row.Video.ThumbnailUrl, row.Video.DurationSeconds, row.Video.KnowledgeTags, row.Video.IsGeneral, row.Video.Difficulty, row.Video.SortOrder, row.Video.IsActive, row.Video.Metadata))) .ToArrayAsync(cancellationToken); return new CatalogList(items); } private static int ResolveLimit(int? limit) { return Math.Clamp(limit ?? DefaultLimit, 1, MaxLimit); } }