using System.Diagnostics;
using Fast.Logic.RateControl;
using Fast.Web.Logic;
using static Fast.Models.Enums;
using static Fast.Web.Logic.SosCrudeHelper;
using RequiredData = Fast.Models.Crude.RequiredData;
using SosDataSet = Fast.Models.Crude.SosDataSet;
using SosItem = Fast.Models.Crude.SosItem;
using SosNom = Fast.Models.Crude.SosNom;

namespace Fast.Web.Controllers;

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

    public SosCrudeController(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, DateOnly nomDate)
    {
#if DEBUG
        long totalMs = 0;
        var sw = Stopwatch.StartNew();
#endif

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

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

        var items = (await t1)
            .OrderBy(x => x.SupplyTicket)
            .ThenBy(x => x.ReceiptMeter)
            .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();

#if DEBUG
        sw.Stop();
        Debug.WriteLine($"GetDataSet queries took {sw.ElapsedMilliseconds} ms");
        totalMs += sw.ElapsedMilliseconds;
        sw.Restart();
#endif

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

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

#if DEBUG
        sw.Stop();
        Debug.WriteLine($"GetDataSet tasks took {sw.ElapsedMilliseconds} ms");
        totalMs += sw.ElapsedMilliseconds;
        sw.Restart();
#endif

        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.MarketDeliveryPointId = market.PointId;
                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.MarketDeliveryPointId = transferMarketSupply?.MarketPointId ?? 0; // TODO:  do we need to set PointId here?
                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);
        FillNomReceiptVols(items);
        FillDeliveryPoint(items, pointNames);

        //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;

#if DEBUG
        sw.Stop();
        Debug.WriteLine($"GetDataSet local took {sw.ElapsedMilliseconds} ms");
        totalMs += sw.ElapsedMilliseconds;
        Debug.WriteLine($"GetDataSet total took {totalMs} ms");
#endif

        return sosData;
    }

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

    private async Task<List<ContractInfo>> GetContracts()
    {
        var today = DateTime.Today;

        var items = await (
            from q in db.Contracts
            let isActive = !q.TerminationEffectiveDate.HasValue || q.TerminationEffectiveDate.Value > today
            where q.ProductId == (int)Enums.Product.CrudeOil
            orderby q.EffectiveDate descending, q.ContractNum
            select new ContractInfo
            {
                ContractId = q.Id,
                ContractText = isActive ? q.ContractNum : "{Inactive} " + q.ContractNum,
                ContractNumber = q.ContractNum,
                CounterpartyId = q.CounterpartyId,
                InternalEntityId = q.InternalEntityId,
                IsActive = isActive
            }
        ).ToListAsync();

        return items;
    }

    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();
        var pointIdsThatHaveAMeter = new HashSet<int>();
        int autoSelectPipelineId = 0;

        int userId = Util.GetAppUserId(user);

        List<Task> tasks = new()
        {
            Task.Run(async () => { hasModifyPermission = await authHelper.IsAuthorizedAsync(user, "SOS Crude Nomination", PermissionType.Modify); }),
            Task.Run(async () => { counterparties = (await DataHelper.GetCounterpartiesAsync(true, Enums.ProductCategory.CrudeOil)).ToList() ; }),
            Task.Run(async () => { pipelines = (await DataHelper.GetPipelinesAsync(false)).Where(x => x.IsCrudePipe).ToList(); }),
            Task.Run(async () => { points = (await DataHelper.GetPointsAsync(false, Enums.ProductCategory.CrudeOil)).ToList(); }),
            Task.Run(async () => { meters = await DataHelper.GetMetersByProductAsync(Enums.ProductCategory.CrudeOil); }),
            Task.Run(async () => { pointIdsThatHaveAMeter = (await Main.CreateContext().VwMeterPointCrudes.Select(x => x.PointId ?? 0).ToListAsync()).Distinct().ToHashSet(); }),
            Task.Run(async () => { autoSelectPipelineId = (await Main.CreateContext().SosCrudeSettings.Where(x => x.UserId == userId).Select(x => x.AutoSelectPipelineId).FirstOrDefaultAsync()); }),
        };
        await Task.WhenAll(tasks);

        points = points.Where(x => pointIdsThatHaveAMeter.Contains(x.PointId)).ToList();

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

        return result;
    }

    [Permission("SOS Crude 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 Crude Nomination", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> SaveNoms([FromBody] SaveNomsObject saveNomsObject, [FromQuery] DateOnly fromDate, [FromQuery] DateOnly toDate, [FromQuery] int pipeId)
    {
        var sw = Stopwatch.StartNew();
        int userId = Util.GetAppUserId(User);
        var nomSaver = new SosCrudeNomSaver(db, userId, saveNomsObject.Items, fromDate, toDate, saveNomsObject.SaveNotes, pipeId, env);
        await nomSaver.SaveAsync();
        _ = Task.Run(nomSaver.CreateSnapshotAsync);
        sw.Stop();
        Debug.WriteLine($"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; }
    }

    [Permission("SOS Crude Nomination", PermissionType.View)]
    [Route("[action]")]
    public async Task<IActionResult> GetSnapshotHistory(int pipeId, DateOnly nomDate)
    {
        var nameInfo = await (
           from pi in db.Pipelines
           from po in db.Points
           where pi.Id == pipeId
           select new SnapshotHistoryNameInfo
           {
               PipeName = pi.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.ProductId == (int)Enums.Product.CrudeOil && 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 = "",
            },
            Snapshots = snapshots,
        };

        return Ok(response);
    }

    [Permission("SOS Crude 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 = "Crude";
            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)
        {
            string messagePrefix = $"File Not Found: {fileNameOnDisk}";
            var bytes = Util.GetExceptionFilesBytes(ex, messagePrefix);
            return File(bytes, "text/plain");
        }
    }


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

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

        Dictionary<int, string> pointNames = new();
        var t1 = GetPointNamesAsync();

        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)); }
        );

        pointNames = await t1;
        FillNomReceiptVols(new List<SosItem> { item });
        FillDeliveryPoint(new List<SosItem> { item }, pointNames);

        return Ok(item);
    }

    private void FillDeliveryPoint(List<SosItem> sosItems, Dictionary<int, string> pointNames)
    {
        foreach (var sosItem in sosItems)
        {

            var nomsWithVol = sosItem.Noms.Where(x => x.Volume.GetValueOrDefault() != 0).ToList();
            var distinctPointIds = nomsWithVol.Where(x => x.MarketDeliveryPointId.HasValue).Select(x => x.MarketDeliveryPointId.GetValueOrDefault()).Distinct().ToList();
            var pointCount = distinctPointIds?.Count ?? 0;

            string deliveryPointName = string.Empty;
            if (pointCount == 1)
            {
                var pointId = distinctPointIds?.First() ?? 0;
                deliveryPointName = pointNames.GetValueOrDefault<int, string>(pointId, string.Empty);
            }
            else if (pointCount > 1)
            {
                deliveryPointName = "Multiple";
            }

            sosItem.DeliveryPoint = deliveryPointName;
        }
    }

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

        var suppliesToDelete = await (
            from q in db.CrudeSupplies
            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.CrudeSupplies.RemoveRange(suppliesToDelete);
        await db.SaveChangesAsync();

        return Ok();
    }

    [Permission("SOS Crude Nomination", PermissionType.Modify)]
    [Route("[action]")]
    public async Task<IActionResult> CloneSupply(int supplyNomId)
    {
        var userId = Util.GetAppUserId(User);
        var supplyRow = await db.CrudeSupplies.FirstOrDefaultAsync(x => x.Id == supplyNomId);
        if (supplyRow != null)
        {
            var newSupply = new CrudeSupply
            {
                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.CrudeSupplies.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 FillNomReceiptVols(List<SosItem> items)
    {
        foreach (var item in items)
            item.NomReceiptVol = item.DeliveryVol;
    }

    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;
                }
            }
        }
    }
}
