using System.Diagnostics;
using Fast.Logic.RateControl;
using static Fast.Logic.Valuation.PathValuaterCommon;

namespace Fast.Logic.Valuation;

public class PathValuaterGas
{
    private ValParams valParams = new();
    private readonly Dictionary<(int?, DateOnly, int, int, bool, bool), decimal> transPipeRateCache = new();
    private readonly Dictionary<(int?, DateOnly, int, int), decimal> transFuelPercentCache = new();
    private readonly Enums.Product product = Enums.Product.NaturalGas;
    private const string ticketNumPrefix = "GAS";
    private RateCalculator? rateCalculator;
    private PathValuaterCommon common = null!;

    public async Task<List<PathValuationResult>> GetValuationValues(ValParams valParams)
    {
        this.valParams = valParams;
        if (valParams.PositionDateRanges.Count == 0)
            throw new Exception("Valuation requires a position date range");

        rateCalculator = await RateCalculator.GetInstanceAsync(product);
        common = new PathValuaterCommon(rateCalculator);

        SetDefaultValParams();

        // Start all async operations
        var getActualsTask = GetRecentActualsByKeyAsync();
        var getSingleLegNomsTask = GetSingleLegNomsAsync();
        var getTransfersTask = GetTransfersThatEndWithMarketsAsync();
        var getAllTransferNomsTask = GetAllTransferNomsByKeyAsync();
        var getDealValsTask = GetDealValsAsync();
        var getContractDataTask = GetContractDataAsync();

        // Wait for all tasks to complete
        await Task.WhenAll(getActualsTask, getSingleLegNomsTask, getTransfersTask, getAllTransferNomsTask, getDealValsTask);

        // Get results
        var actualsDic = await getActualsTask;
        var singleLegNoms = await getSingleLegNomsTask;
        var transfersThatEndWithMarkets = await getTransfersTask;
        var allTransferNomsDic = await getAllTransferNomsTask;
        var dealVals = await getDealValsTask;
        var contractData = await getContractDataTask;

        // Process results
        var multiLegNoms = GetMultiLegNoms(transfersThatEndWithMarkets, allTransferNomsDic, actualsDic);

        // Use List's capacity constructor for better performance when size is known
        var pathVals = new List<PathValuationResult>(singleLegNoms.Count + multiLegNoms.Count);
        pathVals.AddRange(singleLegNoms);
        pathVals.AddRange(multiLegNoms);

        SetFinalVolumes(pathVals, actualsDic);
        SetDealContractValues(pathVals, contractData);
        SetMiscDealValues(pathVals, dealVals);
        SetContractPrices(pathVals, dealVals);
        SetPriceAdjustments(pathVals, dealVals);
        SetOverrideValues(pathVals, actualsDic);
        common.SetTransportAdjustments(pathVals);
        SetInvoiceValues(pathVals);

        return pathVals;
    }

    private class DealContractData(int contractId, string? ourContractNum, string? theirContractNum)
    {
        public int ContractId { get; set; } = contractId;
        public string? OurContractNum { get; set; } = ourContractNum;
        public string? TheirContractNum { get; set; } = theirContractNum;
    }

    private async static Task<Dictionary<int, DealContractData>> GetContractDataAsync()
    {
        using var db = Main.CreateContext();
        var taskDealToContractDict = (
            from d in db.Deals
            join c in db.Contracts on d.NettingContractNumber equals c.ContractNum
            where d.NettingContractNumber != null
            select new { DealId = d.Id, Data = new DealContractData(c.Id, c.ContractNum, c.TheirContractNum) }
        ).Distinct().ToDictionaryAsync(x => x.DealId, x => x.Data);

        var dealToContractDict = await taskDealToContractDict;
        return dealToContractDict;
    }

    private static void SetPriceAdjustments(List<PathValuationResult> pathVals, Dictionary<(int DealId, DateOnly PositionStartDate), ValuationResult> dealVals)
    {
        foreach (var pathVal in pathVals)
        {
            if (pathVal.ReceiptDealId == null || pathVal.DeliveryDealId == null)
                continue;

            var valKeyReceipt = (pathVal.ReceiptDealId.Value, pathVal.Day);
            if (dealVals.TryGetValue(valKeyReceipt, out var valReceipt))
                pathVal.ReceiptPriceAdj =
                    (decimal)(valReceipt.BasisPrice +
                    valReceipt.PremOrDisc);

            var valKeyDelivery = (pathVal.DeliveryDealId.Value, pathVal.Day);
            if (dealVals.TryGetValue(valKeyDelivery, out var valDelivery))
                pathVal.DeliveryPriceAdj =
                    (decimal)(valDelivery.BasisPrice +
                    valDelivery.PremOrDisc);

            if (pathVal.IsNetback)
                pathVal.ReceiptPriceAdj = pathVal.DeliveryPriceAdj;
        }
    }

    private static void SetMiscDealValues(List<PathValuationResult> pathVals, Dictionary<(int DealId, DateOnly PositionStartDate), ValuationResult> dealVals)
    {
        foreach (var pathVal in pathVals)
        {
            if (pathVal.ReceiptDealId == null || pathVal.DeliveryDealId == null)
                continue;

            var valKeyReceipt = (pathVal.ReceiptDealId.Value, pathVal.Day);
            if (dealVals.TryGetValue(valKeyReceipt, out var valReceipt))
                pathVal.ReceiptBaseOrSwing = valReceipt.BaseOrSwingText;

            var valKeyDelivery = (pathVal.DeliveryDealId.Value, pathVal.Day);
            if (dealVals.TryGetValue(valKeyDelivery, out var valDelivery))
                pathVal.DeliveryBaseOrSwing = valDelivery.BaseOrSwingText;
        }
    }

    private static void SetContractPrices(List<PathValuationResult> pathVals, Dictionary<(int DealId, DateOnly PositionStartDate), ValuationResult> dealVals)
    {
        foreach (var pathVal in pathVals)
        {
            if (pathVal.ReceiptDealId == null || pathVal.DeliveryDealId == null)
                continue;

            var valKeyReceipt = (pathVal.ReceiptDealId.Value, pathVal.Day);
            if (dealVals.TryGetValue(valKeyReceipt, out var valReceipt))
                pathVal.ReceiptContractPrice = (decimal)valReceipt.ContractPrice;

            var valKeyDelivery = (pathVal.DeliveryDealId.Value, pathVal.Day);
            if (dealVals.TryGetValue(valKeyDelivery, out var valDelivery))
                pathVal.DeliveryContractPrice = (decimal)valDelivery.ContractPrice;

            if (pathVal.IsNetback)
                pathVal.ReceiptContractPrice = pathVal.DeliveryContractPrice;
        }
    }

    private static void SetOverrideValues(List<PathValuationResult> pathVals, Dictionary<(int ActualTypeId, int SupplyNomId, int MarketNomId, int? LastTransferId), ActualItem> actualsDic)
    {
        foreach (var pathVal in pathVals)
        {
            var receiptKey = ((int)Enums.ActualType.Buy, pathVal.SupplyNomId, pathVal.MarketNomId, pathVal.LastTransferDealId);
            if (actualsDic.TryGetValue(receiptKey, out var receiptActual))
            {
                if (receiptActual.TransportRate != null)
                {
                    pathVal.ReceiptTransportTotal = receiptActual.TransportRate.Value;
                    pathVal.IsReceiptTransportOverridden = true;
                }

                if (receiptActual.Price != null)
                {
                    pathVal.ReceiptContractPrice = receiptActual.Price.Value;
                    pathVal.IsReceiptContractPriceOverridden = true;
                }

                if (receiptActual.Adder != null)
                {
                    pathVal.ReceiptPriceAdj = receiptActual.Adder.Value;
                    pathVal.IsReceiptPriceAdjOverridden = true;
                }

                if (receiptActual.TneMeterId != null)
                {
                    pathVal.TneMeterId = receiptActual.TneMeterId;
                    pathVal.TneMeter = receiptActual.TneMeterName;
                }
            }

            var deliveryKey = ((int)Enums.ActualType.Sell, pathVal.SupplyNomId, pathVal.MarketNomId, pathVal.LastTransferDealId);
            if (actualsDic.TryGetValue(deliveryKey, out var deliveryActual))
            {
                if (deliveryActual.Price != null)
                {
                    pathVal.DeliveryContractPrice = deliveryActual.Price.Value;
                    pathVal.IsDeliveryContractPriceOverridden = true;
                }

                if (deliveryActual.Adder != null)
                {
                    pathVal.DeliveryPriceAdj = deliveryActual.Adder.Value;
                    pathVal.IsDeliveryPriceAdjOverridden = true;
                }
            }
        }
    }

    private async Task<Dictionary<(int ActualTypeId, int SupplyNomId, int MarketNomId, int? LastTransferId), ActualItem>> GetRecentActualsByKeyAsync()
    {
        using var db = Main.CreateContext();
        Dictionary<(int ActualTypeId, int SupplyNomId, int MarketNomId, int? LastTransferId), ActualItem> actualsDic;

        var actualsQuery = (
            from q in db.GasActuals.AsNoTracking()
            select new ActualItem
            {
                Date = q.SupplyNom.Date,
                ActualTypeId = q.ActualTypeId,
                SupplyNomId = q.SupplyNomId,
                MarketNomId = q.MarketNomId,
                LastTransferId = q.LastTransferId,
                Volume = q.Volume,
                Price = q.Price,
                Adder = q.Adder,
                TransportRate = q.TransportRate,
                IsLinked = q.IsLinked,
                SavedBy = q.SavedBy,
                SaveDate = q.SaveDate,
                TneMeterId = q.TneMeterId,
                TneMeterName = q.TneMeter == null ? null : q.TneMeter.Name
            }
        );
        actualsQuery = GetActualQueryWithFilters(actualsQuery, valParams);

        actualsDic = await actualsQuery
            .GroupBy(q => new { q.ActualTypeId, q.SupplyNomId, q.MarketNomId, q.LastTransferId })
            .Select(g => g.OrderByDescending(x => x.SaveDate).First())
            .ToDictionaryAsync(x => (x.ActualTypeId, x.SupplyNomId, x.MarketNomId, x.LastTransferId));

        return actualsDic;
    }

    private IQueryable<ActualItem> GetActualQueryWithFilters(IQueryable<ActualItem> query, ValParams valParams)
    {
        var positionDateExp = Util.Date.GetDateRangeExpression<ActualItem>(valParams.PositionDateRanges, "Date", false);
        query = query.Where(positionDateExp);

        return query;
    }

    private async Task<List<PathValuationResult>> GetSingleLegNomsAsync()
    {
        using var db = Main.CreateContext();
        var singleLegNomsQuery = (
            from q in db.GasMarketSupplies.AsNoTracking()
            join receiptMeterProduct in DataHelper.RecentMeterProductsQueryable(db) on q.SupplyNom.MeterId equals receiptMeterProduct.MeterId
            where receiptMeterProduct.ProductId == (int)product
            join deliveryMeterProduct in DataHelper.RecentMeterProductsQueryable(db) on q.MarketNom.MeterId equals deliveryMeterProduct.MeterId
            where deliveryMeterProduct.ProductId == (int)product
            let receiptPipeObj = receiptMeterProduct.SourceZone!.Pipeline!
            let deliveryPipeObj = deliveryMeterProduct.SourceZone!.Pipeline!
            where q.SupplyNom.DealId != null
                && q.MarketNom.DealId != null
                && q.Volume != null && q.Volume != 0
            select new PathValuationResult
            {
                Day = q.Date,
                ReceiptDeal = q.SupplyNom.Deal!.TicketNum,
                ReceiptInternalEntity = q.SupplyNom.Deal!.InternalEntity!.Name,
                ReceiptCounterparty = q.SupplyNom.Deal!.Counterparty!.Name,
                ReceiptCounterpartyShort = q.SupplyNom.Deal!.Counterparty!.ShortName ?? "",
                ReceiptDealPurpose = q.SupplyNom.Deal!.DealPurpose != null ? q.SupplyNom.Deal!.DealPurpose!.Name ?? "" : "",
                ReceiptPipe = receiptPipeObj.PipeShort ?? receiptPipeObj.Name!,
                ReceiptPoint = q.SupplyNom.Point != null ? q.SupplyNom.Point!.Name! : "",
                ReceiptMeter = q.SupplyNom.Meter!.Name,
                ReceiptPipeContract = q.SupplyNom.PipelineContract != null ? q.SupplyNom.PipelineContract!.ContractId : "",
                ReceiptNomVol = q.Volume ?? 0,
                DeliveryDeal = q.MarketNom.Deal!.TicketNum,
                DeliveryInternalEntity = q.MarketNom.Deal!.InternalEntity!.Name,
                DeliveryCounterparty = q.MarketNom.Deal!.Counterparty!.Name,
                DeliveryCounterpartyShort = q.MarketNom.Deal!.Counterparty!.ShortName ?? "",
                DeliveryDealPurpose = q.MarketNom.Deal!.DealPurpose != null ? q.MarketNom.Deal!.DealPurpose!.Name ?? "" : "",
                DeliveryPipe = deliveryPipeObj.PipeShort ?? deliveryPipeObj.Name!,
                DeliveryPoint = q.MarketNom.Point!.Name!,
                DeliveryMeter = q.MarketNom.Meter!.Name,
                DeliveryPipeContract = "", //for any single nom, there is only one contract, so we'll set delivery later
                DeliveryNomVol = q.Volume ?? 0,
                ReceiptDealId = q.SupplyNom.DealId,
                ReceiptProductId = q.SupplyNom.Deal != null ? q.MarketNom.Deal!.ProductId : null,
                ReceiptInternalEntityId = q.SupplyNom.Deal!.InternalEntityId,
                ReceiptCounterpartyId = q.SupplyNom.Deal!.CounterpartyId,
                ReceiptTransferDealId = q.SupplyNom.TransferDealId,
                ReceiptPipeId = receiptPipeObj.Id,
                ReceiptPointId = q.SupplyNom.PointId,
                ReceiptMeterId = q.SupplyNom.MeterId!.Value,
                ReceiptPipeContractId = q.SupplyNom.PipelineContractId,
                ReceiptPortfolioId = q.SupplyNom.Deal!.PortfolioId,
                DeliveryDealId = q.MarketNom.DealId,
                DeliveryProductId = q.MarketNom.Deal != null ? q.MarketNom.Deal!.ProductId : null,
                DeliveryInternalEntityId = q.MarketNom.Deal!.InternalEntityId,
                DeliveryCounterpartyId = q.MarketNom.Deal!.CounterpartyId,
                DeliveryTransferDealId = q.MarketNom.TransferDealId,
                DeliveryPipeId = deliveryPipeObj.Id,
                DeliveryPointId = q.MarketNom.PointId,
                DeliveryMeterId = q.MarketNom.MeterId,
                DeliveryPipeContractId = null, //for any single nom, there is only one contract, so we'll set delivery later
                NonJurisdictional = receiptPipeObj.PipelineTypeId == (int)Enums.PipelineType.NonJurisdictional,
                IsAgency = q.SupplyNom.Deal != null && q.SupplyNom.Deal!.Counterparty!.CounterpartyRelationships.Any(x => x.BusinessRelationshipId == (int)Enums.BusinessRelationship.Agency),
                IsNetback = q.SupplyNom.Deal != null && q.SupplyNom.Deal!.IsNetback,
                SupplyNomId = q.SupplyNomId,
                MarketNomId = q.MarketNomId,
                HasTransfers = false,
            }
        );

        singleLegNomsQuery = GetPathQueryWithFilters(singleLegNomsQuery, valParams);
        var singleLegNoms = await singleLegNomsQuery.ToListAsync() ?? new List<PathValuationResult>();

        foreach (var normalNom in singleLegNoms)
        {
            normalNom.ReceiptTransports.Add(common.GetTransportItem(normalNom, null));

            //for single leg noms, the delivery pipe contract is the same as the receipt pipe contract
            normalNom.DeliveryPipeContract = normalNom.ReceiptPipeContract;
            normalNom.DeliveryPipeContractId = normalNom.ReceiptPipeContractId;
        }

        return singleLegNoms;
    }

    private async Task<List<PathValuationResult>> GetTransfersThatEndWithMarketsAsync()
    {
        using var db = Main.CreateContext();
        List<PathValuationResult> transfersThatEndWithMarkets;

        var transfersThatEndWithMarketsQuery = (
            from q in db.GasMarketSupplies.AsNoTracking()
            join receiptMeterProduct in DataHelper.RecentMeterProductsQueryable(db) on q.SupplyNom.MeterId equals receiptMeterProduct.MeterId
            where receiptMeterProduct.ProductId == (int)product
            join deliveryMeterProduct in DataHelper.RecentMeterProductsQueryable(db) on q.MarketNom.MeterId equals deliveryMeterProduct.MeterId
            where deliveryMeterProduct.ProductId == (int)product
            let receiptPipeObj = receiptMeterProduct.SourceZone!.Pipeline!
            let deliveryPipeObj = deliveryMeterProduct.SourceZone!.Pipeline!
            where q.MarketNom.DealId != null
                && q.SupplyNom.TransferDealId != null
                && q.Volume != null && q.Volume != 0
            select new PathValuationResult
            {
                Day = q.Date,
                ReceiptDeal = q.SupplyNom.TransferDeal!.TicketNum!,
                ReceiptInternalEntity = "",
                ReceiptCounterparty = "",
                ReceiptCounterpartyShort = "",
                ReceiptDealPurpose = "",
                ReceiptPipe = receiptPipeObj.PipeShort ?? receiptPipeObj.Name!,
                ReceiptPoint = q.SupplyNom.Point != null ? q.SupplyNom.Point!.Name! : "",
                ReceiptMeter = q.SupplyNom.Meter!.Name,
                ReceiptPipeContract = q.SupplyNom.PipelineContract != null ? q.SupplyNom.PipelineContract!.ContractId : "",
                ReceiptNomVol = q.Volume ?? 0,
                DeliveryDeal = q.MarketNom.Deal!.TicketNum,
                DeliveryInternalEntity = q.MarketNom.Deal!.InternalEntity!.Name,
                DeliveryCounterparty = q.MarketNom.Deal!.Counterparty!.Name,
                DeliveryCounterpartyShort = q.MarketNom.Deal!.Counterparty!.ShortName ?? "",
                DeliveryDealPurpose = q.MarketNom.Deal!.DealPurpose != null ? q.MarketNom.Deal!.DealPurpose!.Name ?? "" : "",
                DeliveryPipe = deliveryPipeObj.PipeShort ?? deliveryPipeObj.Name!,
                DeliveryPoint = q.MarketNom.Point != null ? q.MarketNom.Point!.Name! : "",
                DeliveryMeter = q.MarketNom.Meter!.Name,
                DeliveryPipeContract = "", //for any single nom, there is only one contract, so we'll set delivery later
                DeliveryNomVol = q.Volume ?? 0,
                ReceiptDealId = q.SupplyNom.DealId,
                ReceiptProductId = q.SupplyNom.Deal != null ? q.MarketNom.Deal!.ProductId : null,
                ReceiptInternalEntityId = null,
                ReceiptCounterpartyId = null,
                ReceiptTransferDealId = q.SupplyNom.TransferDealId,
                ReceiptPipeId = receiptPipeObj.Id,
                ReceiptPointId = q.SupplyNom.PointId,
                ReceiptMeterId = q.SupplyNom.MeterId!.Value,
                ReceiptPipeContractId = q.SupplyNom.PipelineContractId,
                ReceiptPortfolioId = q.SupplyNom.Deal != null ? q.SupplyNom.Deal!.PortfolioId : null,
                DeliveryDealId = q.MarketNom.DealId,
                DeliveryProductId = q.MarketNom.Deal != null ? q.MarketNom.Deal!.ProductId : null,
                DeliveryInternalEntityId = q.MarketNom.Deal!.InternalEntityId,
                DeliveryCounterpartyId = q.MarketNom.Deal!.CounterpartyId,
                DeliveryTransferDealId = q.MarketNom.TransferDealId,
                DeliveryPipeId = deliveryPipeObj.Id,
                DeliveryPointId = q.MarketNom.PointId,
                DeliveryMeterId = q.MarketNom.MeterId,
                DeliveryPipeContractId = null, //for any single nom, there is only one contract, so we'll set delivery later
                NonJurisdictional = receiptPipeObj.PipelineTypeId == (int)Enums.PipelineType.NonJurisdictional,
                IsAgency = q.SupplyNom.Deal != null && q.SupplyNom.Deal!.Counterparty!.CounterpartyRelationships.Any(x => x.BusinessRelationshipId == (int)Enums.BusinessRelationship.Agency),
                IsNetback = q.SupplyNom.Deal != null && q.SupplyNom.Deal!.IsNetback,
                SupplyNomId = q.SupplyNomId,
                MarketNomId = q.MarketNomId
            }
        );
        transfersThatEndWithMarketsQuery = GetPathQueryWithFilters(transfersThatEndWithMarketsQuery, valParams);
        transfersThatEndWithMarkets = await transfersThatEndWithMarketsQuery.ToListAsync() ?? new List<PathValuationResult>();
        return transfersThatEndWithMarkets;
    }

    private async Task<Dictionary<(DateOnly Day, string DeliveryDeal), PathValuationResult>> GetAllTransferNomsByKeyAsync()
    {
        using var db = Main.CreateContext();
        Dictionary<(DateOnly Day, string DeliveryDeal), PathValuationResult> allTransferNomsDic;

        var allTransferNomsQuery = (
            from q in db.GasMarketSupplies.AsNoTracking()
            join receiptMeterProduct in DataHelper.RecentMeterProductsQueryable(db) on q.SupplyNom.MeterId equals receiptMeterProduct.MeterId
            where receiptMeterProduct.ProductId == (int)product
            join deliveryMeterProduct in DataHelper.RecentMeterProductsQueryable(db) on q.MarketNom.MeterId equals deliveryMeterProduct.MeterId
            where deliveryMeterProduct.ProductId == (int)product
            let receiptPipeObj = receiptMeterProduct.SourceZone!.Pipeline!
            let deliveryPipeObj = deliveryMeterProduct.SourceZone!.Pipeline!
            where q.MarketNom.TransferDealId != null
            select new PathValuationResult
            {
                Day = q.Date,
                ReceiptDeal = q.SupplyNom.DealId != null ? q.SupplyNom.Deal!.TicketNum! : q.SupplyNom.TransferDeal!.TicketNum!,
                ReceiptInternalEntity = q.SupplyNom.Deal != null ? q.SupplyNom.Deal!.InternalEntity!.Name! : "",
                ReceiptCounterparty = q.SupplyNom.Deal != null ? q.SupplyNom.Deal!.Counterparty!.Name! : "",
                ReceiptCounterpartyShort = q.SupplyNom.Deal != null ? q.SupplyNom.Deal!.Counterparty!.ShortName ?? "" : "",
                ReceiptDealPurpose = q.SupplyNom.Deal!.DealPurpose != null ? q.SupplyNom.Deal!.DealPurpose!.Name ?? "" : "",
                ReceiptPipe = receiptPipeObj.PipeShort ?? receiptPipeObj.Name!,
                ReceiptPoint = q.SupplyNom.Point != null ? q.SupplyNom.Point!.Name! : "",
                ReceiptMeter = q.SupplyNom.Meter!.Name,
                ReceiptPipeContract = q.SupplyNom.PipelineContract != null ? q.SupplyNom.PipelineContract!.ContractId : "",
                ReceiptNomVol = q.Volume ?? 0,
                DeliveryDeal = q.MarketNom.TransferDeal!.TicketNum!,
                DeliveryInternalEntity = "",
                DeliveryCounterparty = "",
                DeliveryCounterpartyShort = "",
                DeliveryDealPurpose = "",
                DeliveryPipe = deliveryPipeObj.PipeShort ?? deliveryPipeObj.Name!,
                DeliveryPoint = q.MarketNom.Point!.Name!,
                DeliveryMeter = q.MarketNom.Meter!.Name,
                DeliveryPipeContract = "", //for any single nom, there is only one contract, so we'll set delivery later
                DeliveryNomVol = q.Volume ?? 0,
                ReceiptDealId = q.SupplyNom.DealId,
                ReceiptProductId = q.SupplyNom.Deal != null ? q.MarketNom.Deal!.ProductId : null,
                ReceiptInternalEntityId = q.SupplyNom.Deal != null ? q.SupplyNom.Deal!.InternalEntityId : null,
                ReceiptCounterpartyId = q.SupplyNom.Deal != null ? q.SupplyNom.Deal!.CounterpartyId : null,
                ReceiptTransferDealId = q.SupplyNom.TransferDealId,
                ReceiptPipeId = receiptPipeObj.Id,
                ReceiptPointId = q.SupplyNom.PointId,
                ReceiptMeterId = q.SupplyNom.MeterId!.Value,
                ReceiptPipeContractId = q.SupplyNom.PipelineContractId,
                ReceiptPortfolioId = q.SupplyNom.Deal != null ? q.SupplyNom.Deal!.PortfolioId : null,
                DeliveryDealId = q.MarketNom.DealId,
                DeliveryProductId = q.MarketNom.Deal != null ? q.MarketNom.Deal!.ProductId : null,
                DeliveryInternalEntityId = null,
                DeliveryCounterpartyId = null,
                DeliveryTransferDealId = q.MarketNom.TransferDealId,
                DeliveryPipeId = deliveryPipeObj.Id,
                DeliveryPointId = q.MarketNom.PointId,
                DeliveryMeterId = q.MarketNom.MeterId,
                DeliveryPipeContractId = null, //for any single nom, there is only one contract, so we'll set delivery later
                NonJurisdictional = receiptPipeObj.PipelineTypeId == (int)Enums.PipelineType.NonJurisdictional,
                IsAgency = q.SupplyNom.Deal != null && q.SupplyNom.Deal!.Counterparty!.CounterpartyRelationships.Any(x => x.BusinessRelationshipId == (int)Enums.BusinessRelationship.Agency),
                IsNetback = q.SupplyNom.Deal != null && q.SupplyNom.Deal!.IsNetback,
                SupplyNomId = q.SupplyNom.Id,
                MarketNomId = q.MarketNom.Id
            }
        );
        allTransferNomsQuery = GetPathQueryWithFilters(allTransferNomsQuery, valParams);
        allTransferNomsDic = await allTransferNomsQuery.ToDictionaryAsync(x => (x.Day, x.DeliveryDeal)) ?? new Dictionary<(DateOnly Day, string DeliveryDeal), PathValuationResult>();
        return allTransferNomsDic;
    }

    private IQueryable<PathValuationResult> GetPathQueryWithFilters(IQueryable<PathValuationResult> query, ValParams valParams)
    {
        var positionDateExp = Util.Date.GetDateRangeExpression<PathValuationResult>(valParams.PositionDateRanges, "Day", false);
        query = query.Where(positionDateExp);

        return query.AsNoTracking();
    }

    private void SetDefaultValParams()
    {
        valParams.IncludeSosDeals = true;
        valParams.IncludeBasisInContractPrice = false;
        valParams.PositionDateRanges = valParams.PositionDateRanges.OrderBy(x => x.FromDate).ToList();
        if (!valParams.ProductIds.Contains((int)product))
            valParams.ProductIds.Add((int)product);
    }

    private async Task<Dictionary<(int DealId, DateOnly PositionStartDate), ValuationResult>> GetDealValsAsync()
    {
        var val = new Valuater();
        //ignore specific BuySell valParam since we always need both buys and sells for netback purchases
        valParams.BuySell = null;
        //ignore specific CounterpartyIds since we need values for both sides, receipt and delivery
        valParams.CounterpartyIds = new List<int>();
        //ignore specific PipelineIds since we need values for both sides, receipt and delivery
        valParams.PipelineIds = new List<int>();

        var vals = await val.GetValuationValues(valParams);
        var dealVals = vals.ToDictionary(x => (x.DealId.GetValueOrDefault(), x.PositionStartDate)) ?? new Dictionary<(int DealId, DateOnly PositionStartDate), ValuationResult>();
        return dealVals;
    }

    private List<PathValuationResult> GetMultiLegNoms(
        List<PathValuationResult> transfersThatEndWithMarkets,
        Dictionary<(DateOnly Day, string DeliveryDeal), PathValuationResult> allTransferNomsDic,
        Dictionary<(int ActualTypeId, int SupplyNomId, int MarketNomId, int? LastTransferId), ActualItem> actualsDic)
    {
        // Pre-allocate the list with a reasonable capacity to avoid resizing
        var multiLegNoms = new List<PathValuationResult>(transfersThatEndWithMarkets.Count);

        foreach (var transferNomEnd in transfersThatEndWithMarkets)
        {
            int legNum = 1; //not needed but helps with debugging
            bool hasMoreTransfers = true;
            // start with the end of the transfer chain and work backwards by setting the delivery deal to the receipt deal
            string deliveryDeal = transferNomEnd.ReceiptDeal;
            DateOnly date = transferNomEnd.Day;
            //set the receipt nom vol to the end of the transfer chain volume
            //and we'll gross it up as we work backwards
            List<TransportItem> transportItems = new();

            transportItems.Add(common.GetTransportItem(transferNomEnd, actualsDic));
            decimal transferDeliveryVol = transferNomEnd.ReceiptNomVol;

            while (hasMoreTransfers)
            {
                if (!allTransferNomsDic.TryGetValue((date, deliveryDeal), out var transferNom))
                {
                    hasMoreTransfers = false;
                    continue;
                }

                legNum++;
                transferNom.DeliveryNomVol = transferDeliveryVol;
                transportItems.Add(common.GetTransportItem(transferNom, actualsDic));
                transferDeliveryVol = transferNom.ReceiptNomVol;

                var isRealReceipt = transferNom.ReceiptDeal.StartsWith(ticketNumPrefix);
                var hasMarketVolume = transferNomEnd.DeliveryNomVol != 0;

                if (!isRealReceipt || !hasMarketVolume)
                {
                    // continue with the next transfer by setting the delivery deal to the receipt deal
                    deliveryDeal = transferNom.ReceiptDeal;
                    continue;
                }

                // here we have a real receipt and market volume, i.e. we have a multi-leg nom and we can stop looking for more transfers
                transferNom.ReceiptTransports = transportItems;
                var newMultiLegNom = GetNewMultiLegNom(transferNom, transferNomEnd);
                multiLegNoms.Add(newMultiLegNom);
                hasMoreTransfers = false;
            }
        }
        return multiLegNoms;
    }

    private static PathValuationResult GetNewMultiLegNom(PathValuationResult receiptNom, PathValuationResult deliveryNom)
    {
        // Use object initializer for better performance and readability
        return new PathValuationResult
        {
            Day = deliveryNom.Day,
            ReceiptDeal = receiptNom.ReceiptDeal,
            ReceiptInternalEntity = receiptNom.ReceiptInternalEntity,
            ReceiptCounterparty = receiptNom.ReceiptCounterparty,
            ReceiptCounterpartyShort = receiptNom.ReceiptCounterpartyShort,
            ReceiptDealPurpose = receiptNom.ReceiptDealPurpose,
            ReceiptPipe = receiptNom.ReceiptPipe,
            ReceiptPoint = receiptNom.ReceiptPoint,
            ReceiptMeter = receiptNom.ReceiptMeter,
            ReceiptPipeContract = receiptNom.ReceiptPipeContract,
            ReceiptNomVol = receiptNom.ReceiptNomVol,
            DeliveryDeal = deliveryNom.DeliveryDeal,
            DeliveryInternalEntity = deliveryNom.DeliveryInternalEntity,
            DeliveryCounterparty = deliveryNom.DeliveryCounterparty,
            DeliveryCounterpartyShort = deliveryNom.DeliveryCounterpartyShort,
            DeliveryDealPurpose = deliveryNom.DeliveryDealPurpose,
            DeliveryPipe = deliveryNom.DeliveryPipe,
            DeliveryPoint = deliveryNom.DeliveryPoint,
            DeliveryMeter = deliveryNom.DeliveryMeter,
            DeliveryPipeContract = deliveryNom.ReceiptPipeContract,  //we use receipt of the delivery nom since pipe contract is always tied to supply (not a market)
            DeliveryNomVol = deliveryNom.DeliveryNomVol,
            ReceiptDealId = receiptNom.ReceiptDealId,
            ReceiptProductId = receiptNom.ReceiptProductId,
            ReceiptInternalEntityId = receiptNom.ReceiptInternalEntityId,
            ReceiptCounterpartyId = receiptNom.ReceiptCounterpartyId,
            ReceiptTransferDealId = receiptNom.ReceiptTransferDealId,
            ReceiptPipeId = receiptNom.ReceiptPipeId,
            ReceiptPointId = receiptNom.ReceiptPointId,
            ReceiptMeterId = receiptNom.ReceiptMeterId,
            ReceiptPipeContractId = receiptNom.ReceiptPipeContractId,
            ReceiptPortfolioId = receiptNom.ReceiptPortfolioId,
            DeliveryDealId = deliveryNom.DeliveryDealId,
            DeliveryProductId = deliveryNom.DeliveryProductId,
            DeliveryInternalEntityId = deliveryNom.DeliveryInternalEntityId,
            DeliveryCounterpartyId = deliveryNom.DeliveryCounterpartyId,
            DeliveryTransferDealId = deliveryNom.DeliveryTransferDealId,
            DeliveryPipeId = deliveryNom.DeliveryPipeId,
            DeliveryPointId = deliveryNom.DeliveryPointId,
            DeliveryMeterId = deliveryNom.DeliveryMeterId,
            DeliveryPipeContractId = deliveryNom.ReceiptPipeContractId, //we use receipt of the delivery nom since pipe contract is always tied to supply (not a market)
            ReceiptTransportTotal = receiptNom.ReceiptTransportTotal,
            ReceiptTransports = receiptNom.ReceiptTransports,
            NonJurisdictional = receiptNom.NonJurisdictional,
            IsAgency = receiptNom.IsAgency,
            IsNetback = receiptNom.IsNetback,
            SupplyNomId = receiptNom.SupplyNomId,
            MarketNomId = deliveryNom.MarketNomId,
            LastTransferDealId = deliveryNom.ReceiptTransferDealId,
            HasTransfers = true,
        };
    }

    private static void SetDealContractValues(List<PathValuationResult> pathVals, Dictionary<int, DealContractData> dealToContractDict)
    {
        foreach (var pathVal in pathVals)
        {
            if (pathVal.DeliveryDealId.HasValue && dealToContractDict.TryGetValue(pathVal.DeliveryDealId.Value, out DealContractData? contractData))
            {
                pathVal.DeliveryInternalContractNum = contractData.OurContractNum;
                pathVal.DeliveryCounterpartyContractNum = contractData.TheirContractNum;
                pathVal.DeliveryDealContractId = contractData.ContractId;
            }
        }
    }

    private static void SetFinalVolumes(List<PathValuationResult> pathVals, Dictionary<(int ActualTypeId, int SupplyNomId, int MarketNomId, int? LastTransferId), ActualItem> actualsDic)
    {
        foreach (var pathVal in pathVals)
        {
            var receiptKey = ((int)Enums.ActualType.Buy, pathVal.SupplyNomId, pathVal.MarketNomId, pathVal.LastTransferDealId);
            if (actualsDic.TryGetValue(receiptKey, out var receiptActual))
                pathVal.ReceiptActualVol = receiptActual.Volume;

            var deliveryKey = ((int)Enums.ActualType.Sell, pathVal.SupplyNomId, pathVal.MarketNomId, pathVal.LastTransferDealId);
            if (actualsDic.TryGetValue(deliveryKey, out var deliveryActual))
                pathVal.DeliveryActualVol = deliveryActual.Volume;

            pathVal.ReceiptVol = pathVal.ReceiptActualVol ?? pathVal.ReceiptNomVol;
            pathVal.DeliveryVol = pathVal.DeliveryActualVol ?? pathVal.DeliveryNomVol;
        }
    }

    private static void SetInvoiceValues(List<PathValuationResult> pathVals)
    {
        foreach (var pathVal in pathVals)
        {
            pathVal.ReceiptPurchasePrice = pathVal.ReceiptContractPrice + pathVal.ReceiptPriceAdj;
            pathVal.ReceiptInvoicePrice = pathVal.ReceiptContractPrice + pathVal.ReceiptPriceAdj - pathVal.ReceiptTransportTotal;
            pathVal.ReceiptInvoiceAmount = pathVal.ReceiptInvoicePrice * pathVal.ReceiptVol;

            pathVal.DeliveryInvoicePrice = pathVal.DeliveryContractPrice + pathVal.DeliveryPriceAdj;
            pathVal.DeliveryInvoiceAmount = pathVal.DeliveryInvoicePrice * pathVal.DeliveryVol;
        }
    }
}
