using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Tiku.Application.Content; using Tiku.Application.Security; using Tiku.Domain.Content; using Tiku.Infrastructure.Persistence; namespace Tiku.Infrastructure.ContentV2; internal sealed class PlatformContentPackageProjectionService( IQuestionBankPersistence content, ITenantExecutionScope tenantExecutionScope) : IPlatformContentPackageProjectionService { public async Task ApplyAsync( ApplyPlatformContentPackageCommand command, CancellationToken cancellationToken = default) { var packageCode = Required(command.PackageCode, nameof(command.PackageCode), 100); var cellId = Required(command.CellId, nameof(command.CellId), 100); var contentHash = Required(command.ContentHash, nameof(command.ContentHash), 64); if (command.EventSequence <= 0) throw new ContentV2Exception("platform_package_sequence_invalid", "The platform package event sequence must be positive."); await using var transaction = await content.Database.BeginTransactionAsync(cancellationToken); var lockKey = $"platform-package-cell:{command.PackageOwnerTenantId:N}:{packageCode}:{cellId}"; await content.Database.ExecuteSqlInterpolatedAsync( $"SELECT pg_advisory_xact_lock(hashtextextended({lockKey}, 0))", cancellationToken); var package = await tenantExecutionScope.ExecuteAsync( new SystemScopeRequest( command.PackageOwnerTenantId, SystemScopeCallerType.PublicQuestionBank, nameof(PlatformContentPackageProjectionService), "Validate an immutable platform package before applying it to a Cell", Guid.NewGuid().ToString("N")), (provider, token) => provider.GetRequiredService() .PlatformContentPackageVersions.AsNoTracking() .SingleOrDefaultAsync(item => item.TenantId == command.PackageOwnerTenantId && item.Id == command.PlatformContentPackageVersionId && item.PackageCode == packageCode && item.ContentHash == contentHash && item.Status == PlatformContentPackageStatus.Published, token), cancellationToken) ?? throw new ContentV2Exception( "platform_package_invalid", "The package event does not match a published immutable platform package version."); var projection = await content.PlatformContentPackageCellProjections .SingleOrDefaultAsync(item => item.PackageOwnerTenantId == command.PackageOwnerTenantId && item.PackageCode == packageCode && item.CellId == cellId, cancellationToken); if (projection is not null && command.EventSequence < projection.EventSequence) { await transaction.CommitAsync(cancellationToken); return Result(CellPackageApplyStatus.IgnoredOutOfOrder, projection); } if (projection is not null && command.EventSequence == projection.EventSequence) { if (projection.PlatformContentPackageVersionId != package.Id || !string.Equals(projection.ContentHash, contentHash, StringComparison.Ordinal)) throw new ContentV2Exception("platform_package_sequence_conflict", "The same package event sequence was received with different immutable content."); await transaction.CommitAsync(cancellationToken); return Result(CellPackageApplyStatus.Duplicate, projection); } if (projection is null) { projection = new PlatformContentPackageCellProjection { PackageOwnerTenantId = command.PackageOwnerTenantId, PackageCode = packageCode, CellId = cellId }; content.PlatformContentPackageCellProjections.Add(projection); } projection.PlatformContentPackageVersionId = package.Id; projection.EventSequence = command.EventSequence; projection.ContentHash = contentHash; projection.Status = CellProjectionStatus.Ready; projection.ProjectedAt = DateTimeOffset.UtcNow; projection.LastError = null; await content.SaveChangesAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); return Result(CellPackageApplyStatus.Applied, projection); } private static CellPackageApplyResult Result( CellPackageApplyStatus status, PlatformContentPackageCellProjection projection) => new( status, projection.PlatformContentPackageVersionId, projection.CellId, projection.EventSequence, projection.ContentHash); private static string Required(string value, string parameterName, int maxLength) { var normalized = value.Trim(); if (normalized.Length == 0 || normalized.Length > maxLength) throw new ContentV2Exception("platform_package_event_invalid", $"{parameterName} is required and must not exceed {maxLength} characters."); return normalized; } }