using Fast.Web.Logic;
using Fast.Web.Models;
using static Fast.Web.Controllers.InvoiceCrudeDetailController;

namespace Fast.Web.Logic.CrudeOil;

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 Crude Approval";

    public async Task<string> SaveInvoiceAsync(InvoiceSaveType saveType, CrudeInvoice 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}");

        // gas doesn't allow an invoice to reference multiple contracts, but crude does allow this.

        using var db = Main.CreateContext();

        InvoiceCrude? newDbItem = null;
        var existingDbItem = await db.InvoiceCrudes
            .Include(x => x.InvoiceCrudeLines)
            .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.InvoiceCrudes.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();

        return invoiceNum;
    }

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

        var newItem = new InvoiceCrude();
        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(InvoiceCrude 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(InvoiceCrude 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(InvoiceCrude dbItem, CrudeInvoice invoice)
    {
        RemoveMissingLines(dbItem, invoice);
        UpdateExistingLines(dbItem, invoice);
        var newLines = invoice.Lines.Where(x => !x.Id.HasValue).ToList();
        AddNewLines(dbItem, newLines);
    }

    private static void RemoveMissingLines(InvoiceCrude dbItem, CrudeInvoice invoice)
    {
        var existingLineIds = dbItem.InvoiceCrudeLines.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.InvoiceCrudeLines.FirstOrDefault(x => x.Id == lineId);
            if (line != null)
                dbItem.InvoiceCrudeLines.Remove(line);
        }
    }

    private void UpdateExistingLines(InvoiceCrude dbItem, CrudeInvoice invoice)
    {
        var existingLines = dbItem.InvoiceCrudeLines.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(InvoiceCrudeLine existingLine, InvoiceCrudeLineItem line)
    {
        var dealIdChanged = existingLine.DealId != line.DealId;
        var lineNumChanged = existingLine.LineNum != line.LineNum;
        var deliveryMeterIdChanged = existingLine.DeliveryMeterId != line.DeliveryMeterId;
        var receiptMeterIdChanged = existingLine.ReceiptMeterId != line.ReceiptMeterId;
        var quantityChanged = existingLine.Quantity != line.Quantity;
        var priceChanged = existingLine.BasePrice != line.BasePrice;
        var priceAdj1Changed = existingLine.PriceAdj1 != line.PriceAdj1;
        var priceAdj2Changed = existingLine.PriceAdj2 != line.PriceAdj2;
        var rollPriceChanged = existingLine.RollPrice != line.RollPrice;
        var netPriceChanged = existingLine.NetPrice != line.NetPrice;
        var pipelinePercentageChanged = existingLine.PipelinePercentage != line.PipelinePercentage;
        var pipelineRateChanged = existingLine.PipelineRate != line.PipelineRate;
        var pipelineDeductChanged = existingLine.PipelineDeduct != line.PipelineDeduct;
        var descriptionChanged = existingLine.Description != line.Description;
        var contractNumsChanged = existingLine.InternalContractNum != line.InternalContractNum || existingLine.CounterpartyContractNum != line.CounterpartyContractNum;
        var grossAmountChanged = existingLine.GrossAmount != line.GrossAmount;
        var qbRateChanged = existingLine.QbRate != line.QbRate;
        var qbAmountChanged = existingLine.QbAmount != line.QbAmount;
        var actualFeeChanged = existingLine.ActualFee != line.ActualFee;
        var adjustmentChanged = existingLine.Adjustment != line.Adjustment;
        var invoiceAmountChanged = existingLine.InvoiceAmount != line.InvoiceAmount;
        var pipelineCustomTextChanged = existingLine.PipelineCustomText != line.PipelineCustomText;
        var meterCustomTextChanged = existingLine.MeterCustomText != line.MeterCustomText;

        var hasChanges = dealIdChanged || lineNumChanged
            || deliveryMeterIdChanged || receiptMeterIdChanged || quantityChanged || priceChanged
            || priceAdj1Changed || priceAdj2Changed || rollPriceChanged || netPriceChanged
            || pipelinePercentageChanged || pipelineRateChanged || pipelineDeductChanged
            || descriptionChanged || contractNumsChanged || grossAmountChanged || qbRateChanged
            || qbAmountChanged || actualFeeChanged || adjustmentChanged || invoiceAmountChanged
            || pipelineCustomTextChanged || meterCustomTextChanged;

        return hasChanges;
    }

    private static void SetValues(InvoiceCrudeLine dbLine, InvoiceCrudeLineItem line)
    {
        dbLine.DealId = line.DealId;
        dbLine.LineNum = line.LineNum;
        dbLine.DeliveryMeterId = line.DeliveryMeterId;
        dbLine.ReceiptMeterId = line.ReceiptMeterId;
        dbLine.Quantity = line.Quantity;
        dbLine.BasePrice = line.BasePrice;
        dbLine.PriceAdj1 = line.PriceAdj1;
        dbLine.PriceAdj2 = line.PriceAdj2;
        dbLine.RollPrice = line.RollPrice;
        dbLine.PriceAdj1 = line.PriceAdj1;
        dbLine.PriceAdj2 = line.PriceAdj2;
        dbLine.PipelinePercentage = line.PipelinePercentage;
        dbLine.PipelineRate = line.PipelineRate;
        dbLine.PipelineDeduct = line.PipelineDeduct;
        dbLine.NetPrice = line.NetPrice;
        dbLine.GrossAmount = line.GrossAmount;
        dbLine.QbRate = line.QbRate;
        dbLine.QbAmount = line.QbAmount;
        dbLine.ActualFee = line.ActualFee;
        dbLine.Adjustment = line.Adjustment;
        dbLine.InvoiceAmount = line.InvoiceAmount;
        dbLine.Description = line.Description;
        dbLine.InternalContractNum = line.InternalContractNum;
        dbLine.CounterpartyContractNum = line.CounterpartyContractNum;
        dbLine.PipelineCustomText = line.PipelineCustomText;
        dbLine.MeterCustomText = line.MeterCustomText;
    }

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

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

            var dbLine = new InvoiceCrudeLine
            {
                InvoiceId = dbItem.Id,
                DealId = line.DealId,
                LineNum = line.LineNum,
                DeliveryMeterId = line.DeliveryMeterId,
                ReceiptMeterId = line.ReceiptMeterId,
                Quantity = line.Quantity,
                BasePrice = line.BasePrice,
                RollPrice = line.RollPrice,
                PriceAdj1 = line.PriceAdj1,
                PriceAdj2 = line.PriceAdj2,
                PipelinePercentage = line.PipelinePercentage,
                PipelineRate = line.PipelineRate,
                PipelineDeduct = line.PipelineDeduct,
                NetPrice = line.NetPrice,
                GrossAmount = line.GrossAmount,
                QbRate = line.QbRate,
                QbAmount = line.QbAmount,
                ActualFee = line.ActualFee,
                Adjustment = line.Adjustment,
                InvoiceAmount = line.InvoiceAmount,
                Description = line.Description,
                InternalContractNum = line.InternalContractNum,
                CounterpartyContractNum = line.CounterpartyContractNum,
                ApprovedBy = setApproved ? appUserId : null,
                ApprovedTime = setApproved ? approvedTime : null,
                PipelineCustomText = line.PipelineCustomText,
                MeterCustomText = line.MeterCustomText
            };

            dbItem.InvoiceCrudeLines.Add(dbLine);
        }
    }

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

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