using System.Collections.Concurrent;

namespace Fast.Logic.RateControl;

public class RateCalculator
{
    private class PipeContract
    {
        public DateOnly StartDate { get; set; }
        public DateOnly EndDate { get; set; }
        public bool? IsCapacityRelease { get; set; }
        public double? DemandCharge { get; set; }
        public int? RateUnitTimeOption { get; set; }
    }

    private readonly DateOnly noStartDate = new(1900, 1, 1);
    private readonly DateOnly noEndDate = new(9999, 12, 31);
    private readonly ConcurrentDictionary<DateOnly, PtrsAndExcludedPair> DatePtrPairs = new();
    private readonly ConcurrentDictionary<DateOnly, Dictionary<SupplyPtrKey, double?>> _PtrsAsOfDate = new();
    private Dictionary<int, PipeContract> pipelineContracts = new();
    private Dictionary<int, int> zoneIdsByMeterId = new();
    private Dictionary<int, int> PipelineRateScheduleIDsByContractID = new();
    private Dictionary<int, IEnumerable<PipelineTariff>> PipelineTariffs = new();
    private Dictionary<int, IEnumerable<PipelineRateDiscounted>> PipelineRateDiscounteds = new();
    private Dictionary<int, IEnumerable<PipelineRateDiscountedFromTo>> PipelineRateDiscountedFromTos = new();
    private Dictionary<int, IEnumerable<PipelineTariffDatum>> PipelineTariffDatas = new();
    private List<MeterPtr> meterPtrs = new();
    private readonly int productId;

    private RateCalculator(Enums.Product product)
    {
        productId = product switch
        {
            Enums.Product.NaturalGas => (int)Enums.Product.NaturalGas,
            Enums.Product.CrudeOil => (int)Enums.Product.CrudeOil,
            Enums.Product.Condensate => (int)Enums.Product.Condensate,
            Enums.Product.Retrograde => (int)Enums.Product.Retrograde,
            _ => throw new NotImplementedException("can't handle both gas and crude zones at the same time here")
        };
    }

    public static async Task<RateCalculator> GetInstanceAsync(Enums.Product product)
    {
        var instance = new RateCalculator(product);
        await instance.LoadDataAsync(product);
        return instance;
    }

    private async Task LoadDataAsync(Enums.Product product)
    {
        await Task.WhenAll(
            Task.Run(async () =>
            {
                using var db = Main.CreateContext();
                pipelineContracts = await (
                    from p in db.PipelineContracts
                    where p.ProductId == productId
                    select new
                    {
                        p.Id,
                        p.StartDate,
                        p.EndDate,
                        p.IsCapacityRelease,
                        p.DemandCharge,
                        p.RateUnitTimeOption
                    }
                ).AsNoTracking().ToDictionaryAsync(
                    x => x.Id,
                    x => new PipeContract()
                    {
                        StartDate = x.StartDate ?? noStartDate,
                        EndDate = x.EndDate ?? noEndDate,
                        IsCapacityRelease = x.IsCapacityRelease,
                        DemandCharge = x.DemandCharge,
                        RateUnitTimeOption = x.RateUnitTimeOption
                    }
                );
            }),
            Task.Run(async () =>
            {
                int productId = product switch
                {
                    Enums.Product.NaturalGas => (int)Enums.Product.NaturalGas,
                    Enums.Product.CrudeOil => (int)Enums.Product.CrudeOil,
                    Enums.Product.Condensate => (int)Enums.Product.Condensate,
                    Enums.Product.Retrograde => (int)Enums.Product.Retrograde,
                    _ => throw new NotImplementedException("can't handle both gas and crude zones at the same time here")
                };
                using var db = Main.CreateContext();
                var recentMeters = await DataHelper.GetMetersByProductAsync(Enums.ProductCategory.All);
                zoneIdsByMeterId = recentMeters.Where(x => x.ProductId == productId).ToDictionary(x => x.MeterId, x => x.ZoneId);
            }),
            Task.Run(async () =>
            {
                using var db = Main.CreateContext();
                PipelineRateScheduleIDsByContractID = await (
                    from pc in db.PipelineContracts
                    where pc.RateScheduleId.HasValue
                    select new { pipelineContractId = pc.Id, RateScheduleID = pc.RateScheduleId }
                ).AsNoTracking().ToDictionaryAsync(x => x.pipelineContractId, x => x.RateScheduleID.GetValueOrDefault());
            }),
            Task.Run(async () =>
            {
                using var db = Main.CreateContext();
                PipelineTariffs = await db.PipelineTariffs.AsNoTracking().GroupBy(x => x.RateScheduleId).ToDictionaryAsync(x => x.Key, x => x.Select(y => y));
            }),
            Task.Run(async () =>
            {
                using var db = Main.CreateContext();
                PipelineTariffDatas = await db.PipelineTariffData.AsNoTracking().GroupBy(x => x.TariffId).ToDictionaryAsync(x => x.Key, x => x.Select(y => y));
            }),
            Task.Run(async () =>
            {
                using var db = Main.CreateContext();
                PipelineRateDiscounteds = await db.PipelineRateDiscounteds.AsNoTracking().GroupBy(x => x.RateScheduleId).ToDictionaryAsync(x => x.Key, x => x.Select(y => y));
            }),
            Task.Run(async () =>
            {
                using var db = Main.CreateContext();
                PipelineRateDiscountedFromTos = await db.PipelineRateDiscountedFromTos.AsNoTracking().GroupBy(x => x.DiscountedRateId).ToDictionaryAsync(x => x.Key, x => x.Select(y => y));
            })
        );
    }

    private PtrsAndExcludedPair GetDataForGasDay(DateOnly gasDay)
    {
        return DatePtrPairs.GetOrAdd(gasDay, day =>
        {
            using var db = Main.CreateContext();
            var ptrsAsOf = GetMeterPTRs(db, day);
            var mtrPtrIds = ptrsAsOf.Select(x => x.Id);
            var meterPtrExcludeds = (
                from mpe in db.MeterPtrExcludeds
                where mtrPtrIds.Contains(mpe.MeterPtrId)
                select mpe
            ).AsNoTracking().ToLookup(x =>
                new MeterCounterpartyKey() { MeterPtrID = x.MeterPtrId, CounterpartyID = x.CounterpartyId },
                new MeterCounterpartyComparer()
            );

            return new PtrsAndExcludedPair()
            {
                RecentPtrs = ptrsAsOf.ToDictionary(x => x.MeterId),
                MeterPtrExcludeds = meterPtrExcludeds
            };
        });
    }

    private List<MeterPtr> GetMeterPTRs(MyDbContext db, DateOnly LastEffectiveDate)
    {
        var asOf = DateOnly.FromDateTime(DateTime.Today);

        if (meterPtrs.Count == 0)
            meterPtrs = (from q in db.MeterPtrs where q.DataDate <= asOf select q).ToList();

        var all = from v in meterPtrs
                  where v.EffectiveDate <= LastEffectiveDate
                  select v;

        var GroupWithMaxEffectiveDates = from a in all
                                         group a by a.MeterId into g
                                         select new { MeterID = g.Key, MaxEffectiveDate = g.Max(a => a.EffectiveDate) };

        var RecentOfMaxEffectiveDates = from a in all
                                        join g in GroupWithMaxEffectiveDates on new { a.MeterId, a.EffectiveDate } equals new { MeterId = g.MeterID, EffectiveDate = g.MaxEffectiveDate }
                                        select a;

        var GroupWithMaxDataDates = from a in RecentOfMaxEffectiveDates
                                    group a by new { a.MeterId, a.EffectiveDate } into g
                                    select new
                                    {
                                        MeterID = g.Key.MeterId,
                                        g.Key.EffectiveDate,
                                        MaxDataDate = g.Max(a => a.DataDate)
                                    };

        var RecentofAll = from a in RecentOfMaxEffectiveDates
                          join g in GroupWithMaxDataDates on new { a.MeterId, a.EffectiveDate, a.DataDate } equals new { MeterId = g.MeterID, g.EffectiveDate, DataDate = g.MaxDataDate }
                          select a;

        var results = RecentofAll.ToList();
        return results;
    }

    private double? GetPtrPercentAsOf(DateOnly gasDay, int? dealId, int? transferDealId, int meterId, int pipelineContractId)
    {
        var ptrsForAsOfDate = _PtrsAsOfDate.GetOrAdd(gasDay, day =>
           {
               using var db = Main.CreateContext();
               var supplyPtrs = (
                   from s in db.GasSupplies
                   where s.Date == gasDay && s.Ptr.HasValue
                   select s
               ).AsNoTracking();

               return supplyPtrs.ToDictionary(
                   x => new SupplyPtrKey()
                   {
                       Date = x.Date,
                       DealId = x.DealId,
                       TransferDealId = x.TransferDealId,
                       MeterID = x.MeterId.GetValueOrDefault(),
                       PipelineContractId = x.PipelineContractId
                   },
                   x => x.Ptr,
                   new SupplyPtrComparer());
           });

        SupplyPtrKey key = new() { Date = gasDay, DealId = dealId, TransferDealId = transferDealId, MeterID = meterId, PipelineContractId = pipelineContractId };
        return ptrsForAsOfDate.TryGetValue(key, out double? value) ? value : null;
    }

    public double? GetPtrPercent(DateOnly GasDay, int FromMeterID, int? ToMeterID, int? SupplyCounterpartyID, int? supplyDealId, int? supplyTransferDealId, int? pipelineContractId)
    {
        double? PtrPercent = null;
        if ((supplyDealId.HasValue || supplyTransferDealId.HasValue) && pipelineContractId.HasValue)
            PtrPercent = GetPtrPercentAsOf(GasDay, supplyDealId, supplyTransferDealId, FromMeterID, pipelineContractId.Value);

        if (!PtrPercent.HasValue)
        {
            var ptrsAndExcludedPair = GetDataForGasDay(GasDay);

            if (ptrsAndExcludedPair.RecentPtrs.TryGetValue(FromMeterID, out MeterPtr? Ptr))
            {
                bool IsExcluded = false;
                int MeterPtrID = Ptr.Id;
                if (SupplyCounterpartyID != null)
                {
                    MeterCounterpartyKey meterCounterpartyKey = new() { MeterPtrID = MeterPtrID, CounterpartyID = SupplyCounterpartyID.Value };
                    if (ptrsAndExcludedPair.MeterPtrExcludeds != null && ptrsAndExcludedPair.MeterPtrExcludeds.Contains(meterCounterpartyKey))
                        IsExcluded = true;
                }
                if (!IsExcluded)
                    PtrPercent = Ptr.Ptr;
            }
        }

        if (PtrPercent == null)
            return null;
        else if (!ToMeterID.HasValue)
            return null;
        else if (ToMeterID.GetValueOrDefault() != FromMeterID)
            return PtrPercent;
        else
            return 0;
    }

    public double? GetFuelPercent(int? PipelineContractID, DateOnly GasDay, int FromMeterID, int ToMeterID)
    {
        double? FuelPercent = null;
        if (FromMeterID != ToMeterID)
        {
            if (PipelineContractID != null)
            {
                if (pipelineContracts.ContainsKey(PipelineContractID.Value))
                    FuelPercent = GetRate(PipelineContractID, GasDay, FromMeterID, ToMeterID, Enums.TariffType.FuelLoss);
            }
        }

        return FuelPercent;
    }

    public double? GetRate(int? PipeContractId, DateOnly GasDay, int FromMeterID, int ToMeterID, Enums.TariffType TariffType)
    {
        double? Rate = null;
        double? DiscountedRate = null;
        int TariffTypeID = (int)TariffType;

        int ZoneFromID;
        int ZoneToID;

        if (ToMeterID > 0 && FromMeterID > 0 && PipeContractId != null && pipelineContracts.TryGetValue(PipeContractId.Value, out PipeContract? contract) && PipelineRateScheduleIDsByContractID.ContainsKey(PipeContractId.Value))
        {
            if (contract.StartDate <= GasDay && contract.EndDate >= GasDay)
            {
                ZoneFromID = zoneIdsByMeterId[FromMeterID];
                ZoneToID = zoneIdsByMeterId[ToMeterID];

                var RateScheduleID = PipelineRateScheduleIDsByContractID[PipeContractId.Value];
                DiscountedRate = GetDiscountedRate(RateScheduleID, GasDay, FromMeterID, ToMeterID, TariffTypeID);

                if (DiscountedRate == null && PipelineTariffs.TryGetValue(RateScheduleID, out IEnumerable<PipelineTariff>? _PipelineTariffs))
                {
                    var TariffDatesBeforeGasDay = (from pt in _PipelineTariffs
                                                   where pt.EffectiveDate <= GasDay && pt.TariffTypeId == TariffTypeID
                                                   select pt.EffectiveDate);

                    if (TariffDatesBeforeGasDay.Any())
                    {
                        var MaxTariffDate = TariffDatesBeforeGasDay.Max();

                        var PipeTariff = (from pt in _PipelineTariffs
                                          where pt.EffectiveDate == MaxTariffDate && pt.TariffTypeId == TariffTypeID
                                          select pt).FirstOrDefault();

                        if (TariffTypeID == 4)
                            // Annual Charge Adjustment
                            Rate = PipeTariff?.TariffRate;
                        else if (PipeTariff != null && PipelineTariffDatas.TryGetValue(PipeTariff.Id, out IEnumerable<PipelineTariffDatum>? _PipeTariffDatas))
                        {
                            var FuelData = (from ptd in _PipeTariffDatas
                                            let SeasonStart = new DateOnly(GasDay.Year, ptd.SeasonStartMonth, ptd.SeasonStartDay)
                                            let SeasonEndDaysInMonth = DateTime.DaysInMonth(GasDay.Year, ptd.SeasonEndMonth)
                                            let SeasonEnd = new DateOnly(GasDay.Year, ptd.SeasonEndMonth, SeasonEndDaysInMonth)
                                            where ptd.ZoneFromId == ZoneFromID && ptd.ZoneToId == ZoneToID && (SeasonStart <= GasDay && SeasonEnd >= GasDay)
                                            select ptd);

                            if (FuelData.Any())
                                Rate = FuelData.First().Rate;
                        }
                    }
                }
                else
                    Rate = DiscountedRate;
            }
            else
                return null;
        }

        return Rate;
    }

    public double GetTransRate(int? ContractID, DateOnly NomDate, int FromMeterID, int ToMeterID, bool NonJurisdictional, bool IsAgency)
    {
        double Rate;

        //Bryce says we no longer need to set (NonJurisdictional && IsAgency) rates to zero since zero-rates are
        // handled by special no-rate pipeline contracts or schedules

        double CommodityCharge = Util.Nz(GetRate(ContractID, NomDate, FromMeterID, ToMeterID, Enums.TariffType.CommodityCharge));
        double AnnualChargeAdjustment = Util.Nz(GetRate(ContractID, NomDate, FromMeterID, ToMeterID, Enums.TariffType.AnnualChargeAdjustment));
        double GatheringCharge = Util.Nz(GetRate(ContractID, NomDate, FromMeterID, ToMeterID, Enums.TariffType.GatheringCharge));
        double HurricaneCharge = Util.Nz(GetRate(ContractID, NomDate, FromMeterID, ToMeterID, Enums.TariffType.HurricaneCharge));
        double OtherCharge = Util.Nz(GetRate(ContractID, NomDate, FromMeterID, ToMeterID, Enums.TariffType.OtherCharge));
        double DemandCharge = Util.Nz(GetDemandCharge(ContractID, NomDate));
        double ElectricCharge = Util.Nz(GetRate(ContractID, NomDate, FromMeterID, ToMeterID, Enums.TariffType.ElectricPower));

        Rate = CommodityCharge + AnnualChargeAdjustment + GatheringCharge + HurricaneCharge + OtherCharge + DemandCharge + ElectricCharge;

        return Rate;
    }

    private double? GetDemandCharge(int? ContractID, DateOnly NomDate)
    {
        double? DemandCharge = null;

        if (ContractID != null)
        {
            PipeContract Contract = pipelineContracts[ContractID.Value];

            if (Contract.IsCapacityRelease.GetValueOrDefault() == true && Contract.DemandCharge.HasValue)
            {
                if (Contract.RateUnitTimeOption == 1)
                    // daily
                    DemandCharge = Contract.DemandCharge;
                else
                    // monthly
                    DemandCharge = Contract.DemandCharge / (double)DateTime.DaysInMonth(NomDate.Year, NomDate.Month);
            }
        }

        return DemandCharge;
    }

    private double? GetDiscountedRate(int RateScheduleID, DateOnly GasDay, int FromMeterID, int ToMeterID, int TariffTypeID)
    {
        double? DiscountedRate = null;
        if (PipelineRateDiscounteds.TryGetValue(RateScheduleID, out IEnumerable<PipelineRateDiscounted>? _pipelineRateDiscounteds))
        {
            var Rates = from r in _pipelineRateDiscounteds
                        where r.EffectiveDate <= GasDay && r.EndDate >= GasDay && r.TariffTypeId == TariffTypeID
                        orderby r.EffectiveDate descending
                        select r;

            foreach (var RateItem in Rates)
            {
                bool IsFromMeterValid = false;
                bool IsToMeterValid = false;
                List<PipelineRateDiscountedFromTo> _pipelineRateDiscountedFromTos = new();
                if (PipelineRateDiscountedFromTos != null && PipelineRateDiscountedFromTos.TryGetValue(RateItem.Id, out IEnumerable<PipelineRateDiscountedFromTo>? value))
                    _pipelineRateDiscountedFromTos = [.. value];
                // check if FROM Meter is included by the discounted rate

                if (RateItem.AreAllFromZonesSelected && RateItem.AreAllFromMetersSelected)
                    // if All Zones and All Meters checked
                    IsFromMeterValid = true;
                else if (RateItem.AreAllFromMetersSelected)
                {
                    // if All Meters and only some Zones checked
                    var ZoneID = zoneIdsByMeterId[FromMeterID];
                    List<PipelineRateDiscountedFromTo> FromZones = new();
                    if (_pipelineRateDiscountedFromTos != null)
                        FromZones = [.. _pipelineRateDiscountedFromTos.Where(x => x.IsFrom == true && x.ZoneId == ZoneID)];
                    if (FromZones != null && FromZones.Any())
                        IsFromMeterValid = true;
                }
                else
                {
                    // if (All Zones and Some Meters Checked) or (Some Zones and Some Meters checked)
                    List<PipelineRateDiscountedFromTo> FromMeters = new();
                    if (_pipelineRateDiscountedFromTos != null)
                        FromMeters = [.. _pipelineRateDiscountedFromTos.Where(x => x.IsFrom == true && x.MeterId == FromMeterID)];
                    if (FromMeters != null && FromMeters.Count != 0)
                        IsFromMeterValid = true;
                }

                if (IsFromMeterValid)
                {
                    // check if TO Meter is included by the discounted rate

                    if (RateItem.AreAllToZonesSelected && RateItem.AreAllToMetersSelected)
                        // if All Zones and All Meters checked
                        IsToMeterValid = true;
                    else if (RateItem.AreAllToMetersSelected && _pipelineRateDiscountedFromTos != null)
                    {
                        // if All Meters and only some Zones checked
                        var ZoneID = zoneIdsByMeterId[ToMeterID];
                        List<PipelineRateDiscountedFromTo> ToZones = [.. _pipelineRateDiscountedFromTos.Where(x => x.IsFrom == false && x.ZoneId == ZoneID)];
                        if (ToZones.Count != 0)
                            IsToMeterValid = true;
                    }
                    else if (_pipelineRateDiscountedFromTos != null)
                    {
                        // if (All Zones and Some Meters Checked) or (Some Zones and Some Meters checked)
                        List<PipelineRateDiscountedFromTo> ToMeters = [.. _pipelineRateDiscountedFromTos.Where(x => x.IsFrom == false && x.MeterId == ToMeterID)];
                        if (ToMeters.Count != 0)
                            IsToMeterValid = true;
                    }
                }

                if (IsFromMeterValid && IsToMeterValid)
                {
                    DiscountedRate = RateItem.Rate;
                    break;
                }
            }
        }

        return DiscountedRate;
    }

    private class PtrsAndExcludedPair
    {
        public ILookup<MeterCounterpartyKey, MeterPtrExcluded>? MeterPtrExcludeds { get; set; }
        public Dictionary<int, MeterPtr> RecentPtrs { get; set; } = new();
    }

    private class SupplyPtrKey
    {
        public int? DealId { get; set; }
        public int? TransferDealId { get; set; }
        public int MeterID { get; set; }
        public DateOnly Date { get; set; }
        public int? PipelineContractId { get; set; }
    }

    private class SupplyPtrComparer : EqualityComparer<SupplyPtrKey>
    {
        public override bool Equals(SupplyPtrKey? x, SupplyPtrKey? y)
        {
            if (x?.Date == y?.Date && x?.DealId.GetValueOrDefault() == y?.DealId.GetValueOrDefault() && x?.MeterID == y?.MeterID && x?.TransferDealId.GetValueOrDefault() == y?.TransferDealId.GetValueOrDefault() && x?.PipelineContractId.GetValueOrDefault() == y?.PipelineContractId.GetValueOrDefault())
                return true;
            else
                return false;
        }

        public override int GetHashCode(SupplyPtrKey obj)
        {
            return (obj.Date.GetHashCode() ^ obj.DealId.GetValueOrDefault().GetHashCode() ^ obj.MeterID.GetHashCode() ^ obj.TransferDealId.GetValueOrDefault().GetHashCode() ^ obj.PipelineContractId.GetValueOrDefault().GetHashCode());
        }
    }

    private class MeterCounterpartyKey
    {
        public int MeterPtrID { get; set; }
        public int CounterpartyID { get; set; }
    }

    private class MeterCounterpartyComparer : EqualityComparer<MeterCounterpartyKey>
    {
        public override bool Equals(MeterCounterpartyKey? x, MeterCounterpartyKey? y)
        {
            if (x?.CounterpartyID == y?.CounterpartyID && x?.MeterPtrID == y?.MeterPtrID)
                return true;
            else
                return false;
        }

        public override int GetHashCode(MeterCounterpartyKey obj)
        {
            return (obj.MeterPtrID.GetHashCode() ^ obj.CounterpartyID.GetHashCode());
        }
    }
}
