import { HttpErrorResponse } from '@angular/common/http';
import {
  ChangeDetectorRef,
  Directive,
  OnDestroy,
  OnInit,
  ViewChild,
  inject,
} from '@angular/core';
import {
  AbstractControl,
  AsyncValidatorFn,
  UntypedFormControl,
  UntypedFormGroup,
  ValidationErrors,
  ValidatorFn,
} from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { CrmCustomerService } from '@domain/crm';
import {
  AddressDTO,
  CustomerDTO,
  PayerDTO,
  ShippingAddressDTO,
} from '@generated/swagger/crm-api';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { UiValidators } from '@ui/validators';
import { isNull } from 'lodash';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import {
  first,
  map,
  distinctUntilChanged,
  shareReplay,
  delay,
  mergeMap,
  catchError,
  tap,
  bufferCount,
  startWith,
  takeUntil,
} from 'rxjs/operators';
import {
  AddressFormBlockComponent,
  DeviatingAddressFormBlockComponent,
  DeviatingAddressFormBlockData,
} from '../components/form-blocks';
import { FormBlock } from '../components/form-blocks/form-block';
import {
  CustomerCreateFormData,
  decodeFormData,
  encodeFormData,
  mapCustomerInfoDtoToCustomerCreateFormData,
} from './customer-create-form-data';
import { AddressSelectionModalService } from '../modals';
import {
  CustomerCreateNavigation,
  CustomerSearchNavigation,
} from '@shared/services/navigation';

@Directive()
export abstract class AbstractCreateCustomer implements OnInit, OnDestroy {
  protected modal = inject(UiModalService);

  protected activatedRoute = inject(ActivatedRoute);
  protected router = inject(Router);
  protected customerService = inject(CrmCustomerService);
  protected addressVlidationModal = inject(AddressSelectionModalService);
  protected breadcrumb = inject(BreadcrumbService);
  protected cdr = inject(ChangeDetectorRef);
  protected customerSearchNavigation = inject(CustomerSearchNavigation);

  protected onDestroy$ = new Subject<void>();

  abstract validateAddress?: boolean;
  abstract validateShippingAddress?: boolean;

  private _formData = new BehaviorSubject<CustomerCreateFormData>({});

  formData$ = this._formData.asObservable();

  get formData() {
    return this._formData.getValue();
  }

  form = new UntypedFormGroup({});

  latestProcessId: number;

  processId$: Observable<number>;

  busy$ = new BehaviorSubject(false);

  customerExists$ = new Subject<boolean>();

  @ViewChild(DeviatingAddressFormBlockComponent)
  deviatingDeliveryAddressFormBlock: DeviatingAddressFormBlockComponent;

  @ViewChild(AddressFormBlockComponent)
  addressFormBlock: AddressFormBlockComponent;

  abstract customerType: string;

  readonly customerCreateNavigation = inject(CustomerCreateNavigation);

  constructor() {
    this._initProcessId$();
  }

  ngOnInit() {}

  private _initProcessId$(): void {
    this.processId$ = this.activatedRoute.parent.parent.data.pipe(
      map((data) => +data.processId),
      tap((processId) => (this.latestProcessId = processId)),
      distinctUntilChanged(),
      shareReplay(1),
    );

    this.processId$
      .pipe(
        startWith(undefined),
        bufferCount(2, 1),
        takeUntil(this.onDestroy$),
        delay(100),
      )
      .subscribe(async ([previous, current]) => {
        if (previous === undefined) {
          await this._initFormData();
          await this.updateBreadcrumb(current, this.formData);
        } else if (previous !== current) {
          await this.updateBreadcrumb(previous, this.formData);
          await this._initFormData();
          await this.updateBreadcrumb(current, this.formData);
        }
      });
  }

  ngOnDestroy(): void {
    // Fix für #4676 - Breadcrumb wurde beim Schließen des Prozesses neu erstellt und nicht korrekt gelöscht
    // this.updateBreadcrumb(this.latestProcessId, this.formData);
    this.onDestroy$.next();
    this.onDestroy$.complete();
    this.busy$.complete();
    this._formData.complete();
  }

  private async _initFormData() {
    const formData = await this.activatedRoute.queryParams
      .pipe(
        map((params) => params['formData']),
        first(),
      )
      .toPromise();

    if (formData) {
      const parsedFormData = decodeFormData(formData);
      this._formData.next(parsedFormData);
    }
  }

  async updateBreadcrumb(processId: number, formData: CustomerCreateFormData) {
    await this.cleanupBreadcrumb(processId);
    await this.addOrUpdateBreadcrumb(processId, formData);
  }

  async cleanupBreadcrumb(processId: number) {
    const crumbs = await this.breadcrumb
      .getBreadcrumbsByKeyAndTags$(processId, ['customer', 'main'])
      .pipe(first())
      .toPromise();
    for (const crumb of crumbs) {
      await this.breadcrumb.removeBreadcrumbsAfter(crumb.id);
    }
  }

  async addOrUpdateBreadcrumb(
    processId: number,
    formData: CustomerCreateFormData,
  ) {
    await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
      key: processId,
      name: 'Kundendaten erfassen',
      path: this.getPath(processId),
      params: this.getQueryParams(formData),
      tags: ['customer', 'create'],
      section: 'customer',
    });
  }

  getPath(processId: number) {
    const route = this.customerCreateNavigation.createCustomerRoute({
      processId,
      customerType: this.customerType,
    });

    return route.path;
  }

  getQueryParams(formData: CustomerCreateFormData): Record<string, string> {
    const param = formData ? encodeFormData(formData) : undefined;
    return { ...this.activatedRoute.snapshot.queryParams, formData: param };
  }

  async customerTypeChanged(customerType: string) {
    const processId = await this.processId$.pipe(first()).toPromise();

    const route = this.customerCreateNavigation.createCustomerRoute({
      processId,
      customerType,
    });

    await this.router.navigate(route.path, {
      queryParams: this.getQueryParams(this.formData),
    });

    console.log('customerTypeChanged', customerType);
  }

  addFormBlock(
    key: keyof CustomerCreateFormData,
    block: FormBlock<any, AbstractControl>,
  ) {
    this.form.addControl(key, block.control);
    this.cdr.markForCheck();
  }

  patchFormData(key: keyof CustomerCreateFormData, value: any) {
    this._formData.next({ ...this.formData, [key]: value });
    this.cdr.markForCheck();
  }

  minBirthDateValidator = (): ValidatorFn => {
    return (control: AbstractControl): ValidationErrors | null => {
      const minAge = 16; // 16 years

      if (!control.value) {
        return null;
      }

      const controlBirthDate = new Date(control.value);
      const minBirthDate = new Date();
      minBirthDate.setFullYear(minBirthDate.getFullYear() - minAge);

      // Check if customer is over 16 years old
      if (this._checkIfAgeOver16(controlBirthDate, minBirthDate)) {
        return null;
      } else {
        return { minBirthDate: `Teilnahme ab ${minAge} Jahren` };
      }
    };
  };

  private _checkIfAgeOver16(inputDate: Date, minBirthDate: Date): boolean {
    // Check year
    if (inputDate.getFullYear() < minBirthDate.getFullYear()) {
      return true;
    }
    // Check Year + Month
    else if (
      inputDate.getFullYear() === minBirthDate.getFullYear() &&
      inputDate.getMonth() < minBirthDate.getMonth()
    ) {
      return true;
    }
    // Check Year + Month + Day
    else if (
      inputDate.getFullYear() === minBirthDate.getFullYear() &&
      inputDate.getMonth() === minBirthDate.getMonth() &&
      inputDate.getDate() <= minBirthDate.getDate()
    ) {
      return true;
    }
    return false;
  }

  emailExistsValidator: AsyncValidatorFn = (control) => {
    return of(control.value).pipe(
      tap((_) => this.customerExists$.next(false)),
      delay(500),
      mergeMap((value) => {
        return this.customerService.emailExists(value).pipe(
          map((response) => {
            if (response?.result) {
              return { exists: response?.message };
            }
            return null;
          }),
          catchError((error) => {
            if (error instanceof HttpErrorResponse) {
              if (error?.error?.invalidProperties?.email) {
                return of({ invalid: error.error.invalidProperties.email });
              } else {
                return of({ invalid: 'E-Mail ist ungültig' });
              }
            }
          }),
        );
      }),
      tap((error) => {
        if (error) {
          this.customerExists$.next(true);
        }
        control.markAsTouched();
        this.cdr.markForCheck();
      }),
    );
  };

  // checkLoyalityCardValidator: AsyncValidatorFn = (control) => {
  //   return of(control.value).pipe(
  //     delay(500),
  //     mergeMap((value) => {
  //       const customerId = this.formData?._meta?.customerDto?.id ?? this.formData?._meta?.customerInfoDto?.id;
  //       return this.customerService.checkLoyaltyCard({ loyaltyCardNumber: value, customerId }).pipe(
  //         map((response) => {
  //           if (response.error) {
  //             throw response.message;
  //           }

  //           /**
  //            * #4485 Kubi // Verhalten mit angelegte aber nicht verknüpfte Kundenkartencode in Kundensuche und Kundendaten erfassen ist nicht gleich
  //            * Fall1: Kundenkarte hat Daten in point4more:
  //            * Sobald Kundenkartencode in Feld "Kundenkartencode" reingegeben wird- werden die Daten von point4more in Formular "Kundendaten Erfassen" eingefügt und ersetzen (im Ganzen, nicht inkremental) die Daten in Felder, falls welche schon reingetippt werden.
  //            * Fall2: Kundenkarte hat keine Daten in point4more:
  //            * Sobald Kundenkartencode in Feld "Kundenkartencode" reingegeben wird- bleiben die Daten in Formular "Kundendaten Erfassen" in Felder, falls welche schon reingetippt werden.
  //            */
  //           if (response.result && response.result.customer) {
  //             const customer = response.result.customer;
  //             const data = mapCustomerInfoDtoToCustomerCreateFormData(customer);

  //             if (data.name.firstName && data.name.lastName) {
  //               // Fall1
  //               this._formData.next(data);
  //             } else {
  //               // Fall2 Hier müssen die Metadaten gesetzt werden um eine verknüfung zur kundenkarte zu ermöglichen.
  //               const current = this.formData;
  //               current._meta = data._meta;
  //               current.p4m = data.p4m;
  //             }
  //           }

  //           return null;
  //         }),
  //         catchError((error) => {
  //           if (error instanceof HttpErrorResponse) {
  //             if (error?.error?.invalidProperties?.loyaltyCardNumber) {
  //               return of({ invalid: error.error.invalidProperties.loyaltyCardNumber });
  //             } else {
  //               return of({ invalid: 'Kundenkartencode ist ungültig' });
  //             }
  //           }
  //         }),
  //       );
  //     }),
  //     tap(() => {
  //       control.markAsTouched();
  //       this.cdr.markForCheck();
  //     }),
  //   );
  // };

  async navigateToCustomerDetails(customer: CustomerDTO) {
    const processId = await this.processId$.pipe(first()).toPromise();
    const route = this.customerSearchNavigation.detailsRoute({
      processId,
      customerId: customer.id,
      customer,
    });

    return this.router.navigate(route.path, {
      queryParams: route.urlTree.queryParams,
    });
  }

  async validateAddressData(address: AddressDTO): Promise<AddressDTO> {
    const addressValidationResult =
      await this.addressVlidationModal.validateAddress(address);

    if (
      addressValidationResult !== undefined &&
      (addressValidationResult as any) !== 'continue'
    ) {
      address = addressValidationResult;
    }

    return address;
  }

  async getCustomerFromFormData(): Promise<CustomerDTO> {
    const data: CustomerCreateFormData = this.form.value;

    const customer: CustomerDTO = {
      communicationDetails: {},
      attributes: [],
      features: [],
    };

    if (data.name) {
      customer.gender = data.name.gender;
      customer.title = data.name.title;
      customer.firstName = data.name.firstName;
      customer.lastName = data.name.lastName;
    }

    if (data.organisation) {
      customer.organisation = data.organisation;
    }

    if (data.email) {
      customer.communicationDetails.email = data.email;
    }

    if (data.phoneNumbers) {
      customer.communicationDetails.mobile = data.phoneNumbers.mobile;
      customer.communicationDetails.phone = data.phoneNumbers.phone;
    }

    if (data.address) {
      customer.address = data.address;

      if (this.validateAddress) {
        try {
          const address = await this.validateAddressData(customer.address);
          this.addressFormBlock.data = address;
          customer.address = address;
        } catch (error) {
          this.form.enable();
          setTimeout(() => {
            this.addressFormBlock.setAddressValidationError(
              error.error.invalidProperties,
            );
          }, 10);

          return;
        }
      }
    }

    if (
      data.birthDate &&
      isNull(UiValidators.date(new UntypedFormControl(data.birthDate)))
    ) {
      customer.dateOfBirth = data.birthDate;
    }

    if (data.billingAddress?.deviatingAddress) {
      const billingAddress = this.mapToBillingAddress(data.billingAddress);

      if (this.validateShippingAddress) {
        try {
          billingAddress.address = await this.validateAddressData(
            billingAddress.address,
          );
        } catch (error) {
          this.form.enable();
          setTimeout(() => {
            this.addressFormBlock.setAddressValidationError(
              error.error.invalidProperties,
            );
          }, 10);

          return;
        }
      }

      customer.payers = [
        {
          payer: { data: billingAddress },
          isDefault: new Date().toISOString(),
        },
      ];
    }

    if (data.deviatingDeliveryAddress?.deviatingAddress) {
      const shippingAddress = this.mapToShippingAddress(
        data.deviatingDeliveryAddress,
      );

      if (this.validateShippingAddress) {
        try {
          shippingAddress.address = await this.validateAddressData(
            shippingAddress.address,
          );
        } catch (error) {
          this.form.enable();
          setTimeout(() => {
            this.deviatingDeliveryAddressFormBlock.setAddressValidationError(
              error.error.invalidProperties,
            );
          }, 10);

          return;
        }
      }

      customer.shippingAddresses = [
        {
          data: { ...shippingAddress, isDefault: new Date().toISOString() },
        },
      ];
    }

    return customer;
  }

  mapToShippingAddress({
    name,
    address,
    email,
    organisation,
    phoneNumbers,
  }: DeviatingAddressFormBlockData): ShippingAddressDTO {
    return {
      gender: name?.gender,
      title: name?.title,
      firstName: name?.firstName,
      lastName: name?.lastName,
      address,
      communicationDetails: {
        email: email ? email : null,
        mobile: phoneNumbers?.mobile ? phoneNumbers.mobile : null,
        phone: phoneNumbers?.phone ? phoneNumbers.phone : null,
      },
      organisation,
      isDefault: new Date().toJSON(),
    };
  }

  mapToBillingAddress({
    name,
    address,
    email,
    organisation,
    phoneNumbers,
  }: DeviatingAddressFormBlockData): PayerDTO {
    return {
      gender: name?.gender,
      title: name?.title,
      firstName: name?.firstName,
      lastName: name?.lastName,
      address,
      communicationDetails: {
        email: email ? email : null,
        mobile: phoneNumbers?.mobile ? phoneNumbers.mobile : null,
        phone: phoneNumbers?.phone ? phoneNumbers.phone : null,
      },
      organisation,
    };
  }

  async save() {
    if (this.busy$.value) {
      return;
    }

    if (this.form.invalid) {
      this.form.markAllAsTouched();
      return;
    }

    try {
      this.busy$.next(true);

      this.form.disable();
      const customer: CustomerDTO = await this.getCustomerFromFormData();

      if (!customer) {
        this.form.enable();
        return;
      }

      const response = await this.saveCustomer(customer);

      if (response) {
        this.navigateToCustomerDetails(response);
      }
    } catch (error) {
      this.form.enable();
      this.modal.open({
        content: UiErrorModalComponent,
        data: error,
      });
    } finally {
      this.busy$.next(false);
    }
  }

  abstract saveCustomer(customer: CustomerDTO): Promise<CustomerDTO>;
}
