namespace Fast.Logic.Valuation;

public class UnitConverter
{
    private static Dictionary<MeasureConversionItem, double> measureConversions = new();
    private static Dictionary<int, UnitPriceCombinationitem> unitpriceCombinations = new();
    private static readonly SemaphoreSlim DataRefreshSemaphore = new(1, 1);
    private static DateTime LastDataRefresh = DateTime.MinValue;
    private static TimeSpan RefreshInterval { get; set; } = TimeSpan.FromDays(1);

    public UnitConverter()
    {
        PreLoadData();
    }

    public double GetVolume(int fromUnitOfMeasureId, int toUnitOfMeasureId, double volumeToConvert)
    {
        if (volumeToConvert == 0 || (fromUnitOfMeasureId == toUnitOfMeasureId && fromUnitOfMeasureId > 0 && toUnitOfMeasureId > 0))
            return volumeToConvert; // no conversion required
        else
        {
            MeasureConversionItem volConversionKey = new() { FromUnitOfMeasureId = fromUnitOfMeasureId, ToUnitOfMeasureId = toUnitOfMeasureId };
            if (measureConversions.TryGetValue(volConversionKey, out double factor))
                return factor * volumeToConvert;
            else
                throw new KeyNotFoundException("The specified combination of units didn't return a valid Conversion Factor.");
        }
    }

    public double GetPrice(int fromUnitPriceCombinationId, int toUnitPriceCombinationId, double priceToConvert)
    {
        if (priceToConvert == 0 || (fromUnitPriceCombinationId == toUnitPriceCombinationId && fromUnitPriceCombinationId > 0 && toUnitPriceCombinationId > 0))
            return priceToConvert; // no conversion required
        else
        {
            if (!unitpriceCombinations.ContainsKey(fromUnitPriceCombinationId))
                throw new KeyNotFoundException("The specified FromUnitPriceCombinationId does not exist.");
            if (!unitpriceCombinations.ContainsKey(toUnitPriceCombinationId))
                throw new KeyNotFoundException("The specified ToUnitPriceCombinationId does not exist.");
            var from = unitpriceCombinations[fromUnitPriceCombinationId];
            var to = unitpriceCombinations[toUnitPriceCombinationId];
            // We pass the To in the From and the From in the To to get the oposite result....1$/gal = 42$/bbl
            var priceInNewUnitVol = GetVolume(to.UnitOfMeasureId, from.UnitOfMeasureId, priceToConvert);
            return priceInNewUnitVol;
        }
    }

    private static void PreLoadData()
    {
        var isExpired = (DateTime.UtcNow - LastDataRefresh) > RefreshInterval;
        var hasData = measureConversions.Count > 0 && unitpriceCombinations.Count > 0;
        if (!hasData || isExpired)
        {
            if (DataRefreshSemaphore.Wait(0))
            {
                try
                {
                    LoadMeasureConversion();
                    LoadPriceCombinations();
                }
                finally
                {
                    LastDataRefresh = DateTime.UtcNow;
                    DataRefreshSemaphore.Release();
                }
            }
        }

        while (LastDataRefresh == DateTime.MinValue)
            Thread.Sleep(100);
    }

    //Creates a dictionary with the combination From-To and To-From so that we just get the desired combination and multiply the measure by the factor.
    private static void LoadMeasureConversion()
    {
        using var db = Main.CreateContext();
        var unitofmeasureconversions = db.UnitOfMeasureConversions.ToArray();

        var firstConversion = from uvc in unitofmeasureconversions
                              select new MeasureConversionItem() { FromUnitOfMeasureId = uvc.FromUnitOfMeasureId, ToUnitOfMeasureId = uvc.ToUnitOfMeasureId, Factor = uvc.ConversionFactor };

        var opositeConversion = from uvc in unitofmeasureconversions
                                select new MeasureConversionItem() { FromUnitOfMeasureId = uvc.ToUnitOfMeasureId, ToUnitOfMeasureId = uvc.FromUnitOfMeasureId, Factor = 1 / (double)uvc.ConversionFactor };

        var allCombinations = firstConversion.Concat(opositeConversion);

        measureConversions = allCombinations.ToDictionary(x => x, x => x.Factor, new VolumeConversionItemComparer());
    }

    private static void LoadPriceCombinations()
    {
        using var db = Main.CreateContext();
        unitpriceCombinations = db.UnitPriceCombinations.ToDictionary(
            x => x.Id, x => new UnitPriceCombinationitem() { CurrencyTypeId = x.CurrencyTypeId, UnitOfMeasureId = x.UnitOfMeasureId }
        );
    }

    private class UnitPriceCombinationitem
    {
        public int UnitOfMeasureId { get; set; }
        public int CurrencyTypeId { get; set; }
    }

    private class MeasureConversionItem
    {
        public int FromUnitOfMeasureId { get; set; }
        public int ToUnitOfMeasureId { get; set; }
        public double Factor { get; set; }
    }

    private class CurrencyConversionItem
    {
        public int FromCurrencyTypeId { get; set; }
        public int ToCurrencyTypeId { get; set; }
        public double Rate { get; set; }
    }

    private class VolumeConversionItemComparer : EqualityComparer<MeasureConversionItem>
    {
        public override bool Equals(MeasureConversionItem? x, MeasureConversionItem? y)
        {
            return x?.FromUnitOfMeasureId == y?.FromUnitOfMeasureId && x?.ToUnitOfMeasureId == y?.ToUnitOfMeasureId;
        }

        public override int GetHashCode(MeasureConversionItem obj)
        {
            return obj.ToUnitOfMeasureId.GetHashCode() ^ obj.FromUnitOfMeasureId.GetHashCode();
        }
    }

    private class CurrencyConversionItemComparer : EqualityComparer<CurrencyConversionItem>
    {
        public override bool Equals(CurrencyConversionItem? x, CurrencyConversionItem? y)
        {
            return x?.FromCurrencyTypeId == y?.FromCurrencyTypeId && x?.ToCurrencyTypeId == y?.ToCurrencyTypeId;
        }

        public override int GetHashCode(CurrencyConversionItem obj)
        {
            return obj.ToCurrencyTypeId.GetHashCode() ^ obj.FromCurrencyTypeId.GetHashCode();
        }
    }
}
