import { ChangeDetectionStrategy, Component, inject, input, OnDestroy, output, signal, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core';
import * as util from '../_shared/utils/util';
import { FAST_KENDO_COMMON, FAST_PAGE_COMMON } from '../app.config';
import { BehaviorSubject, catchError, combineLatest, debounceTime, delay, distinctUntilChanged, filter, map, Observable, of, retry, shareReplay, Subject, switchMap, takeUntil, tap } from 'rxjs';
import { InvoiceSummaryLine, Invoice, InvoiceGasDetailService, InvoiceLineItem, RequiredData, RevisionParams, SaveInvoiceResult, SaveType, InvoiceSummary } from './invoice-gas-detail.service';
import { CustomFormBuilder } from '../_shared/services/custom-form-builder.service';
import { MessageService, promptAction, PromptSettings } from '../_shared/services/message.service';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { CurrencyPipe, DecimalPipe } from '@angular/common';
import { FormArray, FormsModule, Validators } from '@angular/forms';
import { GridDataResult, PageChangeEvent, RowArgs, RowClassArgs, SelectableSettings } from '@progress/kendo-angular-grid';
import dayjs, { Dayjs } from 'dayjs';
import { InvoiceGasService } from '../invoice-gas/invoice-gas.service';
import { MainInvoice } from '../invoice-gas/models/MainInvoice';
import * as models from '../invoice-gas/models';
import { KENDO_DROPDOWNBUTTON } from '@progress/kendo-angular-buttons';
import { PipelineMetersPipe } from '../pipes/pipeline-meters.pipe';
import { DealTraderPipe } from '../pipes/deal-trader.pipe';
import { DropDownFilterSettings } from '@progress/kendo-angular-dropdowns';
import { BigNumber } from 'decimal-eval';
import { FastPanelbarItemComponent } from '../_shared/elements/fast-panelbar-item.component';
import { ToastService } from '../_shared/services/fast-toast.service';
import { AccountingCurrencyPipe } from '../_shared/pipes/accounting-currency.pipe';
import { GasSnapshotLinesComponent } from "../gas-snapshot-lines/gas-snapshot-lines.component";

interface InvoiceNumAndFileName {
  invoiceNum: string;
  fileName: string;
}

@Component({
  selector: 'app-invoice-gas-detail',
  standalone: true,
  imports: [FAST_KENDO_COMMON, FAST_PAGE_COMMON, CurrencyPipe, DecimalPipe, FormsModule, KENDO_DROPDOWNBUTTON, PipelineMetersPipe, DealTraderPipe, AccountingCurrencyPipe, GasSnapshotLinesComponent],
  templateUrl: './invoice-gas-detail.component.html',
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class InvoiceGasDetailComponent implements OnDestroy {
  //generalized component variables and inputs
  util = util;
  dayjs = dayjs;

  localRequiredData: RequiredData;
  invoiceId = input.required<number>();
  counterpartyId = input.required<number>();
  internalEntityId = input.required<number>();
  month = input.required<string>()
  closed = output();
  refreshItems = output();
  invoice: Invoice;
  mainInvoice: MainInvoice = {} as MainInvoice;
  lineItemFormArray: FormArray<util.FormModel<InvoiceLineItem>>;
  invoiceDateForm: util.FormModel<{ invoiceDate: Date }>;
  invoiceDueDateForm: util.FormModel<{ invoiceDueDate: Date }>;
  counterpartyPanelBarTitle = signal("Counterparty");
  internalEntityPanelBarTitle = signal("Internal Entity");
  invoiceDetailsPanelBarTitle = signal("Details");
  lineSelection: number[] = [];
  linesTotalQuantity = signal<number>(null);
  linesDifference = signal<number>(null);
  linesSubTotal = signal<number>(null);
  linesGrandTotal = signal<number>(null);
  linesSalesTaxAmount = signal<number>(null);
  matchingInvoices: InvoiceSummaryLine[];
  matchingInvoicesTotalQuantity = signal<number>(null);
  matchingInvoicesTotalAmount = signal<number>(null);
  totalNomAmount = signal<number>(null);
  invalidDeals = signal<number[]>(null);
  minDate: Date = new Date(1900, 0, 1);
  isInternalEntityExpanded = signal(false);
  isPayInstructionsExpanded = signal(false);
  isCounterpartyExpanded = signal(false);
  isInvoiceDetailsExpanded = signal(false);
  notesAndTotalWidth = signal<number>(null);
  differenceLoading = signal(true);
  gridView: GridDataResult;
  lastSelectedIndex: number | null = null;
  initialLinesOpened$ = new BehaviorSubject<boolean>(false);

  @ViewChild('invoiceHeaderButtons') invoiceHeaderButtons: TemplateRef<unknown>;

  filterSettings: DropDownFilterSettings = {
    operator: "contains",
  };
  selectableSettings: SelectableSettings = {
    checkboxOnly: true,
    mode: 'multiple',
    enabled: true
  };

  //pageSize and skip are needed for virtual scrolling of kendo grid
  // https://www.telerik.com/kendo-angular-ui/components/grid/scroll-modes/virtual
  pageSize = 75;
  skip = 0;

  minimumDynamicColWidth = 150;
  colWidths = {
    spacer1: this.minimumDynamicColWidth,
    checkbox: 35,
    approved: 35,
    dealNum: 115,
    trader: 65,
    flowDays: 70,
    description: 150,
    deliveryPipe: this.minimumDynamicColWidth,
    deliveryMeter: this.minimumDynamicColWidth,
    quantity: 80,
    price: 80,
    adder: 85,
    invoicePrice: 75,
    amount: 95,
    spacer2: this.minimumDynamicColWidth
  };
  totalMinColWidths = Object.values(this.colWidths).reduce((a, b) => a + b, 0);

  //observables, behaviorsubjects, etc.
  destroy$ = new Subject<void>();
  detailLoading$ = new BehaviorSubject<boolean>(true);
  invoice$: Observable<Invoice>;
  refreshInvoice$ = new BehaviorSubject<void>(null);
  requiredData$: Observable<RequiredData>;
  refreshRequiredData$ = new BehaviorSubject<util.RefreshType>(null);
  taxRate$: Observable<number>;
  refreshSalesTaxRate$ = new BehaviorSubject<void>(null);
  downloadInvoiceResult$: Observable<object>;
  downloadInvoice$ = new Subject<InvoiceNumAndFileName>();
  saveResult$: Observable<SaveInvoiceResult>;
  save$ = new Subject<SaveType>();
  deleteResult$: Observable<object>;
  delete$ = new Subject<void>();
  regenerateHeadersResult$: Observable<models.MainInvoice>;
  regenerateHeaders$ = new Subject<void>();
  regenerateLinesResult$: Observable<InvoiceLineItem[]>;
  regenerateLines$ = new Subject<void>();
  pipelines$: Observable<util.Pipe[]>;
  dealNums$: Observable<util.IdName[]>;
  dynamicColumnWidth$: Observable<number>;
  refreshDynamicColumnWidth$ = new Subject<void>();
  reviseResult$: Observable<InvoiceLineItem[]>;
  revise$ = new Subject<void>();
  matchingInvoiceResult$: Observable<InvoiceSummary>;
  refreshMatchingInvoices$ = new Subject<void>();

  //injects
  service = inject(InvoiceGasDetailService);
  fb = inject(CustomFormBuilder);
  messageService = inject(MessageService);
  titleService = inject(Title);
  toast = inject(ToastService);
  activatedRoute = inject(ActivatedRoute);
  InvoiceGasOverviewService = inject(InvoiceGasService);

  //constructor containing observable definitions
  constructor() {
    this.mainInvoice.emailedBy = null;
    this.lineItemFormArray = this.fb.array<util.FormModel<InvoiceLineItem>>([]);

    this.requiredData$ = this.refreshRequiredData$.pipe(
      tap(() => this.detailLoading$.next(true)),
      switchMap(refreshType => {
        const counterpartyId = this.counterpartyId();
        const internalEntityId = this.internalEntityId();
        const month = dayjs(this.month()).toDate();

        return combineLatest([this.service.getRequiredData(counterpartyId, internalEntityId, month), of(refreshType)]);
      }),
      map(([requiredData, refreshType]) => {
        if (refreshType === util.RefreshType.SelfOnly)
          this.detailLoading$.next(false);
        return requiredData;
      }),
      tap((requiredData) => {
        this.localRequiredData = requiredData;
      }),
      shareReplay(1),
      catchError(err => {
        return util.handleError(err, this.messageService)
      }), retry(3)
    );

    this.invoice$ = this.refreshInvoice$.pipe(
      switchMap(() => {
        this.detailLoading$.next(true);
        const month = dayjs(this.month()).toDate();
        return this.service.getInvoice(this.invoiceId(), this.counterpartyId(), this.internalEntityId(), month);
      }),
      map(invoice => {
        if (invoice) {
          util.convertToDates(invoice, ['flowDays']);
          this.invoice = invoice;
          this.mainInvoice = invoice.mainInvoice;
          this.initMainInvoiceDisplay();
          this.setLines(invoice.lines);
        }
        return invoice;
      }),
      tap(() => {
        this.lineSelection = [];
        this.refreshSalesTaxRate$.next(null);
        this.refreshMatchingInvoices$.next(null);
        this.detailLoading$.next(false);
      }),
      shareReplay(1),
      catchError(err => {
        this.detailLoading$.next(false);
        return util.handleError(err, this.messageService);
      }),
      retry(3)
    );

    this.downloadInvoiceResult$ = this.downloadInvoice$.pipe(
      filter(invoiceNumAndFileName => {
        return invoiceNumAndFileName && invoiceNumAndFileName.invoiceNum !== null;
      }),
      switchMap(invoiceNumAndFileName => {
        return this.InvoiceGasOverviewService.downloadInvoice(invoiceNumAndFileName.invoiceNum, invoiceNumAndFileName.fileName);
      }),
      tap(res => {
        util.openOrSaveFile(res.fileBlob, res.fileName);
      }),
      catchError(err => {
        return util.handleError(err, this.messageService);
      }), retry(3)
    );

    this.matchingInvoiceResult$ = this.refreshMatchingInvoices$.pipe(
      delay(200),
      switchMap(() => {
        this.differenceLoading.set(true);

        const month = dayjs(this.month()).toDate();
        return this.service.getInvoiceVolAndQuantity(month, this.internalEntityId(), this.counterpartyId());
      }),
      tap(result => {
        this.mainInvoice.actualVolume = result.totalQuantity;
        this.totalNomAmount.set(result.totalAmount);
        this.matchingInvoices = result.lines ?? [];
        const mainInvNumber = this.mainInvoice.invoiceNum;

        // Exclude original invoice item from quantity, but still include it in volume
        const { totalQuantity, totalAmounts: totalNom } = result.lines.reduce(
          (acc, item) => {
            if (item.invoiceNum !== mainInvNumber) {
              acc.totalQuantity += item.totalQuantity;
              acc.totalAmounts += item.totalAmount;
            }
            return acc;
          },
          {
            totalQuantity: 0,
            totalAmounts: 0,
          }
        );

        this.matchingInvoicesTotalQuantity.set(totalQuantity);
        this.matchingInvoicesTotalAmount.set(totalNom);
        this.refreshTotals();
        this.differenceLoading.set(false);
      }),
      shareReplay(1),
      catchError(err => {
        return util.handleError(err, this.messageService);
      }), retry(3)
    )


    this.saveResult$ = this.save$.pipe(
      switchMap(saveType => {
        this.detailLoading$.next(true);
        this.invoice.lines = this.lineItemFormArray.getRawValue();
        this.invoice.lines.forEach(line => {
          // Here we are checking if the combobox is a string from [allowCustom] and if so then we know it's custom text
          // If it is not a string, we can leave it as is.
          const pipelineCustomText = typeof line.deliveryPipelineId === 'string' ? line.deliveryPipelineId : null;
          const meterCustomText = typeof line.meterId === 'string' ? line.meterId : null;
          if (pipelineCustomText) {
            line.pipelineCustomText = pipelineCustomText;
            line.deliveryPipelineId = null;
          }
          if (meterCustomText) {
            line.meterCustomText = meterCustomText;
            line.meterId = null;
          }
        })
        const mainInvoice = this.mainInvoice;
        mainInvoice.salesTaxAmount = this.linesSalesTaxAmount();
        mainInvoice.grandTotal = this.linesGrandTotal();
        mainInvoice.subtotal = this.linesSubTotal();
        mainInvoice.totalQuantity = this.linesTotalQuantity();
        mainInvoice.usageDiff = this.linesDifference();
        mainInvoice.invoiceDate = this.invoiceDateForm.getRawValue().invoiceDate;
        mainInvoice.dueDate = this.invoiceDueDateForm.getRawValue().invoiceDueDate;
        this.invoice.mainInvoice = util.convertDatesToDateOnlyStrings(this.mainInvoice, ['month', 'invoiceDate', 'dueDate']);
        return this.service.saveInvoice(saveType, this.invoice);
      }),
      tap(() => {
        this.toast.success('save successful');
        this.closedClicked();
      }),
      shareReplay(1),
      catchError(err => {
        this.detailLoading$.next(false);
        this.closedClicked();
        return util.handleError(err, this.messageService)
      }), retry(3)
    );

    this.deleteResult$ = this.delete$.pipe(
      switchMap(() => {
        this.detailLoading$.next(true);
        this.lineItemFormArray.clear();
        return this.service.deleteInvoice(this.mainInvoice.id);
      }),
      tap(() => {
        this.toast.success('delete successful');
        this.detailLoading$.next(false);
        this.closedClicked();
        this.refreshItems.emit();
      }),
      shareReplay(1),
      catchError(err => {
        return util.handleError(err, this.messageService)
      }), retry(3)
    );

    this.taxRate$ = this.refreshSalesTaxRate$.pipe(
      switchMap(() => {
        const hasLines = this.lineItemFormArray.length > 0;
        if (!hasLines)
          return of(0);

        const firstMeterId = this.lineItemFormArray.controls[0].controls.meterId.value;
        // if type is number then it's a valid meterId, otherwise it's probably custom text
        const hasValidMeter = typeof firstMeterId === 'number';

        if (!hasValidMeter)
          return of(0);

        const month = dayjs(this.month()).toDate();
        return this.service.getTaxRate(firstMeterId, this.counterpartyId(), month);
      }),
      tap(taxRate => {
        this.mainInvoice.salesTaxRate = taxRate;
        this.refreshTotals();
      }),
      shareReplay(1),
      catchError(err => {
        return util.handleError(err, this.messageService)
      }), retry(3)
    );

    this.regenerateHeadersResult$ = this.regenerateHeaders$.pipe(
      switchMap(() => {
        this.detailLoading$.next(true);
        const counterpartyId = this.counterpartyId();
        const internalEntityId = this.internalEntityId();
        const month = dayjs(this.month()).toDate();
        return this.service.getMainInvoice(counterpartyId, internalEntityId, month);
      }),
      tap((newMainInvoice) => {
        this.mainInvoice = {
          ...newMainInvoice,
          id: this.mainInvoice.id,
          invoiceNum: this.mainInvoice.invoiceNum,
          month: this.mainInvoice.month,
          fileNameOriginal: this.mainInvoice.fileNameOriginal,
          fileNameOnDisk: this.mainInvoice.fileNameOnDisk,
          subtotal: this.mainInvoice.subtotal,
          salesTaxRate: this.mainInvoice.salesTaxRate,
          salesTaxAmount: this.mainInvoice.salesTaxAmount,
          grandTotal: this.mainInvoice.grandTotal,
          notes: this.mainInvoice.notes,
          totalQuantity: this.mainInvoice.totalQuantity,
          actualVolume: this.mainInvoice.actualVolume,
          usageDiff: this.mainInvoice.usageDiff
        };

        this.lineItemFormArray.controls.forEach(fg => {
          fg.controls.isApproved.setValue(false);
        });

        this.detailLoading$.next(false);
        this.toast.warning('Save to complete changes');
      }),
      shareReplay(1),
      catchError(err => {
        this.detailLoading$.next(false);
        return util.handleError(err, this.messageService);
      }),
      retry(3)
    );

    this.regenerateLinesResult$ = this.regenerateLines$.pipe(
      tap(() => {
        this.detailLoading$.next(true);
      }),
      switchMap(() => {
        const counterpartyId = this.counterpartyId();
        const internalEntityId = this.internalEntityId();
        const month = dayjs(this.month()).toDate();
        const existingLines = this.lineItemFormArray.getRawValue();
        // we need to switch the custom text back to the correct properites
        existingLines.forEach(line => {
          const pipelineCustomText = typeof line.deliveryPipelineId === 'string' ? line.deliveryPipelineId : null;
          const meterCustomText = typeof line.meterId === 'string' ? line.meterId : null;
          if (pipelineCustomText) {
            line.pipelineCustomText = pipelineCustomText;
            line.deliveryPipelineId = null;
          }
          if (meterCustomText) {
            line.meterCustomText = meterCustomText;
            line.meterId = null;
          }
        });
        return this.service.regenerateLines(counterpartyId, internalEntityId, month, existingLines);
      }),
      tap(newLines => {
        if (newLines.length === 0) {
          this.toast.info('All lines already up to date.');
          this.detailLoading$.next(false);
          return;
        }

        const existingLines = this.lineItemFormArray.getRawValue();
        const detailLines = [...existingLines, ...newLines];
        this.setLines(detailLines);
        this.detailLoading$.next(false);

        this.refreshSalesTaxRate$.next(null);
        this.refreshMatchingInvoices$.next(null);
      }),
      shareReplay(1),
      catchError(err => {
        this.detailLoading$.next(false);
        return util.handleError(err, this.messageService);
      }),
      retry(3)
    );

    this.reviseResult$ = this.revise$.pipe(
      filter(() => {
        const hasSelectedLines = this.lineItemFormArray.getRawValue().length > 0 && this.lineSelection.length > 0;
        if (!hasSelectedLines)
          this.toast.error('Please select at least one line to revise.');
        return hasSelectedLines;
      }),
      tap(() => {
        this.detailLoading$.next(true);
      }),
      switchMap(() => {
        const revisionParams: RevisionParams = {
          counterpartyId: this.counterpartyId(),
          internalEntityId: this.internalEntityId(),
          month: dayjs(this.month()).toDate(),
          existingLines: this.lineItemFormArray.getRawValue(),
          lineSelection: this.lineSelection
        };
        revisionParams.existingLines.forEach(line => {
          const pipelineCustomText = typeof line.deliveryPipelineId === 'string' ? line.deliveryPipelineId : null;
          const meterCustomText = typeof line.meterId === 'string' ? line.meterId : null;
          if (pipelineCustomText) {
            line.pipelineCustomText = pipelineCustomText;
            line.deliveryPipelineId = null;
          }
          if (meterCustomText) {
            line.meterCustomText = meterCustomText;
            line.meterId = null;
          }
        })
        return this.service.reviseInvoice(revisionParams);
      }),
      tap(newLines => {
        this.lineItemFormArray.clear();
        this.lineSelection = [];
        this.mainInvoice.id = 0;
        this.mainInvoice.invoiceNum = "";
        this.mainInvoice.fileNameOnDisk = "";
        this.mainInvoice.fileNameOriginal = "";
        this.mainInvoice.emailedBy = null;
        this.mainInvoice.emailedByName = "";
        this.mainInvoice.emailedTime = null;
        this.mainInvoice.invoiceTypeID = 2;
        this.mainInvoice.invoiceType = "Revised Invoice";
        this.initMainInvoiceDisplay();
        this.setLines(newLines);
        this.refreshTotals();
        this.detailLoading$.next(false);
      }),
      shareReplay(1),
      catchError(err => {
        this.detailLoading$.next(false);
        return util.handleError(err, this.messageService);
      }),
      retry(3)
    );

    this.dynamicColumnWidth$ = this.refreshDynamicColumnWidth$.pipe(
      debounceTime(150),
      switchMap(() => {
        return new Observable<number>(subscriber => {
          requestAnimationFrame(() => {
            const winElem = document.querySelector('app-invoice-gas-detail fast-window dialog') as HTMLElement;
            const winWidth = winElem?.offsetWidth ?? 0;

            let columnWidth: number;
            if (winWidth > this.totalMinColWidths)
              columnWidth = null; // Return null to auto-size the column
            else
              columnWidth = this.minimumDynamicColWidth;

            subscriber.next(columnWidth);
            subscriber.complete();
          });
        });
      }),
      distinctUntilChanged(),
      tap(() => {
        requestAnimationFrame(() => {
          setTimeout(() => {
            const winElem = document.querySelector('app-invoice-gas-detail fast-window dialog') as HTMLElement;
            const winWidth = winElem?.offsetWidth ?? 0;
            const colSpacerElem = document.querySelector('.columnSpacer') as HTMLElement;
            const colSpacerWidth = colSpacerElem?.offsetWidth ?? 0;
            const scrollBarWidth = 20;
            const newNotesAndTotalWidth = winWidth - (colSpacerWidth * 2) - scrollBarWidth;
            this.notesAndTotalWidth.set(newNotesAndTotalWidth);
          }, 100);
        });
      }),
      shareReplay(1)
    );

    this.pipelines$ = this.requiredData$.pipe(map(data => data.pipelines));
    this.dealNums$ = this.requiredData$.pipe(map(data => data.dealNums));
  }

  //methods
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }

  private getLineItemFormGroup(item: InvoiceLineItem): util.FormModel<InvoiceLineItem> {
    const fb = this.fb;
    const fg: util.FormModel<InvoiceLineItem> = fb.group({
      id: fb.ctr(item.id),
      lineNum: fb.ctr(item.lineNum),
      dealId: fb.ctr(item.dealId),
      flowDays: fb.ctr(item.flowDays),
      deliveryPipelineId: fb.ctr(item.deliveryPipelineId),
      meterId: fb.ctr(item.meterId),
      quantity: fb.ctr(item.quantity),
      price: fb.ctr(item.price),
      adder: fb.ctr(item.adder),
      invoicePrice: fb.ctr(item.invoicePrice),
      amount: fb.ctr(item.amount),
      description: fb.ctr(item.description),
      isApproved: fb.ctr(item.isApproved),
      isRegenerated: fb.ctr(item.isRegenerated)
    });

    const resetApproval = (): void => {
      if (fg.controls.isApproved.value) {
        fg.controls.isApproved.setValue(false);
      }
    };

    fg.controls.flowDays.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
      resetApproval();
    });
    fg.controls.description.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
      resetApproval();
    });
    fg.controls.deliveryPipelineId.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
      if (typeof fg.controls.deliveryPipelineId.value === 'number' || value === undefined) {
        fg.controls.meterId.setValue(null);
      }
      this.checkForSingleMeter(fg);
      resetApproval();
    });
    fg.controls.meterId.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
      this.refreshSalesTaxRate$.next(null);
      resetApproval();
    });
    fg.controls.quantity.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
      this.refreshTotals();
      resetApproval();
    });
    fg.controls.price.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
      this.refreshTotals();
      resetApproval();
    });
    fg.controls.adder.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
      this.refreshTotals();
      resetApproval();
    });
    return fg;
  }

  private initMainInvoiceDisplay(): void {
    const invoiceNum = this.mainInvoice.invoiceNum;
    const invoiceType = this.mainInvoice.invoiceType;
    this.invoiceDetailsPanelBarTitle.set(invoiceType + " " + invoiceNum);
    this.counterpartyPanelBarTitle.set(this.mainInvoice.counterparty);
    this.internalEntityPanelBarTitle.set(this.mainInvoice.internalEntity);
    this.invoiceDateForm = this.getInvoiceDateForm();
    this.invoiceDueDateForm = this.getInvoiceDueDateForm();
  }

  private getInvoiceDateForm(): util.FormModel<{ invoiceDate: Date }> {
    const fb = this.fb;
    const fg: util.FormModel<{ invoiceDate: Date }> = fb.group({
      invoiceDate: fb.ctr(this.mainInvoice.invoiceDate, Validators.required),
    });

    return fg;
  }

  private getInvoiceDueDateForm(): util.FormModel<{ invoiceDueDate: Date }> {
    const fb = this.fb;
    const fg: util.FormModel<{ invoiceDueDate: Date }> = fb.group({
      invoiceDueDate: fb.ctr(this.mainInvoice.dueDate),
    });

    return fg;
  }

  selectByLineNumber(item: RowArgs): number {
    const value = (item.dataItem as util.FormModel<InvoiceLineItem>).controls.lineNum.value;
    return value;
  }

  private getMetersForPipeline(pipeId: number | string): util.Meter[] {
    if (!pipeId || !this.localRequiredData?.meters || typeof pipeId === "string") return [];
    return this.localRequiredData.meters.filter(meter => meter.pipeId === pipeId);
  }

  private checkForSingleMeter(fg: util.FormModel<InvoiceLineItem>): void {
    const pipeId = fg.controls.deliveryPipelineId.value;
    const metersForPipeline = this.getMetersForPipeline(pipeId);

    const hasSingleMeter = metersForPipeline.length === 1;
    if (hasSingleMeter) {
      const singleMeterId = metersForPipeline[0].meterId;
      fg.controls.meterId.setValue(singleMeterId);
    }
  }

  private calculateAmounts(): void {
    //use bignumber to avoid floating point errors; we need to match c# decimal values
    this.lineItemFormArray.controls.forEach((fg) => {
      const quantity = BigNumber(fg.controls.quantity.value);
      const price = BigNumber(fg.controls.price.value);
      const adder = BigNumber(fg.controls.adder.value);
      const invoicePrice: number = BigNumber.sum(price, adder).toNumber();
      const amount: number = quantity.multipliedBy(invoicePrice).toNumber();

      fg.controls.invoicePrice.setValue(invoicePrice);
      fg.controls.amount.setValue(amount);
    })
  }

  private calculateLinesTotalQuantity(): void {
    //use bignumber to avoid floating point errors; we need to match c# decimal values
    let totalQuantity = BigNumber(0);
    this.lineItemFormArray.controls.forEach((fg) => {
      totalQuantity = BigNumber.sum(totalQuantity, BigNumber(fg.controls.quantity.value));
    })
    this.linesTotalQuantity.set(totalQuantity.toNumber());
  }

  private calculateLinesDifference(): void {
    //use bignumber to avoid floating point errors; we need to match c# decimal values
    const linesTotalQuantity = BigNumber(this.linesTotalQuantity());
    const linesActualVolume = BigNumber(this.mainInvoice.actualVolume ?? 0);
    const lineDifferenceCalc = linesActualVolume.minus(linesTotalQuantity);
    this.linesDifference.set(lineDifferenceCalc.toNumber());
  }

  private calculateLinesSubtotal(): void {
    //use bignumber to avoid floating point errors; we need to match c# decimal values
    //Gas and Crude invoicing grand total should be based off the rounded totals for each line
    let linesSubTotal = BigNumber(0);
    this.lineItemFormArray.controls.forEach((fg) => {
      const lineTotalAmount = BigNumber(fg.controls.amount.value).decimalPlaces(2, BigNumber.ROUND_HALF_UP);
      linesSubTotal = BigNumber.sum(linesSubTotal, lineTotalAmount);
    })
    const salesTaxAmount = linesSubTotal.multipliedBy(BigNumber(this.mainInvoice.salesTaxRate ?? 0)).decimalPlaces(2, BigNumber.ROUND_HALF_UP);;

    this.linesSubTotal.set(linesSubTotal.toNumber());
    this.linesSalesTaxAmount.set(salesTaxAmount.toNumber());
  }

  private calculateLinesGrandTotal(): void {
    //use bignumber to avoid floating point errors; we need to match c# decimal values
    const salesTaxAmount = BigNumber(this.linesSalesTaxAmount() ?? 0);
    const linesSubTotal = BigNumber(this.linesSubTotal() ?? 0);
    const linesGrandTotal = BigNumber.sum(linesSubTotal, salesTaxAmount);
    this.linesGrandTotal.set(linesGrandTotal.toNumber());
  }

  protected onPanelBarSelect(event: FastPanelbarItemComponent): void {
    if (event)
      console.log('should be focused here' + event.id)
    // event.focused = false;
  }

  protected onPanelExpand(event: FastPanelbarItemComponent): void {
    const id = event.panelId();

    if (id === "internalEntityPanelbar" || id === "counterpartyPanelbar") {
      this.internalEntityPanelBarTitle.set("Internal Entity");
      this.counterpartyPanelBarTitle.set("Counterparty");
      this.isInternalEntityExpanded.set(true);
      this.isCounterpartyExpanded.set(true);
    }
    else if (id === "payInstructionsPanelBar" || id === "invoiceDetailsPanelbar") {
      this.invoiceDetailsPanelBarTitle.set("Details");
      this.isPayInstructionsExpanded.set(true);
      this.isInvoiceDetailsExpanded.set(true);
    }
  }

  protected onPanelCollapse(event: FastPanelbarItemComponent): void {
    const id = event.panelId();

    if (id === "internalEntityPanelbar" || id === "counterpartyPanelbar") {
      this.internalEntityPanelBarTitle.set(this.mainInvoice.internalEntity);
      this.counterpartyPanelBarTitle.set(this.mainInvoice.counterparty);
      this.isInternalEntityExpanded.set(false);
      this.isCounterpartyExpanded.set(false);
    }
    else if (id === "payInstructionsPanelBar" || id === "invoiceDetailsPanelbar") {
      const invoiceNum = this.mainInvoice.invoiceNum;
      const invoiceType = this.mainInvoice.invoiceType;
      this.invoiceDetailsPanelBarTitle.set(invoiceType + " " + invoiceNum);
      this.isPayInstructionsExpanded.set(false);
      this.isInvoiceDetailsExpanded.set(false);
    }
  }

  protected onMoveLineUpClicked(): void {
    const newLineSelections: number[] = [];
    const formGroups = [...this.lineItemFormArray.controls];
    const selectionNumbers = this.lineSelection;

    // starting at the top, skipping the first line, loop through all the lines in order
    for (let i = 1; i < formGroups.length; i++) {
      const isCurrentLineSelected = selectionNumbers.includes(formGroups[i].controls.lineNum.value);
      if (!isCurrentLineSelected)
        continue;

      const isLineAboveSelected = selectionNumbers.includes(formGroups[i - 1].controls.lineNum.value);
      if (isLineAboveSelected) {
        newLineSelections.push(i + 1);
      } else {
        // swap positions with the line above
        const temp = formGroups[i];
        formGroups[i] = formGroups[i - 1];
        formGroups[i - 1] = temp;

        newLineSelections.push(i);
      }
    }

    if (selectionNumbers.includes(1))
      newLineSelections.push(1);

    this.lineItemFormArray.clear();
    formGroups.forEach((fg, index) => {
      fg.controls.lineNum.setValue(index + 1);
      this.lineItemFormArray.push(this.getLineItemFormGroup(fg.getRawValue() as InvoiceLineItem));
    });

    this.lineSelection = newLineSelections;
    this.loadGridView();
    this.refreshSalesTaxRate$.next(null);
  }

  protected onMoveLineDownClicked(): void {
    const newLineSelections: number[] = [];
    const formGroups = [...this.lineItemFormArray.controls];
    const selectionNumbers = this.lineSelection;

    // starting at the bottom, skipping the last line, loop through all the lines in reverse
    for (let i = formGroups.length - 2; i >= 0; i--) {
      const isCurrentLineSelected = selectionNumbers.includes(formGroups[i].controls.lineNum.value);
      if (!isCurrentLineSelected)
        continue;

      const isLineBelowSelected = selectionNumbers.includes(formGroups[i + 1].controls.lineNum.value);
      if (isLineBelowSelected) {
        newLineSelections.push(i + 1);
      } else {
        // swap positions with the line below
        const temp = formGroups[i];
        formGroups[i] = formGroups[i + 1];
        formGroups[i + 1] = temp;

        newLineSelections.push(i + 2);
      }
    }

    if (selectionNumbers.includes(formGroups.length))
      newLineSelections.push(formGroups.length);

    this.lineItemFormArray.clear();
    formGroups.forEach((fg, index) => {
      fg.controls.lineNum.setValue(index + 1);
      this.lineItemFormArray.push(this.getLineItemFormGroup(fg.getRawValue() as InvoiceLineItem));
    });

    this.lineSelection = newLineSelections;
    this.loadGridView();
    this.refreshSalesTaxRate$.next(null);
  }

  protected onAddLinesClicked(): void {
    let newBlankLineNum = 1;
    const detailLines = this.lineItemFormArray.controls;
    if (detailLines.length > 0) {
      const lastLine = detailLines[detailLines.length - 1];
      newBlankLineNum = lastLine.controls.lineNum.value + 1;
    }
    const newLine = this.getBlankLine(newBlankLineNum);
    detailLines.push(this.getLineItemFormGroup(newLine));
    this.loadGridView();
    this.refreshSalesTaxRate$.next(null);
  }

  protected onRemoveLinesClicked(): void {
    this.lineSelection.forEach(itemLineNum => {
      const selectedLineIndex = this.lineItemFormArray.controls.findIndex(fg => fg.controls.lineNum.value === itemLineNum);
      this.lineItemFormArray.removeAt(selectedLineIndex);
    });
    this.lineSelection = [];
    this.lastSelectedIndex = null;
    this.setSequentialLineNums();
    this.loadGridView();
    this.refreshSalesTaxRate$.next(null);
    this.refreshMatchingInvoices$.next(null);
  }

  private setSequentialLineNums(): void {
    let newLineNum = 1;
    const sortedFormGroups = this.lineItemFormArray.controls.sort(fg => fg.controls.lineNum.value);
    for (const fg of sortedFormGroups)
      fg.controls.lineNum.setValue(newLineNum++);
  }

  protected onDuplicateLinesClicked(): void {
    let lastLineNum = this.lineItemFormArray.controls[this.lineItemFormArray.controls.length - 1].controls.lineNum.value;
    this.lineSelection.forEach(selectedlineNum => {
      const lastLineIndex = this.lineItemFormArray.controls.length - 1;
      const selectedLineIndex = this.lineItemFormArray.controls.findIndex(fg => fg.controls.lineNum.value === selectedlineNum);
      const selectedLine = this.lineItemFormArray.controls[selectedLineIndex].value;
      const newLineNum = ++lastLineNum;
      this.lineItemFormArray.insert(lastLineIndex + 1, this.getLineItemFormGroup({
        id: null,
        lineNum: newLineNum,
        dealId: selectedLine.dealId,
        flowDays: selectedLine.flowDays,
        deliveryPipelineId: selectedLine.deliveryPipelineId,
        meterId: selectedLine.meterId,
        quantity: selectedLine.quantity,
        price: selectedLine.price,
        adder: selectedLine.adder,
        invoicePrice: selectedLine.invoicePrice,
        amount: selectedLine.amount,
        description: selectedLine.description,
        isApproved: false,
        isRegenerated: false
      }));
    });
    this.loadGridView();
    this.refreshSalesTaxRate$.next(null);
  }

  protected onToggleApprovalClicked(): void {
    this.lineSelection.forEach(selectedlineNum => {
      const selectedLineIndex = this.lineItemFormArray.controls.findIndex(fg => fg.controls.lineNum.value === selectedlineNum);
      const currentlyApproved = this.lineItemFormArray.controls[selectedLineIndex].controls.isApproved.value === true;
      if (!currentlyApproved) {
        this.lineItemFormArray.controls[selectedLineIndex].controls.isApproved.setValue(true);
      }
      if (currentlyApproved) {
        this.lineItemFormArray.controls[selectedLineIndex].controls.isApproved.setValue(false);
      }
    });
    this.lineSelection = [];
  }

  protected onApproveAllClicked(): void {
    this.lineItemFormArray.controls.forEach(fg => {
      fg.controls.isApproved.setValue(true);
    });
    this.save$.next(SaveType.Draft);
  }

  protected onReverseLinesClicked(): void {
    this.lineSelection.forEach(selectedLineNum => {
      const lineForm = this.lineItemFormArray.controls.find(fg => fg.controls.lineNum.value === selectedLineNum);
      const lineValue = lineForm.value as InvoiceLineItem;
      if (lineValue.quantity !== 0)
        lineForm.controls.quantity.setValue(-lineValue.quantity);
    });
    this.refreshTotals();
  }

  protected downloadInvoice(): void {
    const invoiceNum = this.mainInvoice.invoiceNum;
    const fileNameOriginal = this.mainInvoice.fileNameOriginal;
    const invoiceNumAndFileName: InvoiceNumAndFileName = { invoiceNum: invoiceNum, fileName: fileNameOriginal };
    this.downloadInvoice$.next(invoiceNumAndFileName);
  }

  protected revise = () => {
    this.revise$.next();
  }

  protected saveNew = () => {
    this.save(SaveType.New);
  }

  protected saveDraft = () => {
    this.save(SaveType.Draft);
  }

  private save = (saveType: SaveType) => {
    let isFormValid = true;

    const invoiceMonthStart = dayjs(this.month()).startOf('month');
    const invoiceMonthEnd = dayjs(invoiceMonthStart).endOf('month');
    const monthDaysList = this.getDaysBetween(invoiceMonthStart, invoiceMonthEnd);

    const hasCounterpartyEmail = !util.isNullOrWhitespace(this.mainInvoice.counterpartyEmailAddresses);
    if (!hasCounterpartyEmail) {
      isFormValid = false;
      this.toast.error(`Counterparty email is required`);
    }

    this.lineItemFormArray.controls.forEach(fg => {
      const flowDays = fg.controls.flowDays.value;
      const flowDaysEmpty = flowDays == null || flowDays === '';
      const isFlowDaysValid = this.validateFlowDays(flowDays, monthDaysList);
      if (!flowDaysEmpty && !isFlowDaysValid) {
        isFormValid = false;
        const lineNum = fg.controls.lineNum.value;
        this.toast.error(`Line ${lineNum} validation failed, flow days must be a single day or range within the month of this invoice`);
      }
    });

    const noDeals = this.getDealIds().length === 0;
    if (noDeals) {
      isFormValid = false;
      this.toast.error(`At least one valid deal is required`);
    }

    if (isFormValid) {
      this.save$.next(saveType);
    }
  }

  private validateFlowDays(flowDays: string, monthDaysList: string[]): boolean {
    let isValidDateRange = false;
    let isValidSingleDay = false;

    if (flowDays?.includes('-')) {
      const dateRangeStart = flowDays.split('-')[0];
      const dateRangeEnd = flowDays.split('-')[1];
      isValidDateRange = flowDays.includes('-') && monthDaysList.includes(dateRangeStart) && monthDaysList.includes(dateRangeEnd);
    }

    if (!isValidDateRange) {
      isValidSingleDay = monthDaysList.includes(flowDays);
    }

    return isValidSingleDay || isValidDateRange;
  }

  private getDaysBetween(startOfMonth: Dayjs, endOfMonth: Dayjs): string[] {
    const days: string[] = [];
    let currentDay: Dayjs = startOfMonth.clone();

    while (currentDay.isBefore(endOfMonth) || currentDay.isSame(endOfMonth, 'day')) {
      days.push(currentDay.format('DD'));
      currentDay = currentDay.add(1, 'day');
    }

    // This function will return both the 1 and 2-digit versions of days 1-9.
    // For example, it will contain both '1' and '01' for the first day of the month.
    const singleDigitDays = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
    return [...singleDigitDays, ...days];
  }

  protected delete(): void {
    const ps: PromptSettings = {
      title: "Please Confirm",
      content: "Are you sure you want to delete this item?",
      type: "Yes-No"
    }

    this.messageService.prompt(ps).then((result) => {
      if (result === promptAction.Yes) {
        this.delete$.next(null);
      }
    });
  }

  protected regenOrRefresh(invoiceCounterparty: string): void {
    const ps: PromptSettings = {
      title: "Please Confirm",
      content: "Please choose an action for " + invoiceCounterparty,
      type: "Custom",
      customButtons: this.invoiceHeaderButtons
    }

    this.messageService.prompt(ps).then((result) => {
      if (result === promptAction.Refresh) {
        this.regenerateHeaders$.next(null);
      }
      else if (result === promptAction.Regenerate) {
        this.regenerateLines$.next(null);
      }
      else if (result === promptAction.SnapshotLines) {
        this.initialLinesOpened$.next(true);
      }
    });
  }

  protected closedClicked(): void {
    this.closed.emit();
  }

  private setLines(lines: InvoiceLineItem[]): void {
    let lineNum = 1;
    lines.forEach(line => line.lineNum = lineNum++);
    this.lineItemFormArray.clear();
    if (lines.length === 0)
      this.onAddLinesClicked();
    else {
      lines.forEach(line => {
        const meterCustomText = line.meterCustomText;
        const pipelineCustomText = line.pipelineCustomText;
        line.meterId = meterCustomText ? meterCustomText : line.meterId;
        line.deliveryPipelineId = pipelineCustomText ? pipelineCustomText : line.deliveryPipelineId;
        this.lineItemFormArray.push(this.getLineItemFormGroup(line));
      });
    }
    this.lastSelectedIndex = null;
    this.loadGridView();
  }

  private getBlankLine(lineNum: number): InvoiceLineItem {
    return {
      id: null,
      lineNum: lineNum,
      dealId: null,
      flowDays: null,
      deliveryPipelineId: null,
      meterId: null,
      quantity: null,
      price: null,
      adder: null,
      invoicePrice: null,
      amount: null,
      description: null,
      isApproved: false,
      isRegenerated: false,
      pipelineCustomText: null,
      meterCustomText: null
    };
  }

  private getDealIds(): number[] {
    const dealIds = this.lineItemFormArray.controls
      .map(fg => fg.controls.dealId.value)
      .filter(dealId => dealId);

    const distinctDealIds = [...new Set(dealIds)];
    return distinctDealIds;
  }

  refreshTotals() {
    this.calculateAmounts();
    this.calculateLinesTotalQuantity();
    this.calculateLinesDifference();
    this.calculateLinesSubtotal();
    this.calculateLinesGrandTotal();
    this.setDynamicColumnWidth();
  }

  setDynamicColumnWidth(): void {
    this.refreshDynamicColumnWidth$.next();
  }

  pageChange(event: PageChangeEvent): void {
    this.skip = event.skip;
    this.loadGridView();
  }

  loadGridView() {
    this.gridView = {
      data: this.lineItemFormArray.controls.slice(this.skip, this.skip + this.pageSize),
      total: this.lineItemFormArray.controls.length,
    };
    this.lastSelectedIndex = null;
  }

  selectAllToggled(e: MouseEvent) {
    const isCheckingAll = util.getCheckboxMetadata(e).isChecked;
    this.lineSelection = util.selectAllGridItems(isCheckingAll, this.lineItemFormArray.controls, this.lineSelection, () => true, item => item.value.lineNum);
  }

  getRowClass = (context: RowClassArgs) => {
    const dataItem = context.dataItem.value as InvoiceLineItem;
    if (dataItem.isRegenerated)
      return { "isRegenerated": true };
    else
      return { "isRegenerated": false };
  };


  toggleSelection(e: Event, lineNum: number, rowIndex: number): void {
    const result = util.toggleGridSelection({
      event: e,
      id: lineNum,
      rowIndex: rowIndex,
      lastSelectedIndex: this.lastSelectedIndex,
      gridData: this.lineItemFormArray.controls,
      skip: 0,
      selection: this.lineSelection,
      isCheckable: () => true,
      getId: item => item.value.lineNum
    });
    this.lineSelection = result.selection;
    this.lastSelectedIndex = result.lastSelectedIndex;
  }

  clearPipelineCheck(fg: util.FormModel<InvoiceLineItem>): void {
    if (typeof fg.controls.deliveryPipelineId.value === "number" && typeof fg.controls.meterId.value === "string") {
      fg.controls.deliveryPipelineId.setValue(null);
    } else if (typeof fg.controls.deliveryPipelineId.value === "string" && typeof fg.controls.meterId.value === "number") {
      fg.controls.meterId.setValue(null);
    }
  }

  closeInitialLines() {
    this.initialLinesOpened$.next(false);
  }
}
