using System.IO;
using System.Net.Http;
using CompatBot.ThumbScrapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;

namespace CompatBot.Database.Providers;

internal static class ThumbnailProvider
{
    private static readonly HttpClient HttpClient = HttpClientFactory.Create();
    private static readonly PsnClient.Client PsnClient = new();
    private static readonly MemoryCache ColorCache = new(new MemoryCacheOptions{ ExpirationScanFrequency = TimeSpan.FromDays(1) });

    public static async ValueTask<string?> GetThumbnailUrlAsync(this DiscordClient client, string? productCode)
    {
        if (string.IsNullOrEmpty(productCode))
            return null;
            
        productCode = productCode.ToUpperInvariant();
        var tmdbInfo = await PsnClient.GetTitleMetaAsync(productCode, Config.Cts.Token).ConfigureAwait(false);
        if (tmdbInfo is { Icon.Url: string tmdbIconUrl })
            return tmdbIconUrl;

        await using (var db = await ThumbnailDb.OpenWriteAsync().ConfigureAwait(false))
        {
            //todo: add search task if not found
            if (await db.Thumbnail
                    .AsNoTracking()
                    .FirstOrDefaultAsync(t => t.ProductCode == productCode)
                    .ConfigureAwait(false) is { EmbeddableUrl: { Length: > 0 } embeddableUrl }
            )
            {
                return embeddableUrl;
            }
        }

        await using var wdb = await ThumbnailDb.OpenWriteAsync().ConfigureAwait(false);
        var thumb = await wdb.Thumbnail.FirstOrDefaultAsync(t => t.ProductCode == productCode).ConfigureAwait(false);
        if (string.IsNullOrEmpty(thumb?.Url) || !ScrapeStateProvider.IsFresh(thumb.Timestamp))
        {
            var gameTdbCoverUrl = await GameTdbScraper.GetThumbAsync(productCode).ConfigureAwait(false);
            if (!string.IsNullOrEmpty(gameTdbCoverUrl))
            {
                if (thumb is null)
                    thumb = (await wdb.Thumbnail.AddAsync(new() {ProductCode = productCode, Url = gameTdbCoverUrl}).ConfigureAwait(false)).Entity;
                else
                    thumb.Url = gameTdbCoverUrl;
                thumb.Timestamp = DateTime.UtcNow.Ticks;
                await wdb.SaveChangesAsync().ConfigureAwait(false);
            }
        }

        if (string.IsNullOrEmpty(thumb?.Url))
            return null;
            
        var contentName = thumb.ContentId ?? thumb.ProductCode;
        var (embedUrl, _) = await GetEmbeddableUrlAsync(client, contentName, thumb.Url).ConfigureAwait(false);
        if (embedUrl is null)
            return null;
            
        thumb.EmbeddableUrl = embedUrl;
        await wdb.SaveChangesAsync().ConfigureAwait(false);
        return embedUrl;
    }

    public static async ValueTask<string?> GetTitleNameAsync(string? productCode, CancellationToken cancellationToken)
    {
        if (string.IsNullOrEmpty(productCode))
            return null;

        productCode = productCode.ToUpperInvariant();
        await using (var db = await ThumbnailDb.OpenReadAsync().ConfigureAwait(false))
        {
            if (await db.Thumbnail
                    .AsNoTracking()
                    .FirstOrDefaultAsync(t => t.ProductCode == productCode, cancellationToken: cancellationToken)
                    .ConfigureAwait(false) is { Name: { Length: > 0 } result }
               )
            {
                return result;
            }
        }

        await using var wdb = await ThumbnailDb.OpenWriteAsync().ConfigureAwait(false);
        var thumb = await wdb.Thumbnail.FirstOrDefaultAsync(
            t => t.ProductCode == productCode,
            cancellationToken: cancellationToken
        ).ConfigureAwait(false);
        var title = (await PsnClient.GetTitleMetaAsync(productCode, cancellationToken).ConfigureAwait(false))?.Name;
        try
        {
            if (!string.IsNullOrEmpty(title))
            {
                if (thumb is null)
                    await wdb.Thumbnail.AddAsync(new()
                    {
                        ProductCode = productCode,
                        Name = title,
                    }, cancellationToken).ConfigureAwait(false);
                else
                    thumb.Name = title;
                await wdb.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
            }
        }
        catch (Exception e)
        {
            Config.Log.Warn(e);
        }

        return title;
    }

    public static async ValueTask<(string? url, DiscordColor color)> GetThumbnailUrlWithColorAsync(DiscordClient client, string contentId, DiscordColor defaultColor, string? url = null)
    {
        if (string.IsNullOrEmpty(contentId))
            throw new ArgumentException("ContentID can't be empty", nameof(contentId));

        contentId = contentId.ToUpperInvariant();
        await using var wdb = await ThumbnailDb.OpenWriteAsync().ConfigureAwait(false); //todo: fix this if it's ever get re-used
        var info = await wdb.Thumbnail.FirstOrDefaultAsync(ti => ti.ContentId == contentId, Config.Cts.Token).ConfigureAwait(false);
        info ??= new() {Url = url};
        if (info.Url is null)
            return (null, defaultColor);

        DiscordColor? analyzedColor = null;
        if (string.IsNullOrEmpty(info.EmbeddableUrl))
        {
            var (embedUrl, image) = await GetEmbeddableUrlAsync(client, contentId, info.Url).ConfigureAwait(false);
            if (embedUrl is string eUrl)
            {
                info.EmbeddableUrl = eUrl;
                if (image is byte[] jpg)
                {
                    Config.Log.Trace("Getting dominant color for " + eUrl);
                    await using var stream = Config.MemoryStreamManager.GetStream(jpg);
                    analyzedColor = await ColorGetter.GetDominantColorAsync(stream, defaultColor).ConfigureAwait(false);
                    if (analyzedColor.HasValue
                        && analyzedColor.Value.Value != defaultColor.Value)
                        info.EmbedColor = analyzedColor.Value.Value;
                }
                await wdb.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
            }
        }
        if (!info.EmbedColor.HasValue && !analyzedColor.HasValue
            || info.EmbedColor.HasValue && info.EmbedColor.Value == defaultColor.Value)
        {
            var c = await GetImageColorAsync(info.EmbeddableUrl, defaultColor).ConfigureAwait(false);
            if (c.HasValue && c.Value.Value != defaultColor.Value)
            {
                info.EmbedColor = c.Value.Value;
                await wdb.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
            }
        }
        var color = info.EmbedColor.HasValue ? new(info.EmbedColor.Value) : defaultColor;
        return (info.EmbeddableUrl, color);
    }

    public static async ValueTask<(string? url, byte[]? image)> GetEmbeddableUrlAsync(DiscordClient client, string contentId, string url)
    {
        try
        {
            if (!string.IsNullOrEmpty(Path.GetExtension(url)))
                return (url, null);

            await using var imgStream = await HttpClient.GetStreamAsync(url).ConfigureAwait(false);
            await using var memStream = Config.MemoryStreamManager.GetStream();
            await imgStream.CopyToAsync(memStream).ConfigureAwait(false);
            // minimum jpg size is 119 bytes, png is 67 bytes
            if (memStream.Length < 64)
                return (null, null);

            memStream.Seek(0, SeekOrigin.Begin);
            var spam = await client.GetChannelAsync(Config.ThumbnailSpamId).ConfigureAwait(false);
            var message = await spam.SendMessageAsync(new DiscordMessageBuilder().AddFile(contentId + ".jpg", memStream).WithContent(contentId)).ConfigureAwait(false);
            url = message.Attachments[0].Url;
            return (url, memStream.ToArray());
        }
        catch (Exception e)
        {
            Config.Log.Warn(e);
        }
        return (null, null);
    }

    public static async ValueTask<DiscordColor?> GetImageColorAsync(string? url, DiscordColor? defaultColor = null)
    {
        try
        {
            if (string.IsNullOrEmpty(url))
                return null;

            if (ColorCache.TryGetValue(url, out DiscordColor? result))
                return result;

            await using var imgStream = await HttpClient.GetStreamAsync(url).ConfigureAwait(false);
            await using var memStream = Config.MemoryStreamManager.GetStream();
            await imgStream.CopyToAsync(memStream).ConfigureAwait(false);
            // minimum jpg size is 119 bytes, png is 67 bytes
            if (memStream.Length < 64)
                return null;

            memStream.Seek(0, SeekOrigin.Begin);

            Config.Log.Trace("Getting dominant color for " + url);
            result = await ColorGetter.GetDominantColorAsync(memStream, defaultColor).ConfigureAwait(false);
            ColorCache.Set(url, result, TimeSpan.FromHours(1));
            return result;
        }
        catch (Exception e)
        {
            Config.Log.Warn(e);
        }
        return null;
    }
}