import { AdminMarketDataService } from '@ghostfolio/client/components/admin-market-data/admin-market-data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
  ASSET_CLASS_MAPPING,
  PROPERTY_IS_DATA_GATHERING_ENABLED
} from '@ghostfolio/common/config';
import { UpdateAssetProfileDto } from '@ghostfolio/common/dtos';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
  AdminMarketDataDetails,
  AssetClassSelectorOption,
  AssetProfileIdentifier,
  LineChartItem,
  ScraperConfiguration,
  User
} from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types';
import { validateObjectForForm } from '@ghostfolio/common/utils';
import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { GfHistoricalMarketDataEditorComponent } from '@ghostfolio/ui/historical-market-data-editor';
import { translate } from '@ghostfolio/ui/i18n';
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
import { AdminService, DataService } from '@ghostfolio/ui/services';
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete';
import { GfValueComponent } from '@ghostfolio/ui/value';

import { TextFieldModule } from '@angular/cdk/text-field';
import { HttpErrorResponse } from '@angular/common/http';
import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  ElementRef,
  Inject,
  OnDestroy,
  OnInit,
  ViewChild
} from '@angular/core';
import {
  AbstractControl,
  FormBuilder,
  FormControl,
  FormsModule,
  ReactiveFormsModule,
  ValidationErrors,
  Validators
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import {
  MatCheckboxChange,
  MatCheckboxModule
} from '@angular/material/checkbox';
import {
  MAT_DIALOG_DATA,
  MatDialogModule,
  MatDialogRef
} from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTabsModule } from '@angular/material/tabs';
import { IonIcon } from '@ionic/angular/standalone';
import {
  AssetClass,
  AssetSubClass,
  MarketData,
  Prisma,
  SymbolProfile
} from '@prisma/client';
import { isUUID } from 'class-validator';
import { format } from 'date-fns';
import { StatusCodes } from 'http-status-codes';
import { addIcons } from 'ionicons';
import {
  codeSlashOutline,
  createOutline,
  ellipsisVertical,
  readerOutline,
  serverOutline
} from 'ionicons/icons';
import ms from 'ms';
import { EMPTY, Subject } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators';

import { AssetProfileDialogParams } from './interfaces/interfaces';

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'd-flex flex-column h-100' },
  imports: [
    FormsModule,
    GfCurrencySelectorComponent,
    GfEntityLogoComponent,
    GfHistoricalMarketDataEditorComponent,
    GfLineChartComponent,
    GfPortfolioProportionChartComponent,
    GfSymbolAutocompleteComponent,
    GfValueComponent,
    IonIcon,
    MatButtonModule,
    MatCheckboxModule,
    MatDialogModule,
    MatInputModule,
    MatMenuModule,
    MatSelectModule,
    MatSnackBarModule,
    MatTabsModule,
    ReactiveFormsModule,
    TextFieldModule
  ],
  providers: [AdminMarketDataService],
  selector: 'gf-asset-profile-dialog',
  styleUrls: ['./asset-profile-dialog.component.scss'],
  templateUrl: 'asset-profile-dialog.html'
})
export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
  private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format(
    new Date(),
    DATE_FORMAT
  )};123.45`;

  @ViewChild('assetProfileFormElement')
  assetProfileFormElement: ElementRef<HTMLFormElement>;

  public assetClassLabel: string;
  public assetSubClassLabel: string;

  public assetClassOptions: AssetClassSelectorOption[] = Object.keys(AssetClass)
    .map((id) => {
      return { id, label: translate(id) } as AssetClassSelectorOption;
    })
    .sort((a, b) => {
      return a.label.localeCompare(b.label);
    });

  public assetSubClassOptions: AssetClassSelectorOption[] = [];

  public assetProfile: AdminMarketDataDetails['assetProfile'];

  public assetProfileForm = this.formBuilder.group({
    assetClass: new FormControl<AssetClass>(undefined),
    assetSubClass: new FormControl<AssetSubClass>(undefined),
    comment: '',
    countries: '',
    currency: '',
    historicalData: this.formBuilder.group({
      csvString: ''
    }),
    isActive: [true],
    name: ['', Validators.required],
    scraperConfiguration: this.formBuilder.group({
      defaultMarketPrice: null,
      headers: JSON.stringify({}),
      locale: '',
      mode: '',
      selector: '',
      url: ''
    }),
    sectors: '',
    symbolMapping: '',
    url: ''
  });

  public assetProfileIdentifierForm = this.formBuilder.group(
    {
      assetProfileIdentifier: new FormControl<AssetProfileIdentifier>(
        { symbol: null, dataSource: null },
        [Validators.required]
      )
    },
    {
      validators: (control) => {
        return this.isNewSymbolValid(control);
      }
    }
  );

  public benchmarks: Partial<SymbolProfile>[];

  public countries: {
    [code: string]: { name: string; value: number };
  };

  public currencies: string[] = [];
  public dateRangeOptions = [
    {
      label: $localize`Current week` + ' (' + $localize`WTD` + ')',
      value: 'wtd'
    },
    {
      label: $localize`Current month` + ' (' + $localize`MTD` + ')',
      value: 'mtd'
    },
    {
      label: $localize`Current year` + ' (' + $localize`YTD` + ')',
      value: 'ytd'
    },
    {
      label: '1 ' + $localize`year` + ' (' + $localize`1Y` + ')',
      value: '1y'
    },
    {
      label: '5 ' + $localize`years` + ' (' + $localize`5Y` + ')',
      value: '5y'
    },
    {
      label: $localize`Max`,
      value: 'max'
    }
  ];
  public historicalDataItems: LineChartItem[];
  public isBenchmark = false;
  public isDataGatheringEnabled: boolean;
  public isEditAssetProfileIdentifierMode = false;
  public isUUID = isUUID;
  public marketDataItems: MarketData[] = [];

  public modeValues = [
    {
      value: 'lazy',
      viewValue: $localize`Lazy` + ' (' + $localize`end of day` + ')'
    },
    {
      value: 'instant',
      viewValue: $localize`Instant` + ' (' + $localize`real-time` + ')'
    }
  ];

  public sectors: {
    [name: string]: { name: string; value: number };
  };

  public user: User;

  private unsubscribeSubject = new Subject<void>();

  public constructor(
    public adminMarketDataService: AdminMarketDataService,
    private adminService: AdminService,
    private changeDetectorRef: ChangeDetectorRef,
    @Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams,
    private dataService: DataService,
    public dialogRef: MatDialogRef<GfAssetProfileDialogComponent>,
    private formBuilder: FormBuilder,
    private notificationService: NotificationService,
    private snackBar: MatSnackBar,
    private userService: UserService
  ) {
    addIcons({
      codeSlashOutline,
      createOutline,
      ellipsisVertical,
      readerOutline,
      serverOutline
    });
  }

  public get canSaveAssetProfileIdentifier() {
    return !this.assetProfileForm.dirty;
  }

  public ngOnInit() {
    const { benchmarks, currencies } = this.dataService.fetchInfo();

    this.benchmarks = benchmarks;
    this.currencies = currencies;

    this.initialize();
  }

  public initialize() {
    this.historicalDataItems = undefined;

    this.adminService
      .fetchAdminData()
      .pipe(takeUntil(this.unsubscribeSubject))
      .subscribe(({ settings }) => {
        this.isDataGatheringEnabled =
          settings[PROPERTY_IS_DATA_GATHERING_ENABLED] === false ? false : true;

        this.changeDetectorRef.markForCheck();
      });

    this.userService.stateChanged
      .pipe(takeUntil(this.unsubscribeSubject))
      .subscribe((state) => {
        if (state?.user) {
          this.user = state.user;
        }
      });

    this.assetProfileForm
      .get('assetClass')
      .valueChanges.pipe(takeUntil(this.unsubscribeSubject))
      .subscribe((assetClass) => {
        const assetSubClasses = ASSET_CLASS_MAPPING.get(assetClass) ?? [];

        this.assetSubClassOptions = assetSubClasses
          .map((assetSubClass) => {
            return {
              id: assetSubClass,
              label: translate(assetSubClass)
            };
          })
          .sort((a, b) => a.label.localeCompare(b.label));

        this.assetProfileForm.get('assetSubClass').setValue(null);

        this.changeDetectorRef.markForCheck();
      });

    this.dataService
      .fetchMarketDataBySymbol({
        dataSource: this.data.dataSource,
        symbol: this.data.symbol
      })
      .pipe(takeUntil(this.unsubscribeSubject))
      .subscribe(({ assetProfile, marketData }) => {
        this.assetProfile = assetProfile;

        this.assetClassLabel = translate(this.assetProfile?.assetClass);
        this.assetSubClassLabel = translate(this.assetProfile?.assetSubClass);
        this.countries = {};

        this.isBenchmark = this.benchmarks.some(({ id }) => {
          return id === this.assetProfile.id;
        });

        this.historicalDataItems = marketData.map(({ date, marketPrice }) => {
          return {
            date: format(date, DATE_FORMAT),
            value: marketPrice
          };
        });

        this.marketDataItems = marketData;
        this.sectors = {};

        if (this.assetProfile?.countries?.length > 0) {
          for (const { code, name, weight } of this.assetProfile.countries) {
            this.countries[code] = {
              name,
              value: weight
            };
          }
        }

        if (this.assetProfile?.sectors?.length > 0) {
          for (const { name, weight } of this.assetProfile.sectors) {
            this.sectors[name] = {
              name,
              value: weight
            };
          }
        }

        this.assetProfileForm.setValue({
          assetClass: this.assetProfile.assetClass ?? null,
          assetSubClass: this.assetProfile.assetSubClass ?? null,
          comment: this.assetProfile?.comment ?? '',
          countries: JSON.stringify(
            this.assetProfile?.countries?.map(({ code, weight }) => {
              return { code, weight };
            }) ?? []
          ),
          currency: this.assetProfile?.currency,
          historicalData: {
            csvString: GfAssetProfileDialogComponent.HISTORICAL_DATA_TEMPLATE
          },
          isActive: this.assetProfile?.isActive,
          name: this.assetProfile.name ?? this.assetProfile.symbol,
          scraperConfiguration: {
            defaultMarketPrice:
              this.assetProfile?.scraperConfiguration?.defaultMarketPrice ??
              null,
            headers: JSON.stringify(
              this.assetProfile?.scraperConfiguration?.headers ?? {}
            ),
            locale: this.assetProfile?.scraperConfiguration?.locale ?? '',
            mode: this.assetProfile?.scraperConfiguration?.mode ?? 'lazy',
            selector: this.assetProfile?.scraperConfiguration?.selector ?? '',
            url: this.assetProfile?.scraperConfiguration?.url ?? ''
          },
          sectors: JSON.stringify(this.assetProfile?.sectors ?? []),
          symbolMapping: JSON.stringify(this.assetProfile?.symbolMapping ?? {}),
          url: this.assetProfile?.url ?? ''
        });

        this.assetProfileForm.markAsPristine();

        this.changeDetectorRef.markForCheck();
      });
  }

  public onCancelEditAssetProfileIdentifierMode() {
    this.isEditAssetProfileIdentifierMode = false;

    this.assetProfileForm.enable();

    this.assetProfileIdentifierForm.reset();
  }

  public onClose() {
    this.dialogRef.close();
  }

  public onDeleteProfileData({ dataSource, symbol }: AssetProfileIdentifier) {
    this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol });

    this.dialogRef.close();
  }

  public onGatherProfileDataBySymbol({
    dataSource,
    symbol
  }: AssetProfileIdentifier) {
    this.adminService
      .gatherProfileDataBySymbol({ dataSource, symbol })
      .pipe(takeUntil(this.unsubscribeSubject))
      .subscribe();
  }

  public onGatherSymbol({
    dataSource,
    range,
    symbol
  }: {
    range?: DateRange;
  } & AssetProfileIdentifier) {
    this.adminService
      .gatherSymbol({ dataSource, range, symbol })
      .pipe(takeUntil(this.unsubscribeSubject))
      .subscribe();
  }

  public onMarketDataChanged(withRefresh: boolean = false) {
    if (withRefresh) {
      this.initialize();
    }
  }

  public onSetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
    this.dataService
      .postBenchmark({ dataSource, symbol })
      .pipe(takeUntil(this.unsubscribeSubject))
      .subscribe(() => {
        this.dataService.updateInfo();

        this.isBenchmark = true;

        this.changeDetectorRef.markForCheck();
      });
  }

  public onSetEditAssetProfileIdentifierMode() {
    this.isEditAssetProfileIdentifierMode = true;

    this.assetProfileForm.disable();
  }

  public async onSubmitAssetProfileForm() {
    let countries = [];
    let scraperConfiguration: ScraperConfiguration = {
      selector: '',
      url: ''
    };
    let sectors = [];
    let symbolMapping = {};

    try {
      countries = JSON.parse(this.assetProfileForm.get('countries').value);
    } catch {}

    try {
      scraperConfiguration = {
        defaultMarketPrice:
          (this.assetProfileForm.controls['scraperConfiguration'].controls[
            'defaultMarketPrice'
          ].value as number) || undefined,
        headers: JSON.parse(
          this.assetProfileForm.controls['scraperConfiguration'].controls[
            'headers'
          ].value
        ),
        locale:
          this.assetProfileForm.controls['scraperConfiguration'].controls[
            'locale'
          ].value || undefined,
        mode: this.assetProfileForm.controls['scraperConfiguration'].controls[
          'mode'
        ].value as ScraperConfiguration['mode'],
        selector:
          this.assetProfileForm.controls['scraperConfiguration'].controls[
            'selector'
          ].value,
        url: this.assetProfileForm.controls['scraperConfiguration'].controls[
          'url'
        ].value
      };

      if (!scraperConfiguration.selector || !scraperConfiguration.url) {
        scraperConfiguration = undefined;
      }
    } catch (error) {
      console.error($localize`Could not parse scraper configuration`, error);

      this.snackBar.open(
        '😞 ' + $localize`Could not parse scraper configuration`,
        undefined,
        {
          duration: ms('3 seconds')
        }
      );

      return;
    }

    try {
      sectors = JSON.parse(this.assetProfileForm.get('sectors').value);
    } catch {}

    try {
      symbolMapping = JSON.parse(
        this.assetProfileForm.get('symbolMapping').value
      );
    } catch {}

    const assetProfile: UpdateAssetProfileDto = {
      countries,
      sectors,
      symbolMapping,
      assetClass: this.assetProfileForm.get('assetClass').value,
      assetSubClass: this.assetProfileForm.get('assetSubClass').value,
      comment: this.assetProfileForm.get('comment').value || null,
      currency: this.assetProfileForm.get('currency').value,
      isActive: this.assetProfileForm.get('isActive').value,
      name: this.assetProfileForm.get('name').value,
      scraperConfiguration:
        scraperConfiguration as unknown as Prisma.InputJsonObject,
      url: this.assetProfileForm.get('url').value || null
    };

    try {
      await validateObjectForForm({
        classDto: UpdateAssetProfileDto,
        form: this.assetProfileForm,
        object: assetProfile
      });
    } catch (error) {
      console.error($localize`Could not validate form`, error);

      this.snackBar.open(
        '😞 ' + $localize`Could not validate form`,
        undefined,
        {
          duration: ms('3 seconds')
        }
      );

      return;
    }

    this.adminService
      .patchAssetProfile(
        {
          dataSource: this.data.dataSource,
          symbol: this.data.symbol
        },
        assetProfile
      )
      .subscribe({
        next: () => {
          this.snackBar.open(
            '✅ ' + $localize`Asset profile has been saved`,
            undefined,
            {
              duration: ms('3 seconds')
            }
          );

          this.initialize();
        },
        error: (error) => {
          console.error($localize`Could not save asset profile`, error);

          this.snackBar.open(
            '😞 ' + $localize`Could not save asset profile`,
            undefined,
            {
              duration: ms('3 seconds')
            }
          );
        }
      });
  }

  public async onSubmitAssetProfileIdentifierForm() {
    const assetProfileIdentifier: UpdateAssetProfileDto = {
      dataSource: this.assetProfileIdentifierForm.get('assetProfileIdentifier')
        .value.dataSource,
      symbol: this.assetProfileIdentifierForm.get('assetProfileIdentifier')
        .value.symbol
    };

    try {
      await validateObjectForForm({
        classDto: UpdateAssetProfileDto,
        form: this.assetProfileIdentifierForm,
        object: assetProfileIdentifier
      });
    } catch (error) {
      console.error(error);

      return;
    }

    this.adminService
      .patchAssetProfile(
        {
          dataSource: this.data.dataSource,
          symbol: this.data.symbol
        },
        assetProfileIdentifier
      )
      .pipe(
        catchError((error: HttpErrorResponse) => {
          if (error.status === StatusCodes.CONFLICT) {
            this.snackBar.open(
              $localize`${assetProfileIdentifier.symbol} (${assetProfileIdentifier.dataSource}) is already in use.`,
              undefined,
              {
                duration: ms('3 seconds')
              }
            );
          } else {
            this.snackBar.open(
              $localize`An error occurred while updating to ${assetProfileIdentifier.symbol} (${assetProfileIdentifier.dataSource}).`,
              undefined,
              {
                duration: ms('3 seconds')
              }
            );
          }

          return EMPTY;
        }),
        takeUntil(this.unsubscribeSubject)
      )
      .subscribe(() => {
        const newAssetProfileIdentifier = {
          dataSource: assetProfileIdentifier.dataSource,
          symbol: assetProfileIdentifier.symbol
        };

        this.dialogRef.close(newAssetProfileIdentifier);
      });
  }

  public onTestMarketData() {
    this.adminService
      .testMarketData({
        dataSource: this.data.dataSource,
        scraperConfiguration: {
          defaultMarketPrice: this.assetProfileForm.controls[
            'scraperConfiguration'
          ].controls['defaultMarketPrice'].value as number,
          headers: JSON.parse(
            this.assetProfileForm.controls['scraperConfiguration'].controls[
              'headers'
            ].value
          ),
          locale:
            this.assetProfileForm.controls['scraperConfiguration'].controls[
              'locale'
            ].value || undefined,
          mode: this.assetProfileForm.controls['scraperConfiguration'].controls[
            'mode'
          ].value,
          selector:
            this.assetProfileForm.controls['scraperConfiguration'].controls[
              'selector'
            ].value,
          url: this.assetProfileForm.controls['scraperConfiguration'].controls[
            'url'
          ].value
        },
        symbol: this.data.symbol
      })
      .pipe(
        catchError(({ error }) => {
          this.notificationService.alert({
            message: error?.message,
            title: $localize`Error`
          });
          return EMPTY;
        }),
        takeUntil(this.unsubscribeSubject)
      )
      .subscribe(({ price }) => {
        this.notificationService.alert({
          title:
            $localize`The current market price is` +
            ' ' +
            price +
            ' ' +
            this.assetProfileForm.get('currency').value
        });
      });
  }

  public onToggleIsActive({ checked }: MatCheckboxChange) {
    if (checked) {
      this.assetProfileForm.get('isActive')?.setValue(true);
    } else {
      this.assetProfileForm.get('isActive')?.setValue(false);
    }

    if (checked === this.assetProfile.isActive) {
      this.assetProfileForm.get('isActive')?.markAsPristine();
    } else {
      this.assetProfileForm.get('isActive')?.markAsDirty();
    }
  }

  public onUnsetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
    this.dataService
      .deleteBenchmark({ dataSource, symbol })
      .pipe(takeUntil(this.unsubscribeSubject))
      .subscribe(() => {
        this.dataService.updateInfo();

        this.isBenchmark = false;

        this.changeDetectorRef.markForCheck();
      });
  }

  public ngOnDestroy() {
    this.unsubscribeSubject.next();
    this.unsubscribeSubject.complete();
  }

  public onTriggerSubmitAssetProfileForm() {
    if (this.assetProfileForm.valid) {
      this.onSubmitAssetProfileForm();
    }
  }

  private isNewSymbolValid(control: AbstractControl): ValidationErrors {
    const currentAssetProfileIdentifier: AssetProfileIdentifier | undefined =
      control.get('assetProfileIdentifier').value;

    if (
      currentAssetProfileIdentifier?.dataSource === this.data?.dataSource &&
      currentAssetProfileIdentifier?.symbol === this.data?.symbol
    ) {
      return {
        equalsPreviousProfileIdentifier: true
      };
    }
  }
}
