using DysonNetwork.Shared.Cache;
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);

    /// <summary>
    /// Creates a new reference to a file for a specific resource
    /// </summary>
    /// <param name="fileId">The ID of the file to reference</param>
    /// <param name="usage">The usage context (e.g., "avatar", "post-attachment")</param>
    /// <param name="resourceId">The ID of the resource using the file</param>
    /// <param name="expiredAt">Optional expiration time for the file</param>
    /// <param name="duration">Optional duration after which the file expires (alternative to expiredAt)</param>
    /// <returns>The created file reference</returns>
    public async Task<CloudFileReference> 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<List<CloudFileReference>> CreateReferencesAsync(
        List<string> 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;
    }

    /// <summary>
    /// Gets all references to a file
    /// </summary>
    /// <param name="fileId">The ID of the file</param>
    /// <returns>A list of all references to the file</returns>
    public async Task<List<CloudFileReference>> GetReferencesAsync(string fileId)
    {
        var cacheKey = $"{CacheKeyPrefix}list:{fileId}";

        var cachedReferences = await cache.GetAsync<List<CloudFileReference>>(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<Dictionary<string, List<CloudFileReference>>> GetReferencesAsync(IEnumerable<string> 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;
    }

    /// <summary>
    /// Gets the number of references to a file
    /// </summary>
    /// <param name="fileId">The ID of the file</param>
    /// <returns>The number of references to the file</returns>
    public async Task<int> GetReferenceCountAsync(string fileId)
    {
        var cacheKey = $"{CacheKeyPrefix}count:{fileId}";

        var cachedCount = await cache.GetAsync<int?>(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;
    }

    /// <summary>
    /// Gets all references for a specific resource
    /// </summary>
    /// <param name="resourceId">The ID of the resource</param>
    /// <returns>A list of file references associated with the resource</returns>
    public async Task<List<CloudFileReference>> GetResourceReferencesAsync(string resourceId)
    {
        var cacheKey = $"{CacheKeyPrefix}resource:{resourceId}";

        var cachedReferences = await cache.GetAsync<List<CloudFileReference>>(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;
    }

    /// <summary>
    /// Gets all file references for a specific usage context
    /// </summary>
    /// <param name="usage">The usage context</param>
    /// <returns>A list of file references with the specified usage</returns>
    public async Task<List<CloudFileReference>> GetUsageReferencesAsync(string usage)
    {
        return await db.FileReferences
            .Where(r => r.Usage == usage)
            .ToListAsync();
    }

    /// <summary>
    /// Deletes references for a specific resource
    /// </summary>
    /// <param name="resourceId">The ID of the resource</param>
    /// <returns>The number of deleted references</returns>
    public async Task<int> 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;
    }

    /// <summary>
    /// Deletes references for a specific resource and usage
    /// </summary>
    /// <param name="resourceId">The ID of the resource</param>
    /// <param name="usage">The usage context</param>
    /// <returns>The number of deleted references</returns>
    public async Task<int> 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<int> DeleteResourceReferencesBatchAsync(IEnumerable<string> 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;
    }

    /// <summary>
    /// Deletes a specific file reference
    /// </summary>
    /// <param name="referenceId">The ID of the reference to delete</param>
    /// <returns>True if the reference was deleted, false otherwise</returns>
    public async Task<bool> 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;
    }

    /// <summary>
    /// Updates the files referenced by a resource
    /// </summary>
    /// <param name="resourceId">The ID of the resource</param>
    /// <param name="newFileIds">The new list of file IDs</param>
    /// <param name="usage">The usage context</param>
    /// <param name="expiredAt">Optional expiration time for newly added files</param>
    /// <param name="duration">Optional duration after which newly added files expire</param>
    /// <returns>A list of the updated file references</returns>
    public async Task<List<CloudFileReference>> UpdateResourceFilesAsync(
        string resourceId,
        IEnumerable<string>? newFileIds,
        string usage,
        Instant? expiredAt = null,
        Duration? duration = null)
    {
        if (newFileIds == null)
            return new List<CloudFileReference>();

        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();
    }

    /// <summary>
    /// Gets all files referenced by a resource
    /// </summary>
    /// <param name="resourceId">The ID of the resource</param>
    /// <param name="usage">Optional filter by usage context</param>
    /// <returns>A list of files referenced by the resource</returns>
    public async Task<List<CloudFile>> 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();
    }

    /// <summary>
    /// Purges all caches related to a resource
    /// </summary>
    private async Task PurgeCacheForResourceAsync(string resourceId)
    {
        var cacheKey = $"{CacheKeyPrefix}resource:{resourceId}";
        await cache.RemoveAsync(cacheKey);
    }

    /// <summary>
    /// Purges all caches related to a file
    /// </summary>
    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);
    }

    /// <summary>
    /// Updates the expiration time for a file reference
    /// </summary>
    /// <param name="referenceId">The ID of the reference</param>
    /// <param name="expiredAt">The new expiration time, or null to remove expiration</param>
    /// <returns>True if the reference was found and updated, false otherwise</returns>
    public async Task<bool> 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;
    }

    /// <summary>
    /// Updates the expiration time for all references to a file
    /// </summary>
    /// <param name="fileId">The ID of the file</param>
    /// <param name="expiredAt">The new expiration time, or null to remove expiration</param>
    /// <returns>The number of references updated</returns>
    public async Task<int> 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;
    }

    /// <summary>
    /// Get all file references for a specific resource and usage type
    /// </summary>
    /// <param name="resourceId">The resource ID</param>
    /// <param name="usageType">The usage type</param>
    /// <returns>List of file references</returns>
    public async Task<List<CloudFileReference>> GetResourceReferencesAsync(string resourceId, string usageType)
    {
        return await db.FileReferences
            .Where(r => r.ResourceId == resourceId && r.Usage == usageType)
            .ToListAsync();
    }

    /// <summary>
    /// Check if a file has any references
    /// </summary>
    /// <param name="fileId">The file ID to check</param>
    /// <returns>True if the file has references, false otherwise</returns>
    public async Task<bool> HasFileReferencesAsync(string fileId)
    {
        return await db.FileReferences.AnyAsync(r => r.FileId == fileId);
    }

    /// <summary>
    /// Updates the expiration time for a file reference using a duration from now
    /// </summary>
    /// <param name="referenceId">The ID of the reference</param>
    /// <param name="duration">The duration after which the reference expires, or null to remove expiration</param>
    /// <returns>True if the reference was found and updated, false otherwise</returns>
    public async Task<bool> SetReferenceExpirationDurationAsync(Guid referenceId, Duration? duration)
    {
        Instant? expiredAt = null;
        if (duration.HasValue)
        {
            expiredAt = SystemClock.Instance.GetCurrentInstant() + duration.Value;
        }

        return await SetReferenceExpirationAsync(referenceId, expiredAt);
    }
}