using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Fast.Logic.RateControl;
using Fast.Web.Models;

namespace Fast.Web.Logic;

public class SosTransferAdder
{
    private readonly MyDbContext db;
    private readonly SosTransferAdd transferAddItem;
    private readonly int userId;

    public SosTransferAdder(MyDbContext db, SosTransferAdd transferAddItem, int userId)
    {
        this.db = db;
        this.transferAddItem = transferAddItem;
        this.userId = userId;
    }

    private class SupplyMarketMeters
    {
        public int SupplyMeterId;
        public int MarketMeterId;
    }

    public async Task SaveTransferAsync()
    {
        var productId = (int)Enums.Product.NaturalGas;

        Meter MeterA;
        Meter MeterB;
        bool IsSupplyTransferDeal;
        bool IsStartingWithMarketDeal;
        int numDays = Convert.ToInt32((transferAddItem.EndDate.ToDateTime(TimeOnly.MinValue) - transferAddItem.StartDate.ToDateTime(TimeOnly.MinValue)).TotalDays) + 1;
        int[] NommedVolumes = new int[numDays + 1];
        var supplyMarketMeters = new SupplyMarketMeters();
        bool hasPreviousNormalDeal;
        TransferDeal? PreviousTransferDeal = null;
        int PreviousDealID;
        int? PreviousMapPathPipeContractID = null;
        var rateCalculator = await RateCalculator.GetInstanceAsync(Enums.Product.NaturalGas);
        List<int> NewlyAddedTransferDealIds = new();

        PreviousDealID = transferAddItem.SupplyDealId; //initialize PreviousDealID
        for (int index = 0; index <= numDays; index++) //initialize NommedVolumes()
            NommedVolumes[index] = transferAddItem.SourceVolume;

        await db.Database.CreateExecutionStrategy().Execute(async () =>
        {
            await using var dbContextTransaction = await db.Database.BeginTransactionAsync();

            //0.)Check if transfer dates are within the supply-deal period
            var supplyDeal = await (
                from d in db.Deals
                where d.Id == transferAddItem.SupplyDealId
                select new { d.StartDate, d.EndDate }
            ).FirstOrDefaultAsync() ?? throw new Exception("Supply deal not found.");

            if (transferAddItem.StartDate < supplyDeal.StartDate || transferAddItem.EndDate > supplyDeal.EndDate)
            {
                var msg = $"\n\nTransfer dates must be within the supply-deal period ({supplyDeal.StartDate:yyyy-MM-dd} to {supplyDeal.EndDate:yyyy-MM-dd}).\n\n";
                throw new ArgumentOutOfRangeException(msg);
            }

            //1.)Get StartDeal
            int StartDealBuyButton = await (from d in db.Deals where d.Id == transferAddItem.SupplyDealId select d.BuyButton).FirstOrDefaultAsync() ?? 1;
            if (StartDealBuyButton == -1)
                IsStartingWithMarketDeal = true;
            else
                IsStartingWithMarketDeal = false;

            //2.)Get routes, join them to transfer meter maps
            var pRoutes = from p in db.GasPathRoutes where p.PathId == transferAddItem.PathId select p;
            var MeterMapsPathRoutes = (
                from tmm in db.TransferMeters
                where tmm.ProductId == (int)Enums.Product.NaturalGas
                join pr in pRoutes on tmm.Id equals pr.MeterMapId
                select new { tmm.Id, tmm.Meter1Id, tmm.Meter2Id, pr.IsMeter1First, pr.OrderId, pr.ToPipeContractId, pr.Path.SourcePipeContractId }
            );

            //3.)Get the first and last routes
            int MaxOrderID = await (from p in db.GasPathRoutes where p.PathId == transferAddItem.PathId select p.OrderId).MaxAsync();
            var Route1 = await (
                from x in db.GasPathRoutes
                    .Include(x => x.MeterMap)
                    .ThenInclude(x => x!.Meter1)
                    .Include(x => x.MeterMap)
                    .ThenInclude(x => x!.Meter2)
                where x.OrderId == 1 && x.PathId == transferAddItem.PathId
                select x
            ).AsNoTracking().FirstAsync();

            var RouteMax = await (
                from x in db.GasPathRoutes
                    .Include(x => x.MeterMap)
                    .ThenInclude(x => x!.Meter1)
                    .Include(x => x.MeterMap)
                    .ThenInclude(x => x!.Meter2)
                where x.OrderId == MaxOrderID && x.PathId == transferAddItem.PathId
                select x
            ).AsNoTracking().FirstAsync();

            //4.)Get the first and last meters, then get BeginMeterId and EndMeterId
            if (Route1.MeterMap == null || RouteMax.MeterMap == null)
                throw new Exception("MeterMap is null");

            if (Route1.IsMeter1First.GetValueOrDefault())
                MeterA = Route1.MeterMap.Meter1;
            else
                MeterA = Route1.MeterMap.Meter2;

            if (RouteMax.IsMeter1First.GetValueOrDefault())
                MeterB = RouteMax.MeterMap.Meter2;
            else
                MeterB = RouteMax.MeterMap.Meter1;

            MeterMapsPathRoutes = MeterMapsPathRoutes.OrderBy(n => n.OrderId);

            //5.)  Loop to add transferdeals to db, and nomination.marketsupplies for them with nominsert
            int? supplyReceiptMeterId = -1;

            foreach (var i in MeterMapsPathRoutes)
            {
                var NewTransferDeal = new TransferDeal();
                string TicketNum = await Util.GetNewDealNumAsync(14, db); //transfer ticket

                if (PreviousDealID == transferAddItem.SupplyDealId)
                {
                    hasPreviousNormalDeal = true;
                    PreviousTransferDeal = null;
                }
                else
                {
                    hasPreviousNormalDeal = false;
                    PreviousTransferDeal = await (
                        from td in db.TransferDeals
                            .Include(td => td.TransferMeterMap)
                            .ThenInclude(td => td.Meter1)
                            .Include(td => td.TransferMeterMap)
                            .ThenInclude(td => td.Meter2)
                        where td.Id == PreviousDealID
                        select td
                    ).AsNoTracking().FirstOrDefaultAsync();
                }

                //5.2) Get previousmeter and nextmeter
                supplyMarketMeters = GetSupplyAndMarketMeters(i.IsMeter1First.GetValueOrDefault(), i.Meter1Id, i.Meter2Id, hasPreviousNormalDeal, PreviousTransferDeal, IsStartingWithMarketDeal, transferAddItem.ReceiptPointId);

                //5.4) add newtransferdeal
                NewTransferDeal.TransferMeterMapId = i.Id;
                NewTransferDeal.TicketNum = TicketNum;
                NewTransferDeal.StartDate = transferAddItem.StartDate;
                NewTransferDeal.EndDate = transferAddItem.EndDate;
                NewTransferDeal.PathId = transferAddItem.PathId;
                NewTransferDeal.PathName = await (from p in db.GasPaths where p.Id == transferAddItem.PathId select p.Name).FirstOrDefaultAsync();

                if (i.IsMeter1First.GetValueOrDefault())
                    NewTransferDeal.IsMeter1Supply = false;
                else
                    NewTransferDeal.IsMeter1Supply = true;

                db.TransferDeals.Add(NewTransferDeal);
                await db.SaveChangesAsync();
                NewTransferDeal = await db.TransferDeals.Include(td => td.TransferMeterMap).Where(x => x.Id == NewTransferDeal.Id).AsNoTracking().FirstAsync();
                NewlyAddedTransferDealIds.Add(NewTransferDeal.Id);

                //5.6)get default pipelinecontract for this pipeline
                if (!PreviousMapPathPipeContractID.HasValue)
                    PreviousMapPathPipeContractID = i.SourcePipeContractId;

                var asOfDate = DateOnly.FromDateTime(DateTime.Now.Date);
                int thisPipelineID = await (
                    from m in db.Meters
                    where m.Id == supplyMarketMeters.SupplyMeterId
                    let mp = m.MeterProducts
                        .Where(x => x.ProductId == productId && x.EffectiveDate <= asOfDate).OrderByDescending(x => x.EffectiveDate).FirstOrDefault()
                    where mp != null
                    let zone = mp.SourceZone
                    where zone != null
                    select zone.PipelineId
                ).FirstOrDefaultAsync();

                int? thisPipelinesDefaultContractID;
                if (PreviousMapPathPipeContractID.HasValue)
                    thisPipelinesDefaultContractID = PreviousMapPathPipeContractID;
                else
                    thisPipelinesDefaultContractID = await (from p in db.PipelineContractDefaults where p.PipelineId == thisPipelineID && p.ProductId == (int)Enums.Product.NaturalGas select p.PipeContractId).FirstOrDefaultAsync();

                PreviousMapPathPipeContractID = i.ToPipeContractId;

                int? thisPipelineContractId = await (from p in db.PipelineContracts
                                                     where p.PipelineId == thisPipelineID && p.Id == thisPipelinesDefaultContractID && transferAddItem.StartDate <= p.EndDate && transferAddItem.EndDate >= p.StartDate
                                                     select p.Id).FirstOrDefaultAsync();

                var thisPipelineContractStartEnd = await (
                    from q in db.PipelineContracts
                    where q.Id == thisPipelineContractId
                    select new SosStartEndDatePair(q.StartDate.GetValueOrDefault(), q.EndDate.GetValueOrDefault(DateOnly.MaxValue))
                ).FirstOrDefaultAsync();

                if (thisPipelineContractId == null)
                {
                    string pipeName = (await db.Pipelines.Where(x => x.Id == thisPipelineID).FirstOrDefaultAsync())?.Name ?? "";
                    throw new Exception("A valid pipeline contract was not found for pipeline {" + pipeName + "}");
                }

                int? thisPipelinesDefaultPtrContractID = await (from p in db.Pipelines where p.Id == thisPipelineID select p.DefaultPtrPipelineContractId).FirstOrDefaultAsync();
                int? thisPipelinePtrContractId = await (from p in db.PipelineContracts
                                                        where p.PipelineId == thisPipelineID && p.Id == thisPipelinesDefaultPtrContractID && transferAddItem.StartDate <= p.EndDate && transferAddItem.EndDate >= p.StartDate
                                                        select p.Id).FirstOrDefaultAsync();

                //5.9. increment/decrement NommedVolumes() with loopdate's fuel percent and ptr
                if (supplyReceiptMeterId.GetValueOrDefault() == -1)
                {
                    supplyReceiptMeterId = supplyMarketMeters.SupplyMeterId;
                }
                NommedVolumes = UpdateNommedVolumes(
                    numDays,
                    NommedVolumes,
                    transferAddItem.StartDate,
                    thisPipelineContractId,
                    thisPipelinePtrContractId,
                    IsStartingWithMarketDeal,
                    supplyMarketMeters.SupplyMeterId,
                    supplyMarketMeters.MarketMeterId,
                    null,
                    rateCalculator,
                    NewTransferDeal.Id,
                    supplyReceiptMeterId
                );

                var loopDates = new List<DateOnly>();

                for (int loopday = 0; loopday <= numDays; loopday++)
                    loopDates.Add(transferAddItem.StartDate.AddDays(loopday));

                // Preload PipelineContractIDs from Nomination_Supplies.
                var lookupSuppies = (from s in db.GasSupplies
                                     where loopDates.Contains(s.Date)
                                     select s).ToLookup(x => new SupplyUniqueKey
                                     {
                                         DealID = x.DealId,
                                         Day = x.Date,
                                         TransferDealID = x.TransferDealId,
                                         PointID = x.PointId,
                                         MeterID = x.MeterId
                                     }, new SosSupplyUniqueKeyComparer());


                GasSupply? getSupply(SupplyUniqueKey key)
                {
                    var matches = lookupSuppies[key];
                    var match = matches?.FirstOrDefault();
                    return match;
                }

                async Task ensurePipeContract(SupplyUniqueKey key, int pipeContractToEnsure)
                {
                    var matches = lookupSuppies[key];
                    if (matches != null)
                    {
                        var match = matches.FirstOrDefault(x => x.PipelineContractId.GetValueOrDefault() == pipeContractToEnsure);
                        if (match == null)
                        {
                            // no row for this pipecontract, is there a null match where we can use this pipecontract?
                            var secondMatch = matches.FirstOrDefault(x => !x.PipelineContractId.HasValue);
                            if (secondMatch != null)
                            {
                                secondMatch.PipelineContractId = pipeContractToEnsure;
                                // submit changes to ensure in DB
                                await db.SaveChangesAsync();
                            }
                        }
                    }
                }

                // 6.0) Loop to add nominations per day between startdate and enddate
                foreach (var loopDate in loopDates)
                {
                    IsSupplyTransferDeal = PreviousDealID != transferAddItem.SupplyDealId;

                    int ReceiptMeterID;
                    int? ReceiptPointID = null;

                    if (IsSupplyTransferDeal && PreviousTransferDeal != null)
                        ReceiptMeterID = PreviousTransferDeal.IsMeter1Supply ? PreviousTransferDeal.TransferMeterMap.Meter1Id : PreviousTransferDeal.TransferMeterMap.Meter2Id;
                    else
                    {
                        ReceiptPointID = transferAddItem.ReceiptPointId;
                        ReceiptMeterID = transferAddItem.ReceiptMeterId;
                    }

                    var nd = new SosNomDetails
                    {
                        Day = loopDate,
                        SupplyDealID = PreviousDealID,
                        ReceiptPointID = ReceiptPointID,
                        ReceiptMeterID = ReceiptMeterID,
                        SupplyRank = FastValues.DefaultSosRank,
                        PipelineCycleID = 1,
                        PtrDeliveryMeterID = null,
                        DunsID = null,
                        ActNumber = null,
                        MarketDealID = NewTransferDeal.Id,
                        MarketRank = FastValues.DefaultSosRank,
                        NominatedVolume = NommedVolumes[loopDates.IndexOf(loopDate)],
                        IsSupplyTransferDeal = IsSupplyTransferDeal,
                        IsMarketTransferDeal = true,
                        NomNotes = null,
                        IsKeepWhole = false,
                        IncludeInSave = true,
                        UserID = userId,
                        IsManualPtr = false
                    };

                    int deliveryMeterId;
                    if (i.IsMeter1First.GetValueOrDefault())
                        // if is supply = false, we need the market side.
                        deliveryMeterId = i.Meter1Id;
                    else
                        deliveryMeterId = i.Meter2Id;
                    nd.DeliveryMeterId = deliveryMeterId;

                    SupplyUniqueKey key;
                    if (nd.IsSupplyTransferDeal)
                    {
                        key = new SupplyUniqueKey
                        {
                            DealID = null,
                            Day = nd.Day,
                            TransferDealID = nd.SupplyDealID,
                            PointID = null,
                            MeterID = nd.ReceiptMeterID
                        };
                    }
                    else
                    {
                        key = new SupplyUniqueKey
                        {
                            DealID = nd.SupplyDealID,
                            Day = nd.Day,
                            TransferDealID = null,
                            PointID = nd.ReceiptPointID,
                            MeterID = nd.ReceiptMeterID
                        };
                    }
                    if (thisPipelineContractStartEnd != null && loopDate <= thisPipelineContractStartEnd.EndDate && loopDate >= thisPipelineContractStartEnd.StartDate)
                    {
                        nd.PipelineContractID = thisPipelinesDefaultContractID;
                        nd.PtrPipelineContractID = thisPipelinesDefaultContractID;
                    }
                    else
                    {
                        // if default contract not found, then let's try to get it from Nomination_Supply, first match.
                        var matches = lookupSuppies[key];
                        if (matches.Any())
                            nd.PipelineContractID = matches.First().PipelineContractId;
                        else
                            nd.PipelineContractID = null;
                        nd.PtrPipelineContractID = null;
                    }
                    if (nd.PipelineContractID.HasValue)
                        await ensurePipeContract(key, nd.PipelineContractID.Value);

                    var supplySourceNotes = getSupply(key)?.SourceNotes;
                    nd.SupplySourceNotes = supplySourceNotes;

                    var retVal = SosNomSaver.SaveSingleNomination(nd, db);

                    if (i.Id == MeterMapsPathRoutes.Last().Id)
                    {
                        // if final segment then save the supply without nom volumes since we don't
                        // know what market it will go to yet but we need to give it a contract
                        int ReceiptMeterID2 = NewTransferDeal.IsMeter1Supply ? NewTransferDeal.TransferMeterMap.Meter1Id : NewTransferDeal.TransferMeterMap.Meter2Id;
                        var nd2 = new SosNomDetails
                        {
                            Day = loopDate,
                            SupplyDealID = NewTransferDeal.Id,
                            ReceiptMeterID = ReceiptMeterID2,
                            SupplyRank = FastValues.DefaultSosRank,
                            PipelineCycleID = 1,
                            PtrDeliveryMeterID = null,
                            DunsID = null,
                            ActNumber = null,
                            IsSupplyTransferDeal = true,
                            IsMarketTransferDeal = false,
                            IsKeepWhole = false,
                            IncludeInSave = true,
                            UserID = userId,
                            PipelineContractID = i.ToPipeContractId,
                            IsManualPtr = false,
                            SupplySourceNotes = supplySourceNotes
                        };
                        SosNomSaver.SaveSingleNomination(nd2, db);
                    }

                    await db.SaveChangesAsync();
                }

                PreviousDealID = NewTransferDeal.Id;
            } // end of loop through MeterMapsPathRoutes

            await dbContextTransaction.CommitAsync();
        });
    }

    private SupplyMarketMeters GetSupplyAndMarketMeters(bool IsMeter1First, int Meter1ID, int Meter2ID, bool hasPreviousNormalDeal, TransferDeal? PreviousTransferDeal, bool IsStartingWithMarketDeal, int startDealPointID)
    {
        var supplyAndMarketMeter = new SupplyMarketMeters();
        Meter? supplyMeter = null;
        Meter? marketMeter = null;

        if (IsStartingWithMarketDeal)
        {
            if (hasPreviousNormalDeal) // normal deal
            {
                if (IsMeter1First)
                    supplyMeter = (from m in db.Meters where m.Id == Meter2ID select m).FirstOrDefault();
                else
                    supplyMeter = (from m in db.Meters where m.Id == Meter1ID select m).FirstOrDefault();

                var marketMeterId = (from mp in db.VwMeterPointGas where mp.PointId == startDealPointID select mp.MeterId).FirstOrDefault();
                if (marketMeterId != 0)
                    marketMeter = (from m in db.Meters where m.Id == marketMeterId select m).FirstOrDefault();
            }
            else if (PreviousTransferDeal != null) // transfer deal
            {
                if (IsMeter1First)
                    supplyMeter = (from m in db.Meters where m.Id == Meter2ID select m).FirstOrDefault();
                else
                    supplyMeter = (from m in db.Meters where m.Id == Meter1ID select m).FirstOrDefault();

                if (PreviousTransferDeal.IsMeter1Supply)
                    marketMeter = PreviousTransferDeal.TransferMeterMap.Meter2;
                else
                    marketMeter = PreviousTransferDeal.TransferMeterMap.Meter1;
            }
        }
        else
        {
            if (hasPreviousNormalDeal) // normal deal
            {
                var supplyMeterId = (from mp in db.VwMeterPointGas where mp.PointId == startDealPointID select mp.MeterId).FirstOrDefault();
                if (supplyMeterId != 0)
                    supplyMeter = (from m in db.Meters where m.Id == supplyMeterId select m).FirstOrDefault();

                if (IsMeter1First)
                    marketMeter = (from m in db.Meters where m.Id == Meter1ID select m).FirstOrDefault();
                else
                    marketMeter = (from m in db.Meters where m.Id == Meter2ID select m).FirstOrDefault();
            }
            else if (PreviousTransferDeal != null) // transfer deal
            {
                if (PreviousTransferDeal.IsMeter1Supply)
                    supplyMeter = PreviousTransferDeal.TransferMeterMap.Meter1;
                else
                    supplyMeter = PreviousTransferDeal.TransferMeterMap.Meter2;

                if (IsMeter1First)
                    marketMeter = (from m in db.Meters where m.Id == Meter1ID select m).FirstOrDefault();
                else
                    marketMeter = (from m in db.Meters where m.Id == Meter2ID select m).FirstOrDefault();
            }
        }

        if (supplyMeter == null)
            throw new Exception("Supply meter not found");

        if (marketMeter == null)
            throw new Exception("Market meter not found");

        supplyAndMarketMeter.MarketMeterId = marketMeter.Id;
        supplyAndMarketMeter.SupplyMeterId = supplyMeter.Id;
        return supplyAndMarketMeter;
    }

    private int[] UpdateNommedVolumes(int numDays,
        int[] NommedVolumes,
        DateOnly Startdate,
        int? thisPipelineContractId,
        int? thisPipelinePtrContractId,
        bool IsStartingWithMarketDeal,
        int fromMeterId,
        int toMeterId,
        int? ptrDeliveryMeterID,
        RateCalculator rateCalculator,
        int supplyTransferDealId,
        int? supplyReceiptMeterId)
    {
        for (int loopday = 0; loopday <= numDays; loopday++)
        {
            DateOnly loopDate = Startdate.AddDays(loopday);

            int nomVolume = NommedVolumes[loopday];
            double? NomFuelPercent = null;
            double? PtrFuelPercent = null;
            double? PtrPercent = null;

            if (thisPipelineContractId != null)
                NomFuelPercent = rateCalculator.GetFuelPercent(thisPipelineContractId.Value, loopDate, fromMeterId, toMeterId);

            if (supplyReceiptMeterId.HasValue && thisPipelineContractId != null)
                PtrPercent = rateCalculator.GetPtrPercent(loopDate, supplyReceiptMeterId.Value, ptrDeliveryMeterID, null, null, supplyTransferDealId, thisPipelineContractId.Value);

            if (ptrDeliveryMeterID.HasValue && thisPipelinePtrContractId != null)
                PtrFuelPercent = rateCalculator.GetFuelPercent(thisPipelinePtrContractId.Value, loopDate, fromMeterId, ptrDeliveryMeterID.Value);

            int PTRAmt;
            double NomFuelAmt;
            double PtrFuelAmt;
            int Vol;

            if (IsStartingWithMarketDeal)
            {
                PTRAmt = (int)Math.Round(nomVolume / (1 - PtrPercent.GetValueOrDefault()) - nomVolume, 0);
                NomFuelAmt = Math.Round(nomVolume / (1 - NomFuelPercent.GetValueOrDefault()) - nomVolume, 0);
                PtrFuelAmt = Math.Round(PTRAmt / (1 - PtrFuelPercent.GetValueOrDefault()) - PTRAmt, 0);
                int NomRecVol = nomVolume + (int)NomFuelAmt;
                int PtrRecVol = PTRAmt + (int)PtrFuelAmt;
                Vol = NomRecVol + PtrRecVol;
            }
            else
            {
                Vol = (int)(nomVolume / (1 + (1 / (1 - NomFuelPercent.GetValueOrDefault()) - 1) + (1 / (1 - PtrPercent.GetValueOrDefault()) - 1) + ((1 / (1 - PtrPercent.GetValueOrDefault()) - 1) / (1 - PtrFuelPercent.GetValueOrDefault()) - (1 / (1 - PtrPercent.GetValueOrDefault()) - 1))));
            }

            NommedVolumes[loopday] = Vol;
        }
        return NommedVolumes;
    }
}
