import { State, Action, StateContext, Store } from '@ngxs/store';
import * as actions from '../actions/cart-entry.actions';
import { CartEntry } from '../../models/cart-entry.model';
import { SetCartData } from '../actions/cart.actions';
import { BranchSelectors } from '../selectors/branch.selector';
import { DeliveryOption, DeliveryType } from '../../models/delivery-option.model';
import {
  ShippingTarget,
  BranchDTO,
  EntityDTOContainerOfDestinationDTO,
  AddToShoppingCartDTO,
  ShoppingCartDTO,
  AvailabilityDTO as UpdateDestinationAvailability,
  DestinationDTO,
  AddressDTO,
  UpdateShoppingCartItemDTO,
  ShippingAddressDTO,
} from '@swagger/checkout';
import { ItemDTO } from '@swagger/cat';
import { ProcessSelectors } from '../selectors/process.selectors';
import { CheckoutService } from '../../services/checkout.service';
import { ProductSelectors } from '../selectors/product.selector';
import { AvailabilityDTO } from '@swagger/availability';
import { tap, map, flatMap, concatMap, switchMap } from 'rxjs/operators';
import { Observable, of, from } from 'rxjs';
import { UserStateSyncData } from '../../models/user-state-sync.model';
import { AppUserDataSync } from '../actions/app.actions';
import { SupplierDTO } from '@swagger/oms';
import { isArray } from 'util';
import { User } from '../../models/user.model';
import { CustomerMapping } from '../../mappings/customer.mapping';
import { CustomerSelectors } from '../selectors/customer.selectors';
import { CartState } from './cart.state';
import { Injectable } from '@angular/core';

export class CartEntryStateModel {
  cartEntries: { [key: number]: CartEntry };
}

@State<CartEntryStateModel>({
  name: 'cartEntries',
  defaults: {
    cartEntries: {},
  },
})
@Injectable()
export class CartEntryState {
  constructor(private store: Store, private checkoutService: CheckoutService, private customerMapping: CustomerMapping) {}

  @Action(actions.SetCartEntry)
  setCartEntryData(
    ctx: StateContext<CartEntryStateModel>,
    { quantity, deliveryType, bookId, availability, branch, promoPoints }: actions.SetCartEntry
  ) {
    const products = this.store.selectSnapshot(ProductSelectors.getProducts);
    const branches = this.store.selectSnapshot(BranchSelectors.getBranches);
    const currentProcess = this.store.selectSnapshot(ProcessSelectors.getCurrentProcess);
    const state = ctx.getState();

    if (!currentProcess || !currentProcess.cartId) {
      return;
    }

    const cartId = currentProcess.cartId;
    const existingItemId = this.itemExistInCart(state.cartEntries, bookId, deliveryType, branch, cartId);

    if (existingItemId) {
      this.store.dispatch(
        new actions.UpdateCartEntryQuantity(state.cartEntries[existingItemId].quantity + quantity, existingItemId, availability, '')
      );
    } else {
      return this.addItemToCart$(
        deliveryType,
        products[bookId],
        quantity,
        availability as any,
        currentProcess.cartId,
        ctx,
        branches[branch],
        promoPoints
      );
    }
  }

  @Action(actions.UpdateCartEntryType)
  updateCartEntryData(
    ctx: StateContext<CartEntryStateModel>,
    { deliveryType, bookId, availability, branch, oldDeliveryType, oldBranch }: actions.UpdateCartEntryType
  ) {
    const branches = this.store.selectSnapshot(BranchSelectors.getBranches);
    const currentProcess = this.store.selectSnapshot(ProcessSelectors.getCurrentProcess);
    if (!currentProcess) {
      return;
    }
    const state = ctx.getState();
    const existingItemId = this.itemExistInCart(state.cartEntries, bookId, oldDeliveryType, oldBranch, currentProcess.cartId);
    const newBranch = branches[branch];

    const itemToUpdate = { ...state.cartEntries[existingItemId] };
    const cartEntries = { ...state.cartEntries };
    const destination = this.createDestinationObject(deliveryType, newBranch, availability as any);
    const ava: UpdateDestinationAvailability = this.availibilityObjectForUpdate(availability as any, deliveryType);

    itemToUpdate.quantity = itemToUpdate.quantity;
    itemToUpdate.deliveryType = deliveryType;
    itemToUpdate.branch = newBranch.id;
    itemToUpdate.orderTypeChanged = true;
    itemToUpdate.pickUpPrice =
      deliveryType === DeliveryOption.PICK_UP &&
      availability &&
      availability.price &&
      availability.price.value &&
      availability.price.value.value
        ? availability.price.value.value
        : undefined;
    itemToUpdate.deliveryPrice =
      deliveryType === DeliveryOption.DELIVERY &&
      availability &&
      availability.price &&
      availability.price.value &&
      availability.price.value.value
        ? availability.price.value.value
        : undefined;
    itemToUpdate.downloadPrice =
      deliveryType === DeliveryOption.DOWNLOAD &&
      availability &&
      availability.price &&
      availability.price.value &&
      availability.price.value.value
        ? availability.price.value.value
        : undefined;
    cartEntries[existingItemId] = itemToUpdate;

    return this.checkoutService.updateItemInCart(currentProcess.cartId, existingItemId, destination, itemToUpdate.quantity, ava).pipe(
      tap(() => {
        ctx.patchState({
          cartEntries,
        });
        this.syncApiState(cartEntries);
      })
    );
  }

  @Action(actions.SetShippingForDownloadCart)
  setShippingForDownloadCart(ctx: StateContext<CartEntryStateModel>, { address, cartEntriesIds }: actions.SetShippingForDownloadCart) {
    const state = ctx.getState();
    const currentProcess = this.store.selectSnapshot(ProcessSelectors.getCurrentProcess);

    if (currentProcess) {
      const cartEntries = { ...state.cartEntries };
      if (cartEntriesIds && isArray(cartEntriesIds)) {
        return from(cartEntriesIds).pipe(
          concatMap((cartEntryId) =>
            this.checkoutService
              .updateDestinationForItemInCart(currentProcess.cartId, cartEntryId, <EntityDTOContainerOfDestinationDTO>{
                data: <DestinationDTO>{ shippingAddress: address },
              })
              .pipe(
                tap(() => {
                  const cartEntry = {
                    ...cartEntries[cartEntryId],
                    shippingAddress: address,
                  };
                  cartEntries[cartEntryId] = cartEntry;

                  ctx.patchState({
                    cartEntries,
                  });

                  this.syncApiState(cartEntries);
                })
              )
          )
        );
      }
    }
  }

  @Action(actions.UpdateCartEntryQuantity)
  updateCartEntryQuantity(
    ctx: StateContext<CartEntryStateModel>,
    { quantity, cartEntryId, availabilityMsg, availability }: actions.UpdateCartEntryQuantity
  ) {
    const state = ctx.getState();
    const currentProcess = this.store.selectSnapshot(ProcessSelectors.getCurrentProcess);

    const cartEntries = { ...state.cartEntries };
    const updateEntry = { ...cartEntries[cartEntryId] };
    updateEntry.quantity = quantity;
    updateEntry.orderTypeChanged = false;
    updateEntry.avaMessage = availabilityMsg;
    if (availability) {
      updateEntry.availability = availability as any;
    }

    cartEntries[cartEntryId] = updateEntry;

    ctx.patchState({
      cartEntries,
    });

    const products = this.store.selectSnapshot(ProductSelectors.getProducts);
    const branches = this.store.selectSnapshot(BranchSelectors.getBranches);
    return this.createQuantityUpdateObject$(
      updateEntry.deliveryType,
      products[updateEntry.bookId],
      quantity,
      branches[updateEntry.branch],
      (updateEntry.availability ? updateEntry.availability : products[updateEntry.bookId].catalogAvailability) as any, // TODO: Typisierung
      products[updateEntry.bookId].promoPoints
    ).pipe(
      switchMap((updatedItem) => this.checkoutService.updateQuantityForItemInCart(currentProcess.cartId, cartEntryId, updatedItem)),
      tap(() => {
        this.syncApiState(cartEntries);
      })
    );
  }

  @Action(actions.DeleteCartEntry)
  deleteCartItem(ctx: StateContext<CartEntryStateModel>, { cartEntryId, hardDelete }: actions.DeleteCartEntry) {
    const state = ctx.getState();
    const currentProcess = this.store.selectSnapshot(ProcessSelectors.getCurrentProcess);
    const cartEntries = { ...state.cartEntries };
    const cartEntry = cartEntries[cartEntryId];

    if (hardDelete && currentProcess) {
      const products = this.store.selectSnapshot(ProductSelectors.getProducts);
      const branches = this.store.selectSnapshot(BranchSelectors.getBranches);
      return this.createQuantityUpdateObject$(
        cartEntry.deliveryType,
        products[cartEntry.bookId],
        0,
        branches[cartEntry.branch],
        (cartEntry.availability ? cartEntry.availability : products[cartEntry.bookId].catalogAvailability) as any, // TODO: Typisierung
        products[cartEntry.bookId].promoPoints
      ).pipe(
        switchMap((updatedItem) => this.checkoutService.updateQuantityForItemInCart(currentProcess.cartId, cartEntryId, updatedItem)),
        tap(() => {
          delete cartEntries[cartEntryId];

          ctx.patchState({
            cartEntries,
          });
          this.syncApiState(cartEntries);
        })
      );
    }
  }

  @Action(actions.ReloadCartEntryData)
  reloadCartEntries(ctx: StateContext<CartEntryStateModel>, { cartEntries }: actions.ReloadCartEntryData) {
    ctx.setState({ cartEntries: { ...cartEntries } });
  }

  private availibilityObjectForUpdate(ava: AvailabilityDTO, deliveryType: string): UpdateDestinationAvailability {
    let data: UpdateDestinationAvailability = { availabilityType: ava.status };

    if (deliveryType === DeliveryOption.DELIVERY) {
      data = {
        ssc: ava.ssc,
        sscText: ava.sscText,
        availabilityType: ava.status,
        isPrebooked: ava.isPrebooked,
        inStock: ava.qty,
        price: ava.price,
        supplierProductNumber: ava.supplierProductNumber,
        estimatedShippingDate: ava.at,
        supplier: { id: ava.supplierId },
      };
    } else if (deliveryType === DeliveryOption.TAKE_NOW) {
      data = {
        ssc: ava.ssc,
        availabilityType: ava.status,
        price: ava.price,
        supplier: { id: ava.supplierId },
      };
    } else {
      data = {
        ssc: ava.ssc,
        sscText: ava.sscText,
        availabilityType: ava.status,
        isPrebooked: ava.isPrebooked,
        inStock: ava.qty,
        price: ava.price,
        estimatedShippingDate: ava.at,
        supplier: { id: ava.supplierId },
      };
    }

    return data;
  }

  private syncApiState(cartEntries: { [key: number]: CartEntry }) {
    const userSyncData: UserStateSyncData = {
      cartEntries,
    };
    const userData = JSON.stringify(userSyncData);
    this.store.dispatch(new AppUserDataSync(userData));
  }

  private itemExistInCart(
    cartEntries: { [key: number]: CartEntry },
    bookId: number,
    deliveryType: string,
    branch: number,
    cartId: number
  ): null | number {
    const existingCartEntriesIds = this.store.selectSnapshot(CartState.getCartEntries)(cartId);
    const existingEntries = existingCartEntriesIds.map((id) => cartEntries[id]);
    const updateItem = existingEntries.find(
      (cartitem: CartEntry) => cartitem.bookId === bookId && cartitem.deliveryType === deliveryType && cartitem.branch === branch
    );

    if (updateItem) {
      let itemId: number = null;

      for (const key of existingCartEntriesIds) {
        const currentItem = cartEntries[key];
        if (currentItem.bookId === bookId && currentItem.deliveryType === deliveryType && currentItem.branch === branch) {
          itemId = key;
          break;
        }
      }

      return itemId;
    }

    return null;
  }

  private addItemToCart$(
    deliveryType: string,
    book: ItemDTO,
    quantity: number,
    availability: AvailabilityDTO,
    cartId: number,
    ctx: StateContext<CartEntryStateModel>,
    branch: BranchDTO,
    promoPoints: number
  ): Observable<ShoppingCartDTO> {
    let getBranchSupplierId$ = of(availability.supplierId);

    if (deliveryType === DeliveryOption.TAKE_NOW) {
      getBranchSupplierId$ = this.checkoutService
        .getSuppliers()
        .pipe(map((suppliers) => suppliers.find((supplier) => supplier.supplierNumber === 'F').id));
    }

    const addItemToCart$ = (item: AddToShoppingCartDTO) =>
      this.checkoutService.addItemToCart(cartId, [item]).pipe(
        tap((shoppingCart: ShoppingCartDTO) => {
          const newItemIndex = Object.keys(shoppingCart.items).length - 1;
          const newItemId = shoppingCart.items[newItemIndex].id;
          const deliveryDate = shoppingCart.items[newItemIndex].data.availability.estimatedShippingDate
            ? shoppingCart.items[newItemIndex].data.availability.estimatedShippingDate
            : null;
          const pickUpDate = availability.at ? availability.at : null;

          const state = ctx.getState();

          const cartEntries = { ...state.cartEntries };
          cartEntries[newItemId] = {
            quantity: quantity,
            bookId: book.id,
            deliveryType: deliveryType,
            branch: branch ? branch.id : null,
            deliveryDate: deliveryDate,
            pickUpDate: pickUpDate,
            orderTypeChanged: false,
            avaMessage: '',
            pickUpPrice:
              deliveryType === DeliveryOption.PICK_UP &&
              availability &&
              availability.price &&
              availability.price.value &&
              availability.price.value.value
                ? availability.price.value.value
                : undefined,
            deliveryPrice:
              deliveryType === DeliveryOption.DELIVERY &&
              availability &&
              availability.price &&
              availability.price.value &&
              availability.price.value.value
                ? availability.price.value.value
                : undefined,
            downloadPrice:
              deliveryType === DeliveryOption.DOWNLOAD &&
              availability &&
              availability.price &&
              availability.price.value &&
              availability.price.value.value
                ? availability.price.value.value
                : undefined,
            availability,
          };

          // Update local state
          ctx.patchState({
            cartEntries,
          });

          this.syncApiState(cartEntries);

          // Add item to cart
          this.store.dispatch(new SetCartData(newItemId));
        })
      );

    if (deliveryType === DeliveryOption.DOWNLOAD) {
      const customer = this.store.selectSnapshot(CustomerSelectors.getCurrentProcessActiveUser);
      return this.checkoutService.getSuppliers().pipe(
        map((suppliers) => suppliers.find((supplier) => supplier.supplierNumber === 'DIG')),
        map((supplier) =>
          this.createObjectForDonwload(
            deliveryType,
            book,
            quantity,
            availability,
            supplier,
            branch,
            promoPoints,
            customer ? customer : null
          )
        ),
        flatMap((item) => addItemToCart$(item))
      );
    } else {
      return getBranchSupplierId$.pipe(
        map((supplierId) => this.createObjectForAPI(deliveryType, book, quantity, availability, supplierId, branch, promoPoints)),
        flatMap((item) => addItemToCart$(item))
      );
    }
  }

  private createObjectForAPI(
    deliveryType: string,
    book: ItemDTO,
    quantity: number,
    availability: AvailabilityDTO,
    supplierId: number,
    branch: BranchDTO,
    promoPoints: number
  ): AddToShoppingCartDTO {
    const destination: EntityDTOContainerOfDestinationDTO = this.createDestinationObject(deliveryType, branch, availability);

    const item: AddToShoppingCartDTO = {
      product: {
        ...book.product,
        catalogProductNumber: String(book.id),
      },
      quantity: quantity,
      destination: destination,
      availability: {
        availabilityType: availability.status,
        price: availability.price,
        supplier: { id: supplierId },
        ssc: availability.ssc ? availability.ssc : null,
        sscText: availability.sscText ? availability.sscText : null,
        estimatedShippingDate: availability.at ? availability.at : null,
        supplierProductNumber: availability.supplierProductNumber ? availability.supplierProductNumber : null,
        isPrebooked: availability.isPrebooked,
      },
      promotion: { points: promoPoints },
    };

    return item;
  }

  private createObjectForDonwload(
    deliveryType: string,
    book: ItemDTO,
    quantity: number,
    availability: AvailabilityDTO,
    supplier: SupplierDTO,
    branch: BranchDTO,
    promoPoints: number,
    customer?: User
  ): AddToShoppingCartDTO {
    const destination: EntityDTOContainerOfDestinationDTO = this.createDestinationObject(deliveryType, branch, availability, customer);

    const item: AddToShoppingCartDTO = {
      product: {
        ...book.product,
        catalogProductNumber: String(book.id),
      },
      quantity: quantity,
      destination: destination,
      availability: {
        availabilityType: availability.status,
        price: availability.price,
        supplier: supplier,
        ssc: availability.ssc ? availability.ssc : null,
        sscText: availability.sscText ? availability.sscText : null,
        estimatedShippingDate: availability.at ? availability.at : null,
        supplierProductNumber: availability.supplierProductNumber ? availability.supplierProductNumber : null,
        isPrebooked: availability.isPrebooked,
      },
      promotion: { points: promoPoints },
    };

    return item;
  }

  private createDestinationObject(
    deliveryType: string,
    branch: BranchDTO,
    availability: AvailabilityDTO,
    customer?: User
  ): EntityDTOContainerOfDestinationDTO {
    let targetType: ShippingTarget = DeliveryType['Branch'];
    let destination: EntityDTOContainerOfDestinationDTO = {};

    if (deliveryType === DeliveryOption.DELIVERY) {
      targetType = DeliveryType['ShippingAddress'];
    }

    if (deliveryType === DeliveryOption.DOWNLOAD) {
      targetType = DeliveryType['Download'];
    }

    if (targetType === DeliveryType['Branch']) {
      destination = {
        data: {
          target: targetType,
          targetBranch: { id: branch.id },
        },
      };
    } else if (targetType === DeliveryType['Download']) {
      const address = customer ? (customer.delivery_addres ? customer.delivery_addres : customer.invoice_address) : null;
      const addressDto: AddressDTO = address ? this.customerMapping.fromAddressToAddressDTO(address) : null;
      destination = {
        data: {
          target: targetType,
          logistician: { id: availability.logisticianId },
          shippingAddress:
            customer && addressDto
              ? ({
                  firstName: customer.first_name,
                  lastName: customer.last_name,
                  address: addressDto,
                } as ShippingAddressDTO)
              : undefined,
        },
      };
    } else {
      destination = {
        data: {
          target: targetType,
          logistician: { id: availability.logisticianId },
        },
      };
    }

    return destination;
  }

  private createQuantityUpdateObject$(
    deliveryType: string,
    book: ItemDTO,
    quantity: number,
    branch: BranchDTO,
    availability: AvailabilityDTO,
    promoPoints: number
  ): Observable<UpdateShoppingCartItemDTO> {
    let item$: Observable<AddToShoppingCartDTO>;
    if (deliveryType === DeliveryOption.DOWNLOAD) {
      item$ = this.checkoutService.getSuppliers().pipe(
        map((suppliers) => suppliers.find((supplier) => supplier.supplierNumber === 'DIG')),
        flatMap((supplier) => {
          const customer = this.store.selectSnapshot(CustomerSelectors.getCurrentProcessActiveUser);
          return of(
            this.createObjectForDonwload(
              deliveryType,
              book,
              quantity,
              availability,
              supplier,
              branch,
              promoPoints,
              customer ? customer : null
            )
          );
        })
      );
    } else if (deliveryType === DeliveryOption.TAKE_NOW) {
      item$ = this.checkoutService.getSuppliers().pipe(
        map((suppliers) => suppliers.find((supplier) => supplier.supplierNumber === 'F').id),
        flatMap((supplierId) => {
          return of(this.createObjectForAPI(deliveryType, book, quantity, availability, supplierId, branch, promoPoints));
        })
      );
    } else {
      return of(this.createObjectForAPI(deliveryType, book, quantity, availability, availability.supplierId, branch, promoPoints));
    }
    return item$.pipe(
      map((it) => {
        return <UpdateShoppingCartItemDTO>{
          quantity: it.quantity,
          retailPrice: it.retailPrice,
          destination: it.destination,
          availability: it.availability,
          promotion: it.promotion,
        };
      })
    );
  }
}
