import { inject, Injectable } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import {
  StoreCheckoutService,
  StoreCheckoutShoppingCartService,
  StoreCheckoutBuyerService,
  StoreCheckoutPayerService,
  StoreCheckoutPaymentService,
  DestinationDTO,
  BuyerDTO,
  PayerDTO,
  AvailabilityDTO,
} from '@generated/swagger/checkout-api';

import {
  EntityContainer,
  ResponseArgsError,
  takeUntilAborted,
} from '@isa/common/data-access';
import { logger } from '@isa/core/logging';
import { firstValueFrom } from 'rxjs';
import { ShoppingCartService } from './shopping-cart.service';
import {
  Buyer,
  CompleteOrderParams,
  CompleteOrderParamsSchema,
  NotificationChannel,
  Payer,
  PaymentType,
} from '../schemas';
import {
  Order,
  OrderOptionsAnalysis,
  CustomerTypeAnalysis,
  Checkout,
  ShippingAddress,
  ShoppingCartItem,
} from '../models';
import { CheckoutCompletionError } from '../errors';
import {
  AvailabilityService,
  OrderTypeFeature,
  Availability as AvailabilityModel,
} from '@isa/availability/data-access';
import { AvailabilityAdapter } from '../adapters';
import {
  analyzeOrderOptions,
  analyzeCustomerTypes,
  shouldSetPayer,
  needsDestinationUpdate,
  determinePaymentType,
  filterShippingItems,
  filterDeliveryDestinations,
  hasValidItemId,
  hasValidDestinationData,
} from '../helpers';

/**
 * Service for checkout completion operations.
 *
 * @remarks
 * This service orchestrates the complete checkout workflow including:
 * - Shopping cart validation
 * - Order type analysis
 * - Customer information setup (buyer, payer, payment type)
 * - Destination and shipping address updates
 * - Availability validation
 * - Order creation
 *
 * All required data must be provided as parameters (no state management dependencies).
 */
@Injectable({ providedIn: 'root' })
export class CheckoutService {
  #logger = logger(() => ({ service: 'CheckoutService' }));

  // Generated API services (checkout domain only)
  #storeCheckoutService = inject(StoreCheckoutService);
  #shoppingCartService = inject(StoreCheckoutShoppingCartService);
  #buyerService = inject(StoreCheckoutBuyerService);
  #payerService = inject(StoreCheckoutPayerService);
  #paymentService = inject(StoreCheckoutPaymentService);

  // Domain data-access services
  #shoppingCartDataService = inject(ShoppingCartService);
  #availabilityService = inject(AvailabilityService);

  /**
   * Completes the checkout process.
   *
   * @param params - Complete checkout parameters with all required data
   * @param abortSignal - Optional cancellation signal
   * @returns Promise resolving to the completed checkout ID
   * @throws {CheckoutCompletionError} for business logic failures
   * @throws {ResponseArgsError} for API failures
   *
   * @remarks
   * This method prepares the checkout but does NOT create orders.
   * Order creation should be handled separately by calling order creation services.
   *
   * @example
   * ```typescript
   * const checkoutId = await checkoutService.complete({
   *   checkoutId: 123,
   *   shoppingCartId: 456,
   *   buyer: buyerDTO,
   *   notificationChannels: 1,
   *   customerFeatures: { webshop: 'true', b2b: 'false' },
   *   payer: payerDTO,
   *   shippingAddress: addressDTO,
   *   specialComment: 'Rush delivery'
   * }, abortController.signal);
   * ```
   */
  async complete(
    params: CompleteOrderParams,
    abortSignal?: AbortSignal,
  ): Promise<number> {
    const validated = CompleteOrderParamsSchema.parse(params);

    this.#logger.info('Starting checkout completion');

    try {
      // Step 1: Fetch and validate shopping cart
      this.#logger.debug('Fetching shopping cart');
      const shoppingCart = await this.#shoppingCartDataService.getShoppingCart(
        validated.shoppingCartId,
        abortSignal,
      );

      if (!shoppingCart || !shoppingCart.items?.length) {
        throw CheckoutCompletionError.shoppingCartEmpty(
          validated.shoppingCartId,
        );
      }

      // Step 2: Analyze order types and customer type
      this.#logger.debug('Analyzing order options and customer type');
      const orderOptions = this.analyzeOrderOptions(shoppingCart.items);
      const customerTypes = this.analyzeCustomerTypes(
        validated.customerFeatures,
      );

      this.#logger.debug('Order analysis complete');

      // Step 3: Determine if payer is needed
      const needsPayer = this.shouldSetPayer(orderOptions, customerTypes);
      if (needsPayer && !validated.payer) {
        throw CheckoutCompletionError.missingRequiredData(
          'payer',
          'Required for B2B/delivery/download orders',
        );
      }

      // Step 4: Create or refresh checkout to get checkoutId
      this.#logger.debug('Creating or refreshing checkout');
      const initialCheckout = await this.refreshCheckout(
        validated.shoppingCartId,
        abortSignal,
      );
      const checkoutId = initialCheckout.id;

      if (!checkoutId) {
        throw CheckoutCompletionError.checkoutNotFound(0);
      }

      // Step 5: Update destinations for customer (if delivery items exist)
      if (this.needsDestinationUpdate(orderOptions)) {
        this.#logger.debug('Updating destinations for customer');
        await this.updateDestinationsForCustomer(
          validated.shoppingCartId,
          validated.customerFeatures,
          abortSignal,
        );
      }

      // Step 6: Refresh shopping cart to ensure latest state
      // TODO: Check if this is really necessary here
      this.#logger.debug('Refreshing shopping cart');
      await this.#shoppingCartDataService.getShoppingCart(
        validated.shoppingCartId,
        abortSignal,
      );

      // Step 7: Set special comment on all items (if provided)
      if (validated.specialComment) {
        this.#logger.debug('Setting special comment on items');
        await this.setSpecialCommentOnItems(
          validated.shoppingCartId,
          shoppingCart.items,
          validated.specialComment,
          abortSignal,
        );
      }

      // Step 8: Check and update availabilities
      this.#logger.debug('Validating download availabilities');
      await this.validateDownloadAvailabilities(
        validated.shoppingCartId,
        shoppingCart.items,
        abortSignal,
      );

      this.#logger.debug('Updating shipping availabilities');
      await this.updateShippingAvailabilities(
        validated.shoppingCartId,
        shoppingCart.items,
        abortSignal,
      );

      // Step 9: Set buyer on checkout
      this.#logger.debug('Setting buyer on checkout');
      await this.setBuyerOnCheckout(checkoutId, validated.buyer, abortSignal);

      // Step 10: Set notification channels
      this.#logger.debug('Setting notification channels');
      await this.setNotificationChannelsOnCheckout(
        checkoutId,
        validated.notificationChannels ?? 0,
        abortSignal,
      );

      // Step 11: Set payer (conditional)
      if (needsPayer && validated.payer) {
        this.#logger.debug('Setting payer on checkout');
        await this.setPayerOnCheckout(checkoutId, validated.payer, abortSignal);
      }

      // Step 12: Set payment type based on order types
      const paymentType = this.determinePaymentType(orderOptions);
      this.#logger.debug('Setting payment type');
      await this.setPaymentTypeOnCheckout(checkoutId, paymentType, abortSignal);

      // Step 13: Update destination shipping addresses (if delivery or download)
      // Refresh checkout only when we need the destinations data
      if (
        (orderOptions.hasDownload ||
          orderOptions.hasDelivery ||
          orderOptions.hasDigDelivery ||
          orderOptions.hasB2BDelivery) &&
        validated.shippingAddress
      ) {
        this.#logger.debug('Refreshing checkout to get destinations');
        const checkout = await this.refreshCheckout(
          validated.shoppingCartId,
          abortSignal,
        );

        this.#logger.debug('Updating destination shipping addresses');
        await this.updateDestinationShippingAddresses(
          checkoutId,
          checkout,
          validated.shippingAddress,
          abortSignal,
        );
      }

      // Checkout completion is done - return checkoutId for order creation
      this.#logger.info('Checkout completed successfully', () => ({
        checkoutId,
      }));

      return checkoutId;
    } catch (error) {
      // Handle HTTP 409 conflict (order already exists)
      if (error instanceof HttpErrorResponse && error.status === 409) {
        this.#logger.warn('Checkout conflict - order already exists');
        throw CheckoutCompletionError.checkoutConflict(error.error.result);
      }

      this.#logger.error('Checkout completion failed', error);
      throw error;
    }
  }

  // ============================================================================
  // Private Helper Methods - Analysis
  // ============================================================================

  /**
   * Analyzes shopping cart items to determine which order types are present.
   */
  private analyzeOrderOptions(
    items: EntityContainer<ShoppingCartItem>[],
  ): OrderOptionsAnalysis {
    return analyzeOrderOptions(items);
  }

  /**
   * Analyzes customer features to determine customer type characteristics.
   */
  private analyzeCustomerTypes(
    features: Record<string, string>,
  ): CustomerTypeAnalysis {
    return analyzeCustomerTypes(features);
  }

  /**
   * Determines if payer should be set based on order options and customer type.
   */
  private shouldSetPayer(
    options: OrderOptionsAnalysis,
    customer: CustomerTypeAnalysis,
  ): boolean {
    return shouldSetPayer(options, customer);
  }

  /**
   * Determines if destination update is needed based on order types.
   */
  private needsDestinationUpdate(options: OrderOptionsAnalysis): boolean {
    return needsDestinationUpdate(options);
  }

  /**
   * Determines payment type based on order types.
   * Returns 128 (Rechnung/Invoice) for delivery/download orders, 4 (Bar/Cash) or 2 (Free) otherwise.
   */
  private determinePaymentType(options: OrderOptionsAnalysis): PaymentType {
    return determinePaymentType(options);
  }

  // ============================================================================
  // Private Helper Methods - API Operations
  // ============================================================================

  /**
   * Updates destinations for customer based on features.
   * Sets logistician on destinations by buyer features.
   */
  private async updateDestinationsForCustomer(
    shoppingCartId: number,
    customerFeatures: Record<string, string>,
    abortSignal?: AbortSignal,
  ): Promise<void> {
    let req$ =
      this.#shoppingCartService.StoreCheckoutShoppingCartSetLogisticianOnDestinationsByBuyer(
        {
          shoppingCartId,
          payload: { customerFeatures },
        },
      );

    if (abortSignal) {
      req$ = req$.pipe(takeUntilAborted(abortSignal));
    }

    const res = await firstValueFrom(req$);

    if (res.error) {
      const error = new ResponseArgsError(res);
      this.#logger.error('Failed to update destinations for customer', error);
      throw error;
    }
  }

  /**
   * Sets special comment on all shopping cart items.
   */
  private async setSpecialCommentOnItems(
    shoppingCartId: number,
    items: EntityContainer<ShoppingCartItem>[],
    comment: string,
    abortSignal?: AbortSignal,
  ): Promise<void> {
    // Update all items in parallel
    await Promise.all(
      items.map(async (item) => {
        if (!item.id) return;

        let req$ =
          this.#shoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItem(
            {
              shoppingCartId,
              shoppingCartItemId: item.id,
              values: { specialComment: comment },
            },
          );

        if (abortSignal) {
          req$ = req$.pipe(takeUntilAborted(abortSignal));
        }

        const res = await firstValueFrom(req$);

        if (res.error) {
          const error = new ResponseArgsError(res);
          this.#logger.error('Failed to set special comment on item');
          throw error;
        }
      }),
    );
  }

  /**
   * Refreshes checkout to get latest state.
   */
  private async refreshCheckout(
    shoppingCartId: number,
    abortSignal?: AbortSignal,
  ): Promise<Checkout> {
    let req$ = this.#storeCheckoutService.StoreCheckoutCreateOrRefreshCheckout({
      shoppingCartId,
    });

    if (abortSignal) {
      req$ = req$.pipe(takeUntilAborted(abortSignal));
    }

    const res = await firstValueFrom(req$);

    if (res.error) {
      const error = new ResponseArgsError(res);
      this.#logger.error('Failed to refresh checkout', error);
      throw error;
    }

    return res.result as Checkout;
  }

  /**
   * Validates availability for download items.
   * Throws CheckoutCompletionError if any items are unavailable.
   */
  private async validateDownloadAvailabilities(
    shoppingCartId: number,
    items: EntityContainer<ShoppingCartItem>[],
    abortSignal?: AbortSignal,
  ): Promise<void> {
    // Filter download items only
    const downloadItems = items.filter(
      (item) => item.data?.features?.['orderType'] === 'Download',
    );

    if (downloadItems.length === 0) {
      return;
    }

    // Transform to availability service format
    const availabilityItems = downloadItems
      .filter((item) => item.id && item.data?.product?.ean)
      .map((item) => ({
        itemId: item.id!,
        ean: item.data!.product!.ean!,
        price: item.data?.availability?.price,
      }));

    if (availabilityItems.length === 0) {
      return;
    }

    // Call new availability service
    const availabilitiesDict =
      await this.#availabilityService.getAvailabilities(
        {
          orderType: OrderTypeFeature.Download,
          items: availabilityItems,
        },
        abortSignal,
      );

    const unavailableItemIds: number[] = [];

    // Process availability results and update cart items
    for (const item of downloadItems) {
      if (!item.id) continue;

      const availability = availabilitiesDict[item.id.toString()];

      if (availability) {
        // Item is available (already validated by new service)
        // Convert availability-api format to checkout format using adapter
        const availabilityDTO =
          AvailabilityAdapter.fromAvailabilityApi(availability);

        let updateReq$ =
          this.#shoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability(
            {
              shoppingCartId,
              shoppingCartItemId: item.id,
              availability: availabilityDTO,
            },
          );

        if (abortSignal) {
          updateReq$ = updateReq$.pipe(takeUntilAborted(abortSignal));
        }

        const updateRes = await firstValueFrom(updateReq$);

        if (updateRes.error) {
          this.#logger.error('Failed to update item availability', {
            itemId: item.id,
          });
        }
      } else {
        // No availability data returned = item failed validation (unavailable)
        unavailableItemIds.push(item.id);
      }
    }

    // Throw error if any items are unavailable
    if (unavailableItemIds.length > 0) {
      throw CheckoutCompletionError.downloadItemsUnavailable(
        unavailableItemIds,
      );
    }
  }

  /**
   * Updates availability for shipping items (DIG-Versand, B2B-Versand).
   */
  private async updateShippingAvailabilities(
    shoppingCartId: number,
    items: EntityContainer<ShoppingCartItem>[],
    abortSignal?: AbortSignal,
  ): Promise<void> {
    // Filter shipping items
    const shippingItems = filterShippingItems(items);

    if (shippingItems.length === 0) {
      return;
    }

    // Update each shipping item
    await Promise.all(
      shippingItems.filter(hasValidItemId).map(async (item) => {
        const orderType = item.data.features?.['orderType'];
        const originalPrice = item.data.availability?.price?.value?.value;

        if (!item.data?.product?.ean || !item.id) {
          this.#logger.warn('Skipping item with missing EAN or ID');
          return;
        }

        try {
          // Transform item to availability service format
          const availabilityItem = {
            itemId: item.id,
            ean: item.data.product.ean,
            price: item.data.availability?.price,
            quantity: item.data.quantity ?? 1,
          };

          let availability: AvailabilityModel | undefined;

          // Call new availability service based on order type
          if (orderType === 'DIG-Versand') {
            availability = await this.#availabilityService.getAvailability(
              {
                orderType: OrderTypeFeature.DigitalShipping,
                item: availabilityItem,
              },
              abortSignal,
            );
          } else if (orderType === 'B2B-Versand') {
            availability = await this.#availabilityService.getAvailability(
              {
                orderType: OrderTypeFeature.B2BShipping,
                item: availabilityItem,
              },
              abortSignal,
            );
          } else {
            return;
          }

          if (!availability) {
            return;
          }

          // Convert availability-api format to checkout format, preserving original price
          const availabilityDTO =
            AvailabilityAdapter.fromAvailabilityApi(availability);

          // Preserve original price if it exists
          if (originalPrice !== undefined && availabilityDTO.price) {
            availabilityDTO.price.value = {
              value: originalPrice,
              currency: availabilityDTO.price.value?.currency ?? 'EUR',
              currencySymbol:
                availabilityDTO.price.value?.currencySymbol ?? '€',
            };
          }

          let req$ =
            this.#shoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItem(
              {
                shoppingCartId,
                shoppingCartItemId: item.id,
                values: { availability: availabilityDTO },
              },
            );

          if (abortSignal) {
            req$ = req$.pipe(takeUntilAborted(abortSignal));
          }

          const res = await firstValueFrom(req$);

          if (res.error) {
            this.#logger.error('Failed to update item availability', {
              itemId: item.id,
            });
          }
        } catch (error) {
          this.#logger.error('Error updating shipping availability', {
            itemId: item.id,
            error,
          });
        }
      }),
    );
  }

  /**
   * Sets buyer on checkout.
   */
  private async setBuyerOnCheckout(
    checkoutId: number,
    buyerDTO: Buyer,
    abortSignal?: AbortSignal,
  ): Promise<Checkout> {
    let req$ = this.#buyerService.StoreCheckoutBuyerSetBuyerPOST({
      checkoutId,
      buyerDTO,
    });

    if (abortSignal) {
      req$ = req$.pipe(takeUntilAborted(abortSignal));
    }

    const res = await firstValueFrom(req$);

    if (res.error) {
      const error = new ResponseArgsError(res);
      this.#logger.error('Failed to set buyer', error);
      throw error;
    }

    return res.result as Checkout;
  }

  /**
   * Sets payer on checkout.
   */
  private async setPayerOnCheckout(
    checkoutId: number,
    payer: Payer,
    abortSignal?: AbortSignal,
  ): Promise<Checkout> {
    let req$ = this.#payerService.StoreCheckoutPayerSetPayerPOST({
      checkoutId,
      payerDTO: payer,
    });

    if (abortSignal) {
      req$ = req$.pipe(takeUntilAborted(abortSignal));
    }

    const res = await firstValueFrom(req$);

    if (res.error) {
      const error = new ResponseArgsError(res);
      this.#logger.error('Failed to set payer', error);
      throw error;
    }

    return res.result as Checkout;
  }

  /**
   * Sets notification channels on checkout.
   */
  private async setNotificationChannelsOnCheckout(
    checkoutId: number,
    channels: NotificationChannel,
    abortSignal?: AbortSignal,
  ): Promise<Checkout> {
    let req$ = this.#storeCheckoutService.StoreCheckoutSetNotificationChannels({
      checkoutId,
      notificationChannel: channels,
    });

    if (abortSignal) {
      req$ = req$.pipe(takeUntilAborted(abortSignal));
    }

    const res = await firstValueFrom(req$);

    if (res.error) {
      const error = new ResponseArgsError(res);
      this.#logger.error('Failed to set notification channels', error);
      throw error;
    }

    return res.result as Checkout;
  }

  /**
   * Sets payment type on checkout.
   */
  private async setPaymentTypeOnCheckout(
    checkoutId: number,
    paymentType: PaymentType,
    abortSignal?: AbortSignal,
  ): Promise<Checkout> {
    let req$ = this.#paymentService.StoreCheckoutPaymentSetPaymentType({
      checkoutId,
      paymentType: paymentType,
    });

    if (abortSignal) {
      req$ = req$.pipe(takeUntilAborted(abortSignal));
    }

    const res = await firstValueFrom(req$);

    if (res.error) {
      const error = new ResponseArgsError(res);
      this.#logger.error('Failed to set payment type', error);
      throw error;
    }

    return res.result as Checkout;
  }

  /**
   * Updates shipping addresses on delivery destinations.
   */
  private async updateDestinationShippingAddresses(
    checkoutId: number,
    checkout: Checkout,
    shippingAddress: ShippingAddress,
    abortSignal?: AbortSignal,
  ): Promise<void> {
    // Get shipping destinations (target 2 or 16)
    const destinations = filterDeliveryDestinations(
      checkout.destinations || [],
    );

    if (destinations.length === 0) {
      this.#logger.debug('No shipping destinations to update');
      return;
    }

    // Update all destinations in parallel
    await Promise.all(
      destinations.filter(hasValidDestinationData).map(async (dest) => {
        const updatedDestination: DestinationDTO = {
          ...dest.data,
          shippingAddress: { ...shippingAddress },
        };

        let req$ = this.#storeCheckoutService.StoreCheckoutUpdateDestination({
          checkoutId,
          destinationId: dest.id,
          destinationDTO: updatedDestination,
        });

        if (abortSignal) {
          req$ = req$.pipe(takeUntilAborted(abortSignal));
        }

        const res = await firstValueFrom(req$);

        if (res.error) {
          const error = new ResponseArgsError(res);
          this.#logger.error('Failed to update destination');
          throw error;
        }
      }),
    );
  }
}
