import { inject, Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import {
  AddToShoppingCartDTO,
  AvailabilityDTO,
  BranchDTO,
  BuyerDTO,
  BuyerResult,
  CheckoutDTO,
  CommunicationDetailsDTO,
  DestinationDTO,
  NotificationChannel,
  OLAAvailabilityDTO,
  PayerDTO,
  PaymentDTO,
  PaymentType,
  ResponseArgsOfDestinationDTO,
  ShippingAddressDTO,
  ShoppingCartDTO,
  StoreCheckoutService,
  UpdateShoppingCartItemDTO,
  InputDTO,
  ItemPayload,
  StoreCheckoutShoppingCartService,
  StoreCheckoutPaymentService,
  StoreCheckoutBuyerService,
  StoreCheckoutPayerService,
  StoreCheckoutBranchService,
  ItemsResult,
  KulturPassService,
  ProductDTO,
  KulturPassResult,
} from '@generated/swagger/checkout-api';
import {
  DisplayOrderDTO,
  DisplayOrderItemDTO,
  OrderCheckoutService,
  ReorderValues,
  ResponseArgsOfValueTupleOfIEnumerableOfDisplayOrderDTOAndIEnumerableOfKeyValueDTOOfStringAndString,
} from '@generated/swagger/oms-api';
import { isNullOrUndefined, memorize } from '@utils/common';
import {
  combineLatest,
  Observable,
  of,
  concat,
  isObservable,
  throwError,
  interval as rxjsInterval,
  firstValueFrom,
} from 'rxjs';
import {
  bufferCount,
  catchError,
  distinctUntilChanged,
  filter,
  first,
  map,
  mergeMap,
  shareReplay,
  switchMap,
  tap,
  withLatestFrom,
  startWith,
} from 'rxjs/operators';

import * as DomainCheckoutSelectors from './store/domain-checkout.selectors';
import * as DomainCheckoutActions from './store/domain-checkout.actions';
import { DomainAvailabilityService, ItemData } from '@domain/availability';
import { HttpErrorResponse } from '@angular/common/http';
import { ApplicationService } from '@core/application';
import { CustomerDTO } from '@generated/swagger/crm-api';
import { Config } from '@core/config';
import parseDuration from 'parse-duration';
import {
  CheckoutMetadataService,
  ShoppingCart,
} from '@isa/checkout/data-access';
import {
  ShoppingCartEvent,
  ShoppingCartEvents,
} from '@isa/checkout/data-access';

/**
 * Domain service for managing the complete checkout flow including shopping cart operations,
 * checkout creation, buyer/payer management, payment processing, and order completion.
 *
 * This service orchestrates interactions between:
 * - NgRx Store for state management
 * - Multiple Swagger API clients (checkout, OMS, shopping cart, payment, buyer, payer, branch, kulturpass)
 * - Shopping cart event system for cross-component synchronization
 * - Availability service for real-time product availability checks
 *
 * @remarks
 * **Process ID Pattern**: All methods require a `processId` (typically `Date.now()`) to isolate
 * checkout sessions. Multiple concurrent checkout processes can run independently.
 *
 * **Observable-First Design**: Most methods return Observables for reactive composition. Consumers
 * should use RxJS operators for transformation and error handling.
 *
 * **Auto-Creation**: Shopping carts auto-create if missing. The service uses `filter()` operators
 * to trigger lazy initialization and prevent race conditions.
 *
 * **Event Sourcing**: Publishes shopping cart events (Created, ItemAdded, ItemUpdated, ItemRemoved)
 * for synchronization across components. Subscribes to events from `ShoppingCartService` to maintain
 * consistency.
 *
 * **OLA Management**: Tracks Order Level Agreement (OLA) expiration timestamps per item and order type.
 * Validates availability freshness before checkout completion.
 *
 * @example
 * ```typescript
 * // Basic shopping cart flow
 * const processId = Date.now();
 *
 * // Add items to cart
 * this.checkoutService.addItemToShoppingCart({
 *   processId,
 *   items: [{ productId: 123, quantity: 2 }]
 * }).subscribe();
 *
 * // Get cart (auto-creates if missing)
 * this.checkoutService.getShoppingCart({ processId })
 *   .subscribe(cart => console.log(cart));
 *
 * // Complete checkout (orchestrates all steps)
 * this.checkoutService.completeCheckout({ processId })
 *   .subscribe(orders => console.log('Orders created:', orders));
 * ```
 *
 * @see DomainCheckoutSelectors For state selection patterns
 * @see DomainCheckoutActions For available actions
 * @see ShoppingCartEvents For event system integration
 */
@Injectable()
export class DomainCheckoutService {
  /** Metadata service for shopping cart persistence */
  #checkoutMetadataService = inject(CheckoutMetadataService);

  /** Event bus for shopping cart synchronization across components */
  #shoppingCartEvents = inject(ShoppingCartEvents);

  /**
   * Gets the OLA (Order Level Agreement) expiration duration in milliseconds.
   *
   * OLA expiration defines how long availability data remains valid before requiring refresh.
   * Default is 5 minutes if not configured.
   *
   * @returns Duration in milliseconds
   */
  get olaExpiration() {
    const exp = this._config.get('@domain/checkout.olaExpiration') ?? '5m';
    return parseDuration(exp);
  }

  constructor(
    private store: Store<any>,
    private _config: Config,
    private applicationService: ApplicationService,
    private storeCheckoutService: StoreCheckoutService,
    private orderCheckoutService: OrderCheckoutService,
    private availabilityService: DomainAvailabilityService,
    private _shoppingCartService: StoreCheckoutShoppingCartService,
    private _paymentService: StoreCheckoutPaymentService,
    private _buyerService: StoreCheckoutBuyerService,
    private _payerService: StoreCheckoutPayerService,
    private _branchService: StoreCheckoutBranchService,
    private _kulturpassService: KulturPassService,
  ) {
    // Subscribe to shopping cart events from ShoppingCartService
    this.#shoppingCartEvents.events$
      .pipe(
        // Only process events from ShoppingCartService to avoid circular updates
        filter((payload) => payload.source === 'ShoppingCartService'),
      )
      .subscribe((payload) => {
        // Update the store with the shopping cart from the event
        this.store.dispatch(
          DomainCheckoutActions.setShoppingCartByShoppingCartId({
            shoppingCartId: payload.shoppingCart.id!,
            shoppingCart: payload.shoppingCart as any,
          }),
        );
      });
  }

  //#region shoppingcart
  /**
   * Retrieves the shopping cart for a given process ID. Auto-creates the cart if it doesn't exist.
   *
   * @remarks
   * **Auto-Creation**: If no cart exists for the process ID, triggers `createShoppingCart()` automatically.
   * The Observable filters out null/undefined and waits for cart creation to complete.
   *
   * **Latest Data**: Setting `latest: true` forces a refresh from the API instead of using cached state.
   * This is useful before critical operations like checkout completion.
   *
   * **Memoization**: Uses `@memorize()` decorator to cache results by parameters, reducing duplicate calls.
   *
   * @param params - Parameters object
   * @param params.processId - Unique process identifier (typically `Date.now()`)
   * @param params.latest - If true, fetches fresh data from API; if false/undefined, uses store state
   *
   * @returns Observable of the shopping cart DTO. Never emits null/undefined.
   *
   * @example
   * ```typescript
   * // Get cart from store (creates if missing)
   * this.checkoutService.getShoppingCart({ processId: 123 })
   *   .pipe(first())
   *   .subscribe(cart => console.log('Items:', cart.items));
   *
   * // Force refresh from API
   * this.checkoutService.getShoppingCart({ processId: 123, latest: true })
   *   .pipe(first())
   *   .subscribe(cart => console.log('Fresh cart:', cart));
   * ```
   */
  @memorize()
  getShoppingCart({
    processId,
    latest,
  }: {
    processId: number;
    latest?: boolean;
  }): Observable<ShoppingCartDTO> {
    let _latest = latest;
    return this.store
      .select(DomainCheckoutSelectors.selectShoppingCartByProcessId, {
        processId,
      })
      .pipe(
        filter((cart) => {
          if (isNullOrUndefined(cart)) {
            this.createShoppingCart({ processId }).subscribe();
            return false;
          } else if (cart && _latest) {
            console.log('Reloading shopping cart from API');
            _latest = false;
            this._shoppingCartService
              .StoreCheckoutShoppingCartGetShoppingCart({
                shoppingCartId: cart.id,
              })
              .pipe(
                map((response) => response.result),
                tap((shoppingCart) => {
                  this.store.dispatch(
                    DomainCheckoutActions.setShoppingCart({
                      processId,
                      shoppingCart,
                    }),
                  );
                }),
              )
              .subscribe();
            return false;
          }
          return true;
        }),
      );
  }

  /**
   * Reloads the shopping cart from the API and updates the store with fresh data.
   *
   * This is an async method that fetches the latest cart state from the backend
   * and dispatches an action to update the NgRx store. Unlike `getShoppingCart({ latest: true })`,
   * this method doesn't return the cart Observable - it's fire-and-forget.
   *
   * @param params - Parameters object
   * @param params.processId - Process identifier
   *
   * @returns Promise that resolves when reload completes (or immediately if no cart exists)
   *
   * @example
   * ```typescript
   * await this.checkoutService.reloadShoppingCart({ processId: 123 });
   * console.log('Cart reloaded');
   * ```
   */
  async reloadShoppingCart({ processId }: { processId: number }) {
    const shoppingCart = await firstValueFrom(
      this.store.select(DomainCheckoutSelectors.selectShoppingCartByProcessId, {
        processId,
      }),
    );
    if (!shoppingCart) {
      return;
    }
    const cart = await firstValueFrom(
      this._shoppingCartService.StoreCheckoutShoppingCartGetShoppingCart({
        shoppingCartId: shoppingCart.id,
      }),
    );

    this.store.dispatch(
      DomainCheckoutActions.setShoppingCart({
        processId,
        shoppingCart: cart.result,
      }),
    );
  }

  /**
   * Creates a new shopping cart and associates it with the given process ID.
   *
   * @remarks
   * **State Updates**:
   * - Saves shopping cart ID to metadata service for persistence
   * - Publishes `ShoppingCartEvent.Created` event for component synchronization
   * - Dispatches `setShoppingCart` action to update NgRx store
   *
   * **Auto-Invocation**: Usually called automatically by `getShoppingCart()` when no cart exists.
   * Rarely needs to be called directly.
   *
   * @param params - Parameters object
   * @param params.processId - Process identifier to associate with the new cart
   *
   * @returns Observable of the newly created shopping cart DTO
   *
   * @example
   * ```typescript
   * this.checkoutService.createShoppingCart({ processId: Date.now() })
   *   .subscribe(cart => console.log('Cart created:', cart.id));
   * ```
   */
  createShoppingCart({
    processId,
  }: {
    processId: number;
  }): Observable<ShoppingCartDTO> {
    return this._shoppingCartService
      .StoreCheckoutShoppingCartCreateShoppingCart()
      .pipe(
        map((response) => response.result),
        tap((shoppingCart) => {
          this.#checkoutMetadataService.setShoppingCartId(
            processId,
            shoppingCart.id,
          );
          this.#shoppingCartEvents.pub(
            ShoppingCartEvent.Created,
            shoppingCart as ShoppingCart,
            'DomainCheckoutService',
          );
          this.store.dispatch(
            DomainCheckoutActions.setShoppingCart({
              processId,
              shoppingCart,
            }),
          );
        }),
      );
  }

  /**
   * Adds one or more items to the shopping cart.
   *
   * @remarks
   * **Process Flow**:
   * 1. Retrieves existing shopping cart (creates if missing)
   * 2. Calls API to add items
   * 3. Publishes `ShoppingCartEvent.ItemAdded` event
   * 4. Updates NgRx store with modified cart
   *
   * **Validation**: Ensure items are validated via `canAddItem()` or `canAddItems()` before calling
   * this method to avoid API errors.
   *
   * @param params - Parameters object
   * @param params.processId - Process identifier
   * @param params.items - Array of items to add (product, quantity, availability, destination, etc.)
   *
   * @returns Observable of the updated shopping cart with new items
   *
   * @example
   * ```typescript
   * this.checkoutService.addItemToShoppingCart({
   *   processId: 123,
   *   items: [{
   *     product: { ean: '1234567890', catalogProductNumber: 456 },
   *     quantity: 2,
   *     availability: availabilityDto,
   *     destination: destinationDto
   *   }]
   * }).subscribe(cart => console.log('Items:', cart.items.length));
   * ```
   *
   * @see canAddItem For single item validation
   * @see canAddItems For bulk item validation
   */
  addItemToShoppingCart({
    processId,
    items,
  }: {
    processId: number;
    items: AddToShoppingCartDTO[];
  }): Observable<ShoppingCartDTO> {
    return this.getShoppingCart({ processId }).pipe(
      first(),
      mergeMap((cart) =>
        this._shoppingCartService
          .StoreCheckoutShoppingCartAddItemToShoppingCart({
            items,
            shoppingCartId: cart.id,
          })
          .pipe(
            map((response) => response.result),
            tap((shoppingCart) => {
              this.#shoppingCartEvents.pub(
                ShoppingCartEvent.ItemAdded,
                shoppingCart as ShoppingCart,
                'DomainCheckoutService',
              );
              this.store.dispatch(
                DomainCheckoutActions.setShoppingCart({
                  processId,
                  shoppingCart,
                }),
              );
            }),
          ),
      ),
    );
  }

  setDestinationForCustomer({
    processId,
    customerFeatures,
  }: {
    processId: number;
    customerFeatures: { [key: string]: string };
  }): Observable<BuyerResult> {
    return this.getShoppingCart({ processId }).pipe(
      first(),
      mergeMap((shoppingCart) =>
        this._shoppingCartService
          .StoreCheckoutShoppingCartSetLogisticianOnDestinationsByBuyer({
            shoppingCartId: shoppingCart?.id,
            payload: { customerFeatures },
          })
          .pipe(map((response) => response.result)),
      ),
    );
  }

  canAddDestination({
    processId,
    destinationDTO,
  }: {
    processId: number;
    destinationDTO: DestinationDTO;
  }) {
    return this.getShoppingCart({ processId }).pipe(
      first(),
      mergeMap((cart) =>
        this._shoppingCartService.StoreCheckoutShoppingCartCanAddDestination({
          shoppingCartId: cart.id,
          payload: destinationDTO,
        }),
      ),
      map((response) => {
        if (response.result?.ok) {
          return true;
        }
        return (
          Object.values(response.invalidProperties).join('; ') ||
          response.message
        );
      }),
    );
  }

  /**
   * Validates if a single item can be added to the shopping cart.
   *
   * Checks business rules, customer features, and cart compatibility before adding items.
   * Use this before calling `addItemToShoppingCart()` to prevent API errors.
   *
   * @param params - Parameters object
   * @param params.processId - Process identifier
   * @param params.availability - OLA availability data for the item
   * @param params.orderType - Order type (e.g., 'Abholung', 'Versand', 'Download', 'Rücklage')
   *
   * @returns Observable of `true` if item can be added, or error message string if not allowed
   *
   * @example
   * ```typescript
   * this.checkoutService.canAddItem({
   *   processId: 123,
   *   availability: olaAvailability,
   *   orderType: 'Versand'
   * }).subscribe(result => {
   *   if (result === true) {
   *     // Proceed with adding item
   *     this.checkoutService.addItemToShoppingCart(...);
   *   } else {
   *     console.error('Cannot add item:', result);
   *   }
   * });
   * ```
   *
   * @see canAddItems For bulk validation
   */
  canAddItem({
    processId,
    availability,
    orderType,
  }: {
    processId: number;
    availability: OLAAvailabilityDTO;
    orderType: string;
  }): Observable<true | string> {
    return this.getShoppingCart({ processId }).pipe(
      first(),
      withLatestFrom(
        this.store.select(
          DomainCheckoutSelectors.selectCustomerFeaturesByProcessId,
          { processId },
        ),
      ),
      mergeMap(([shoppingCart, customerFeatures]) =>
        this._shoppingCartService
          .StoreCheckoutShoppingCartCanAddItem({
            shoppingCartId: shoppingCart?.id,
            payload: {
              customerFeatures,
              availabilities: [availability],
              orderType,
            },
          })
          .pipe(
            map((response) => {
              if (response.result.ok) {
                return true;
              }
              return response.message;
            }),
          ),
      ),
    );
  }

  canAddItemsKulturpass(payload: ProductDTO[]): Observable<KulturPassResult[]> {
    return this._kulturpassService
      .KulturPassCanAddForKulturPass({ payload })
      .pipe(map((response) => response?.result));
  }

  /**
   * Validates if multiple items can be added to the shopping cart in bulk.
   *
   * More efficient than calling `canAddItem()` multiple times. Returns validation
   * results for each item in the payload.
   *
   * @param params - Parameters object
   * @param params.processId - Process identifier
   * @param params.payload - Array of item payloads to validate
   * @param params.orderType - Order type for all items
   *
   * @returns Observable array of validation results (one per item). Each result contains
   * `ok` flag and optional error messages.
   *
   * @example
   * ```typescript
   * this.checkoutService.canAddItems({
   *   processId: 123,
   *   payload: [
   *     { availabilities: [avail1], productId: 111, quantity: 2 },
   *     { availabilities: [avail2], productId: 222, quantity: 1 }
   *   ],
   *   orderType: 'Versand'
   * }).subscribe(results => {
   *   results.forEach((result, index) => {
   *     console.log(`Item ${index}:`, result.ok ? 'Valid' : result.message);
   *   });
   * });
   * ```
   *
   * @see canAddItem For single item validation
   */
  canAddItems({
    processId,
    payload,
    orderType,
  }: {
    processId: number;
    payload: ItemPayload[];
    orderType: string;
  }): Observable<ItemsResult[]> {
    return this.getShoppingCart({ processId }).pipe(
      first(),
      withLatestFrom(
        this.store.select(
          DomainCheckoutSelectors.selectCustomerFeaturesByProcessId,
          { processId },
        ),
      ),
      mergeMap(([shoppingCart, customerFeatures]) => {
        payload = payload?.map((p) => {
          return {
            ...p,
            customerFeatures,
            orderType,
          };
        });
        return this._shoppingCartService
          .StoreCheckoutShoppingCartCanAddItems({
            shoppingCartId: shoppingCart.id,
            payload,
          })
          .pipe(
            map((response) => {
              // TODO: remove this when the API is fixed
              return response.result as unknown as ItemsResult[];
            }),
          );
      }),
    );
  }

  updateShoppingCartItemAvailability({
    shoppingCartId,
    shoppingCartItemId,
    availability,
  }: {
    shoppingCartId: number;
    shoppingCartItemId: number;
    availability: AvailabilityDTO;
  }) {
    return this._shoppingCartService
      .StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability({
        shoppingCartId,
        shoppingCartItemId,
        availability,
      })
      .pipe(
        map((response) => response.result),
        tap((shoppingCart) => {
          this.store.dispatch(
            DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId(
              {
                shoppingCartId,
                availability,
                shoppingCartItemId,
              },
            ),
          );
        }),
      );
  }

  /**
   * Updates an existing item in the shopping cart (quantity, availability, special comment, etc.).
   *
   * @remarks
   * **Special Behavior**:
   * - Setting `quantity: 0` removes the item and publishes `ItemRemoved` event instead of `ItemUpdated`
   * - Always fetches latest cart state (`latest: true`) to avoid stale data conflicts
   * - If availability is updated, adds timestamp to history for OLA validation
   *
   * **Event Publishing**:
   * - Publishes `ShoppingCartEvent.ItemRemoved` if quantity is 0
   * - Publishes `ShoppingCartEvent.ItemUpdated` for all other changes
   *
   * @param params - Parameters object
   * @param params.processId - Process identifier
   * @param params.shoppingCartItemId - ID of the cart item to update
   * @param params.update - Fields to update (quantity, availability, specialComment, etc.)
   *
   * @returns Observable of the updated shopping cart
   *
   * @example
   * ```typescript
   * // Update quantity
   * this.checkoutService.updateItemInShoppingCart({
   *   processId: 123,
   *   shoppingCartItemId: 456,
   *   update: { quantity: 5 }
   * }).subscribe();
   *
   * // Remove item (quantity = 0)
   * this.checkoutService.updateItemInShoppingCart({
   *   processId: 123,
   *   shoppingCartItemId: 456,
   *   update: { quantity: 0 }
   * }).subscribe();
   *
   * // Update availability
   * this.checkoutService.updateItemInShoppingCart({
   *   processId: 123,
   *   shoppingCartItemId: 456,
   *   update: { availability: newAvailabilityDto }
   * }).subscribe();
   * ```
   */
  updateItemInShoppingCart({
    processId,
    shoppingCartItemId,
    update,
  }: {
    processId: number;
    shoppingCartItemId: number;
    update: UpdateShoppingCartItemDTO;
  }): Observable<ShoppingCartDTO> {
    return this.getShoppingCart({ processId, latest: true }).pipe(
      first(),
      mergeMap((shoppingCart) =>
        this._shoppingCartService
          .StoreCheckoutShoppingCartUpdateShoppingCartItem({
            shoppingCartId: shoppingCart.id,
            shoppingCartItemId,
            values: update,
          })
          .pipe(
            map((response) => response.result),
            tap((shoppingCart) => {
              // Check if item was removed (quantity === 0)
              const eventType =
                update.quantity === 0
                  ? ShoppingCartEvent.ItemRemoved
                  : ShoppingCartEvent.ItemUpdated;
              this.#shoppingCartEvents.pub(
                eventType,
                shoppingCart as ShoppingCart,
                'DomainCheckoutService',
              );

              this.store.dispatch(
                DomainCheckoutActions.setShoppingCart({
                  processId,
                  shoppingCart,
                }),
              );

              if (update.availability) {
                this.store.dispatch(
                  DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory(
                    {
                      processId,
                      availability: update.availability,
                      shoppingCartItemId,
                    },
                  ),
                );
              }
            }),
          ),
      ),
    );
  }

  //#endregion

  //#region Checkout

  /**
   * Retrieves the checkout entity for a given process. Auto-creates if missing.
   *
   * @remarks
   * **Auto-Creation**: Similar to `getShoppingCart()`, automatically triggers `createCheckout()`
   * if no checkout exists for the process ID.
   *
   * **Refresh**: Setting `refresh: true` forces recreation of the checkout entity from the API.
   *
   * **Purpose**: The checkout entity aggregates buyer, payer, payment, destinations, and
   * notification channels. It's required before order completion.
   *
   * @param params - Parameters object
   * @param params.processId - Process identifier
   * @param params.refresh - If true, recreates checkout from API; if false/undefined, uses store state
   *
   * @returns Observable of the checkout DTO. Never emits null/undefined.
   *
   * @example
   * ```typescript
   * this.checkoutService.getCheckout({ processId: 123 })
   *   .pipe(first())
   *   .subscribe(checkout => {
   *     console.log('Buyer:', checkout.buyer);
   *     console.log('Payment:', checkout.payment);
   *   });
   * ```
   */
  getCheckout({
    processId,
    refresh,
  }: {
    processId: number;
    refresh?: boolean;
  }): Observable<CheckoutDTO> {
    let _refresh = refresh;
    return this.store
      .select(DomainCheckoutSelectors.selectCheckoutByProcessId, { processId })
      .pipe(
        filter((checkout) => {
          if (isNullOrUndefined(checkout) || _refresh) {
            _refresh = false;
            this.createCheckout({ processId }).subscribe();
            return false;
          }
          return true;
        }),
      );
  }

  createCheckout({ processId }: { processId: number }) {
    return this.getShoppingCart({ processId }).pipe(
      first(),
      mergeMap((shoppingCart) =>
        this.storeCheckoutService
          .StoreCheckoutCreateOrRefreshCheckout({
            shoppingCartId: shoppingCart?.id,
          })
          .pipe(
            map((response) => response.result),
            tap((checkout) =>
              this.store.dispatch(
                DomainCheckoutActions.setCheckout({ processId, checkout }),
              ),
            ),
          ),
      ),
    );
  }

  /* @internal */
  _setNotificationChannels({
    processId,
    notificationChannels,
  }: {
    processId: number;
    notificationChannels: NotificationChannel;
  }): Observable<CheckoutDTO> {
    return this.getCheckout({ processId }).pipe(
      first(),
      mergeMap((checkout) =>
        this.storeCheckoutService
          .StoreCheckoutSetNotificationChannels({
            checkoutId: checkout.id,
            notificationChannel: notificationChannels,
          })
          .pipe(
            map((response) => response.result),
            tap((checkout) =>
              this.store.dispatch(
                DomainCheckoutActions.setCheckout({ processId, checkout }),
              ),
            ),
          ),
      ),
    );
  }

  getPayment({ processId }: { processId: number }): Observable<PaymentDTO> {
    return this.getCheckout({ processId }).pipe(
      first(),
      mergeMap((checkout) =>
        this._paymentService
          .StoreCheckoutPaymentGetCheckoutPayment({
            checkoutId: checkout.id,
          })
          .pipe(map((response) => response.result)),
      ),
    );
  }

  getOlaErrors({ processId }: { processId: number }): Observable<number[]> {
    return this.store.select(
      DomainCheckoutSelectors.selectOlaErrorsByProcessId,
      { processId },
    );
  }

  setPayment({
    processId,
    paymentType,
  }: {
    processId: number;
    paymentType: PaymentType;
  }): Observable<CheckoutDTO> {
    return this.getCheckout({ processId }).pipe(
      first(),
      mergeMap((checkout) =>
        this._paymentService
          .StoreCheckoutPaymentSetPaymentType({
            checkoutId: checkout?.id,
            paymentType,
          })
          .pipe(
            map((response) => response.result),
            tap((checkout) =>
              this.store.dispatch(
                DomainCheckoutActions.setCheckout({ processId, checkout }),
              ),
            ),
          ),
      ),
    );
  }

  _setBuyer({
    processId,
    buyer,
  }: {
    processId: number;
    buyer: BuyerDTO;
  }): Observable<CheckoutDTO> {
    return this.getCheckout({ processId }).pipe(
      first(),
      mergeMap((checkout) => {
        console.log('checkout', checkout, processId);
        return this._buyerService
          .StoreCheckoutBuyerSetBuyerPOST({
            checkoutId: checkout?.id,
            buyerDTO: buyer,
          })
          .pipe(
            map((response) => response.result),
            tap((checkout) =>
              this.store.dispatch(
                DomainCheckoutActions.setCheckout({ processId, checkout }),
              ),
            ),
          );
      }),
    );
  }

  _setPayer({
    processId,
    payer,
  }: {
    processId: number;
    payer: PayerDTO;
  }): Observable<CheckoutDTO> {
    return this.getCheckout({ processId }).pipe(
      first(),
      mergeMap((checkout) =>
        this._payerService
          .StoreCheckoutPayerSetPayerPOST({
            checkoutId: checkout?.id,
            payerDTO: payer,
          })
          .pipe(
            map((response) => response.result),
            tap((checkout) =>
              this.store.dispatch(
                DomainCheckoutActions.setCheckout({ processId, checkout }),
              ),
            ),
          ),
      ),
    );
  }

  setSpecialCommentOnItem({
    processId,
    specialComment,
  }: {
    processId: number;
    specialComment: string;
  }) {
    const shoppingCart$ = this.getShoppingCart({ processId }).pipe(first());

    return shoppingCart$.pipe(
      mergeMap((cart) =>
        concat(
          ...cart.items.map((item) =>
            this._shoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItem(
              {
                shoppingCartId: cart.id,
                shoppingCartItemId: item.id,
                values: { specialComment },
              },
            ),
          ),
        ).pipe(bufferCount(cart.items?.length)),
      ),
    );
  }

  checkAvailabilities({ processId }: { processId: number }): Observable<any> {
    const shoppingCart$ = this.getShoppingCart({ processId }).pipe(first());
    const itemsToCheck$ = shoppingCart$.pipe(
      map((cart) =>
        cart?.items?.filter(
          (item) =>
            item?.data?.features?.orderType === 'Download' &&
            !item.data.availability.lastRequest,
        ),
      ),
    );

    return itemsToCheck$.pipe(
      withLatestFrom(shoppingCart$),
      switchMap(async ([items, cart]) => {
        const errorIds = [];

        for (const item of items) {
          const availability = await this.availabilityService
            .getDownloadAvailability({
              item: {
                ean: item.data.product.ean,
                itemId: Number(item.data.product.catalogProductNumber),
                price: item.data.availability.price,
              },
            })
            .toPromise();

          if (
            !availability ||
            !this.availabilityService.isAvailable({ availability })
          ) {
            errorIds.push(item.id);
          } else {
            await this.updateShoppingCartItemAvailability({
              shoppingCartId: cart.id,
              shoppingCartItemId: item.id,
              availability: {
                ...availability,
                lastRequest: new Date().toISOString(),
              },
            }).toPromise();
          }
        }

        this.setOlaErrors({ processId, errorIds });

        if (errorIds.length > 0) {
          throw throwError(new Error(`Artikel nicht verfügbar`));
        } else {
          return of(undefined);
        }
      }),
    );
  }

  updateAvailabilities({ processId }: { processId: number }): Observable<any> {
    const shoppingCart$ = this.getShoppingCart({ processId }).pipe(first());
    const itemsToUpdate$ = shoppingCart$.pipe(
      map((cart) =>
        cart?.items?.filter(
          (item) =>
            item?.data?.features?.orderType === 'DIG-Versand' ||
            item?.data?.features?.orderType === 'B2B-Versand',
        ),
      ),
    );

    return itemsToUpdate$.pipe(
      mergeMap((itemsToUpdate) => {
        if (!(itemsToUpdate?.length > 0)) {
          return of(undefined);
        }

        return concat(
          ...itemsToUpdate.map(async (itemToUpdate) => {
            const orderType = itemToUpdate?.data?.features?.orderType;
            let availability$: Observable<AvailabilityDTO>;
            if (orderType === 'DIG-Versand') {
              availability$ =
                this.availabilityService.getDigDeliveryAvailability({
                  item: {
                    ean: itemToUpdate.data.product.ean,
                    itemId: Number(
                      itemToUpdate.data.product.catalogProductNumber,
                    ),
                    price: itemToUpdate.data.availability.price,
                  },
                  quantity: itemToUpdate.data.quantity,
                });
            } else if (orderType === 'B2B-Versand') {
              const branch = await this.applicationService
                .getSelectedBranch$(processId)
                .pipe(first())
                .toPromise();
              availability$ =
                this.availabilityService.getB2bDeliveryAvailability({
                  item: {
                    ean: itemToUpdate.data.product.ean,
                    itemId: Number(
                      itemToUpdate.data.product.catalogProductNumber,
                    ),
                    price: itemToUpdate.data.availability.price,
                  },
                  quantity: itemToUpdate.data.quantity,
                  branch,
                });
            }

            if (!isObservable(availability$)) {
              return of(undefined);
            }

            return availability$.pipe(
              mergeMap((availability) => {
                let updatedAvailability = availability;
                // Bei Preisupdate der Versandoptionen soll immer der Preis genommen werden, der im Warenkorb steht
                if (
                  orderType === 'DIG-Versand' ||
                  orderType === 'B2B-Versand'
                ) {
                  const itemPrice = itemToUpdate.data.availability.price;
                  updatedAvailability = {
                    ...availability,
                    price: itemPrice,
                  };
                }
                return this.updateItemInShoppingCart({
                  processId,
                  shoppingCartItemId: itemToUpdate.id,
                  update: {
                    availability: updatedAvailability,
                  },
                });
              }),
            );
          }),
        ).pipe(bufferCount(itemsToUpdate.length));
      }),
    );
  }

  /**
   * Refreshes the availability data for a single shopping cart item.
   *
   * Fetches fresh availability from the appropriate service based on order type
   * (Abholung, Rücklage, Download, Versand, DIG-Versand, B2B-Versand) and updates
   * the cart item.
   *
   * @remarks
   * **Order Type Handling**:
   * - **Abholung** (Pickup): Requires branch for availability check
   * - **Rücklage** (TakeAway): Requires branch for availability check
   * - **Download**: No additional parameters needed
   * - **Versand** (Delivery): Standard delivery availability
   * - **DIG-Versand** (Digital Delivery): Digital goods delivery
   * - **B2B-Versand** (B2B Delivery): Business customer delivery
   *
   * **Updates**: Automatically calls `updateItemInShoppingCart()` with the new availability
   * after fetching.
   *
   * @param params - Parameters object
   * @param params.processId - Process identifier
   * @param params.shoppingCartItemId - ID of the cart item to refresh
   *
   * @returns Promise of the refreshed availability DTO, or undefined if item not found
   *
   * @example
   * ```typescript
   * const availability = await this.checkoutService.refreshAvailability({
   *   processId: 123,
   *   shoppingCartItemId: 456
   * });
   * console.log('Updated availability:', availability);
   * ```
   */
  async refreshAvailability({
    processId,
    shoppingCartItemId,
  }: {
    processId: number;
    shoppingCartItemId: number;
  }): Promise<AvailabilityDTO> {
    const shoppingCart = await this.getShoppingCart({ processId })
      .pipe(first())
      .toPromise();
    const item = shoppingCart?.items.find(
      (item) => item.id === shoppingCartItemId,
    )?.data;

    if (!item) {
      return;
    }

    const itemData: ItemData = {
      ean: item.product.ean,
      itemId: Number(item.product.catalogProductNumber),
      price: item.availability.price,
    };

    let availability: AvailabilityDTO;

    switch (item.features.orderType) {
      case 'Abholung': {
        const abholung = await this.availabilityService
          .getPickUpAvailability({
            item: itemData,
            branch: item.destination?.data?.targetBranch?.data,
            quantity: item.quantity,
          })
          .toPromise();
        availability = abholung[0];
        break;
      }
      case 'Rücklage': {
        const ruecklage = await this.availabilityService
          .getTakeAwayAvailability({
            item: itemData,
            quantity: item.quantity,
            branch: item.destination?.data?.targetBranch?.data,
          })
          .toPromise();
        availability = ruecklage;
        break;
      }
      case 'Download': {
        const download = await this.availabilityService
          .getDownloadAvailability({
            item: itemData,
          })
          .toPromise();

        availability = download;
        break;
      }
      case 'Versand': {
        const versand = await this.availabilityService
          .getDeliveryAvailability({
            item: itemData,
            quantity: item.quantity,
          })
          .toPromise();

        availability = versand;
        break;
      }
      case 'DIG-Versand': {
        const digVersand = await this.availabilityService
          .getDigDeliveryAvailability({
            item: itemData,
            quantity: item.quantity,
          })
          .toPromise();

        availability = digVersand;
        break;
      }
      case 'B2B-Versand': {
        const b2bVersand = await this.availabilityService
          .getB2bDeliveryAvailability({
            item: itemData,
            quantity: item.quantity,
          })
          .toPromise();

        availability = b2bVersand;
        break;
      }
    }

    await this.updateItemInShoppingCart({
      processId,
      update: { availability },
      shoppingCartItemId: item.id,
    }).toPromise();

    return availability;
  }

  /**
   * Validates if all shopping cart items have fresh availability data (OLA not expired).
   *
   * OLA (Order Level Agreement) defines how long availability data remains valid.
   * This method polls the store at regular intervals and checks if the oldest
   * availability timestamp is still within the expiration window.
   *
   * @remarks
   * **Polling**: Uses `rxjsInterval()` to continuously check OLA status. The Observable emits
   * `true` while all items are valid, `false` when any item expires.
   *
   * **Timestamp Tracking**: Tracks timestamps per `${itemId}_${orderType}` combination.
   * Shipping types (Versand, DIG-Versand, B2B-Versand) fall back to generic 'Versand' timestamp.
   *
   * **Default Interval**: Polls every `olaExpiration / 10` milliseconds (default: 30 seconds for 5-minute expiration).
   *
   * @param params - Parameters object
   * @param params.processId - Process identifier
   * @param params.interval - Custom polling interval in milliseconds (optional)
   *
   * @returns Observable that emits `true` when OLA is valid, `false` when expired.
   * Emits only on changes (`distinctUntilChanged()`).
   *
   * @example
   * ```typescript
   * this.checkoutService.validateOlaStatus({ processId: 123 })
   *   .subscribe(isValid => {
   *     if (!isValid) {
   *       console.warn('Availability data expired! Refresh required.');
   *       // Trigger availability refresh
   *     }
   *   });
   * ```
   *
   * @see olaExpiration For expiration duration configuration
   * @see checkoutIsValid For combined OLA + availability validation
   */
  validateOlaStatus({
    processId,
    interval,
  }: {
    processId: number;
    interval?: number;
  }): Observable<boolean> {
    return rxjsInterval(interval ?? this.olaExpiration / 10).pipe(
      startWith(0),
      switchMap(() =>
        this.store
          .select(DomainCheckoutSelectors.selectCheckoutEntityByProcessId, {
            processId,
          })
          .pipe(
            map((entity) => {
              const now = Date.now();
              if (
                !entity ||
                !entity.shoppingCart ||
                !entity.shoppingCart.items
              ) {
                return;
              }

              const itemAvailabilityTimestamp =
                entity.itemAvailabilityTimestamp ?? {};
              const shoppingCart = entity.shoppingCart;

              const timestamps = shoppingCart.items
                ?.map((i) => i.data)
                ?.filter((item) => !!item?.features?.orderType)
                ?.map((item) => {
                  const orderType = item.features.orderType;

                  let timestamp =
                    itemAvailabilityTimestamp[`${item.id}_${orderType}`];

                  if (timestamp) {
                    return timestamp;
                  }

                  if (orderType.endsWith('Versand')) {
                    timestamp =
                      itemAvailabilityTimestamp[`${item.id}_Versand`] ??
                      itemAvailabilityTimestamp[`${item.id}_DIG-Versand`] ??
                      itemAvailabilityTimestamp[`${item.id}_B2B-Versand`];
                  }

                  return timestamp;
                })
                ?.filter((timestamp) => !!timestamp);

              if (timestamps?.length > 0) {
                const oldestTimestamp = Math.min(...timestamps);
                const expirationTimestamp =
                  oldestTimestamp + this.olaExpiration;
                return expirationTimestamp > now;
              }

              return false;
            }),
          ),
      ),
      distinctUntilChanged(),
    );
  }

  validateAvailabilities({
    processId,
  }: {
    processId: number;
  }): Observable<boolean> {
    return this.getShoppingCart({ processId }).pipe(
      map((shoppingCart) => {
        const items = shoppingCart?.items?.map((item) => item.data) || [];
        return items.every((i) =>
          this.availabilityService.isAvailable({
            availability: i.availability,
          }),
        );
      }),
    );
  }

  /**
   * Validates if the checkout is ready for order completion.
   *
   * Combines OLA status validation with availability validation. Both must be true
   * for checkout to proceed.
   *
   * @remarks
   * **Validation Checks**:
   * - OLA Status: All item availabilities are within expiration window
   * - Availabilities: All items are marked as available (not out of stock)
   *
   * **Polling**: Uses fast polling (250ms) for OLA status to catch expiration quickly.
   *
   * @param params - Parameters object
   * @param params.processId - Process identifier
   *
   * @returns Observable that emits `true` when checkout is valid, `false` otherwise.
   * Emits on every change in either validation.
   *
   * @example
   * ```typescript
   * this.checkoutService.checkoutIsValid({ processId: 123 })
   *   .subscribe(isValid => {
   *     this.checkoutButton.disabled = !isValid;
   *     if (!isValid) {
   *       this.showWarning('Checkout unavailable: Availability expired');
   *     }
   *   });
   * ```
   *
   * @see validateOlaStatus For OLA-only validation
   * @see validateAvailabilities For availability-only validation
   */
  checkoutIsValid({ processId }: { processId: number }): Observable<boolean> {
    const olaStatus$ = this.validateOlaStatus({ processId, interval: 250 });

    const availabilities$ = this.validateAvailabilities({ processId });

    return combineLatest([olaStatus$, availabilities$]).pipe(
      map(([olaStatus, availabilities]) => olaStatus && availabilities),
    );
  }

  /**
   * Orchestrates the complete checkout process from cart validation to order creation.
   *
   * This is the most complex method in the service, executing a 12-step sequence that
   * validates data, updates entities, and creates orders. Each step must complete before
   * the next begins.
   *
   * @remarks
   * **Execution Sequence** (sequential, not parallel):
   * 1. **Update Destination**: Sets shipping addresses on delivery destinations
   * 2. **Refresh Shopping Cart**: Gets latest cart state from API
   * 3. **Set Special Comments**: Applies agent comments to all cart items
   * 4. **Refresh Checkout**: Recreates checkout entity from current state
   * 5. **Check Availabilities**: Validates download items are available
   * 6. **Update Availabilities**: Refreshes DIG-Versand and B2B-Versand prices
   * 7. **Set Buyer**: Submits buyer information to checkout
   * 8. **Set Notification Channels**: Configures email/SMS preferences
   * 9. **Set Payer**: Submits payer information (if needed for order type)
   * 10. **Set Payment Type**: Configures payment method (Rechnung/Bar)
   * 11. **Set Destination**: Updates destinations with shipping addresses
   * 12. **Complete Order**: Submits to OMS for order creation
   *
   * **Payment Type Logic**:
   * - Download/Versand/DIG-Versand/B2B-Versand → Payment type 128 (Rechnung/Invoice)
   * - Pickup/TakeAway only → Payment type 4 (Bar/Cash)
   *
   * **Payer Requirement**:
   * - Required for B2B customers or Download/Delivery order types
   * - Skipped for in-store pickup/takeaway only
   *
   * **Error Handling**:
   * - HTTP 409 (Conflict): Order already exists - dispatches existing orders to store
   * - Other errors propagate to consumer for handling
   * - Failed availability checks throw error preventing order creation
   *
   * **Side Effects**:
   * - Logs each step to console (for debugging)
   * - Updates NgRx store at multiple points
   * - Dispatches final orders to store on success
   *
   * @param params - Parameters object
   * @param params.processId - Process identifier
   *
   * @returns Observable array of created orders (DisplayOrderDTO[])
   *
   * @throws Observable error if availability validation fails or API returns non-409 error
   *
   * @example
   * ```typescript
   * this.checkoutService.completeCheckout({ processId: 123 })
   *   .subscribe({
   *     next: (orders) => {
   *       console.log('Orders created:', orders);
   *       this.router.navigate(['/order-confirmation']);
   *     },
   *     error: (error) => {
   *       if (error.status === 409) {
   *         console.log('Order already exists');
   *       } else {
   *         console.error('Checkout failed:', error);
   *       }
   *     }
   *   });
   * ```
   *
   * @see completeKulturpassOrder For Kulturpass-specific checkout flow
   */
  completeCheckout({
    processId,
  }: {
    processId: number;
  }): Observable<DisplayOrderDTO[]> {
    const refreshShoppingCart$ = this.getShoppingCart({
      processId,
      latest: true,
    }).pipe(first());
    const refreshCheckout$ = this.getCheckout({
      processId,
      refresh: true,
    }).pipe(first());

    const itemOrderOptions$ = this.getShoppingCart({ processId }).pipe(
      first(),
      map((shoppingCart) => {
        const shoppingCartItems =
          shoppingCart?.items?.map((item) => item.data) || [];

        const hasTakeAway = shoppingCartItems.some(
          (item) => item.features.orderType === 'Rücklage',
        );
        const hasPickUp = shoppingCartItems.some(
          (item) => item.features.orderType === 'Abholung',
        );
        const hasDownload = shoppingCartItems.some(
          (item) => item.features.orderType === 'Download',
        );
        const hasDelivery = shoppingCartItems.some(
          (item) => item.features.orderType === 'Versand',
        );
        const hasDigDelivery = shoppingCartItems.some(
          (item) => item.features.orderType === 'DIG-Versand',
        );
        const hasB2BDelivery = shoppingCartItems.some(
          (item) => item.features.orderType === 'B2B-Versand',
        );

        return {
          hasTakeAway,
          hasPickUp,
          hasDownload,
          hasDelivery,
          hasDigDelivery,
          hasB2BDelivery,
        };
      }),
      shareReplay(),
    );

    const customerTypes$ = this.getCustomerFeatures({ processId }).pipe(
      first(),
      map((features) => {
        const isOnline = !!features?.webshop;
        const isGuest = !!features?.guest;
        const isB2B = !!features?.b2b;
        const hasCustomerCard = !!features?.p4mUser;
        const isStaff = !!features?.staff;

        return { isOnline: isOnline, isGuest, isB2B, hasCustomerCard, isStaff };
      }),
    );

    const setSpecialComment$ = this.getSpecialComment({ processId }).pipe(
      first(),
      mergeMap((specialComment) => {
        if (specialComment) {
          return this.setSpecialCommentOnItem({ processId, specialComment });
        }
        return of(specialComment);
      }),
    );

    const shippingAddressDestination$ = this.getCheckout({ processId }).pipe(
      first(),
      map((checkout) =>
        checkout?.destinations?.filter(
          (dest) => dest.data.target === 2 || dest.data.target === 16,
        ),
      ),
      shareReplay(),
    );

    const setBuyer$ = this.getBuyer({ processId }).pipe(
      first(),
      mergeMap((buyer) => this._setBuyer({ processId, buyer })),
    );

    const setNotificationChannels$ = this.getNotificationChannels({
      processId,
    }).pipe(
      first(),
      mergeMap((notificationChannels) =>
        this._setNotificationChannels({ processId, notificationChannels }),
      ),
    );

    const setPayer$ = combineLatest([itemOrderOptions$, customerTypes$]).pipe(
      first(),
      mergeMap(([itemOrderOptions, customerTypes]) => {
        if (
          customerTypes.isB2B ||
          itemOrderOptions.hasB2BDelivery ||
          itemOrderOptions.hasDelivery ||
          itemOrderOptions.hasDigDelivery ||
          itemOrderOptions.hasDownload
        ) {
          return this.getPayer({ processId }).pipe(first());
        }
        return of(undefined);
      }),
      mergeMap((payer) =>
        payer ? this._setPayer({ processId, payer }) : of(undefined),
      ),
    );

    const updateDestination$ = itemOrderOptions$.pipe(
      withLatestFrom(this.getCustomerFeatures({ processId })),
      mergeMap(
        ([
          { hasDownload, hasDelivery, hasDigDelivery, hasB2BDelivery },
          customerFeatures,
        ]) => {
          const needsUpdate =
            hasDownload || hasDelivery || hasDigDelivery || hasB2BDelivery;
          return needsUpdate
            ? this.setDestinationForCustomer({ processId, customerFeatures })
            : of(undefined);
        },
      ),
    );

    const checkAvailabilities$ = this.checkAvailabilities({ processId });

    const updateAvailabilities$ = this.updateAvailabilities({ processId });

    const setPaymentType$ = itemOrderOptions$.pipe(
      mergeMap(
        ({ hasDownload, hasDelivery, hasDigDelivery, hasB2BDelivery }) => {
          const paymentType =
            hasDownload || hasDelivery || hasDigDelivery || hasB2BDelivery
              ? 128 /* Rechnung */
              : 4; /* Bar */
          return this.setPayment({ processId, paymentType });
        },
      ),
      shareReplay(),
    );

    const setDestination$ = combineLatest([
      this.getCheckout({ processId }).pipe(first()),
      shippingAddressDestination$,
      this.getShippingAddress({ processId }).pipe(first()),
    ]).pipe(
      mergeMap(([checkout, destinations, shippingAddress]) => {
        const obs: Observable<ResponseArgsOfDestinationDTO>[] = [];
        if (destinations?.length > 0) {
          destinations.forEach((destination) => {
            const updatedDestination: DestinationDTO = {
              ...destination.data,
              shippingAddress: { ...shippingAddress },
            };
            obs.push(
              this.storeCheckoutService.StoreCheckoutUpdateDestination({
                checkoutId: checkout.id,
                destinationId: destination.id,
                destinationDTO: updatedDestination,
              }),
            );
          });
          return concat(...obs).pipe(bufferCount(destinations?.length));
        }
        return of(destinations);
      }),
      shareReplay(),
    );

    const completeOrder$ = this.getCheckout({ processId }).pipe(
      first(),
      mergeMap((checkout) =>
        this.orderCheckoutService
          .OrderCheckoutCreateOrderPOST({
            checkoutId: checkout.id,
          })
          .pipe(
            catchError((error) => {
              if (error instanceof HttpErrorResponse) {
                if (error.status === 409) {
                  this.store.dispatch(
                    DomainCheckoutActions.setOrders({
                      orders: error.error.result,
                    }),
                  );
                }
                return throwError(error);
              }
            }),
            map((response) => {
              this.store.dispatch(
                DomainCheckoutActions.setOrders({ orders: response.result }),
              );
              return response.result;
            }),
          ),
      ),
    );

    return of(undefined)
      .pipe(
        mergeMap((_) =>
          updateDestination$.pipe(
            tap(console.log.bind(window, 'updateDestination$')),
          ),
        ),
        mergeMap((_) =>
          refreshShoppingCart$.pipe(
            tap(console.log.bind(window, 'refreshShoppingCart$')),
          ),
        ),
        mergeMap((_) =>
          setSpecialComment$.pipe(
            tap(console.log.bind(window, 'setSpecialComment$')),
          ),
        ),
        mergeMap((_) =>
          refreshCheckout$.pipe(
            tap(console.log.bind(window, 'refreshCheckout$')),
          ),
        ),
        mergeMap((_) =>
          checkAvailabilities$.pipe(
            tap(console.log.bind(window, 'checkAvailabilities$')),
          ),
        ),
        mergeMap((_) =>
          updateAvailabilities$.pipe(
            tap(console.log.bind(window, 'updateAvailabilities$')),
          ),
        ),
      )
      .pipe(
        mergeMap((_) =>
          setBuyer$.pipe(tap(console.log.bind(window, 'setBuyer$'))),
        ),
        mergeMap((_) =>
          setNotificationChannels$.pipe(
            tap(console.log.bind(window, 'setNotificationChannels$')),
          ),
        ),
        mergeMap((_) =>
          setPayer$.pipe(tap(console.log.bind(window, 'setPayer$'))),
        ),
        mergeMap((_) =>
          setPaymentType$.pipe(
            tap(console.log.bind(window, 'setPaymentType$')),
          ),
        ),
        mergeMap((_) =>
          setDestination$.pipe(
            tap(console.log.bind(window, 'setDestination$')),
          ),
        ),
        mergeMap((_) =>
          completeOrder$.pipe(tap(console.log.bind(window, 'completeOrder$'))),
        ),
      );
  }

  completeKulturpassOrder({
    processId,
    orderItemSubsetId,
  }: {
    processId: number;
    orderItemSubsetId: number;
  }): Observable<ResponseArgsOfValueTupleOfIEnumerableOfDisplayOrderDTOAndIEnumerableOfKeyValueDTOOfStringAndString> {
    const refreshShoppingCart$ = this.getShoppingCart({
      processId,
      latest: true,
    }).pipe(first());
    const refreshCheckout$ = this.getCheckout({
      processId,
      refresh: true,
    }).pipe(first());

    const setBuyer$ = this.getBuyer({ processId }).pipe(
      first(),
      mergeMap((buyer) => this._setBuyer({ processId, buyer })),
    );

    const setPayer$ = this.getPayer({ processId }).pipe(
      first(),
      mergeMap((payer) => this._setPayer({ processId, payer })),
    );

    const checkAvailabilities$ = this.checkAvailabilities({ processId });

    const updateAvailabilities$ = this.updateAvailabilities({ processId });

    return refreshShoppingCart$.pipe(
      mergeMap((_) => refreshCheckout$),
      mergeMap((_) => checkAvailabilities$),
      mergeMap((_) => updateAvailabilities$),
      mergeMap((_) => setBuyer$),
      mergeMap((_) => setPayer$),
      mergeMap((checkout) =>
        this.orderCheckoutService.OrderCheckoutCreateKulturPassOrder({
          payload: {
            checkoutId: checkout.id,
            orderItemSubsetId: String(orderItemSubsetId),
          },
        }),
      ),
    );
  }

  updateDestination({
    processId,
    destinationId,
    destination,
  }: {
    processId: number;
    destinationId: number;
    destination: DestinationDTO;
  }): Observable<DestinationDTO> {
    return this.getCheckout({ processId }).pipe(
      first(),
      mergeMap((checkout) =>
        this.storeCheckoutService
          .StoreCheckoutUpdateDestination({
            destinationId,
            checkoutId: checkout.id,
            destinationDTO: destination,
          })
          .pipe(
            map((response) => response.result),
            tap((destination) =>
              this.store.dispatch(
                DomainCheckoutActions.setCheckoutDestination({
                  processId,
                  destination,
                }),
              ),
            ),
          ),
      ),
    );
  }

  //#endregion

  //#region Common

  /**
   * Validates if a customer can be set on the shopping cart based on cart contents and customer features.
   *
   * @remarks
   * **Memoization Disabled**: The `@memorize()` decorator was intentionally disabled for this method
   * due to shallow comparison issues. The decorator couldn't detect when `customerFeatures` object
   * changed, causing stale cached results. See Ticket #4619.
   *
   * **Response Fields**:
   * - `ok`: True if customer can be set without issues
   * - `filter`: Customer search filters (e.g., `{ customertype: 'webshop;guest' }`)
   * - `message`: Error message if validation fails
   * - `create`: Options for creating new customer types (store, guest, webshop, b2b)
   *
   * **Use Cases**:
   * - Determine which customer types are compatible with cart contents
   * - Pre-filter customer search results
   * - Enable/disable customer type creation buttons
   *
   * @param params - Parameters object
   * @param params.processId - Process identifier
   * @param params.customerFeatures - Customer feature flags (optional: webshop, guest, b2b, staff, etc.)
   *
   * @returns Observable with validation result containing ok, filter, message, and create options
   *
   * @example
   * ```typescript
   * this.checkoutService.canSetCustomer({
   *   processId: 123,
   *   customerFeatures: { webshop: 'true', guest: 'false' }
   * }).subscribe(result => {
   *   if (result.ok) {
   *     console.log('Customer types allowed:', result.create.options.values);
   *   } else {
   *     console.error('Cannot set customer:', result.message);
   *   }
   * });
   * ```
   */
  canSetCustomer({
    processId,
    customerFeatures,
  }: {
    processId: number;
    customerFeatures?: { [key: string]: string };
  }): Observable<{
    ok?: boolean;
    filter?: { [key: string]: string };
    message?: string;
    create?: InputDTO;
  }> {
    return this.getShoppingCart({ processId })
      .pipe(
        first(),
        mergeMap((shoppingCart) =>
          this._shoppingCartService
            .StoreCheckoutShoppingCartCanAddBuyer({
              shoppingCartId: shoppingCart.id,
              payload: { customerFeatures },
            })
            .pipe(
              map((response) => ({
                ok: response.result.ok,
                filter: response.result.queryToken?.filter || {},
                message: response.message,
                create: response.result.create,
              })),
            ),
        ),
      )
      .pipe(shareReplay(1));
  }

  setNotificationChannels({
    processId,
    notificationChannels,
  }: {
    processId: number;
    notificationChannels: NotificationChannel;
  }): void {
    this.store.dispatch(
      DomainCheckoutActions.setNotificationChannels({
        processId,
        notificationChannels,
      }),
    );
  }

  getNotificationChannels({
    processId,
  }: {
    processId: number;
  }): Observable<NotificationChannel> {
    return this.store.select(
      DomainCheckoutSelectors.selectNotificationChannels,
      { processId },
    );
  }

  setBuyerCommunicationDetails({
    processId,
    mobile,
    email,
  }: {
    processId: number;
    mobile?: string;
    email?: string;
  }): void {
    this.store.dispatch(
      DomainCheckoutActions.setBuyerCommunicationDetails({
        processId,
        mobile,
        email,
      }),
    );
  }

  getBuyerCommunicationDetails({
    processId,
  }: {
    processId: number;
  }): Observable<CommunicationDetailsDTO> {
    return this.store.select(
      DomainCheckoutSelectors.selectBuyerCommunicationDetails,
      { processId },
    );
  }

  getSetableCustomerTypes(
    processId: number,
  ): Observable<{ [key: string]: boolean }> {
    return this.canSetCustomer({ processId, customerFeatures: undefined }).pipe(
      map((res) => {
        const setableTypes: { [key: string]: boolean } = {
          store: false,
          guest: false,
          webshop: false,
          b2b: false,
        };

        res.create?.options?.values?.forEach((option) => {
          setableTypes[option.value] = option.enabled !== false;
        });

        // if (Object.keys(res.filter).length === 0) {
        //   return setableTypes;
        // }

        // const customerTypes = res.filter?.customertype?.split(';') || [];
        // const customerAttributes = res.filter?.customerattributes?.split(';') || [];

        // const typesAndAttributes = [...customerTypes, ...customerAttributes];
        // if (typesAndAttributes.includes('webshop') && !typesAndAttributes.includes('!guest')) {
        //   typesAndAttributes.push('guest');
        // }

        // for (const key in setableTypes) {
        //   if (Object.prototype.hasOwnProperty.call(setableTypes, key)) {
        //     if (typesAndAttributes.includes(key)) {
        //       setableTypes[key] = true;
        //     } else if (typesAndAttributes.includes(`!${key}`)) {
        //       setableTypes[key] = false;
        //     } else {
        //       setableTypes[key] = false;
        //     }
        //   }
        // }

        return setableTypes;
      }),
    );
  }

  /**
   * Retrieves all active, online branches that support shipping.
   *
   * @remarks
   * **Filtering**: Returns only branches matching ALL criteria:
   * - `status === 1` (Active)
   * - `branchType === 1` (Standard branch type)
   * - `isOnline === true` (Available online)
   * - `isShippingEnabled === true` (Supports shipping)
   *
   * **Memoization**: Uses `@memorize()` decorator to cache results. Subsequent calls
   * return cached data without API round-trip.
   *
   * **Pagination**: Fetches up to 999 branches (effectively all branches).
   *
   * @returns Observable array of filtered branch DTOs
   *
   * @example
   * ```typescript
   * this.checkoutService.getBranches()
   *   .subscribe(branches => {
   *     console.log('Available branches:', branches.length);
   *     this.branchDropdown.options = branches;
   *   });
   * ```
   */
  @memorize()
  getBranches(): Observable<BranchDTO[]> {
    return this._branchService
      .StoreCheckoutBranchGetBranches({
        take: 999,
      })
      .pipe(
        map((r) => {
          return r.result.filter(
            (branch) =>
              branch.status === 1 &&
              branch.branchType === 1 &&
              branch.isOnline === true &&
              branch.isShippingEnabled === true,
          );
        }),
        shareReplay(),
      );
  }

  reorder(
    orderId: number,
    orderItemId: number,
    orderItemSubsetId: number,
    data: ReorderValues,
  ) {
    return this.orderCheckoutService
      .OrderCheckoutReorder({ orderId, orderItemId, orderItemSubsetId, data })
      .pipe(map((response) => response.result));
  }

  setOlaErrors({
    processId,
    errorIds,
  }: {
    processId: number;
    errorIds: number[];
  }) {
    this.store.dispatch(
      DomainCheckoutActions.setOlaError({
        processId,
        olaErrorIds: errorIds,
      }),
    );
  }

  getCustomerFeatures({
    processId,
  }: {
    processId: number;
  }): Observable<{ [key: string]: string }> {
    return this.store.select(
      DomainCheckoutSelectors.selectCustomerFeaturesByProcessId,
      { processId },
    );
  }

  setShippingAddress({
    processId,
    shippingAddress,
  }: {
    processId: number;
    shippingAddress: ShippingAddressDTO;
  }) {
    this.store.dispatch(
      DomainCheckoutActions.setShippingAddress({ processId, shippingAddress }),
    );
  }

  getShippingAddress({
    processId,
  }: {
    processId: number;
  }): Observable<ShippingAddressDTO> {
    return this.store.select(
      DomainCheckoutSelectors.selectShippingAddressByProcessId,
      { processId },
    );
  }

  /**
   * Removes all checkout data for a process ID from the store.
   *
   * Cleans up shopping cart, checkout entity, buyer, payer, and all associated data.
   * Call this when a checkout session is complete or cancelled.
   *
   * @param params - Parameters object
   * @param params.processId - Process identifier to remove
   *
   * @example
   * ```typescript
   * // After successful order or when user cancels
   * this.checkoutService.removeProcess({ processId: 123 });
   * ```
   */
  removeProcess({ processId }: { processId: number }) {
    this.store.dispatch(
      DomainCheckoutActions.removeCheckoutWithProcessId({ processId }),
    );
  }

  setCustomer({
    processId,
    customerDto,
  }: {
    processId: number;
    customerDto: CustomerDTO;
  }) {
    this.store.dispatch(
      DomainCheckoutActions.setCustomer({ processId, customer: customerDto }),
    );
  }

  getCustomer({ processId }: { processId: number }): Observable<CustomerDTO> {
    return this.store.select(
      DomainCheckoutSelectors.selectCustomerByProcessId,
      { processId },
    );
  }

  setPayer({ processId, payer }: { processId: number; payer: PayerDTO }) {
    this.store.dispatch(DomainCheckoutActions.setPayer({ processId, payer }));
  }

  getPayer({ processId }: { processId: number }): Observable<PayerDTO> {
    return this.store.select(DomainCheckoutSelectors.selectPayerByProcessId, {
      processId,
    });
  }

  setBuyer({ processId, buyer }: { processId: number; buyer: BuyerDTO }) {
    this.store.dispatch(DomainCheckoutActions.setBuyer({ processId, buyer }));
  }

  getBuyer({ processId }: { processId: number }): Observable<BuyerDTO> {
    return this.store.select(DomainCheckoutSelectors.selectBuyerByProcessId, {
      processId,
    });
  }

  getOrders(): Observable<DisplayOrderDTO[]> {
    return this.store.select(DomainCheckoutSelectors.selectOrders);
  }

  updateOrderItem(item: DisplayOrderItemDTO) {
    this.store.dispatch(DomainCheckoutActions.updateOrderItem({ item }));
  }

  removeAllOrders() {
    this.store.dispatch(DomainCheckoutActions.removeAllOrders());
  }

  setSpecialComment({
    processId,
    agentComment,
  }: {
    processId: number;
    agentComment: string;
  }) {
    this.store.dispatch(
      DomainCheckoutActions.setSpecialComment({ processId, agentComment }),
    );
  }

  getSpecialComment({ processId }: { processId: number }) {
    return this.store.select(DomainCheckoutSelectors.selectSpecialComment, {
      processId,
    });
  }
  //#endregion
}
