using System.Collections.Concurrent;
using Microsoft.Extensions.Caching.Memory;

namespace LiquidCode.Tester.Gateway.Services;

public class PackageDownloadService : IPackageDownloadService
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger<PackageDownloadService> _logger;
    private readonly string _downloadDirectory;
    private readonly IMemoryCache _memoryCache;
    private readonly ConcurrentDictionary<long, SemaphoreSlim> _locks = new();

    private sealed record PackageCacheEntry(string FilePath);

    public PackageDownloadService(
        IHttpClientFactory httpClientFactory,
        ILogger<PackageDownloadService> logger,
        IConfiguration configuration,
        IMemoryCache memoryCache)
    {
        _httpClientFactory = httpClientFactory;
        _logger = logger;
        _memoryCache = memoryCache;
        _downloadDirectory = configuration["PackageDownloadDirectory"] ?? Path.Combine(Path.GetTempPath(), "packages");

        if (!Directory.Exists(_downloadDirectory))
        {
            Directory.CreateDirectory(_downloadDirectory);
        }
    }

    public async Task<string> GetOrDownloadPackageAsync(long missionId, string packageUrl)
    {
        if (TryGetCachedFile(missionId, out var cachedFile))
        {
            return cachedFile;
        }

        var missionLock = _locks.GetOrAdd(missionId, _ => new SemaphoreSlim(1, 1));

        await missionLock.WaitAsync();
        try
        {
            if (TryGetCachedFile(missionId, out cachedFile))
            {
                return cachedFile;
            }

            _logger.LogInformation("Downloading package for mission {MissionId} from {Url}", missionId, packageUrl);

            var httpClient = _httpClientFactory.CreateClient();
            var response = await httpClient.GetAsync(packageUrl);
            response.EnsureSuccessStatusCode();

            var fileName = $"package_{Guid.NewGuid()}.zip";
            var filePath = Path.Combine(_downloadDirectory, fileName);

            try
            {
                await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
                await response.Content.CopyToAsync(fileStream);
            }
            catch
            {
                if (File.Exists(filePath))
                {
                    try
                    {
                        File.Delete(filePath);
                    }
                    catch (Exception cleanupEx)
                    {
                        _logger.LogWarning(cleanupEx, "Failed to clean up temporary file {Path} after download error", filePath);
                    }
                }

                throw;
            }

            CacheFile(missionId, filePath);

            _logger.LogInformation("Package downloaded and cached for mission {MissionId} at {Path}", missionId, filePath);

            return filePath;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to download package for mission {MissionId} from {Url}", missionId, packageUrl);
            throw;
        }
        finally
        {
            missionLock.Release();
        }
    }

    private bool TryGetCachedFile(long missionId, out string filePath)
    {
        if (_memoryCache.TryGetValue(missionId, out PackageCacheEntry? cacheEntry))
        {
            if (cacheEntry is not null && File.Exists(cacheEntry.FilePath))
            {
                filePath = cacheEntry.FilePath;
                _logger.LogInformation("Using cached package for mission {MissionId} from {Path}", missionId, filePath);
                return true;
            }

            _memoryCache.Remove(missionId);
        }

        filePath = string.Empty;
        return false;
    }

    private void CacheFile(long missionId, string filePath)
    {
        var cacheEntry = new PackageCacheEntry(filePath);
        var options = new MemoryCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
        };

        options.RegisterPostEvictionCallback((_, value, _, _) =>
        {
            if (value is not PackageCacheEntry entry)
            {
                return;
            }

            try
            {
                if (File.Exists(entry.FilePath))
                {
                    File.Delete(entry.FilePath);
                    _logger.LogInformation("Removed cached package file {Path} after expiration", entry.FilePath);
                }
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Failed to delete cached package file {Path} during eviction", entry.FilePath);
            }
        });

        _memoryCache.Set(missionId, cacheEntry, options);
    }
}
