using Fast.Shared.Logic.ValuationByPath;
using Fast.Web.Logic;
using Fast.Web.Models;
using static Fast.Web.Controllers.InvoiceNatGasDetailController;

namespace Fast.Web.Logic.NaturalGas;

public class SaveInvoiceHelper(
    AuthorizationHelper authHelper,
    System.Security.Claims.ClaimsPrincipal user)
{
    private readonly int appUserId = Util.GetAppUserId(user);
    private readonly bool hasApprovalPermission = authHelper.IsAuthorizedAsync(user, securityActionApprove, PermissionType.Standard).Result;
    private const string securityActionApprove = "Invoice Natural Gas Approval";

    public async Task<string> SaveInvoice(InvoiceSaveType saveType, GasInvoice invoice)
    {
        var mainInvoice = invoice!.MainInvoice;

        if (mainInvoice!.Month.Day != 1)
            throw new Exception("The month specified is not valid: {invoiceDetail.Month:yyyy-MM-dd HH:mm:ss \"GMT\"zzz}");

        var lineDealIds = invoice!.Lines
            .Where(x => x.DealId.HasValue)
            .Select(x => x.DealId!.Value)
            .Distinct().ToList();

        CheckDealContracts(lineDealIds);

        using var db = Main.CreateContext();

        InvoiceGa? newDbItem = null;
        var existingDbItem = await db.InvoiceGas
            .Include(x => x.InvoiceGasLines)
                .ThenInclude(x => x.Deal)
            .Where(x => x.Id == mainInvoice.Id)
            .FirstOrDefaultAsync();
        var isInvoiceFinalized = existingDbItem?.InvoiceNum.Contains("INVC") ?? false;

        var isNewInvoice = saveType == InvoiceSaveType.New;
        var isNewDraft = saveType == InvoiceSaveType.Draft && existingDbItem == null;
        var isExistingDraft = saveType == InvoiceSaveType.Draft && existingDbItem != null;

        string invoiceNum = string.Empty;

        if (saveType == InvoiceSaveType.Draft && isInvoiceFinalized)
        {
            string msg = "This invoice may not be saved as a draft since it was already finalized.\r\n";
            msg += "You may distribute it again, or you could \"save new\" and then delete the original.";
            throw new Exception(msg);
        }
        else if (isNewInvoice || isNewDraft)
        {
            if (isNewInvoice) // i.e. when copying an existing draft via "Save New"
                invoice.Lines.ForEach(x => x.IsApproved = false);

            newDbItem = await GetNewInvoiceAsync();
            db.InvoiceGas.Add(newDbItem);

            FillDbItem(newDbItem, mainInvoice);
            invoiceNum = newDbItem.InvoiceNum;

            AddNewLines(newDbItem, invoice.Lines);
        }
        else if (isExistingDraft)
        {
            FillDbItem(existingDbItem!, mainInvoice);
            invoiceNum = existingDbItem!.InvoiceNum;

            FillExistingLines(existingDbItem, invoice!);
            SetModified(existingDbItem);
        }

        var dbItem = newDbItem ?? existingDbItem;
        SetSequentialLineNums(dbItem);

        // Validate required header fields if all lines are approved
        var allLinesApproved = invoice.Lines.All(x => x.IsApproved);
        if (allLinesApproved)
            InvoiceHeaderValidator.ValidateRequiredFields(mainInvoice, invoiceNum);

        db.SaveChanges();

        // dbItem is guarantee not null here
        await CreateInvoiceSnapshot(dbItem!);

        return invoiceNum;
    }

    private async Task<InvoiceGa> GetNewInvoiceAsync()
    {
        var now = DateTime.UtcNow;

        var newItem = new InvoiceGa();
        newItem.InvoiceNum = await GetNewDraftNumAsync();
        newItem.InvoiceDate = now.ToDateOnly();
        newItem.CreatedTime = now;
        newItem.CreatedBy = appUserId;
        newItem.ModifiedTime = now;
        newItem.ModifiedBy = appUserId;

        return newItem;
    }

    private void SetModified(InvoiceGa dbItem)
    {
        dbItem.ModifiedTime = DateTime.UtcNow;
        dbItem.ModifiedBy = appUserId;
    }

    private static async Task<string> GetNewDraftNumAsync()
    {
        using var db = Main.CreateContext();
        return await Util.GetNewDealNumAsync(17, db);
    }

    private static void FillDbItem(InvoiceGa dbItem, MainInvoice mainInvoice)
    {
        dbItem.Month = mainInvoice.Month;
        dbItem.InvoiceDate = mainInvoice.InvoiceDate;
        dbItem.DueDate = mainInvoice.DueDate;
        dbItem.InternalEntityAttn = mainInvoice.InternalEntityAttn;
        dbItem.InternalEntityTelephoneNum = mainInvoice.InternalEntityTelephoneNum;
        dbItem.InternalEntityEmailAddress = mainInvoice.InternalEntityEmailAddress;
        dbItem.InternalEntityAddressLine1 = mainInvoice.InternalEntityAddressLine1;
        dbItem.InternalEntityAddressLine2 = mainInvoice.InternalEntityAddressLine2;
        dbItem.InternalEntityCity = mainInvoice.InternalEntityCity;
        dbItem.InternalEntityStateId = mainInvoice.InternalEntityStateId;
        dbItem.InternalEntityCountryId = mainInvoice.InternalEntityCountryId;
        dbItem.InternalEntityZip = mainInvoice.InternalEntityZip;
        dbItem.InternalEntityId = mainInvoice.InternalEntityId;
        dbItem.InternalEntityFaxNum = mainInvoice.InternalEntityFaxNum;
        dbItem.CounterpartyId = mainInvoice.CounterpartyId;
        dbItem.CustomerNum = mainInvoice.CustomerNum;
        dbItem.ContactName = mainInvoice.ContactName;
        dbItem.ContactPhone = mainInvoice.ContactTelephoneNum;
        dbItem.ContactEmail = mainInvoice.ContactEmail;
        dbItem.InvoiceTypeId = mainInvoice.InvoiceTypeId;
        dbItem.ContractPaymentTypeId = mainInvoice.ContractPaymentTypeId;
        dbItem.CounterpartyAddressLine1 = mainInvoice.CounterpartyAddressLine1;
        dbItem.CounterpartyAddressLine2 = mainInvoice.CounterpartyAddressLine2;
        dbItem.CounterpartyFaxNum = mainInvoice.CounterpartyFaxNum;
        dbItem.CounterpartyEmailAddresses = mainInvoice.CounterpartyEmailAddresses;
        dbItem.CounterpartyTelephoneNum = mainInvoice.CounterpartyTelephoneNum;
        dbItem.CounterpartyCity = mainInvoice.CounterpartyCity;
        dbItem.CounterpartyZip = mainInvoice.CounterpartyZip;
        dbItem.CounterpartyCountryId = mainInvoice.CounterpartyCountryId;
        dbItem.CounterpartyStateId = mainInvoice.CounterpartyStateId;
        dbItem.CounterpartyAttn = mainInvoice.CounterpartyAttn;
        dbItem.PaymentBankName = mainInvoice.PaymentBankName;
        dbItem.PaymentCityState = mainInvoice.PaymentCityState;
        dbItem.PaymentAbaNumWire = mainInvoice.PaymentAbaNumWire;
        dbItem.PaymentAbaNumAch = mainInvoice.PaymentAbaNumAch;
        dbItem.PaymentAccountName = mainInvoice.PaymentAccountName;
        dbItem.PaymentForFurtherCreditTo = mainInvoice.PaymentForFurtherCreditTo;
        dbItem.PaymentCreditName = mainInvoice.PaymentCreditName;
        dbItem.PaymentAccountNum = mainInvoice.PaymentAccountNum;
        dbItem.Subtotal = mainInvoice.Subtotal;
        dbItem.SalesTaxRate = mainInvoice.SalesTaxRate;
        dbItem.SalesTaxAmount = mainInvoice.SalesTaxAmount;
        dbItem.GrandTotal = mainInvoice.GrandTotal;
        dbItem.TotalQuantity = mainInvoice.TotalQuantity;
        dbItem.ActualVolume = mainInvoice.ActualVolume;
        dbItem.UsageDiff = mainInvoice.UsageDiff;
        dbItem.Notes = mainInvoice.Notes;
    }

    private void FillExistingLines(InvoiceGa dbItem, GasInvoice invoice)
    {
        RemoveMissingLines(dbItem, invoice);
        UpdateExistingLines(dbItem, invoice);
        var newLines = invoice.Lines.Where(x => !x.Id.HasValue).ToList();
        AddNewLines(dbItem, newLines);
    }

    private static void RemoveMissingLines(InvoiceGa dbItem, GasInvoice invoice)
    {
        var existingLineIds = dbItem.InvoiceGasLines.Select(x => x.Id).ToList();
        var incomingLineIds = invoice.Lines.Where(x => x.Id.HasValue).Select(x => x.Id!.Value).ToList();

        var missingLineIds = existingLineIds.Except(incomingLineIds).ToList();

        foreach (var lineId in missingLineIds)
        {
            var line = dbItem.InvoiceGasLines.FirstOrDefault(x => x.Id == lineId);
            if (line != null)
                dbItem.InvoiceGasLines.Remove(line);
        }
    }

    private void UpdateExistingLines(InvoiceGa dbItem, GasInvoice invoice)
    {
        var existingLines = dbItem.InvoiceGasLines.ToList();
        var incomingLines = invoice.Lines.Where(x => x.Id.HasValue).ToList();

        var approvedTime = DateTime.UtcNow;

        foreach (var line in incomingLines)
        {
            var existingLine = existingLines.FirstOrDefault(x => x.Id == line.Id!.Value);
            if (existingLine == null)
                continue;

            void AddApproval()
            {
                existingLine.ApprovedBy = appUserId;
                existingLine.ApprovedTime = approvedTime;
            }

            void RemoveApproval()
            {
                existingLine.ApprovedBy = null;
                existingLine.ApprovedTime = null;
            }

            var canApprove = hasApprovalPermission;

            var hasChanges = HaveValuesChanged(existingLine, line);
            if (hasChanges)
            {
                SetValues(existingLine, line);

                if (canApprove)
                {
                    if (line.IsApproved)
                        AddApproval();
                    else
                        RemoveApproval();
                }
                else
                    RemoveApproval();
            }
            else // no changes
            {
                if (canApprove)
                {
                    if (line.IsApproved)
                    {
                        if (existingLine.ApprovedBy == null)
                            AddApproval();
                    }
                    else
                        RemoveApproval();
                }
                else
                {
                    if (!line.IsApproved)
                        RemoveApproval();
                }
            }
        }
    }

    private static bool HaveValuesChanged(InvoiceGasLine existingLine, InvoiceGasLineItem line)
    {
        var dealIdChanged = existingLine.DealId != line.DealId;
        var lineNumChanged = existingLine.LineNum != line.LineNum;
        var flowDaysChanged = existingLine.FlowDays != line.FlowDays;
        var meterIdChanged = existingLine.MeterId != line.MeterId;
        var quantityChanged = existingLine.Quantity != line.Quantity;
        var priceChanged = existingLine.Price != line.Price;
        var adderChanged = existingLine.Adder != line.Adder;
        var amountChanged = existingLine.Amount != line.Amount;
        var descriptionChanged = existingLine.Description != line.Description;
        var pipelineCustomTextChanged = existingLine.PipelineCustomText != line.PipelineCustomText;
        var meterCustomTextChanged = existingLine.MeterCustomText != line.MeterCustomText;

        var hasChanges = dealIdChanged || lineNumChanged || flowDaysChanged
            || meterIdChanged || quantityChanged || priceChanged
            || adderChanged || amountChanged || descriptionChanged
            || pipelineCustomTextChanged || meterCustomTextChanged;
        return hasChanges;
    }

    private static void SetValues(InvoiceGasLine dbLine, InvoiceGasLineItem line)
    {
        dbLine.DealId = line.DealId;
        dbLine.LineNum = line.LineNum;
        dbLine.FlowDays = line.FlowDays;
        dbLine.MeterId = line.MeterId;
        dbLine.Quantity = line.Quantity;
        dbLine.Price = line.Price;
        dbLine.Adder = line.Adder;
        dbLine.Amount = line.Amount;
        dbLine.Description = line.Description;
        dbLine.PipelineCustomText = line.PipelineCustomText;
        dbLine.MeterCustomText = line.MeterCustomText;
    }

    private void AddNewLines(InvoiceGa dbItem, IEnumerable<InvoiceGasLineItem> lines)
    {
        var approvedTime = DateTime.UtcNow;

        foreach (var line in lines)
        {
            var setApproved = hasApprovalPermission && line.IsApproved;

            var dbLine = new InvoiceGasLine
            {
                InvoiceId = dbItem.Id,
                DealId = line.DealId,
                LineNum = line.LineNum,
                FlowDays = line.FlowDays,
                MeterId = line.MeterId,
                Quantity = line.Quantity,
                Price = line.Price,
                Adder = line.Adder,
                Amount = line.Amount,
                Description = line.Description,
                PipelineCustomText = line.PipelineCustomText,
                MeterCustomText = line.MeterCustomText,
                ApprovedBy = setApproved ? appUserId : null,
                ApprovedTime = setApproved ? approvedTime : null
            };

            dbItem.InvoiceGasLines.Add(dbLine);
        }
    }

    private static void CheckDealContracts(List<int> dealIds)
    {
        using var db = Main.CreateContext();

        var primaryDealContract = (
            from q in db.Deals
            where q.NettingContractNumber != null
                && dealIds.Contains(q.Id)
            select new { q.Id, q.NettingContractNumber }
        ).FirstOrDefault();

        if (primaryDealContract == null)
            return;

        var primaryContractNumber = primaryDealContract.NettingContractNumber;
        var primaryDealId = primaryDealContract.Id;

        var dealsWithInvalidContract = (
            from q in db.Deals
            where q.NettingContractNumber != null
                && q.Id != primaryDealId
                && dealIds.Contains(q.Id)
                && q.NettingContractNumber != primaryContractNumber
            select new { q.Id, q.NettingContractNumber }
        ).ToList();

        if (dealsWithInvalidContract.Count > 0)
        {
            throw new Exception("One or more of the selected deals has a mismatched contract.");
        }
    }

    private static void SetSequentialLineNums(InvoiceGa? dbItem)
    {
        var lines = dbItem?.InvoiceGasLines;
        if (lines == null)
            return;

        var newLineNum = 1;
        var orderedLines = lines.OrderBy(x => x.LineNum).ToList();
        foreach (var line in orderedLines)
        {
            line.LineNum = newLineNum++;
        }
    }

    public async Task CreateInvoiceSnapshot(InvoiceGa invoice)
    {
        using var db = Main.CreateContext();
        var existingDbItem = await db.GasInvoiceSnapshots
            .Where(x => x.InvoiceId == invoice.Id)
            .FirstOrDefaultAsync();


        // snapshot should not exist unless invoice has already been made / has not had one made
        if (existingDbItem != null)
            return;

        // delivery = market, receipt = supply 
        var firstOfMonth = Util.Date.FirstDayOfMonth(invoice.Month);
        var lastOfMonth = Util.Date.LastDayOfMonth(invoice.Month);
        var valParams = new ValParams();

        valParams.TransactionTypeIds.Add((int)Enums.TransactionType.PhysicalGas);
        valParams.PositionDateRanges.Add(new DateRange(Enums.DateStyle.MonthRange, firstOfMonth, lastOfMonth));
        valParams.InternalEntityIds.Add(invoice.InternalEntityId);
        valParams.IncludeBasisInContractPrice = false;

        var val = new PathValuaterGas();
        var valLines = await val.GetValuationValues(valParams);

        var snapshotLines = (
            from q in valLines
            where q.DeliveryCounterpartyId == invoice.CounterpartyId
            select new GasInvoiceSnapshot
            {
                InvoiceId = invoice.Id,
                Day = q.Day,
                ReceiptDeal = q.ReceiptDeal,
                ReceiptInternalEntity = q.ReceiptInternalEntity,
                ReceiptCounterparty = q.ReceiptCounterparty,
                ReceiptCounterpartyShort = q.ReceiptCounterpartyShort,
                ReceiptDealPurpose = q.ReceiptDealPurpose,
                ReceiptBaseOrSwing = q.ReceiptBaseOrSwing,
                ReceiptPipe = q.ReceiptPipe,
                ReceiptPoint = q.ReceiptPoint,
                ReceiptMeter = q.ReceiptMeter,
                ReceiptPipeContract = q.ReceiptPipeContract,
                ReceiptPipeContractOwner = q.ReceiptPipeContractOwner,
                ReceiptNomVol = q.ReceiptNomVol,
                ReceiptActualVol = q.ReceiptActualVol,
                ReceiptVol = q.ReceiptVol,
                ReceiptContractPrice = q.ReceiptContractPrice,
                ReceiptPlaPercentage = q.ReceiptPlaPercentage,
                ReceiptPlaRate = q.ReceiptPlaRate,
                ReceiptPlaDeduct = q.ReceiptPlaDeduct,
                ReceiptQbRate = q.ReceiptQbRate,
                ReceiptQbAmount = q.ReceiptQbAmount,
                ReceiptActualFee = q.ReceiptActualFee,
                ReceiptAdjustment = q.ReceiptAdjustment,
                ReceiptPriceAdj = q.ReceiptPriceAdj,
                ReceiptPurchasePrice = q.ReceiptPurchasePrice,
                ReceiptTransportTotal = q.ReceiptTransportTotal,
                ReceiptInvoicePrice = q.ReceiptInvoicePrice,
                ReceiptInvoiceAmount = q.ReceiptInvoiceAmount,
                ReceiptCrudeBasePrice = q.ReceiptCrudeBasePrice,
                ReceiptCrudeRollPrice = q.ReceiptCrudeRollPrice,
                ReceiptCrudeArgusAdj1 = q.ReceiptCrudeArgusAdj1,
                ReceiptCrudeArgusAdj2 = q.ReceiptCrudeArgusAdj2,
                TneMeter = q.TneMeter,
                DeliveryDeal = q.DeliveryDeal,
                DeliveryInternalEntity = q.DeliveryInternalEntity,
                DeliveryCounterparty = q.DeliveryCounterparty,
                DeliveryCounterpartyShort = q.DeliveryCounterpartyShort,
                DeliveryDealPurpose = q.DeliveryDealPurpose,
                DeliveryBaseOrSwing = q.DeliveryBaseOrSwing,
                DeliveryPipe = q.DeliveryPipe,
                DeliveryPoint = q.DeliveryPoint,
                DeliveryMeter = q.DeliveryMeter,
                DeliveryPipeContract = q.DeliveryPipeContract,
                DeliveryPipeContractOwner = q.DeliveryPipeContractOwner,
                DeliveryNomVol = q.DeliveryNomVol,
                DeliveryActualVol = q.DeliveryActualVol,
                DeliveryVol = q.DeliveryVol,
                DeliveryContractPrice = q.DeliveryContractPrice,
                DeliveryPlaPercentage = q.DeliveryPlaPercentage,
                DeliveryPlaRate = q.DeliveryPlaRate,
                DeliveryPlaDeduct = q.DeliveryPlaDeduct,
                DeliveryQbRate = q.DeliveryQbRate,
                DeliveryQbAmount = q.DeliveryQbAmount,
                DeliveryActualFee = q.DeliveryActualFee,
                DeliveryPriceAdj = q.DeliveryPriceAdj,
                DeliveryInvoicePrice = q.DeliveryInvoicePrice,
                DeliveryInvoiceAmount = q.DeliveryInvoiceAmount,
                DeliveryCrudeBasePrice = q.DeliveryCrudeBasePrice,
                DeliveryCrudeRollPrice = q.DeliveryCrudeRollPrice,
                DeliveryCrudeArgusAdj1 = q.DeliveryCrudeArgusAdj1,
                DeliveryCrudeArgusAdj2 = q.DeliveryCrudeArgusAdj2,
                DeliveryAdjustment = q.DeliveryAdjustment,
                NonJurisdictional = q.NonJurisdictional,
                IsAgency = q.IsAgency,
                IsNetback = q.IsNetback,
                HasTransfers = q.HasTransfers,
                ReceiptInternalContractNum = q.ReceiptInternalContractNum,
                ReceiptCounterpartyContractNum = q.ReceiptCounterpartyContractNum,
                DeliveryInternalContractNum = q.DeliveryInternalContractNum,
                DeliveryCounterpartyContractNum = q.DeliveryCounterpartyContractNum,
                IsReceiptTransportOverridden = q.IsReceiptTransportOverridden,
                IsReceiptPriceAdjOverridden = q.IsReceiptPriceAdjOverridden,
                IsReceiptContractPriceOverridden = q.IsReceiptContractPriceOverridden,
                IsDeliveryPriceAdjOverridden = q.IsDeliveryPriceAdjOverridden,
                IsDeliveryActualFeeOveridden = q.IsDeliveryActualFeeOveridden,
                IsDeliveryAdjustmentOverridden = q.IsDeliveryAdjustmentOverridden,
                IsReceiptAdjustmentOverridden = q.IsReceiptAdjustmentOverridden,
                IsReceiptActualFeeOverridden = q.IsReceiptActualFeeOverridden,
                IsDeliveryContractPriceOverridden = q.IsDeliveryContractPriceOverridden,
                SupplyNomId = q.SupplyNomId,
                MarketNomId = q.MarketNomId,
                LastTransferDealId = q.LastTransferDealId,
                ReceiptDealContractId = q.ReceiptDealContractId,
                DeliveryDealContractId = q.DeliveryDealContractId,
                ReceiptDealId = q.ReceiptDealId,
                ReceiptProductId = q.ReceiptProductId,
                ReceiptInternalEntityId = q.ReceiptInternalEntityId,
                ReceiptCounterpartyId = q.ReceiptCounterpartyId,
                ReceiptTransferDealId = q.ReceiptTransferDealId,
                ReceiptPipeId = q.ReceiptPipeId,
                ReceiptPointId = q.ReceiptPointId,
                ReceiptMeterId = q.ReceiptMeterId,
                ReceiptPipeContractId = q.ReceiptPipeContractId,
                DeliveryDealId = q.DeliveryDealId,
                DeliveryProductId = q.DeliveryProductId,
                DeliveryInternalEntityId = q.DeliveryInternalEntityId,
                DeliveryCounterpartyId = q.DeliveryCounterpartyId,
                DeliveryTransferDealId = q.DeliveryTransferDealId,
                DeliveryPipeId = q.DeliveryPipeId,
                DeliveryPointId = q.DeliveryPointId,
                DeliveryMeterId = q.DeliveryMeterId,
                TneMeterId = q.TneMeterId,
                DeliveryPipeContractId = q.DeliveryPipeContractId,
                ReceiptPortfolioId = q.ReceiptPortfolioId,
                ReceiptDealPurposeId = q.ReceiptDealPurposeId,
                DeliveryDealPurposeId = q.DeliveryDealPurposeId,
            }
        ).ToList();

        db.GasInvoiceSnapshots.AddRange(snapshotLines);
        await db.SaveChangesAsync();
    }
}
