using System.Collections.Concurrent;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace StockFin
{
    public static class FrankfurterCurrencyConverter
    {
        private static readonly HttpClient HttpClient = new()
        {
            BaseAddress = new Uri("https://api.frankfurter.dev/v1/")
        };

        private static readonly ConcurrentDictionary<string, CacheEntry> Cache = new();
        private static readonly ConcurrentDictionary<string, SemaphoreSlim> Locks = new();
        private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
        private static readonly TimeSpan CacheDuration = TimeSpan.FromHours(6);

        public static async Task<double?> GetRateAsync(
            string fromCurrency,
            string toCurrency,
            CancellationToken cancellationToken = default)
        {
            var normalizedFrom = NormalizeCurrency(fromCurrency);
            var normalizedTo = NormalizeCurrency(toCurrency);

            if (normalizedFrom == normalizedTo)
            {
                return 1;
            }

            var cacheKey = BuildCacheKey(normalizedFrom, normalizedTo);
            if (TryGetCachedRate(cacheKey, out var cachedRate))
            {
                return cachedRate;
            }

            var gate = Locks.GetOrAdd(cacheKey, _ => new SemaphoreSlim(1, 1));
            await gate.WaitAsync(cancellationToken);

            try
            {
                if (TryGetCachedRate(cacheKey, out cachedRate))
                {
                    return cachedRate;
                }

                var requestUri = $"latest?from={Uri.EscapeDataString(normalizedFrom)}&to={Uri.EscapeDataString(normalizedTo)}";
                using var response = await HttpClient.GetAsync(requestUri, cancellationToken);
                response.EnsureSuccessStatusCode();

                await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
                var payload = await JsonSerializer.DeserializeAsync<FrankfurterLatestResponse>(responseStream, JsonOptions, cancellationToken);

                if (payload?.Rates == null || !payload.Rates.TryGetValue(normalizedTo, out var rate))
                {
                    return null;
                }

                Cache[cacheKey] = new CacheEntry(
                    rate,
                    DateTimeOffset.UtcNow.Add(CacheDuration),
                    payload.Date);

                return rate;
            }
            finally
            {
                gate.Release();
            }
        }

        public static async Task<double?> ConvertAsync(
            double amount,
            string fromCurrency,
            string toCurrency,
            CancellationToken cancellationToken = default)
        {
            var rate = await GetRateAsync(fromCurrency, toCurrency, cancellationToken);
            return rate.HasValue ? amount * rate.Value : null;
        }

        public static void ClearCache()
        {
            Cache.Clear();
        }

        public static bool TryGetCachedRate(
            string fromCurrency,
            string toCurrency,
            out double rate)
        {
            var normalizedFrom = NormalizeCurrency(fromCurrency);
            var normalizedTo = NormalizeCurrency(toCurrency);
            var cacheKey = BuildCacheKey(normalizedFrom, normalizedTo);
            return TryGetCachedRate(cacheKey, out rate);
        }

        private static bool TryGetCachedRate(string cacheKey, out double rate)
        {
            if (Cache.TryGetValue(cacheKey, out var entry) && entry.ExpiresAt > DateTimeOffset.UtcNow)
            {
                rate = entry.Rate;
                return true;
            }

            Cache.TryRemove(cacheKey, out _);
            rate = 0;
            return false;
        }

        private static string NormalizeCurrency(string currency)
        {
            if (string.IsNullOrWhiteSpace(currency))
            {
                throw new ArgumentException("Currency code cannot be empty.", nameof(currency));
            }

            return currency.Trim().ToUpperInvariant();
        }

        private static string BuildCacheKey(string fromCurrency, string toCurrency)
        {
            return string.Create(CultureInfo.InvariantCulture, $"{fromCurrency}:{toCurrency}");
        }

        private sealed record CacheEntry(double Rate, DateTimeOffset ExpiresAt, string? Date);

        private sealed class FrankfurterLatestResponse
        {
            [JsonPropertyName("amount")]
            public double Amount { get; set; }

            [JsonPropertyName("base")]
            public string Base { get; set; } = string.Empty;

            [JsonPropertyName("date")]
            public string? Date { get; set; }

            [JsonPropertyName("rates")]
            public Dictionary<string, double> Rates { get; set; } = [];
        }
    }
}
