import { Component, ChangeDetectionStrategy, ChangeDetectorRef, ViewEncapsulation, ViewChild, inject } from '@angular/core';
import { MessageService } from '../_shared/services/message.service';
import { MarketPriceService, Index, MarketPriceResult, MarketPriceItem } from './market-price.service';
import { FormGroup, Validators, FormBuilder, FormArray } from '@angular/forms';
import { tap, catchError, map, switchMap, delay, toArray, mergeMap, take, filter, shareReplay } from 'rxjs/operators';
import { of, combineLatest, BehaviorSubject, from, Subject } from 'rxjs';
import { Title } from '@angular/platform-browser';
import { State, SortDescriptor } from '@progress/kendo-data-query';
import { ContextMenuSelectEvent, KENDO_CONTEXTMENU } from '@progress/kendo-angular-menu';
import { DialogService, DialogSettings } from '@progress/kendo-angular-dialog';
import { NotifyService } from '../_shared/services/notify.service';
import { FileRestrictions, SuccessEvent, ErrorEvent, SelectEvent, UploadComponent } from '@progress/kendo-angular-upload';
import { saveAs } from '@progress/kendo-file-saver';
import { FAST_KENDO_COMMON, FAST_PAGE_COMMON } from '../app.config';
import { KENDO_DROPDOWNLIST } from '@progress/kendo-angular-dropdowns';
import { FastHeaderComponent } from "../_shared/elements/fast-header.component";
import { FastLabelComponent } from "../_shared/elements/fast-label.component";
import { FastButtonComponent } from "../_shared/elements/fast-button.component";
import { FastGridComponent } from "../_shared/elements/fast-grid.component";
import { FastWindowComponent } from "../_shared/elements/fast-window.component";
import { FastMultiselectComponent, MultiValueType } from "../_shared/elements/fast-multiselect.component";
import { dialogAction, getDialogAction, handleError, icons, IdName, isObjectArray, trySetTitle } from '../_shared/utils/util';
import { FastGridToolbar } from "../_shared/elements/fast-grid-toolbar.component";

export interface PriceImportResult {
  changesMessage: string;
  notes: string;
}

export enum contextMenuEnum {
  CopyUp,
  CopyDown,
  FillSeriesUp,
  FillSeriesDown
}

export interface ContextMenuItem {
  enum: contextMenuEnum;
  text: string;
}

@Component({
  selector: 'app-market-price',
  templateUrl: './market-price.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  imports: [FAST_PAGE_COMMON, FAST_KENDO_COMMON, KENDO_CONTEXTMENU, KENDO_DROPDOWNLIST, FastHeaderComponent, FastLabelComponent, FastButtonComponent, FastGridComponent, FastWindowComponent, FastMultiselectComponent, FastGridToolbar]
})
export class MarketPriceComponent {
  private messageService = inject(MessageService);
  private titleService = inject(Title);
  private marketPriceService = inject(MarketPriceService);
  private fb = inject(FormBuilder);
  private ref = inject(ChangeDetectorRef);
  private dialogService = inject(DialogService);
  private notify = inject(NotifyService);


  @ViewChild('importTarget') importTargetElem: UploadComponent;

  formSelections: FormGroup;
  formEditor: FormGroup;
  formEditorItems: FormArray;

  opened = false;
  minDate: Date = new Date(1900, 0, 1);
  editorLoading: boolean;
  importing = false;
  exporting = false;

  icons = icons;

  importUrl = `${window.location.origin}/api/MarketPrice/Import`;
  importRestrictions: FileRestrictions = {
    allowedExtensions: ['.xls', '.xlsx', '.xlsm', '.xlsb']
  };

  contextMenuCopyItems: ContextMenuItem[] = [
    { enum: contextMenuEnum.CopyUp, text: 'Copy Up' },
    { enum: contextMenuEnum.CopyDown, text: 'Copy Down' }
  ];

  contextMenuCopyAndFillItems: ContextMenuItem[] = [
    { enum: contextMenuEnum.CopyUp, text: 'Copy Up' },
    { enum: contextMenuEnum.CopyDown, text: 'Copy Down' },
    { enum: contextMenuEnum.FillSeriesUp, text: 'Fill Series Up' },
    { enum: contextMenuEnum.FillSeriesDown, text: 'Fill Series Down' }
  ];

  constructor() {
    const fb = this.fb;

    const today = new Date();
    const firstDayOfYear = new Date(today.getFullYear(), 0, 1);
    const lastyDayOfYear = new Date(today.getFullYear(), 11, 31);

    this.formSelections = fb.group({
      fromDate: [firstDayOfYear, Validators.required],
      toDate: [lastyDayOfYear, Validators.required],
      marketType: [null],
      commodity: [null],
      indexes: [null]
    });

    this.formEditorItems = fb.array([]);

    this.formEditor = fb.group({
      numRowsToAdd: [1, Validators.required],
      items: this.formEditorItems,
    });

    this.formSelections.disable();
  }



  title$ = of('Market Price').pipe(
    tap((title) => trySetTitle(this.titleService, title))
  )

  getNewState(marketTypeId: number): State {
    let sort: SortDescriptor[] = null;

    if (marketTypeId) {
      if (marketTypeId === 1)
        sort = [{ field: 'priceDate', dir: 'desc' }, { field: 'contractMonth', dir: 'asc' }];
      else if (marketTypeId === 2)
        sort = [{ field: 'priceDate', dir: 'desc' }, { field: 'contractMonth', dir: 'asc' }];
      else if (marketTypeId === 3)
        sort = [{ field: 'priceDate', dir: 'asc' }];
    }

    const state: State = {
      filter: null,
      group: null,
      skip: 0,
      sort: sort,
      take: 50
    };
    return state;
  }

  requiredData$ = this.marketPriceService.requiredData$.pipe(
    tap(res => {
      this.formSelections.patchValue({ marketType: res.marketTypes[0] });
      this.formSelections.patchValue({ commodity: res.commodities[0] });
      this.formSelections.enable();
      this.marketTypeSelected$.next(res.marketTypes[0]);
      this.commoditySelected$.next(res.commodities[0]);
      this.fromDateChanged$.next(this.formSelections.value.fromDate);
      this.toDateChanged$.next(this.formSelections.value.toDate);
    }),
    shareReplay(1),
    catchError(err => {
      return handleError(err, this.messageService)
    })
  );

  fromDateChanged$ = new BehaviorSubject<Date>(null)
  toDateChanged$ = new BehaviorSubject<Date>(null)
  marketTypeSelected$ = new BehaviorSubject<IdName>(null)
  commoditySelected$ = new BehaviorSubject<IdName>(null)
  indexesSelected$ = new BehaviorSubject<Index[]>(null)
  indexesFilter$ = new BehaviorSubject<string>(null)

  saveStarted$ = new Subject()

  saveResult$ = this.saveStarted$.pipe(
    mergeMap(() => {
      const itemsToSave: MarketPriceItem[] = [];
      this.formEditorItems.controls.forEach(control => {
        if (control.dirty)
          itemsToSave.push(control.value);
      });
      return this.marketPriceService.saveMarketPrices(this.editResult.marketTypeId, this.editResult.indexId, itemsToSave);
    }),
    tap((savedCount) => {
      if (savedCount > 0) {
        this.notify.success('save successful');
        this.dataStateChange(this.editResult.state, this.editResult);
      }
      this.editorLoading = false;
      this.opened = false;
      this.formEditor.enable();
    }),
    catchError(err => {
      return handleError(err, this.messageService)
    })
  );

  marketTypes$ = this.marketPriceService.requiredData$.pipe(map(x => x.marketTypes))

  commodities$ = this.marketPriceService.requiredData$.pipe(map(x => x.commodities))

  indexes$ = combineLatest([this.marketTypeSelected$, this.commoditySelected$, this.indexesFilter$, this.indexesSelected$]).pipe(
    switchMap(([marketTypeSelected, commoditySelected, filterText, indexesSelected]) => this.marketPriceService.requiredData$.pipe(
      delay(100),
      map(requiredData => {
        //remove any index suffixes from indexes that are not selected
        requiredData.indexes.forEach(index => {
          const isSelected = indexesSelected && indexesSelected.includes(index);
          if (!isSelected)
            index.name = index.name.replace(/\[[F,D,M]\]/, '');
        });

        //only get the indexes that match the selected market type, commodity, and filter text
        const indexes = requiredData.indexes.filter(item => {
          const hasIndexType = !marketTypeSelected ? false : item.selectedMarketTypeId === marketTypeSelected.id;
          const hasCommodity = !commoditySelected ? false : item.commodityId === commoditySelected.id;
          const hasFilterText = !filterText ? true : item.name.toLowerCase().indexOf(filterText.toLowerCase()) !== -1;
          return hasIndexType && hasCommodity && hasFilterText;
        })

        return indexes;
      })
    )),
    catchError(err => {
      return handleError(err, this.messageService)
    })
  )

  localSelectedIndexes: Index[] = [];
  storedResults: MarketPriceResult[] = [];
  marketPriceResults$ = combineLatest([this.indexesSelected$, this.fromDateChanged$, this.toDateChanged$]).pipe(
    filter(() => {
      return this.formSelections.valid;
    }),
    tap(() => {
      this.storedResults.forEach(x => {
        x.loading = true;
      })
    }),
    map(([indexes, fromDate, toDate]) => {
      let nullIndexes: Index[];
      if (!indexes || indexes.length === 0)
        nullIndexes = [null];
      else
        nullIndexes = indexes;

      const result: [Index[], Date, Date] = [nullIndexes, fromDate, toDate];
      return result;
    }),
    switchMap(([indexes, fromDate, toDate]) => from(indexes).pipe(
      mergeMap(index => {
        const marketTypeId: number = index ? index.selectedMarketTypeId : null;
        return this.marketPriceService.getMarketPriceItems(index, fromDate, toDate, this.getNewState(marketTypeId));
      }),
      toArray(),
      map(results => {
        if (this.localSelectedIndexes.length > 0) {
          const orderedResults: MarketPriceResult[] = [];
          this.localSelectedIndexes.forEach(index => {
            const result = results.find(x => x.indexId === index.id && x.marketTypeId === index.selectedMarketTypeId);
            if (result)
              orderedResults.push(result);
          })

          return orderedResults;
        } else {
          return results;
        }
      }),
      tap((results) => {
        this.storedResults = results;
        this.storedResults.forEach(x => {
          x.loading = false;
        })
      }),
    )),
    catchError(err => {
      return handleError(err, this.messageService)
    })
  )

  exportDisabled$ = combineLatest([this.indexesSelected$, this.fromDateChanged$, this.toDateChanged$]).pipe(
    map(([indexes,]) => {
      const hasIndexes = indexes && indexes.length > 0;
      const validFromDate = this.formSelections.controls['fromDate'].valid;
      const validToDate = this.formSelections.controls['toDate'].valid;
      const isDisabled = hasIndexes && validFromDate && validToDate ? false : true;
      return isDisabled;
    })
  )

  dataStateChange(state: State, res: MarketPriceResult): void {
    if (res && res.indexId) {
      res.state = state;
      res.loading = true;
      const index = this.localSelectedIndexes.find(x => x.id === res.indexId && x.selectedMarketTypeId === res.marketTypeId);

      if (index) {
        this.marketPriceService.getMarketPriceItems(index, res.fromDate, res.toDate, state).pipe(take(1)).subscribe(result => {
          res.data = result.data;
          res.total = result.total;
          res.loading = false;

          this.ref.detectChanges();
        }, err => {
          return handleError(err, this.messageService)
        });
      }
    }
  }

  onContextMenuSelect(e: ContextMenuSelectEvent, controlName: string, index: number) {
    const contextMenuItem: ContextMenuItem = e.item;

    if (contextMenuItem.enum === contextMenuEnum.CopyUp)
      this.CopyUp(controlName, index, true);
    else if (contextMenuItem.enum === contextMenuEnum.CopyDown)
      this.CopyDown(controlName, index, true);
    else if (contextMenuItem.enum === contextMenuEnum.FillSeriesUp)
      this.FillSeriesUp(controlName, index, true);
    else if (contextMenuItem.enum === contextMenuEnum.FillSeriesDown)
      this.FillSeriesDown(controlName, index, true);
  }

  private CopyUp(controlName: string, index: number, markDirty: boolean) {
    const controls = this.formEditorItems.controls;
    const valueToCopy = this.formEditorItems.controls[index].value[controlName];

    for (let i = index - 1; i >= 0; i--) {
      controls[i].patchValue({ [controlName]: valueToCopy });
      if (markDirty)
        controls[i].markAsDirty();
    }
  }

  private CopyDown(controlName: string, index: number, markDirty: boolean) {
    const controls = this.formEditorItems.controls;
    const valueToCopy = this.formEditorItems.controls[index].value[controlName];

    for (let i = index + 1; i < controls.length; i++) {
      controls[i].patchValue({ [controlName]: valueToCopy });
      if (markDirty)
        controls[i].markAsDirty();
    }
  }

  private FillSeriesUp(controlName: string, index: number, markDirty: boolean) {
    const controls = this.formEditorItems.controls;
    const valueToCopy = this.formEditorItems.controls[index].value[controlName];
    const marketTypeId = this.editResult.marketTypeId;

    if (valueToCopy !== null) {
      let nextDate: Date = valueToCopy;
      for (let i = index - 1; i >= 0; i--) {
        nextDate = this.getNextDate(marketTypeId, nextDate, controlName);
        controls[i].patchValue({ [controlName]: nextDate });
        if (markDirty)
          controls[i].markAsDirty();
      }
    }
  }

  private FillSeriesDown(controlName: string, index: number, markDirty: boolean) {
    const controls = this.formEditorItems.controls;
    const valueToCopy = this.formEditorItems.controls[index].value[controlName];
    const marketTypeId = this.editResult.marketTypeId;

    if (valueToCopy !== null) {
      let nextDate: Date = valueToCopy;
      for (let i = index + 1; i < controls.length; i++) {
        nextDate = this.getNextDate(marketTypeId, nextDate, controlName);
        controls[i].patchValue({ [controlName]: nextDate });
        if (markDirty)
          controls[i].markAsDirty();
      }
    }
  }

  private getNextDate(marketTypeId: number, currentDate: Date, propertyName: string): Date {
    let nextDate: Date;

    if (propertyName === "priceDate" && (marketTypeId === 1 || marketTypeId === 2))
      nextDate = this.getNextDay(currentDate);
    else if (propertyName === "priceDate" && marketTypeId === 3)
      nextDate = this.getNextMonth(currentDate);
    else if (propertyName === "contractMonth" || propertyName === 'publishDate')
      nextDate = this.getNextMonth(currentDate);

    return nextDate;
  }

  private getNextDay(currentDay: Date): Date {
    const nextDay = new Date(currentDay.getFullYear(), currentDay.getMonth(), currentDay.getDate() + 1);
    return nextDay;
  }

  private getNextMonth(currentMonth: Date): Date {
    const nextMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, currentMonth.getDate());
    return nextMonth;
  }

  fromDateChange(value: Date) {
    this.fromDateChanged$.next(value);
  }

  toDateChange(value: Date) {
    this.toDateChanged$.next(value);
  }

  marketTypeChange(value: IdName) {
    this.marketTypeSelected$.next(value);
  }

  commodityChange(value: IdName) {
    this.commoditySelected$.next(value);
  }

  indexesChange(selectedIndexes: MultiValueType<Index>) {
    if (!isObjectArray<Index>(selectedIndexes))
      return;

    if (selectedIndexes && selectedIndexes.length > 0) {
      const addedIndex = selectedIndexes[selectedIndexes.length - 1];

      const hasSuffix = addedIndex.name.search(/\[[F,D,M]\]/) !== -1;
      if (!hasSuffix) {
        let suffix;
        if (addedIndex.selectedMarketTypeId === 1)
          suffix = '[F]';
        else if (addedIndex.selectedMarketTypeId === 2)
          suffix = '[D]';
        else if (addedIndex.selectedMarketTypeId === 3)
          suffix = '[M]';

        addedIndex.name = `${addedIndex.name} ${suffix}`;
      }
    }
    this.localSelectedIndexes = selectedIndexes;
    this.indexesSelected$.next(selectedIndexes);
  }

  indexesFilter(value: string) {
    this.indexesFilter$.next(value);
  }

  getNewEditorItem(marketTypeId: number, marketPriceItem?: MarketPriceItem) {
    const startingDate = this.formSelections.value.fromDate;
    const startingMonth = new Date(startingDate.getFullYear(), startingDate.getMonth(), 1);

    return this.fb.group({
      price: [marketPriceItem ? marketPriceItem.price : null],
      priceDate: [marketPriceItem ? marketPriceItem.priceDate : marketTypeId === 3 ? startingMonth : startingDate, Validators.required],
      contractMonth: [marketPriceItem ? marketPriceItem.contractMonth : startingMonth, marketTypeId === 1 ? Validators.required : null],
      publishDate: [marketPriceItem ? marketPriceItem.publishDate : startingDate, marketTypeId === 3 ? Validators.required : null],
    })
  }

  editResult: MarketPriceResult = {} as MarketPriceResult;
  editIndex(res: MarketPriceResult) {
    if (this.formSelections.valid) {
      this.editResult = res;
      this.formEditorItems.clear();
      if (res && res.indexId) {
        if (res.data) {
          res.data.forEach(resItem => {
            this.formEditorItems.push(this.getNewEditorItem(res.marketTypeId, resItem));
          })
        }
        this.opened = true;
      }
    } else {
      this.notify.error("validation failed");
    }
  }

  addRows() {
    if (this.formEditor.valid) {
      const numRowsToAdd: number = this.formEditor.value.numRowsToAdd;
      for (let i = 0; i < numRowsToAdd; i++) {
        this.formEditorItems.push(this.getNewEditorItem(this.editResult.marketTypeId));
      }

      const startingIndex = this.formEditorItems.length - numRowsToAdd;
      this.FillSeriesDown('priceDate', startingIndex, false);
      this.FillSeriesDown('contractMonth', startingIndex, false);
      this.FillSeriesDown('publishDate', startingIndex, false);

      const formArrayElement = document.querySelector('[formarrayname="items"]');
      setTimeout(() => {
        formArrayElement.scroll(0, formArrayElement.scrollHeight);
      }, 100);
    }
  }

  saveConfirmSettings: DialogSettings = {
    title: "Please confirm",
    content: "Dates with empty prices may have their data erased. Are you sure you want to continue?",
    actions: [{ text: 'No' }, { text: 'Yes', cssClass: 'k-primary' }],
    cssClass: 'utilPrompt'
  }

  save() {
    if (this.formEditorItems.valid) {
      const hasNullPrice = (this.formEditorItems.controls.findIndex(x => x.dirty && (x.value as MarketPriceItem).price === null) >= 0);
      if (hasNullPrice) {
        this.dialogService.open(this.saveConfirmSettings).result.pipe(take(1)).subscribe(result => {
          if (getDialogAction(result) === dialogAction.Yes)
            this.saveConfirmed();
        });
      } else {
        this.saveConfirmed();
      }
    } else {
      this.notify.error('validation failed');
    }
  }

  saveConfirmed() {
    this.formEditor.disable();
    this.editorLoading = true;
    this.saveStarted$.next(null);
  }

  importStarted() {
    this.importing = true;
  }

  importCompleted(e: SuccessEvent) {
    this.importing = false;
    const result: PriceImportResult = e.response.body;
    const message = result.changesMessage + '\r\n' + result.notes;
    this.messageService.info(message);
    this.fromDateChange(this.formSelections.controls['fromDate'].value); //triggers refresh
  }

  importError(e: ErrorEvent) {
    this.importing = false;
    this.messageService.throw(e.response);
  }

  importSelect(value: SelectEvent) {
    if (value?.files?.[0]?.validationErrors?.[0] === "invalidFileExtension")
      this.notify.error("Invalid file type");
  }

  importComplete() {
    this.importTargetElem.clearFiles();
  }

  export() {
    this.exporting = true;
    const indexes: Index[] = this.formSelections.controls['indexes'].value;
    const fromDate: Date = this.formSelections.controls['fromDate'].value;
    const toDate: Date = this.formSelections.controls['toDate'].value;
    this.marketPriceService.exportMarketPrices(indexes, fromDate, toDate).subscribe(result => {
      saveAs(result, "Market Prices.xlsx");
      this.exporting = false;
      this.ref.detectChanges(); //not sure why this is required, button doesn't update without it
    }, err => {
      this.exporting = false;
      this.ref.detectChanges(); //not sure why this is required, button doesn't update without it
      return handleError(err, this.messageService);
    });
  }
}
