using Dalamud.Interface.Textures.TextureWraps;
using LightlessSync.UI;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Text.Json;

namespace LightlessSync.Services.Chat;

public sealed class ChatEmoteService : IDisposable
{
    private const string GlobalEmoteSetUrl = "https://7tv.io/v3/emote-sets/global";

    private readonly ILogger<ChatEmoteService> _logger;
    private readonly HttpClient _httpClient;
    private readonly UiSharedService _uiSharedService;
    private readonly ConcurrentDictionary<string, EmoteEntry> _emotes = new(StringComparer.Ordinal);
    private readonly SemaphoreSlim _downloadGate = new(3, 3);

    private readonly object _loadLock = new();
    private Task? _loadTask;

    public ChatEmoteService(ILogger<ChatEmoteService> logger, HttpClient httpClient, UiSharedService uiSharedService)
    {
        _logger = logger;
        _httpClient = httpClient;
        _uiSharedService = uiSharedService;
    }

    public void EnsureGlobalEmotesLoaded()
    {
        lock (_loadLock)
        {
            if (_loadTask is not null && !_loadTask.IsCompleted)
            {
                return;
            }

            if (_emotes.Count > 0)
            {
                return;
            }

            _loadTask = Task.Run(LoadGlobalEmotesAsync);
        }
    }

    public IReadOnlyList<string> GetEmoteNames()
    {
        EnsureGlobalEmotesLoaded();
        var names = _emotes.Keys.ToArray();
        Array.Sort(names, StringComparer.OrdinalIgnoreCase);
        return names;
    }

    public bool TryGetEmote(string code, out IDalamudTextureWrap? texture)
    {
        texture = null;
        EnsureGlobalEmotesLoaded();

        if (!_emotes.TryGetValue(code, out var entry))
        {
            return false;
        }

        if (entry.Texture is not null)
        {
            texture = entry.Texture;
            return true;
        }

        entry.EnsureLoading(QueueEmoteDownload);
        return true;
    }

    public void Dispose()
    {
        foreach (var entry in _emotes.Values)
        {
            entry.Texture?.Dispose();
        }

        _downloadGate.Dispose();
    }

    private async Task LoadGlobalEmotesAsync()
    {
        try
        {
            using var stream = await _httpClient.GetStreamAsync(GlobalEmoteSetUrl).ConfigureAwait(false);
            using var document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false);

            if (!document.RootElement.TryGetProperty("emotes", out var emotes))
            {
                _logger.LogWarning("7TV emote set response missing emotes array");
                return;
            }

            foreach (var emoteElement in emotes.EnumerateArray())
            {
                if (!emoteElement.TryGetProperty("name", out var nameElement))
                {
                    continue;
                }

                var name = nameElement.GetString();
                if (string.IsNullOrWhiteSpace(name))
                {
                    continue;
                }

                var url = TryBuildEmoteUrl(emoteElement);
                if (string.IsNullOrWhiteSpace(url))
                {
                    continue;
                }

                _emotes.TryAdd(name, new EmoteEntry(url));
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to load 7TV emote set");
        }
    }

    private static string? TryBuildEmoteUrl(JsonElement emoteElement)
    {
        if (!emoteElement.TryGetProperty("data", out var dataElement))
        {
            return null;
        }

        if (!dataElement.TryGetProperty("host", out var hostElement))
        {
            return null;
        }

        if (!hostElement.TryGetProperty("url", out var urlElement))
        {
            return null;
        }

        var baseUrl = urlElement.GetString();
        if (string.IsNullOrWhiteSpace(baseUrl))
        {
            return null;
        }

        if (baseUrl.StartsWith("//", StringComparison.Ordinal))
        {
            baseUrl = "https:" + baseUrl;
        }

        if (!hostElement.TryGetProperty("files", out var filesElement))
        {
            return null;
        }

        var fileName = PickBestStaticFile(filesElement);
        if (string.IsNullOrWhiteSpace(fileName))
        {
            return null;
        }

        return baseUrl.TrimEnd('/') + "/" + fileName;
    }

    private static string? PickBestStaticFile(JsonElement filesElement)
    {
        string? png1x = null;
        string? webp1x = null;
        string? pngFallback = null;
        string? webpFallback = null;

        foreach (var file in filesElement.EnumerateArray())
        {
            if (file.TryGetProperty("static", out var staticElement) && staticElement.ValueKind == JsonValueKind.False)
            {
                continue;
            }

            if (!file.TryGetProperty("name", out var nameElement))
            {
                continue;
            }

            var name = nameElement.GetString();
            if (string.IsNullOrWhiteSpace(name))
            {
                continue;
            }

            if (name.Equals("1x.png", StringComparison.OrdinalIgnoreCase))
            {
                png1x = name;
            }
            else if (name.Equals("1x.webp", StringComparison.OrdinalIgnoreCase))
            {
                webp1x = name;
            }
            else if (name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) && pngFallback is null)
            {
                pngFallback = name;
            }
            else if (name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase) && webpFallback is null)
            {
                webpFallback = name;
            }
        }

        return png1x ?? webp1x ?? pngFallback ?? webpFallback;
    }

    private void QueueEmoteDownload(EmoteEntry entry)
    {
        _ = Task.Run(async () =>
        {
            await _downloadGate.WaitAsync().ConfigureAwait(false);
            try
            {
                var data = await _httpClient.GetByteArrayAsync(entry.Url).ConfigureAwait(false);
                var texture = _uiSharedService.LoadImage(data);
                entry.SetTexture(texture);
            }
            catch (Exception ex)
            {
                _logger.LogDebug(ex, "Failed to load 7TV emote {Url}", entry.Url);
                entry.MarkFailed();
            }
            finally
            {
                _downloadGate.Release();
            }
        });
    }

    private sealed class EmoteEntry
    {
        private int _loadingState;

        public EmoteEntry(string url)
        {
            Url = url;
        }

        public string Url { get; }
        public IDalamudTextureWrap? Texture { get; private set; }

        public void EnsureLoading(Action<EmoteEntry> queueDownload)
        {
            if (Texture is not null)
            {
                return;
            }

            if (Interlocked.CompareExchange(ref _loadingState, 1, 0) != 0)
            {
                return;
            }

            queueDownload(this);
        }

        public void SetTexture(IDalamudTextureWrap texture)
        {
            Texture = texture;
            Interlocked.Exchange(ref _loadingState, 0);
        }

        public void MarkFailed()
        {
            Interlocked.Exchange(ref _loadingState, 0);
        }
    }
}
