using System.Diagnostics;
using Fast.Logic.RateControl;
using Fast.Web.Logic;
using Serilog;
using static Fast.Models.Enums;
using static Fast.Web.Logic.SosHelper;

namespace Fast.Web.Controllers;

[Authorize]
[ApiController]
[Route("api/[controller]")]
public class SosController : ControllerBase
{
    private readonly AuthorizationHelper authHelper;
    private readonly MyDbContext db;
    private readonly IWebHostEnvironment env;

    public SosController(MyDbContext db, IWebHostEnvironment env)
    {
        authHelper = new AuthorizationHelper(Main.IsAuthenticationEnabled);
        this.db = db;
        this.env = env;
    }

    public async Task<SosDataSet?> GetDataSetTyped(System.Security.Claims.ClaimsPrincipal user, int pipeId, int deliveryPointId, DateOnly nomDate)
    {
        long totalMs = 0;
        var sw = Stopwatch.StartNew();

        var sosParams = new { pipeId, deliveryPointId, nomDate };
        var userId = Util.GetAppUserId(user);
        List<IdName> deliveryMeters = new();
        List<SosPipeContractInfo> pipeContracts = new();
        SosSettingItem settings = new();
        RateCalculator? rateCalculator = null;
        List<int> validReceiptMeterIds = new();

        var t1 = SosDatabaseHelper.GetSuppliesAsync(pipeId, deliveryPointId, nomDate);
        var t2 = SosDatabaseHelper.GetMarketsAsync(pipeId, deliveryPointId, nomDate);
        var t3 = SosDatabaseHelper.GetMarketSuppliesAsync(pipeId, nomDate);

        var items = (await t1)
            .OrderBy(x => x.SupplyTicket)
            .ThenBy(x => x.ReceiptMeter)
            .ThenBy(x => x.Ownership)
            .ThenBy(x => x.ReceiptPoint)
            .ThenBy(x => x.SupplyNomId)
            .ToList();
        var markets = (await t2).ToList();
        var marketSupplies = (await t3)
            .OrderBy(x => x.SupplyNomID)
            .ThenBy(x => x.MarketNomID)
            .ToList();

        sw.Stop();
        Log.Information($"GetDataSet queries took {sw.ElapsedMilliseconds} ms");
        totalMs += sw.ElapsedMilliseconds;
        sw.Restart();

        List<Task> tasks = new()
        {
            Task.Run(async () => { settings = await SosSettingController.GetSettingsInternal(userId, pipeId); }),
            Task.Run(async () => { deliveryMeters = await GetDeliveryMeters(deliveryPointId, nomDate); }),
            Task.Run(async () => { pipeContracts = await GetPipeContracts(new List<int> {pipeId}, nomDate); }),
            Task.Run(async () => { rateCalculator = await RateCalculator.GetInstanceAsync(Enums.Product.NaturalGas); }),
            Task.Run(async () => { validReceiptMeterIds = await GetValidReceiptMeterIds(items, deliveryPointId); })
        };
        await Task.WhenAll(tasks);

        if (rateCalculator == null)
            return null; //return BadRequest("Rate calculator is null");


        sw.Stop();
        Log.Information($"GetDataSet tasks took {sw.ElapsedMilliseconds} ms");
        totalMs += sw.ElapsedMilliseconds;
        sw.Restart();


        List<int> deliveryMeterIds = deliveryMeters.Select(x => x.Id).ToList();
        Dictionary<(int SupplyNomID, int MarketNomID), SosMarketSupply> marketSupplyDic = new();
        ILookup<int, SosMarketSupply>? transferMarketSupplyLookup = Enumerable.Empty<SosMarketSupply>().ToLookup(x => 0);
        ILookup<int, SosMarketSupply> otherMarketSuppliesByIdLookup = Enumerable.Empty<SosMarketSupply>().ToLookup(x => 0);
        ILookup<(int CounterpartyId, int SupplyMeterId), SosMarketSupply> otherMarketSuppliesByCounterpartyAndMeter = Enumerable.Empty<SosMarketSupply>().ToLookup(x => (0, 0));
        ILookup<(int CounterpartyId, int SupplyMeterId), SosMarketSupply> currentMarketSuppliesByCounterpartyAndMeter = Enumerable.Empty<SosMarketSupply>().ToLookup(x => (0, 0));

        Parallel.Invoke(
            //market supplies for regular deal markets
            () => { marketSupplyDic = marketSupplies.Where(x => deliveryMeterIds.Contains(x.MarketMeterId ?? 0)).ToDictionary(x => (x.SupplyNomID, x.MarketNomID)); },
            //market supplies only for transfer column
            () => { transferMarketSupplyLookup = marketSupplies.Where(x => deliveryMeterIds.Contains(x.MarketMeterId ?? 0) && x.MarketTransferId != null).ToLookup(x => x.SupplyNomID); },
            //market supplies by id for markets that are not this delivery point
            () => { otherMarketSuppliesByIdLookup = marketSupplies.Where(x => !deliveryMeterIds.Contains(x.MarketMeterId ?? 0)).ToLookup(x => x.SupplyDealId ?? 0); },
            //market supplies by counterparty and meter of other delivery points for cumulative vol calculation
            () => { otherMarketSuppliesByCounterpartyAndMeter = marketSupplies.Where(x => !deliveryMeterIds.Contains(x.MarketMeterId ?? 0) && x.SupplyCounterpartyId != null).ToLookup(x => (x.SupplyCounterpartyId ?? 0, x.SupplyMeterId ?? 0)); },
            //market supplies by counterparty and meter of this delivery point for cumulative vol calculation
            () => { currentMarketSuppliesByCounterpartyAndMeter = marketSupplies.Where(x => deliveryMeterIds.Contains(x.MarketMeterId ?? 0) && x.SupplyCounterpartyId != null).ToLookup(x => (x.SupplyCounterpartyId ?? 0, x.SupplyMeterId ?? 0)); }
        );

        int? firstSavedDeliveryMeterId = markets.FirstOrDefault(x => x.MarketDeliveryMeterId != null)?.MarketDeliveryMeterId ?? null;

        //the default delivery meter is the first saved delivery meter, or the first delivery meter in the list when there is only one, otherwise null so that the user is forced to pick
        int? defaultDeliveryMeterId = firstSavedDeliveryMeterId != null ? firstSavedDeliveryMeterId.Value : deliveryMeters.Count == 1 ? deliveryMeters.First().Id : null;

        foreach (var mkt in markets)
        {
            mkt.Guid = Guid.NewGuid().ToString("N")[..8];
            //if the market delivery meter is missing then set it to the default delivery meter, otherwise leave it blank for the user to pick
            mkt.MarketDeliveryMeterId ??= defaultDeliveryMeterId ?? null;
        }

        foreach (var sup in items)
        {
            sup.Guid = Guid.NewGuid().ToString("N")[..8];
            sup.IsManualPtr = sup.PtrPercent.HasValue;

            foreach (var market in markets)
            {
                var marketNom = new SosNom();
                marketNom.MarketTicket = market.MarketTicket;
                var marketSupply = marketSupplyDic.GetValueOrDefault((sup.SupplyNomId ?? 0, market.MarketNomId ?? 0));
                marketNom.Volume = marketSupply?.Volume;
                marketNom.Notes = marketSupply?.Comment ?? "";
                marketNom.IsKeepWhole = marketSupply?.IsKeepWhole ?? false;
                marketNom.MarketDealType = market.MarketDealType;
                marketNom.MarketDeliveryMeterId = market.MarketDeliveryMeterId ?? 0;
                marketNom.MarketDealId = market.MarketDealId;
                sup.Noms.Add(marketNom);
            }

            //these individual transfer noms are generally not used for display
            //purposes since they are summed up into the TransferSumVol column,
            //but they are used to calculate fuel percentages
            var transferMarketSupplies = transferMarketSupplyLookup.Contains(sup.SupplyNomId ?? 0) ? transferMarketSupplyLookup[sup.SupplyNomId ?? 0].ToList() : new List<SosMarketSupply>();
            foreach (var transferMarketSupply in transferMarketSupplies)
            {
                var transferNom = new SosNom();
                transferNom.MarketTicket = transferMarketSupply.MarketTicket;
                transferNom.Volume = transferMarketSupply.Volume;
                transferNom.Notes = transferMarketSupply.Comment ?? "";
                transferNom.IsKeepWhole = transferMarketSupply?.IsKeepWhole ?? false;
                transferNom.MarketDealType = "Transfer";
                transferNom.MarketDeliveryMeterId = transferMarketSupply?.MarketMeterId ?? 0;
                transferNom.MarketDealId = null;
                sup.Noms.Add(transferNom);
            }

            if (transferMarketSupplyLookup.Contains(sup.SupplyNomId ?? 0))
            {
                var transferMarketSupply = transferMarketSupplyLookup[sup.SupplyNomId ?? 0];
                sup.TransferSumVol = transferMarketSupply.Sum(x => x.Volume);
            }
            else if (sup.SupplyTicket.StartsWith("TFR"))
                sup.TransferSumVol = 0;
        }

        //these should be executed in this order
        FillSupplyCloneFlags(items);
        FillDeliveryVols(items);
        FillNomFuels(nomDate, items, rateCalculator);
        FillPtrs(nomDate, items, rateCalculator);
        FillPtrFuels(nomDate, items, rateCalculator);
        FillPtrReceiptVols(items);
        FillNomReceiptVols(items);
        FillTotalReceiptVols(items);
        FillUnscheduledVols(items, otherMarketSuppliesByIdLookup);
        FillCumReceiptVols(items, nomDate, rateCalculator, otherMarketSuppliesByCounterpartyAndMeter, currentMarketSuppliesByCounterpartyAndMeter);

        //this should be executed after FillCumReceiptVols
        items = items.Where(x => validReceiptMeterIds.Contains(x.ReceiptMeterId ?? 0)).ToList();

        SosDataSet sosData = new();
        sosData.Items = items;
        sosData.Markets = markets;
        sosData.Settings = settings;
        sosData.DeliveryMeters = deliveryMeters;
        sosData.PipeContracts = pipeContracts;

        sw.Stop();
        Log.Information($"GetDataSet local took {sw.ElapsedMilliseconds} ms");
        totalMs += sw.ElapsedMilliseconds;
        Log.Information($"GetDataSet total took {totalMs} ms");


        return sosData;
    }

    [Permission("SOS Gas Nomination", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> GetDataSet(int pipeId, int deliveryPointId, DateOnly nomDate)
    {
        var sosData = await GetDataSetTyped(User, pipeId, deliveryPointId, nomDate);
        if (sosData == null)
        {
            return BadRequest("Rate calculator is null");
        }
        else
        {
            return Ok(sosData);
        }
    }

    public async Task<RequiredData> GetRequiredDataTyped(System.Security.Claims.ClaimsPrincipal user)
    {
        bool hasModifyPermission = false;
        List<EntityInfo>? counterparties = new();
        List<PipeInfo>? pipelines = new();
        List<PointInfo>? points = new();
        List<MeterInfo>? meters = new();
        int autoSelectPointId = 0;

        int userId = Util.GetAppUserId(user);

        List<Task> tasks = new()
        {
            Task.Run(async () => { hasModifyPermission = await authHelper.IsAuthorizedAsync(user, "SOS Gas Nomination", PermissionType.Modify); }),
            Task.Run(async () => { counterparties = (await DataHelper.GetCounterpartiesAsync(true, Enums.ProductCategory.NaturalGasAndLng)).ToList() ; }),
            Task.Run(async () => { pipelines = (await DataHelper.GetPipelinesAsync(false)).Where(x => x.IsGasPipe).ToList(); }),
            Task.Run(async () => { points = (await DataHelper.GetPointsAsync(false, Enums.ProductCategory.NaturalGasAndLng)).ToList(); }),
            Task.Run(async () => { meters = await DataHelper.GetMetersByProductAsync(Enums.ProductCategory.NaturalGasAndLng); }),
            Task.Run(async () => { autoSelectPointId = (await (Main.CreateContext()).SosGasSettings.Where(x => x.UserId == userId).Select(x => x.AutoSelectPointId).FirstOrDefaultAsync()); })
        };
        await Task.WhenAll(tasks);

        var result = new RequiredData();
        result.HasModifyPermission = hasModifyPermission;
        result.Counterparties = counterparties;
        result.Pipelines = pipelines;
        result.Points = points;
        result.Meters = meters;
        result.AutoSelectPointId = autoSelectPointId;

        return result;
    }

    [Permission("SOS Gas Nomination", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> GetRequiredData()
    {
        var t1 = await GetRequiredDataTyped(User);
        return Ok(t1);
    }

    public class SaveNomsObject
    {
        public List<SosItem> Items { get; set; } = new();
        public string? SaveNotes { get; set; }
    }

    [Permission("SOS Gas Nomination", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> SaveNoms([FromBody] SaveNomsObject saveNomsObject, [FromQuery] DateOnly fromDate, [FromQuery] DateOnly toDate, [FromQuery] int deliveryPointId)
    {

        var sw = Stopwatch.StartNew();
        int userId = Util.GetAppUserId(User);
        var nomSaver = new SosNomSaver(db, userId, saveNomsObject.Items, fromDate, toDate, deliveryPointId, saveNomsObject.SaveNotes, env);
        await nomSaver.SaveAsync();
        _ = Task.Run(nomSaver.CreateSnapshotAsync);
        sw.Stop();
        Log.Information($"SaveNoms took {sw.ElapsedMilliseconds} ms");
        return Ok();
    }

    public class SnapshotHistoryResponse
    {
        public SnapshotHistoryNameInfo? NameInfo { get; set; }
        public List<SosSnapshotItem> Snapshots { get; set; } = new();
    }

    public class SnapshotHistoryNameInfo
    {
        public string? PipeName { get; set; }
        public string? DeliveryPointName { get; set; }
    }

    [Permission("SOS Gas Nomination", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> GetSnapshotHistory(int pipeId, int deliveryPointId, DateOnly nomDate)
    {
        var nameInfo = await (
           from pi in db.Pipelines
           from po in db.Points
           where pi.Id == pipeId && po.Id == deliveryPointId
           select new SnapshotHistoryNameInfo
           {
               PipeName = pi.Name,
               DeliveryPointName = po.Name
           }).FirstOrDefaultAsync();

        var snapshots = await (
            from x in db.SosSnapshots
            join u in db.Users on x.CreatedBy equals u.Id into userGroup
            from u in userGroup.DefaultIfEmpty()
            where x.PipeId == pipeId && x.PointId == deliveryPointId && x.NomDate == nomDate
            select new SosSnapshotItem
            {
                NomDate = x.NomDate,
                PipeName = x.Pipe != null ? x.Pipe.Name : null,
                PointName = x.Point != null ? x.Point.Name : null,
                FileNameOriginal = x.FileNameOriginal,
                FileNameOnDisk = x.FileNameOnDisk,
                CreatedBy = u != null ? u.DisplayName : null,
                CreatedTime = x.CreatedTime.ToLocalTime(),
                Notes = x.Notes,
            }).ToListAsync();

        var response = new SnapshotHistoryResponse
        {
            NameInfo = nameInfo ?? new SnapshotHistoryNameInfo
            {
                PipeName = "",
                DeliveryPointName = ""
            },
            Snapshots = snapshots,
        };

        return Ok(response);
    }

    [Permission("SOS Gas Nomination", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> DownloadSnapshot(string fileNameOnDisk)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(fileNameOnDisk))
                throw new Exception($"File not found: {fileNameOnDisk}");

            string productFolder = "Gas";
            string subFolder = Path.Combine("SosSnapshots", productFolder);
            var downloadResponse = await Util.File.GetFileAsync(env.ContentRootPath, subFolder, fileNameOnDisk);
            return File(downloadResponse.Stream, downloadResponse.ContentType, downloadResponse.FileName);
        }
        catch (Exception ex)
        {
            var messagePrefix = $"File Not Found: {fileNameOnDisk}";
            var bytes = Util.GetExceptionFilesBytes(ex, messagePrefix);
            return File(bytes, "text/plain");
        }
    }


    [Permission("SOS Gas Nomination", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> GetItemDynamicValues(SosItem item, int pipeId, int deliveryPointId, DateOnly nomDate)
    {
        var rateCalculator = await RateCalculator.GetInstanceAsync(Enums.Product.NaturalGas);
        var sosParams = new { pipeId, deliveryPointId, nomDate };

        var marketSupplies = await SosDatabaseHelper.GetMarketSuppliesAsync(sosParams.pipeId, sosParams.nomDate);
        var deliveryMeters = await GetDeliveryMeters(deliveryPointId, nomDate);
        var deliveryMeterIds = deliveryMeters.Select(x => x.Id).ToList();

        ILookup<int, SosMarketSupply> otherMarketSuppliesByIdLookup = Enumerable.Empty<SosMarketSupply>().ToLookup(x => 0);
        ILookup<(int CounterpartyId, int SupplyMeterId), SosMarketSupply> otherMarketSuppliesByCounterpartyAndMeter = Enumerable.Empty<SosMarketSupply>().ToLookup(x => (0, 0));
        ILookup<(int CounterpartyId, int SupplyMeterId), SosMarketSupply> currentMarketSuppliesByCounterpartyAndMeter = Enumerable.Empty<SosMarketSupply>().ToLookup(x => (0, 0));

        Parallel.Invoke(
            //market supplies by id for markets that are not this delivery point
            () => { otherMarketSuppliesByIdLookup = marketSupplies.Where(x => !deliveryMeterIds.Contains(x.MarketMeterId ?? 0)).ToLookup(x => x.SupplyDealId ?? 0); },
            //market supplies by counterparty and meter of other delivery points for cumulative vol calculation
            () => { otherMarketSuppliesByCounterpartyAndMeter = marketSupplies.Where(x => !deliveryMeterIds.Contains(x.MarketMeterId ?? 0) && x.SupplyCounterpartyId != null).ToLookup(x => (x.SupplyCounterpartyId ?? 0, x.SupplyMeterId ?? 0)); },
            //market supplies by counterparty and meter of this delivery point for cumulative vol calculation
            () => { currentMarketSuppliesByCounterpartyAndMeter = marketSupplies.Where(x => deliveryMeterIds.Contains(x.MarketMeterId ?? 0) && x.SupplyCounterpartyId != null).ToLookup(x => (x.SupplyCounterpartyId ?? 0, x.SupplyMeterId ?? 0)); }
        );

        FillNomFuels(nomDate, new List<SosItem> { item }, rateCalculator);
        FillPtrs(nomDate, new List<SosItem> { item }, rateCalculator);
        FillPtrFuels(nomDate, new List<SosItem> { item }, rateCalculator);
        FillPtrReceiptVols(new List<SosItem> { item });
        FillNomReceiptVols(new List<SosItem> { item });
        FillTotalReceiptVols(new List<SosItem> { item });
        FillUnscheduledVols(new List<SosItem> { item }, otherMarketSuppliesByIdLookup);
        FillCumReceiptVols(new List<SosItem> { item }, nomDate, rateCalculator, otherMarketSuppliesByCounterpartyAndMeter, currentMarketSuppliesByCounterpartyAndMeter);

        return Ok(item);
    }

    [Permission("SOS Gas Nomination", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> ResetSupply(int supplyNomId, DateOnly fromDate, DateOnly toDate)
    {
        using var db = Main.CreateContext();
        var originalSupply = await db.GasSupplies.FirstOrDefaultAsync(x => x.Id == supplyNomId);
        if (originalSupply == null) return Ok();

        var suppliesToDelete = await (
            from q in db.GasSupplies
            where q.DealId == originalSupply.DealId &&
            q.PointId == originalSupply.PointId &&
            q.MeterId == originalSupply.MeterId &&
            q.PipelineContractId == originalSupply.PipelineContractId &&
            q.Date >= fromDate && q.Date <= toDate
            select q
        ).ToListAsync();
        if (!suppliesToDelete.Any()) return Ok();

        db.GasSupplies.RemoveRange(suppliesToDelete);
        await db.SaveChangesAsync();

        return Ok();
    }

    [Permission("SOS Gas Nomination", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> GetChangeTransferRequiredData(int transferDealId, string transferSourceTicket)
    {
        using var db = Main.CreateContext();
        var currentTransferDates = await (
            from q in db.TransferDeals
            where q.Id == transferDealId
            select new { q.StartDate, q.EndDate }
        ).FirstAsync();

        var currentTransferStartDate = currentTransferDates.StartDate;
        var currentTransferEndDate = currentTransferDates.EndDate;

        var sourceSupplyDates = await (
            from q in db.Deals
            where q.TicketNum == transferSourceTicket
            select new { q.StartDate, q.EndDate }
        ).FirstAsync();

        var sourceSupplyStartDate = sourceSupplyDates.StartDate;
        var sourceSupplyEndDate = sourceSupplyDates.EndDate;

        var result = new { currentTransferStartDate, currentTransferEndDate, sourceSupplyStartDate, sourceSupplyEndDate };
        return Ok(result);
    }

    [Permission("SOS Gas Nomination", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> ChangeTransferDates(int transferDealId, string transferSourceTicket, DateOnly fromDate, DateOnly toDate)
    {
        using var db = Main.CreateContext();

        var sourceSupplyDates = await (
            from q in db.Deals
            where q.TicketNum == transferSourceTicket
            select new { q.StartDate, q.EndDate }
        ).FirstAsync();

        if (fromDate < sourceSupplyDates.StartDate || toDate > sourceSupplyDates.EndDate)
            return BadRequest("From and To dates must be within the source ticket supply dates.");

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

            await db.GasActuals.Where(x => x.SupplyNom.TransferDealId == transferDealId && (x.SupplyNom.Date < fromDate || x.SupplyNom.Date > toDate)).ExecuteDeleteAsync();
            await db.GasActuals.Where(x => x.MarketNom.TransferDealId == transferDealId && (x.MarketNom.Date < fromDate || x.MarketNom.Date > toDate)).ExecuteDeleteAsync();

            await db.GasSupplies.Where(q => q.TransferDealId == transferDealId && (q.Date < fromDate || q.Date > toDate)).ExecuteDeleteAsync();
            await db.GasMarkets.Where(q => q.TransferDealId == transferDealId && (q.Date < fromDate || q.Date > toDate)).ExecuteDeleteAsync();

            var td = await db.TransferDeals.FindAsync(transferDealId);
            if (td != null)
            {
                td.StartDate = fromDate;
                td.EndDate = toDate;
                await db.SaveChangesAsync();
            }

            await dbContextTransaction.CommitAsync();
        });

        return Ok();
    }

    [Permission("SOS Gas Nomination", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> CloneSupply(int supplyNomId)
    {
        var userId = Util.GetAppUserId(User);
        using var db = Main.CreateContext();
        var supplyRow = await db.GasSupplies.FirstOrDefaultAsync(x => x.Id == supplyNomId);
        if (supplyRow != null)
        {
            var newSupply = new GasSupply
            {
                DealId = supplyRow.DealId,
                PointId = supplyRow.PointId,
                MeterId = supplyRow.MeterId,
                TransferDealId = null,
                Rank = supplyRow.Rank,
                PipelineCycleId = supplyRow.PipelineCycleId,
                PipelineContractId = null,
                DunsId = supplyRow.DunsId,
                Date = supplyRow.Date,
                ActNumber = null,
                PtrDeliveryMeterId = supplyRow.PtrDeliveryMeterId,
                PtrPipelineContractId = supplyRow.PtrPipelineContractId,
                Ptr = supplyRow.Ptr,
                Comment = null,
                SourceNotes = supplyRow.SourceNotes,
                CreatedBy = userId,
                CreatedTime = DateTime.UtcNow,
                SavedBy = userId,
                SavedTime = DateTime.UtcNow
            };
            db.GasSupplies.Add(newSupply);
            await db.SaveChangesAsync();
        }

        return Ok();
    }

    private static void FillDeliveryVols(List<SosItem> items)
    {
        foreach (var item in items)
            item.DeliveryVol = item.Noms.Sum(x => x.Volume ?? 0);
    }

    private static void FillNomFuels(DateOnly nomDate, List<SosItem> items, RateCalculator rateCalculator)
    {
        foreach (var item in items)
        {
            var volMeterPairs = item.Noms.Select(x => new VolumeMeterPair(x.Volume ?? 0, x.MarketDeliveryMeterId)).ToList();
            item.NomFuelPercent = SosHelper.CalcAvgSupplyNomFuelPercent(nomDate, item.ReceiptMeterId ?? 0, item.PipeContractId, volMeterPairs, rateCalculator);
            item.NomFuelAmount = SosHelper.GetAmountOfPercent(item.DeliveryVol, item.NomFuelPercent);
        }
    }

    private static void FillPtrs(DateOnly nomDate, List<SosItem> items, RateCalculator rateCalculator)
    {
        foreach (var item in items)
        {
            //the item SupplyCounterpartyId is actually filled with the Source counterparty id for transfers
            //for PTR percent calculation of transfers, counterparty id should be null, not the source counterparty id
            bool isTransfer = item.SupplyTicket.StartsWith("TFR");
            int? counterpartyId = isTransfer ? null : item.SupplyCounterpartyId;

            item.PtrPercent = SosHelper.CalcPtrPercent(nomDate, item.PtrPercent, item.ReceiptMeterId ?? 0, item.PtrDeliveryMeterId, counterpartyId, item.DealId, item.TransferDealId, item.PipeContractId, rateCalculator);
            item.PtrAmount = SosHelper.GetAmountOfPercent(item.DeliveryVol, item.PtrPercent);
        }
    }

    private static void FillPtrFuels(DateOnly nomDate, List<SosItem> items, RateCalculator rateCalculator)
    {
        foreach (var item in items)
        {
            item.PtrFuelPercent = 0;
            item.PtrFuelAmount = 0;
            if (item.PtrDeliveryMeterId.HasValue)
            {
                item.PtrFuelPercent = rateCalculator.GetFuelPercent(item.PtrContractId, nomDate, item.ReceiptMeterId ?? 0, item.PtrDeliveryMeterId.Value).GetValueOrDefault();
                item.PtrFuelAmount = SosHelper.GetAmountOfPercent(item.PtrAmount, item.PtrFuelPercent);
            }
        }
    }

    private static void FillNomReceiptVols(List<SosItem> items)
    {
        foreach (var item in items)
            item.NomReceiptVol = item.DeliveryVol + item.NomFuelAmount;
    }

    private static void FillPtrReceiptVols(List<SosItem> items)
    {
        foreach (var item in items)
            item.PtrReceiptVol = item.PtrAmount + item.PtrFuelAmount;
    }

    private static void FillTotalReceiptVols(List<SosItem> items)
    {
        foreach (var item in items)
            item.TotalReceiptVol = item.NomReceiptVol + item.PtrReceiptVol;
    }

    private static void FillUnscheduledVols(List<SosItem> items, ILookup<int, SosMarketSupply> otherMarketSuppliesByIdLookup)
    {
        foreach (var item in items)
        {
            bool isTransfer = item.SupplyTicket.StartsWith("TFR");
            if (isTransfer)
                item.UnscheduledVolume = item.DealVolume - item.NomReceiptVol;
            else
            {
                item.OtherUnscheduledVol = otherMarketSuppliesByIdLookup[item.DealId.GetValueOrDefault()].Sum(x => x.Volume.GetValueOrDefault());
                item.UnscheduledVolume = item.DealVolume - item.OtherUnscheduledVol - item.NomReceiptVol;
            }
        }
    }

    private static void FillCumReceiptVols(List<SosItem> items, DateOnly nomDate, RateCalculator rateCalculator, ILookup<(int CounterpartyId, int SupplyMeterId), SosMarketSupply> otherMarketSuppliesByCounterpartyAndMeter, ILookup<(int CounterpartyId, int SupplyMeterId), SosMarketSupply> currentMarketSuppliesByCounterpartyAndMeter)
    {
        foreach (var item in items)
        {
            bool isTransfer = item.SupplyTicket.StartsWith("TFR");
            if (!isTransfer) //normal deals only
            {
                var otherMktSups = otherMarketSuppliesByCounterpartyAndMeter[(item.SupplyCounterpartyId ?? 0, item.ReceiptMeterId ?? 0)];
                var currentMktSups = currentMarketSuppliesByCounterpartyAndMeter[(item.SupplyCounterpartyId ?? 0, item.ReceiptMeterId ?? 0)];

                var thisCumReceiptVol = SosController.GetCumReceiptVol(currentMktSups.ToList(), nomDate, rateCalculator);
                var otherCumReceiptVol = SosController.GetCumReceiptVol(otherMktSups.ToList(), nomDate, rateCalculator);
                item.OtherCumReceiptVol = otherCumReceiptVol;
                item.CumReceiptVol = thisCumReceiptVol + otherCumReceiptVol;
            }
        }
    }

    private static void FillSupplyCloneFlags(List<SosItem> items)
    {
        var itemsWithDealId = items.Where(x => x.DealId != null).ToList();
        var itemsWithDealIdGrouped = itemsWithDealId.GroupBy(x => (x.DealId, x.ReceiptPointId, x.ReceiptMeterId)).ToList();

        foreach (var group in itemsWithDealIdGrouped)
        {
            if (group.Count() > 1)
            {
                var earliestSupplyNomId = group.Min(x => x.SupplyNomId);
                foreach (var item in group)
                {
                    if (item.SupplyNomId != earliestSupplyNomId)
                        item.IsSupplyClone = true;
                }
            }
        }
    }

    private static double GetCumReceiptVol(List<SosMarketSupply> marketSupplies, DateOnly nomDate, RateCalculator rateCalculator)
    {
        double cumReceiptVol = 0;
        foreach (var mktSup in marketSupplies)
        {
            double deliveryVol = mktSup.Volume.GetValueOrDefault();
            double fuelPercent = SosHelper.CalcSingleNomFuelPercent(nomDate, mktSup.Volume, mktSup.PipeContractId, mktSup.SupplyMeterId ?? 0, mktSup.MarketMeterId ?? 0, rateCalculator);
            double fuelAmount = SosHelper.GetAmountOfPercent(deliveryVol, fuelPercent);
            double nomReceiptVol = deliveryVol + fuelAmount;
            double ptrPercent = SosHelper.CalcPtrPercent(nomDate, mktSup.PtrPercent, mktSup.SupplyMeterId ?? 0, mktSup.PtrDeliveryMeterId, mktSup.SupplyCounterpartyId, mktSup.SupplyDealId, mktSup.SupplyTransferId, mktSup.PipeContractId, rateCalculator);
            double ptrAmount = SosHelper.GetAmountOfPercent(deliveryVol, ptrPercent);
            double ptrFuelPercent = 0;
            double ptrFuelAmount = 0;
            if (mktSup.PtrDeliveryMeterId.HasValue)
            {
                ptrFuelPercent = rateCalculator.GetFuelPercent(mktSup.PtrContractId, nomDate, mktSup.SupplyMeterId ?? 0, mktSup.PtrDeliveryMeterId.Value).GetValueOrDefault();
                ptrFuelAmount = SosHelper.GetAmountOfPercent(ptrAmount, ptrFuelPercent);
            }
            double ptrReceiptVol = ptrAmount + ptrFuelAmount;
            double totalReceiptVol = nomReceiptVol + ptrReceiptVol;
            cumReceiptVol += totalReceiptVol;
        }
        return cumReceiptVol;
    }
}
