using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Models;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Drive.Storage;
public class FileReferenceService(AppDatabase db, FileService fileService, ICacheService cache)
{
    private const string CacheKeyPrefix = "file:ref:";
    private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(15);
    /// 
    /// Creates a new reference to a file for a specific resource
    /// 
    /// The ID of the file to reference
    /// The usage context (e.g., "avatar", "post-attachment")
    /// The ID of the resource using the file
    /// Optional expiration time for the file
    /// Optional duration after which the file expires (alternative to expiredAt)
    /// The created file reference
    public async Task CreateReferenceAsync(
        string fileId,
        string usage,
        string resourceId,
        Instant? expiredAt = null,
        Duration? duration = null
    )
    {
        // Calculate expiration time if needed
        var finalExpiration = expiredAt;
        if (duration.HasValue)
            finalExpiration = SystemClock.Instance.GetCurrentInstant() + duration.Value;
        var reference = new CloudFileReference
        {
            FileId = fileId,
            Usage = usage,
            ResourceId = resourceId,
            ExpiredAt = finalExpiration
        };
        db.FileReferences.Add(reference);
        await db.SaveChangesAsync();
        await fileService._PurgeCacheAsync(fileId);
        return reference;
    }
    public async Task> CreateReferencesAsync(
        List fileId,
        string usage,
        string resourceId,
        Instant? expiredAt = null,
        Duration? duration = null
    )
    {
        var data = fileId.Select(id => new CloudFileReference
        {
            FileId = id,
            Usage = usage,
            ResourceId = resourceId,
            ExpiredAt = expiredAt ?? SystemClock.Instance.GetCurrentInstant() + duration
        }).ToList();
        await db.BulkInsertAsync(data);
        return data;
    }
    /// 
    /// Gets all references to a file
    /// 
    /// The ID of the file
    /// A list of all references to the file
    public async Task> GetReferencesAsync(string fileId)
    {
        var cacheKey = $"{CacheKeyPrefix}list:{fileId}";
        var cachedReferences = await cache.GetAsync>(cacheKey);
        if (cachedReferences is not null)
            return cachedReferences;
        var references = await db.FileReferences
            .Where(r => r.FileId == fileId)
            .ToListAsync();
        await cache.SetAsync(cacheKey, references, CacheDuration);
        return references;
    }
    public async Task>> GetReferencesAsync(IEnumerable fileId)
    {
        var references = await db.FileReferences
            .Where(r => fileId.Contains(r.FileId))
            .GroupBy(r => r.FileId)
            .ToDictionaryAsync(r => r.Key, r => r.ToList());
        return references;
    }
    /// 
    /// Gets the number of references to a file
    /// 
    /// The ID of the file
    /// The number of references to the file
    public async Task GetReferenceCountAsync(string fileId)
    {
        var cacheKey = $"{CacheKeyPrefix}count:{fileId}";
        var cachedCount = await cache.GetAsync(cacheKey);
        if (cachedCount.HasValue)
            return cachedCount.Value;
        var count = await db.FileReferences
            .Where(r => r.FileId == fileId)
            .CountAsync();
        await cache.SetAsync(cacheKey, count, CacheDuration);
        return count;
    }
    /// 
    /// Gets all references for a specific resource
    /// 
    /// The ID of the resource
    /// A list of file references associated with the resource
    public async Task> GetResourceReferencesAsync(string resourceId)
    {
        var cacheKey = $"{CacheKeyPrefix}resource:{resourceId}";
        var cachedReferences = await cache.GetAsync>(cacheKey);
        if (cachedReferences is not null)
            return cachedReferences;
        var references = await db.FileReferences
            .Where(r => r.ResourceId == resourceId)
            .ToListAsync();
        await cache.SetAsync(cacheKey, references, CacheDuration);
        return references;
    }
    /// 
    /// Gets all file references for a specific usage context
    /// 
    /// The usage context
    /// A list of file references with the specified usage
    public async Task> GetUsageReferencesAsync(string usage)
    {
        return await db.FileReferences
            .Where(r => r.Usage == usage)
            .ToListAsync();
    }
    /// 
    /// Deletes references for a specific resource
    /// 
    /// The ID of the resource
    /// The number of deleted references
    public async Task DeleteResourceReferencesAsync(string resourceId)
    {
        var references = await db.FileReferences
            .Where(r => r.ResourceId == resourceId)
            .ToListAsync();
        var fileIds = references.Select(r => r.FileId).Distinct().ToList();
        db.FileReferences.RemoveRange(references);
        var deletedCount = await db.SaveChangesAsync();
        // Purge caches
        var tasks = fileIds.Select(fileService._PurgeCacheAsync).ToList();
        tasks.Add(PurgeCacheForResourceAsync(resourceId));
        await Task.WhenAll(tasks);
        return deletedCount;
    }
    /// 
    /// Deletes references for a specific resource and usage
    /// 
    /// The ID of the resource
    /// The usage context
    /// The number of deleted references
    public async Task DeleteResourceReferencesAsync(string resourceId, string usage)
    {
        var references = await db.FileReferences
            .Where(r => r.ResourceId == resourceId && r.Usage == usage)
            .ToListAsync();
        if (references.Count == 0)
            return 0;
        var fileIds = references.Select(r => r.FileId).Distinct().ToList();
        db.FileReferences.RemoveRange(references);
        var deletedCount = await db.SaveChangesAsync();
        // Purge caches
        var tasks = fileIds.Select(fileService._PurgeCacheAsync).ToList();
        tasks.Add(PurgeCacheForResourceAsync(resourceId));
        await Task.WhenAll(tasks);
        return deletedCount;
    }
    
    public async Task DeleteResourceReferencesBatchAsync(IEnumerable resourceIds, string? usage = null)
    {
        var references = await db.FileReferences
            .Where(r => resourceIds.Contains(r.ResourceId))
            .If(usage != null, q => q.Where(q => q.Usage == usage))
            .ToListAsync();
        if (references.Count == 0)
            return 0;
        var fileIds = references.Select(r => r.FileId).Distinct().ToList();
        db.FileReferences.RemoveRange(references);
        var deletedCount = await db.SaveChangesAsync();
        // Purge caches
        var tasks = fileIds.Select(fileService._PurgeCacheAsync).ToList();
        await Task.WhenAll(tasks);
        return deletedCount;
    }
    /// 
    /// Deletes a specific file reference
    /// 
    /// The ID of the reference to delete
    /// True if the reference was deleted, false otherwise
    public async Task DeleteReferenceAsync(Guid referenceId)
    {
        var reference = await db.FileReferences
            .FirstOrDefaultAsync(r => r.Id == referenceId);
        if (reference == null)
            return false;
        db.FileReferences.Remove(reference);
        await db.SaveChangesAsync();
        // Purge caches
        await fileService._PurgeCacheAsync(reference.FileId);
        await PurgeCacheForResourceAsync(reference.ResourceId);
        await PurgeCacheForFileAsync(reference.FileId);
        return true;
    }
    /// 
    /// Updates the files referenced by a resource
    /// 
    /// The ID of the resource
    /// The new list of file IDs
    /// The usage context
    /// Optional expiration time for newly added files
    /// Optional duration after which newly added files expire
    /// A list of the updated file references
    public async Task> UpdateResourceFilesAsync(
        string resourceId,
        IEnumerable? newFileIds,
        string usage,
        Instant? expiredAt = null,
        Duration? duration = null)
    {
        if (newFileIds == null)
            return new List();
        var existingReferences = await db.FileReferences
            .Where(r => r.ResourceId == resourceId && r.Usage == usage)
            .ToListAsync();
        var existingFileIds = existingReferences.Select(r => r.FileId).ToHashSet();
        var newFileIdsList = newFileIds.ToList();
        var newFileIdsSet = newFileIdsList.ToHashSet();
        // Files to remove
        var toRemove = existingReferences
            .Where(r => !newFileIdsSet.Contains(r.FileId))
            .ToList();
        // Files to add
        var toAdd = newFileIdsList
            .Where(id => !existingFileIds.Contains(id))
            .Select(id => new CloudFileReference
            {
                FileId = id,
                Usage = usage,
                ResourceId = resourceId
            })
            .ToList();
        // Apply changes
        if (toRemove.Any())
            db.FileReferences.RemoveRange(toRemove);
        if (toAdd.Any())
            db.FileReferences.AddRange(toAdd);
        await db.SaveChangesAsync();
        // Update expiration for newly added references if specified
        if ((expiredAt.HasValue || duration.HasValue) && toAdd.Any())
        {
            var finalExpiration = expiredAt;
            if (duration.HasValue)
            {
                finalExpiration = SystemClock.Instance.GetCurrentInstant() + duration.Value;
            }
            // Update newly added references with the expiration time
            var referenceIds = await db.FileReferences
                .Where(r => toAdd.Select(a => a.FileId).Contains(r.FileId) &&
                            r.ResourceId == resourceId &&
                            r.Usage == usage)
                .Select(r => r.Id)
                .ToListAsync();
            await db.FileReferences
                .Where(r => referenceIds.Contains(r.Id))
                .ExecuteUpdateAsync(setter => setter.SetProperty(
                    r => r.ExpiredAt,
                    _ => finalExpiration
                ));
        }
        // Purge caches
        var allFileIds = existingFileIds.Union(newFileIdsSet).ToList();
        var tasks = allFileIds.Select(fileService._PurgeCacheAsync).ToList();
        tasks.Add(PurgeCacheForResourceAsync(resourceId));
        await Task.WhenAll(tasks);
        // Return updated references
        return await db.FileReferences
            .Where(r => r.ResourceId == resourceId && r.Usage == usage)
            .ToListAsync();
    }
    /// 
    /// Gets all files referenced by a resource
    /// 
    /// The ID of the resource
    /// Optional filter by usage context
    /// A list of files referenced by the resource
    public async Task> GetResourceFilesAsync(string resourceId, string? usage = null)
    {
        var query = db.FileReferences.Where(r => r.ResourceId == resourceId);
        if (usage != null)
            query = query.Where(r => r.Usage == usage);
        var references = await query.ToListAsync();
        var fileIds = references.Select(r => r.FileId).ToList();
        return await db.Files
            .Where(f => fileIds.Contains(f.Id))
            .ToListAsync();
    }
    /// 
    /// Purges all caches related to a resource
    /// 
    private async Task PurgeCacheForResourceAsync(string resourceId)
    {
        var cacheKey = $"{CacheKeyPrefix}resource:{resourceId}";
        await cache.RemoveAsync(cacheKey);
    }
    /// 
    /// Purges all caches related to a file
    /// 
    private async Task PurgeCacheForFileAsync(string fileId)
    {
        var cacheKeys = new[]
        {
            $"{CacheKeyPrefix}list:{fileId}",
            $"{CacheKeyPrefix}count:{fileId}"
        };
        var tasks = cacheKeys.Select(cache.RemoveAsync);
        await Task.WhenAll(tasks);
    }
    /// 
    /// Updates the expiration time for a file reference
    /// 
    /// The ID of the reference
    /// The new expiration time, or null to remove expiration
    /// True if the reference was found and updated, false otherwise
    public async Task SetReferenceExpirationAsync(Guid referenceId, Instant? expiredAt)
    {
        var reference = await db.FileReferences
            .FirstOrDefaultAsync(r => r.Id == referenceId);
        if (reference == null)
            return false;
        reference.ExpiredAt = expiredAt;
        await db.SaveChangesAsync();
        await PurgeCacheForFileAsync(reference.FileId);
        await PurgeCacheForResourceAsync(reference.ResourceId);
        return true;
    }
    /// 
    /// Updates the expiration time for all references to a file
    /// 
    /// The ID of the file
    /// The new expiration time, or null to remove expiration
    /// The number of references updated
    public async Task SetFileReferencesExpirationAsync(string fileId, Instant? expiredAt)
    {
        var rowsAffected = await db.FileReferences
            .Where(r => r.FileId == fileId)
            .ExecuteUpdateAsync(setter => setter.SetProperty(
                r => r.ExpiredAt,
                _ => expiredAt
            ));
        if (rowsAffected > 0)
        {
            await fileService._PurgeCacheAsync(fileId);
            await PurgeCacheForFileAsync(fileId);
        }
        return rowsAffected;
    }
    /// 
    /// Get all file references for a specific resource and usage type
    /// 
    /// The resource ID
    /// The usage type
    /// List of file references
    public async Task> GetResourceReferencesAsync(string resourceId, string usageType)
    {
        return await db.FileReferences
            .Where(r => r.ResourceId == resourceId && r.Usage == usageType)
            .ToListAsync();
    }
    /// 
    /// Check if a file has any references
    /// 
    /// The file ID to check
    /// True if the file has references, false otherwise
    public async Task HasFileReferencesAsync(string fileId)
    {
        return await db.FileReferences.AnyAsync(r => r.FileId == fileId);
    }
    /// 
    /// Updates the expiration time for a file reference using a duration from now
    /// 
    /// The ID of the reference
    /// The duration after which the reference expires, or null to remove expiration
    /// True if the reference was found and updated, false otherwise
    public async Task SetReferenceExpirationDurationAsync(Guid referenceId, Duration? duration)
    {
        Instant? expiredAt = null;
        if (duration.HasValue)
        {
            expiredAt = SystemClock.Instance.GetCurrentInstant() + duration.Value;
        }
        return await SetReferenceExpirationAsync(referenceId, expiredAt);
    }
}