using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using DysonNetwork.Drive.Data; using DysonNetwork.Drive.Interfaces; using DysonNetwork.Drive.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NodaTime; namespace DysonNetwork.Drive.Services; public class FileReferenceService : IFileReferenceService, IDisposable { private readonly AppDatabase _dbContext; private readonly IFileService _fileService; private readonly IClock _clock; private readonly ILogger _logger; private bool _disposed = false; public FileReferenceService( AppDatabase dbContext, IFileService fileService, IClock clock, ILogger logger) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _fileService = fileService ?? throw new ArgumentNullException(nameof(fileService)); _clock = clock ?? throw new ArgumentNullException(nameof(clock)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task CreateReferenceAsync( Guid fileId, string resourceId, string resourceType, string referenceType, string? referenceId = null, string? referenceName = null, string? referenceMimeType = null, long? referenceSize = null, string? referenceUrl = null, string? referenceThumbnailUrl = null, string? referencePreviewUrl = null, string? referenceMetadata = null, IDictionary? metadata = null, CancellationToken cancellationToken = default) { // Verify file exists var fileExists = await _fileService.FileExistsAsync(fileId, cancellationToken); if (!fileExists) { throw new FileNotFoundException($"File with ID {fileId} not found."); } var reference = new CloudFileReference { Id = Guid.NewGuid(), FileId = fileId, ResourceId = resourceId, ResourceType = resourceType, ReferenceType = referenceType, ReferenceId = referenceId, ReferenceName = referenceName, ReferenceMimeType = referenceMimeType, ReferenceSize = referenceSize, ReferenceUrl = referenceUrl, ReferenceThumbnailUrl = referenceThumbnailUrl, ReferencePreviewUrl = referencePreviewUrl, ReferenceMetadata = referenceMetadata, IsActive = true, CreatedAt = _clock.GetCurrentInstant().ToDateTimeOffset() }; if (metadata != null && metadata.Any()) { var options = new JsonSerializerOptions { WriteIndented = true }; reference.Metadata = JsonDocument.Parse(JsonSerializer.Serialize(metadata, options)); } _dbContext.FileReferences.Add(reference); await _dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation( "Created reference {ReferenceId} for file {FileId} to resource {ResourceType}/{ResourceId}", reference.Id, fileId, resourceType, resourceId); return reference; } public async Task GetReferenceAsync(Guid referenceId, CancellationToken cancellationToken = default) { var reference = await _dbContext.FileReferences .AsNoTracking() .Include(r => r.File) .FirstOrDefaultAsync(r => r.Id == referenceId, cancellationToken); if (reference == null) { throw new KeyNotFoundException($"Reference with ID {referenceId} not found."); } return reference; } public async Task> GetReferencesForFileAsync(Guid fileId, CancellationToken cancellationToken = default) { return await _dbContext.FileReferences .AsNoTracking() .Where(r => r.FileId == fileId && r.IsActive) .ToListAsync(cancellationToken); } public async Task> GetReferencesForResourceAsync( string resourceId, string resourceType, CancellationToken cancellationToken = default) { return await _dbContext.FileReferences .AsNoTracking() .Where(r => r.ResourceId == resourceId && r.ResourceType == resourceType && r.IsActive) .ToListAsync(cancellationToken); } public async Task> GetReferencesOfTypeAsync( string referenceType, CancellationToken cancellationToken = default) { return await _dbContext.FileReferences .AsNoTracking() .Where(r => r.ReferenceType == referenceType && r.IsActive) .ToListAsync(cancellationToken); } public async Task DeleteReferenceAsync(Guid referenceId, CancellationToken cancellationToken = default) { var reference = await _dbContext.FileReferences .FirstOrDefaultAsync(r => r.Id == referenceId, cancellationToken); if (reference == null) { return false; } reference.IsActive = false; await _dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("Deleted reference {ReferenceId}", referenceId); return true; } public async Task DeleteReferencesForFileAsync(Guid fileId, CancellationToken cancellationToken = default) { var references = await _dbContext.FileReferences .Where(r => r.FileId == fileId && r.IsActive) .ToListAsync(cancellationToken); foreach (var reference in references) { reference.IsActive = false; } var count = await _dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("Deleted {Count} references for file {FileId}", count, fileId); return count; } public async Task DeleteReferencesForResourceAsync( string resourceId, string resourceType, CancellationToken cancellationToken = default) { var references = await _dbContext.FileReferences .Where(r => r.ResourceId == resourceId && r.ResourceType == resourceType && r.IsActive) .ToListAsync(cancellationToken); foreach (var reference in references) { reference.IsActive = false; } var count = await _dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation( "Deleted {Count} references for resource {ResourceType}/{ResourceId}", count, resourceType, resourceId); return count; } public async Task UpdateReferenceMetadataAsync( Guid referenceId, IDictionary metadata, CancellationToken cancellationToken = default) { var reference = await GetReferenceAsync(referenceId, cancellationToken); var options = new JsonSerializerOptions { WriteIndented = true }; reference.Metadata = JsonDocument.Parse(JsonSerializer.Serialize(metadata, options)); reference.UpdatedAt = _clock.GetCurrentInstant().ToDateTimeOffset(); await _dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation("Updated metadata for reference {ReferenceId}", referenceId); return reference; } public async Task ReferenceExistsAsync(Guid referenceId, CancellationToken cancellationToken = default) { return await _dbContext.FileReferences .AsNoTracking() .AnyAsync(r => r.Id == referenceId && r.IsActive, cancellationToken); } public async Task HasReferenceAsync( Guid fileId, string resourceId, string resourceType, string? referenceType = null, CancellationToken cancellationToken = default) { var query = _dbContext.FileReferences .AsNoTracking() .Where(r => r.FileId == fileId && r.ResourceId == resourceId && r.ResourceType == resourceType && r.IsActive); if (!string.IsNullOrEmpty(referenceType)) { query = query.Where(r => r.ReferenceType == referenceType); } return await query.AnyAsync(cancellationToken); } public async Task UpdateReferenceResourceAsync( Guid referenceId, string newResourceId, string newResourceType, CancellationToken cancellationToken = default) { var reference = await GetReferenceAsync(referenceId, cancellationToken); reference.ResourceId = newResourceId; reference.ResourceType = newResourceType; reference.UpdatedAt = _clock.GetCurrentInstant().ToDateTimeOffset(); await _dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation( "Updated reference {ReferenceId} to point to resource {ResourceType}/{ResourceId}", referenceId, newResourceType, newResourceId); return reference; } public async Task> GetFilesForResourceAsync( string resourceId, string resourceType, string? referenceType = null, CancellationToken cancellationToken = default) { var query = _dbContext.FileReferences .AsNoTracking() .Include(r => r.File) .Where(r => r.ResourceId == resourceId && r.ResourceType == resourceType && r.IsActive); if (!string.IsNullOrEmpty(referenceType)) { query = query.Where(r => r.ReferenceType == referenceType); } var references = await query.ToListAsync(cancellationToken); return references.Select(r => r.File!); } public async Task> GetFilesForReferenceTypeAsync( string referenceType, CancellationToken cancellationToken = default) { var references = await _dbContext.FileReferences .AsNoTracking() .Include(r => r.File) .Where(r => r.ReferenceType == referenceType && r.IsActive) .ToListAsync(cancellationToken); return references.Select(r => r.File!); } protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { _dbContext?.Dispose(); } _disposed = true; } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } }