import { Injectable } from '@angular/core';
import {
  AddressDTO,
  AddressService,
  AssignedPayerDTO,
  AutocompleteDTO,
  CommunicationDetailsDTO,
  CountryDTO,
  CountryService,
  CustomerDTO,
  CustomerInfoDTO,
  CustomerService,
  HistoryDTO,
  InputDTO,
  KeyValueDTOOfStringAndString,
  ListResponseArgsOfCustomerInfoDTO,
  LoyaltyCardService,
  NotificationChannel,
  PayerDTO,
  PayerService,
  QueryTokenDTO,
  ResponseArgsOfIEnumerableOfBonusCardInfoDTO,
  ShippingAddressDTO,
  ShippingAddressService,
} from '@generated/swagger/crm-api';
import { isArray, memorize } from '@utils/common';
import { PagedResult, Result } from '@domain/defs';
import { Observable, of, ReplaySubject } from 'rxjs';
import { catchError, map, mergeMap, retry, shareReplay } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class CrmCustomerService {
  constructor(
    private customerService: CustomerService,
    private payerService: PayerService,
    private addressService: AddressService,
    private countryService: CountryService,
    private shippingAddressService: ShippingAddressService,
    private loyaltyCardService: LoyaltyCardService,
  ) {}

  @memorize()
  filterSettings() {
    return this.customerService.CustomerCustomerQuerySettings().pipe(
      map((res) => res.result),
      shareReplay(1),
    );
  }

  complete(
    queryString: string,
    filter?: { [key: string]: string },
  ): Observable<Result<AutocompleteDTO[]>> {
    return this.customerService.CustomerCustomerAutocomplete({
      input: queryString,
      filter: filter || {},
      take: 5,
    });
  }

  canUpgrade(customerId: number): Observable<InputDTO> {
    return this.customerService
      .CustomerCanExtendCustomer({
        customerId,
      })
      .pipe(map((res) => res.result));
  }

  getCustomers(
    queryString: string,
    options: {
      take?: number;
      skip?: number;
      filter?: { [key: string]: string };
    } = { take: 20, skip: 0 },
  ): Observable<ListResponseArgsOfCustomerInfoDTO> {
    return this.customerService.CustomerListCustomers({
      input: queryString ? { qs: queryString } : undefined,
      take: options.take,
      skip: options.skip,
      filter: options.filter || {},
    });
  }

  getCustomersWithQueryToken(queryToken: QueryTokenDTO) {
    if (queryToken.skip === undefined) queryToken.skip = 0;
    if (queryToken.take === undefined) queryToken.take = 20;
    if (queryToken.input === undefined) queryToken.input = { qs: '' };
    if (queryToken.filter === undefined) queryToken.filter = {};

    return this.customerService.CustomerListCustomers(queryToken);
  }

  getCustomersByCustomerCardNumber(
    queryString: string,
  ): Observable<PagedResult<CustomerInfoDTO>> {
    return this.customerService.CustomerGetCustomerByBonuscard(
      queryString ? queryString : undefined,
    );
  }

  getCustomer(
    customerId: number,
    eagerLoading?: number,
  ): Observable<Result<CustomerDTO>> {
    return this.customerService.CustomerGetCustomer({
      customerId,
      eagerLoading,
    });
  }

  getAssignedPayers(
    params: CustomerService.CustomerGetAssignedPayersByCustomerIdParams,
  ) {
    return this.customerService.CustomerGetAssignedPayersByCustomerId(params);
  }

  /* @internal */
  getNotificationChannelForCommunicationDetails({
    communicationDetails,
  }: {
    communicationDetails: CommunicationDetailsDTO;
  }): NotificationChannel {
    let notificationChannels = 0;

    if (communicationDetails?.email) {
      notificationChannels += 1;
    }
    if (communicationDetails?.mobile) {
      notificationChannels += 2;
    }

    return notificationChannels as NotificationChannel;
  }

  patchCustomer(
    customerId: number,
    customer: CustomerDTO,
  ): Observable<Result<CustomerDTO>> {
    const notificationChannels =
      this.getNotificationChannelForCommunicationDetails({
        communicationDetails: customer?.communicationDetails,
      });

    return this.customerService.CustomerPatchCustomer({
      customerId,
      customer: { ...customer, notificationChannels },
    });
  }

  createB2BCustomer(customer: CustomerDTO): Promise<Result<CustomerDTO>> {
    const notificationChannels =
      this.getNotificationChannelForCommunicationDetails({
        communicationDetails: customer?.communicationDetails,
      });

    const payload: CustomerDTO = {
      ...customer,
      customerType: 16,
      notificationChannels,
    };

    payload.shippingAddresses = payload.shippingAddresses ?? [];

    payload.payers = payload.payers ?? [];

    return this.customerService
      .CustomerCreateCustomer({
        customer: payload,
        modifiers: [{ key: 'b2b', group: 'customertype' }],
      })
      .toPromise();
  }

  createOnlineCustomer(customer: CustomerDTO): Observable<Result<CustomerDTO>> {
    const payload: CustomerDTO = {
      ...customer,
      customerType: 8,
      hasOnlineAccount: true,
    };

    const notificationChannels =
      this.getNotificationChannelForCommunicationDetails({
        communicationDetails: customer?.communicationDetails,
      });

    if (
      !(
        isArray(payload.shippingAddresses) &&
        payload.shippingAddresses.length > 0
      )
    ) {
      payload.shippingAddresses = [
        {
          data: {
            address: payload.address,
            communicationDetails: payload.communicationDetails,
            firstName: payload.firstName,
            gender: payload.gender,
            lastName: payload.lastName,
            organisation: payload.organisation,
            title: payload.title,
            type: 1,
            isDefault: new Date().toISOString(),
          },
        },
      ];
    }

    if (!(isArray(payload.payers) && payload.payers.length > 0)) {
      payload.payers = [
        {
          payer: {
            data: {
              address: payload.address,
              communicationDetails: payload.communicationDetails,
              firstName: payload.firstName,
              gender: payload.gender,
              lastName: payload.lastName,
              organisation: payload.organisation,
              title: payload.title,
              payerType: payload.customerType,
            },
          },
        },
      ];
    }

    const p4mUser = customer.features.find((f) => f.key === 'p4mUser')?.value;

    const modifiers: KeyValueDTOOfStringAndString[] = [
      { key: 'webshop', group: 'customertype' },
    ];

    if (p4mUser) {
      modifiers.push({ key: 'add-loyalty-card', value: p4mUser });
    }

    return this.customerService.CustomerCreateCustomer({
      customer: { ...payload, notificationChannels },
      modifiers,
    });
  }

  mapCustomerToPayer(customer: CustomerDTO): PayerDTO {
    return {
      address: customer.address,
      communicationDetails: customer.communicationDetails,
      firstName: customer.firstName,
      lastName: customer.lastName,
      organisation: customer.organisation,
      title: customer.title,
      payerType: 1,
      gender: customer.gender,
    };
  }

  mapCustomerToShippingAddress(customer: CustomerDTO): ShippingAddressDTO {
    return {
      address: customer.address,
      communicationDetails: customer.communicationDetails,
      firstName: customer.firstName,
      gender: customer.gender,
      lastName: customer.lastName,
      organisation: customer.organisation,
      title: customer.title,
      type: 1,
    };
  }

  async updateToOnlineCustomer(
    customer: CustomerDTO,
  ): Promise<Result<CustomerDTO>> {
    const payload: CustomerDTO = {
      shippingAddresses: [],
      payers: [],
      ...customer,
      customerType: 8,
      hasOnlineAccount: true,
    };

    const notificationChannels =
      this.getNotificationChannelForCommunicationDetails({
        communicationDetails: payload?.communicationDetails,
      });

    const shippingAddressesToAdd =
      payload.shippingAddresses?.filter((sa) => !sa.id)?.map((m) => m.data) ??
      [];
    payload.shippingAddresses =
      payload.shippingAddresses?.filter((sa) => !!sa.id) ?? [];

    if (payload.shippingAddresses.length === 0) {
      shippingAddressesToAdd.unshift(
        this.mapCustomerToShippingAddress(payload),
      );
    }

    const payersToAdd =
      payload.payers
        ?.filter((p) => !p.assignedToCustomer)
        ?.map((p) => p.payer?.data) ?? [];
    payload.payers =
      payload.payers?.filter((p) => !!p.assignedToCustomer) ?? [];

    if (payload.payers.length === 0) {
      payersToAdd.unshift({
        ...this.mapCustomerToPayer(payload),
        payerType: payload.customerType,
      });
    }

    const modifiers: KeyValueDTOOfStringAndString[] = [
      { key: 'webshop', group: 'customertype' },
    ];

    const res = await this.customerService
      .CustomerUpdateCustomer({
        customerId: customer.id,
        payload: {
          modifiers,
          customer: { ...payload, notificationChannels },
        },
      })
      .toPromise();

    for (const shippingAddress of shippingAddressesToAdd) {
      await this.createShippingAddress(res.result.id, shippingAddress, true);
    }

    for (const payer of payersToAdd) {
      await this.createPayer(res.result.id, payer, true);
    }

    return res;
  }

  async updateToP4MOnlineCustomer(
    customer: CustomerDTO,
  ): Promise<Result<CustomerDTO>> {
    const shippingAddressesToAdd =
      customer.shippingAddresses?.filter((sa) => !sa.id)?.map((m) => m.data) ??
      [];
    customer.shippingAddresses =
      customer.shippingAddresses?.filter((sa) => !!sa.id) ?? [];

    if (customer.shippingAddresses.length === 0) {
      shippingAddressesToAdd.unshift(
        this.mapCustomerToShippingAddress(customer),
      );
    }

    const payersToAdd =
      customer.payers
        ?.filter((p) => !p.assignedToCustomer)
        ?.map((p) => p.payer?.data) ?? [];
    customer.payers =
      customer.payers?.filter((p) => !!p.assignedToCustomer) ?? [];

    if (customer.payers.length === 0) {
      payersToAdd.unshift({
        ...this.mapCustomerToPayer(customer),
        payerType: customer.customerType,
      });
    }

    const p4mUser = customer.features.find((f) => f.key === 'p4mUser')?.value;

    const modifiers: KeyValueDTOOfStringAndString[] = [
      { key: 'webshop', group: 'customertype' },
    ];

    if (p4mUser) {
      modifiers.push({ key: 'add-loyalty-card', value: p4mUser });
    }

    const res = await this.customerService
      .CustomerUpdateCustomer({
        customerId: customer.id,
        payload: {
          customer,
          modifiers,
        },
      })
      .toPromise();

    for (const shippingAddress of shippingAddressesToAdd) {
      await this.createShippingAddress(res.result.id, shippingAddress, true);
    }

    for (const payer of payersToAdd) {
      await this.createPayer(res.result.id, payer, true);
    }

    return res;
  }

  async updateStoreP4MToWebshopP4M(
    customer: CustomerDTO,
  ): Promise<Result<CustomerDTO>> {
    const shippingAddressesToAdd =
      customer.shippingAddresses?.filter((sa) => !sa.id)?.map((m) => m.data) ??
      [];
    customer.shippingAddresses =
      customer.shippingAddresses?.filter((sa) => !!sa.id) ?? [];

    if (customer.shippingAddresses.length === 0) {
      shippingAddressesToAdd.unshift(
        this.mapCustomerToShippingAddress(customer),
      );
    }

    const payersToAdd =
      customer.payers
        ?.filter((p) => !p.assignedToCustomer)
        ?.map((p) => p.payer?.data) ?? [];
    customer.payers =
      customer.payers?.filter((p) => !!p.assignedToCustomer) ?? [];

    if (customer.payers.length === 0) {
      payersToAdd.unshift({
        ...this.mapCustomerToPayer(customer),
        payerType: customer.customerType,
      });
    }

    const modifiers: KeyValueDTOOfStringAndString[] = [
      { key: 'webshop', group: 'customertype' },
    ];

    const res = await this.customerService
      .CustomerUpdateCustomer({
        customerId: customer.id,
        payload: {
          customer,
          modifiers,
        },
      })
      .toPromise();

    for (const shippingAddress of shippingAddressesToAdd) {
      await this.createShippingAddress(res.result.id, shippingAddress, true);
    }

    for (const payer of payersToAdd) {
      await this.createPayer(res.result.id, payer, true);
    }

    return res;
  }

  async createGuestCustomer(
    customer: CustomerDTO,
  ): Promise<Result<CustomerDTO>> {
    const notificationChannels =
      this.getNotificationChannelForCommunicationDetails({
        communicationDetails: customer?.communicationDetails,
      });
    const payload: CustomerDTO = {
      ...customer,
      customerType: 8,
      isGuestAccount: true,
      notificationChannels,
    };

    payload.shippingAddresses = customer.shippingAddresses ?? [];

    payload.shippingAddresses.push({
      data: this.mapCustomerToShippingAddress(customer),
    });

    payload.payers = customer.payers ?? [];

    payload.payers.push({
      payer: {
        data: {
          ...this.mapCustomerToPayer(customer),
          payerType: payload.customerType,
        },
      },
    });

    const res = await this.customerService
      .CustomerCreateCustomer({
        customer: payload,
        modifiers: [{ key: 'webshop', group: 'customertype' }],
      })
      .toPromise();

    return res;
  }

  createStoreCustomer(customer: CustomerDTO): Observable<Result<CustomerDTO>> {
    const notificationChannels =
      this.getNotificationChannelForCommunicationDetails({
        communicationDetails: customer?.communicationDetails,
      });

    const p4mUser = customer.features.find((f) => f.key === 'p4mUser')?.value;

    const modifiers: KeyValueDTOOfStringAndString[] = [
      { key: 'store', group: 'customertype' },
    ];

    if (p4mUser) {
      modifiers.push({ key: 'add-loyalty-card', value: p4mUser });
    }

    return this.customerService.CustomerCreateCustomer({
      customer: { ...customer, customerType: 8, notificationChannels },
      modifiers,
    });
  }

  validateAddress(address: AddressDTO): Observable<Result<AddressDTO[]>> {
    return this.addressService.AddressValidateAddress(address);
  }

  getOnlineCustomerByEmail(email: string): Observable<CustomerInfoDTO | null> {
    return this.getCustomers(email, {
      take: 1,
      filter: {
        customertype: 'webshop',
      },
    }).pipe(
      map((r) => {
        if (r.hits === 1) {
          return r.result[0];
        } else {
          return null;
        }
      }),
      catchError((err) => [null]),
    );
  }

  private cachedCountriesFailed = false;
  private cachedCountries: ReplaySubject<Result<CountryDTO[]>>;
  getCountries(): Observable<Result<CountryDTO[]>> {
    if (!this.cachedCountries || this.cachedCountriesFailed) {
      this.cachedCountriesFailed = false;
      this.cachedCountries = new ReplaySubject();
      this.countryService
        .CountryGetCountries({})
        .pipe(
          retry(3),
          catchError((err) => {
            this.cachedCountriesFailed = true;
            return of<Result<CountryDTO[]>>({
              error: true,
              message: err?.message,
            });
          }),
        )
        .subscribe(this.cachedCountries);
    }

    return this.cachedCountries.asObservable();
  }

  emailExists(email: string): Observable<Result<boolean>> {
    return this.customerService.CustomerEmailExists(email);
  }

  // checkLoyaltyCard({ loyaltyCardNumber, customerId }: { loyaltyCardNumber: string; customerId?: number }) {
  //   return this.loyaltyCardService.LoyaltyCardCheckLoyaltyCard({ loyaltyCardNumber, customerId });
  // }

  createPayer(
    customerId: number,
    payer: PayerDTO,
    isDefault?: boolean,
  ): Promise<[Result<PayerDTO>, Result<AssignedPayerDTO>]> {
    return this.getCustomer(customerId)
      .pipe(
        mergeMap((customerResponse) =>
          this.payerService
            .PayerCreatePayer({
              ...payer,
              payerType: customerResponse.result.customerType,
            })
            .pipe(
              mergeMap((payerResponse) =>
                this.customerService
                  .CustomerAddPayerReference({
                    customerId: customerId,
                    payerId: payerResponse.result.id,
                    isDefault: isDefault,
                  })
                  .pipe(
                    map(
                      (assigendPayerResponse) =>
                        [payerResponse, assigendPayerResponse] as [
                          Result<PayerDTO>,
                          Result<AssignedPayerDTO>,
                        ],
                    ),
                  ),
              ),
            ),
        ),
      )
      .toPromise();
  }

  updatePayer(
    customerId: number,
    payerId: number,
    payer: PayerDTO,
    isDefault?: boolean,
  ): Observable<Result<PayerDTO>> {
    return this.payerService
      .PayerUpdatePayer({ payerId, payer })
      .pipe(
        mergeMap((response) =>
          this.setDefaultPayer(payerId, customerId, isDefault).pipe(
            map(() => response),
          ),
        ),
      );
  }

  setDefaultPayer(
    payerId: number,
    customerId: number,
    isDefault = true,
  ): Observable<Result<AssignedPayerDTO>> {
    return this.customerService.CustomerModifyPayerReference({
      payerId,
      customerId,
      isDefault,
    });
  }

  createShippingAddress(
    customerId: number,
    shippingAddress: ShippingAddressDTO,
    isDefault?: boolean,
  ): Promise<Result<ShippingAddressDTO>> {
    const data: ShippingAddressDTO = { ...shippingAddress };
    if (isDefault) {
      data.isDefault = new Date().toJSON();
    } else {
      delete data.isDefault;
    }

    return this.shippingAddressService
      .ShippingAddressCreateShippingAddress({
        customerId,
        shippingAddress: data,
      })
      .toPromise();
  }

  updateShippingAddress(
    customerId: number,
    shippingAddressId: number,
    shippingAddress: ShippingAddressDTO,
    isDefault?: boolean,
  ): Promise<Result<ShippingAddressDTO>> {
    const data: ShippingAddressDTO = { ...shippingAddress };

    if (isDefault) {
      data.isDefault = new Date().toJSON();
    } else {
      delete data.isDefault;
    }

    return this.shippingAddressService
      .ShippingAddressUpdateShippingAddress({
        shippingAddressId,
        shippingAddress: data,
        customerId,
      })
      .toPromise();
  }

  getShippingAddress(
    shippingAddressId: number,
  ): Observable<Result<ShippingAddressDTO>> {
    return this.shippingAddressService.ShippingAddressGetShippingaddress(
      shippingAddressId,
    );
  }

  getShippingAddresses(
    params: ShippingAddressService.ShippingAddressGetShippingAddressesParams,
  ): Observable<Result<ShippingAddressDTO[]>> {
    return this.shippingAddressService.ShippingAddressGetShippingAddresses(
      params,
    );
  }

  getPayer(payerId: number): Observable<Result<PayerDTO>> {
    return this.payerService.PayerGetPayer(payerId);
  }

  getCustomerCard(
    customerId: number,
  ): Observable<ResponseArgsOfIEnumerableOfBonusCardInfoDTO> {
    return this.customerService.CustomerGetBonuscards(customerId);
  }

  getCustomerHistory(customerId: number): Observable<HistoryDTO[]> {
    return this.customerService
      .CustomerGetCustomerHistory({ customerId })
      .pipe(map((response) => response?.result));
  }
}
