<?php
namespace front\Controllers;

class ShopBasketController
{
  private const ORDER_SUBMIT_TOKEN_SESSION_KEY = 'order-submit-token';
  private const ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY = 'order-submit-last-order-id';
  private const ORDER_SUBMIT_TOKEN_TTL = 1800;

  public static $title = [
      'mainView' => 'Koszyk'
  ];

  private $orderRepository;
  private $paymentMethodRepository;

  public function __construct( \Domain\Order\OrderRepository $orderRepository, \Domain\PaymentMethod\PaymentMethodRepository $paymentMethodRepository )
  {
    $this->orderRepository = $orderRepository;
    $this->paymentMethodRepository = $paymentMethodRepository;
  }

  public function basketMessageSave()
  {
    \Shared\Helpers\Helpers::set_session( 'basket_message', \Shared\Helpers\Helpers::get( 'basket_message' ) );
    echo json_encode( [ 'result' => 'ok' ] );
    exit;
  }

  public function basketRemoveProduct()
  {
    global $lang_id;

    $basket = \Shared\Helpers\Helpers::get_session( 'basket' );
    $coupon = \Shared\Helpers\Helpers::get_session( 'coupon' );
    $product_hash = \Shared\Helpers\Helpers::get( 'product_hash' );
    $basket_transport_method_id = \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' );

    unset( $basket[ $product_hash ] );

    $basket = (new \Domain\Promotion\PromotionRepository($GLOBALS['mdb']))->findPromotion( $basket );

    \Shared\Helpers\Helpers::set_session( 'basket', $basket );

    $this->jsonBasketResponse( $basket, $coupon, $lang_id, $basket_transport_method_id );
  }

  public function basketIncreaseQuantityProduct()
  {
    global $lang_id;

    $basket = \Shared\Helpers\Helpers::get_session( 'basket' );
    $coupon = \Shared\Helpers\Helpers::get_session( 'coupon' );
    $product_hash = \Shared\Helpers\Helpers::get( 'product_hash' );
    $basket_transport_method_id = \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' );
    $basket[ $product_hash ][ 'quantity' ]++;

    \Domain\Basket\BasketCalculator::checkProductQuantityInStock( $basket, false );

    $basket = \Shared\Helpers\Helpers::get_session( 'basket' );

    $basket = (new \Domain\Promotion\PromotionRepository($GLOBALS['mdb']))->findPromotion( $basket );

    \Shared\Helpers\Helpers::set_session( 'basket', $basket );

    $this->jsonBasketResponse( $basket, $coupon, $lang_id, $basket_transport_method_id );
  }

  public function basketDecreaseQuantityProduct()
  {
    global $lang_id;

    $basket = \Shared\Helpers\Helpers::get_session( 'basket' );
    $coupon = \Shared\Helpers\Helpers::get_session( 'coupon' );
    $product_hash = \Shared\Helpers\Helpers::get( 'product_hash' );
    $basket_transport_method_id = \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' );

    $basket[ $product_hash ][ 'quantity' ]--;

    if ( $basket[ $product_hash ][ 'quantity' ] < 1 )
      unset( $basket[ $product_hash ] );

    $basket = (new \Domain\Promotion\PromotionRepository($GLOBALS['mdb']))->findPromotion( $basket );

    \Shared\Helpers\Helpers::set_session( 'basket', $basket );

    $this->jsonBasketResponse( $basket, $coupon, $lang_id, $basket_transport_method_id );
  }

  public function basketChangeQuantityProduct()
  {
    global $lang_id;

    $basket = \Shared\Helpers\Helpers::get_session( 'basket' );
    $coupon = \Shared\Helpers\Helpers::get_session( 'coupon' );
    $product_hash = \Shared\Helpers\Helpers::get( 'product_hash' );
    $basket_transport_method_id = \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' );

    $basket[ $product_hash ][ 'quantity' ] = (int)\Shared\Helpers\Helpers::get( 'quantity' );

    if ( $basket[ $product_hash ][ 'quantity' ] < 1 )
      unset( $basket[ $product_hash ] );

    $basket = (new \Domain\Promotion\PromotionRepository($GLOBALS['mdb']))->findPromotion( $basket );

    \Domain\Basket\BasketCalculator::checkProductQuantityInStock( $basket, false );

    $basket = \Shared\Helpers\Helpers::get_session( 'basket' );

    $this->jsonBasketResponse( $basket, $coupon, $lang_id, $basket_transport_method_id );
  }

  public function productMessageChange()
  {
    $basket = \Shared\Helpers\Helpers::get_session( 'basket' );
    $basket[ \Shared\Helpers\Helpers::get( 'position_code' ) ]['message'] = \Shared\Helpers\Helpers::get( 'product_message' );
    \Shared\Helpers\Helpers::set_session( 'basket', $basket );
    exit;
  }

  public function basketAddProduct()
  {
    global $lang_id;
    $basket = \Domain\Basket\BasketCalculator::validateBasket( \Shared\Helpers\Helpers::get_session( 'basket' ) );
    $values_tmp = json_decode( \Shared\Helpers\Helpers::get( 'values' ), true );
    $values = [];
    $attributes = [];
    $custom_fields = [];

    foreach( $values_tmp as $key => $val )
      $values[ $val['name'] ] = $val['value'];

    foreach( $values as $key => $val )
    {
      if ( $key != 'product-id' and $key != 'quantity' and $key != 'product-message' and strpos( $key, 'custom_field' ) === false )
        $attributes[] = $val;
    }

    // Sort by attribute ID to match permutation_hash order (generated with ksort)
    usort( $attributes, function ( $a, $b ) {
      return (int) explode( '-', $a )[0] - (int) explode( '-', $b )[0];
    } );

    foreach( $values as $key => $val )
    {
      if ( strpos( $key, 'custom_field' ) !== false )
      {
        preg_match( '/\d+/', $key, $matches );
        $custom_field_id = $matches[0];
        $custom_fields[ $custom_field_id ] = $val;
      }
    }

    if ( \Shared\Helpers\Helpers::is_array_fix( $attributes ) )
    {
      $values['parent_id'] = $values[ 'product-id' ];
      $values['product-id'] = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->getProductIdByAttributes( $values[ 'product-id' ], $attributes );
      $values['attributes'] = $attributes;
    }

    $values['wp'] = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->getWeightCached( (int)$values[ 'product-id' ] );

    $attributes_implode = '';
    if ( is_array( $attributes ) and count( $attributes ) > 0 )
      $attributes_implode = implode( '|', $attributes );

    $product_code = md5( $values['product-id'] . $attributes_implode . $values['product-message'] . json_encode( $custom_fields ) );

    if ( isset( $basket[ $product_code ] ) )
      $basket[ $product_code ][ 'quantity' ] += $values[ 'quantity' ];
    else
      $basket[ $product_code ] = $values;

    $basket[ $product_code ]['message'] = $values['product-message'];
    $basket[ $product_code ]['custom_fields'] = $custom_fields;

    $basket = (new \Domain\Promotion\PromotionRepository($GLOBALS['mdb']))->findPromotion( $basket );

    \Shared\Helpers\Helpers::set_session( 'basket', $basket );

    $coupon = \Shared\Helpers\Helpers::get_session( 'coupon' );

    echo json_encode( [
        'result' => 'ok',
        'basket_mini_count' => \Domain\Basket\BasketCalculator::countProductsText( \Domain\Basket\BasketCalculator::countProducts( $basket ) ),
        'basket_mini_value' => \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon, $lang_id ),
        'product_sets' => ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->productSetsWhenAddToBasket( (int)$values['product-id'] )
    ] );
    exit;
  }

  public function transportMethodInpostCheck()
  {
    $transport_id = \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' );

    if ( $transport_id === '2' or $transport_id === '1' )
    {
      if ( !\Shared\Helpers\Helpers::get_session( 'basket-inpost-info' ) )
      {
        echo json_encode( [ 'result' => 'bad' ] );
        exit;
      }
    }

    if ( $transport_id === '9' )
    {
      if ( !\Shared\Helpers\Helpers::get_session( 'basket_orlen_point_id' ) )
      {
        echo json_encode( [ 'result' => 'bad' ] );
        exit;
      }
    }

    echo json_encode( [ 'result' => 'ok' ] );
    exit;
  }

  public function inpostCheck()
  {
    if ( !\Shared\Helpers\Helpers::get_session( 'basket-inpost-info' ) )
      echo json_encode( [ 'result' => 'bad' ] );
    else
      echo json_encode( [ 'result' => 'ok' ] );
    exit;
  }

  public function orlenSave()
  {
    \Shared\Helpers\Helpers::set_session( 'basket_orlen_point_id', \Shared\Helpers\Helpers::get( 'orlen_point_id' ) );
    \Shared\Helpers\Helpers::set_session( 'basket_orlen_point_info', \Shared\Helpers\Helpers::get( 'orlen_point_name' ) );
    echo json_encode( [ 'result' => 'ok' ] );
    exit;
  }

  public function inpostSave()
  {
    \Shared\Helpers\Helpers::set_session( 'basket-inpost-info', \Shared\Helpers\Helpers::get( 'paczkomat' ) );
    echo json_encode( [ 'result' => 'ok' ] );
    exit;
  }

  public function basketPaymentMethodSet()
  {
    \Shared\Helpers\Helpers::set_session( 'basket-payment-method-id', \Shared\Helpers\Helpers::get( 'payment_method_id' ) );
    echo json_encode( [ 'result' => 'ok' ] );
    exit;
  }

  public function basketTransportMethodSet()
  {
    \Shared\Helpers\Helpers::set_session( 'basket-transport-method-id', \Shared\Helpers\Helpers::get( 'transport_method_id' ) );
    echo json_encode( [ 'result' => 'ok' ] );
    exit;
  }

  public function basketPaymentsMethods()
  {
    \Shared\Helpers\Helpers::set_session( 'basket-transport-method-id', \Shared\Helpers\Helpers::get( 'transport_method_id' ) );

    echo json_encode( [
        'result'          => 'ok',
        'payment_methods' => \front\Views\ShopPaymentMethod::basketPaymentMethods(
          $this->paymentMethodRepository->paymentMethodsByTransport( (int)\Shared\Helpers\Helpers::get( 'transport_method_id' ) ),
          \Shared\Helpers\Helpers::get( 'payment_method_id' )
        )
    ] );
    exit;
  }

  public function summaryView()
  {
    global $lang_id, $settings;

    if ( \Domain\Basket\BasketCalculator::checkProductQuantityInStock( \Shared\Helpers\Helpers::get_session( 'basket' ) ) )
    {
      header( 'Location: /koszyk' );
      exit;
    }

    $client = \Shared\Helpers\Helpers::get_session( 'client' );
    $orderSubmitToken = $this->createOrderSubmitToken();

    $basket = \Shared\Helpers\Helpers::get_session( 'basket' );
    $coupon = \Shared\Helpers\Helpers::get_session( 'coupon' );
    $transport = ( new \Domain\Transport\TransportRepository( $GLOBALS['mdb'] ) )->findActiveByIdCached( \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ) );

    $productsSummary = (float)\Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon );
    $freeDeliveryThreshold = isset( $settings['free_delivery'] ) ? (float)$settings['free_delivery'] : 0.0;
    $transportCalc = $this->calculateTransportCostForSummary( $transport, $productsSummary, $freeDeliveryThreshold );

    return \Shared\Tpl\Tpl::view( 'shop-basket/summary-view', [
          'lang_id'        => $lang_id,
          'client'         => \Shared\Helpers\Helpers::get_session( 'client' ),
          'basket'         => $basket,
          'transport'      => $transport,
          'transport_cost_effective' => $transportCalc['transport_cost_effective'],
          'free_delivery_applies'    => $transportCalc['free_delivery_applies'],
          'payment_method' => $this->paymentMethodRepository->paymentMethodCached( (int)\Shared\Helpers\Helpers::get_session( 'basket-payment-method-id' ) ),
          'addresses'      => ( new \Domain\Client\ClientRepository( $GLOBALS['mdb'] ) )->clientAddresses( (int)$client['id'] ),
          'settings'       => $settings,
          'coupon'         => $coupon,
          'basket_message' => \Shared\Helpers\Helpers::get_session( 'basket_message' ),
          'order_submit_token' => $orderSubmitToken
      ] );
  }

  /**
   * Wylicza efektywny koszt transportu dla widoku /koszyk-podsumowanie.
   * Koszt spada do 0, gdy transport ma flage delivery_free=1 ORAZ wartosc koszyka
   * (po kuponie) osiaga prog darmowej dostawy $freeDeliveryThreshold.
   *
   * @param array|null $transport Aktywny transport (lub null gdy nie wybrany)
   * @param float $productsSummary Wartosc koszyka po kuponie
   * @param float $freeDeliveryThreshold Prog darmowej dostawy z settings.free_delivery
   * @return array{transport_cost_effective: float, free_delivery_applies: bool}
   */
  protected function calculateTransportCostForSummary( $transport, $productsSummary, $freeDeliveryThreshold )
  {
    if ( !is_array( $transport ) )
    {
      return [
        'transport_cost_effective' => 0.0,
        'free_delivery_applies'    => false,
      ];
    }

    $deliveryFree = isset( $transport['delivery_free'] ) ? (int)$transport['delivery_free'] : 0;
    $cost = isset( $transport['cost'] ) ? (float)$transport['cost'] : 0.0;

    $applies = false;
    if ( $deliveryFree === 1 && $freeDeliveryThreshold > 0 )
    {
      $summaryNormalized = \Shared\Helpers\Helpers::normalize_decimal( $productsSummary );
      $thresholdNormalized = \Shared\Helpers\Helpers::normalize_decimal( $freeDeliveryThreshold );

      if ( $summaryNormalized >= $thresholdNormalized )
      {
        $applies = true;
      }
    }

    return [
      'transport_cost_effective' => $applies ? 0.0 : $cost,
      'free_delivery_applies'    => $applies,
    ];
  }

  public function basketSave()
  {
    $orderSubmitToken = (string)\Shared\Helpers\Helpers::get( 'order_submit_token', true );
    $existingOrderId = isset( $_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] ) ? (int)$_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] : 0;

    $basket = \Shared\Helpers\Helpers::get_session( 'basket' );
    if ( empty( $basket ) && $existingOrderId > 0 )
    {
      $existingOrderHash = $this->orderRepository->findHashById( $existingOrderId );
      if ( $existingOrderHash )
      {
        $this->logOrder( 'Double-submit detected, redirecting to existing order id=' . $existingOrderId );
        header( 'Location: /zamowienie/' . $existingOrderHash );
        exit;
      }
    }

    if ( !$this->isValidOrderSubmitToken( $orderSubmitToken ) )
    {
      $this->logOrder( 'Token validation failed. formToken=' . $orderSubmitToken . ' existingOrderId=' . $existingOrderId );
      \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) );
      header( 'Location: /koszyk-podsumowanie' );
      exit;
    }

    $this->consumeOrderSubmitToken();

    $client = \Shared\Helpers\Helpers::get_session( 'client' );

    if ( \Domain\Basket\BasketCalculator::checkProductQuantityInStock( \Shared\Helpers\Helpers::get_session( 'basket' ) ) )
    {
      header( 'Location: /koszyk' );
      exit;
    }

    $order_id = null;
    try
    {
      $order_id = $this->orderRepository->createFromBasket(
        $client[ 'id' ],
        \Shared\Helpers\Helpers::get_session( 'basket' ),
        \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ),
        \Shared\Helpers\Helpers::get_session( 'basket-payment-method-id' ),
        \Shared\Helpers\Helpers::get( 'email', true ),
        \Shared\Helpers\Helpers::get( 'phone', true ),
        \Shared\Helpers\Helpers::get( 'name', true ),
        \Shared\Helpers\Helpers::get( 'surname', true ),
        \Shared\Helpers\Helpers::get( 'street' ),
        \Shared\Helpers\Helpers::get( 'postal_code', true ),
        \Shared\Helpers\Helpers::get( 'city', true ),
        \Shared\Helpers\Helpers::get( 'firm_name', true ),
        \Shared\Helpers\Helpers::get( 'firm_street', true ),
        \Shared\Helpers\Helpers::get( 'firm_postal_code', true ),
        \Shared\Helpers\Helpers::get( 'firm_city', true ),
        \Shared\Helpers\Helpers::get( 'firm_nip', true ),
        \Shared\Helpers\Helpers::get_session( 'basket-inpost-info' ),
        \Shared\Helpers\Helpers::get_session( 'basket_orlen_point_id' ),
        \Shared\Helpers\Helpers::get_session( 'basket_orlen_point_info' ),
        \Shared\Helpers\Helpers::get_session( 'coupon' ),
        \Shared\Helpers\Helpers::get_session( 'basket_message' )
      );
    }
    catch ( \Exception $e )
    {
      $this->logOrder( 'createFromBasket exception: ' . $e->getMessage() );
      error_log( '[basketSave] createFromBasket exception: ' . $e->getMessage() );
      \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) );
      header( 'Location: /koszyk' );
      exit;
    }

    if ( $order_id )
    {
      \Shared\Helpers\Helpers::set_session( self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY, (int)$order_id );
      \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat' ) );
      \Shared\Helpers\Helpers::delete_session( 'basket' );
      \Shared\Helpers\Helpers::delete_session( 'basket-transport-method-id' );
      \Shared\Helpers\Helpers::delete_session( 'basket-payment-method-id' );
      \Shared\Helpers\Helpers::delete_session( 'basket-inpost-info' );
      \Shared\Helpers\Helpers::delete_session( 'basket_orlen_point_id' );
      \Shared\Helpers\Helpers::delete_session( 'basket_orlen_point_info' );
      \Shared\Helpers\Helpers::delete_session( 'coupon' );
      \Shared\Helpers\Helpers::delete_session( 'basket_message' );

      \Shared\Helpers\Helpers::set_session( 'piksel_purchase', true );
      \Shared\Helpers\Helpers::set_session( 'google-adwords-purchase', true );
      \Shared\Helpers\Helpers::set_session( 'google-analytics-purchase', true );
      \Shared\Helpers\Helpers::set_session( 'ekomi-purchase', true );

      $redis = \Shared\Cache\RedisConnection::getInstance() -> getConnection();
      if ( $redis )
        $redis -> flushAll();

      header( 'Location: /zamowienie/' . $this->orderRepository->findHashById( $order_id ) );
      exit;
    }
    else
    {
      $this->logOrder( 'createFromBasket returned falsy order_id. client_id=' . ( $client['id'] ?? '?' ) . ' email=' . ( \Shared\Helpers\Helpers::get( 'email', true ) ?: '?' ) );
      \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) );
      header( 'Location: /koszyk' );
      exit;
    }
  }

  public function mainView()
  {
    global $lang_id, $page, $settings;

    $page[ 'language' ][ 'meta_title' ] = 'Koszyk';

    $basket = \Shared\Helpers\Helpers::get_session( 'basket' );
    $coupon = \Shared\Helpers\Helpers::get_session( 'coupon' );
    $payment_method_id = \Shared\Helpers\Helpers::get_session( 'payment_method_id' );
    $basket_transport_method_id = \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' );

    if ( \Domain\Basket\BasketCalculator::checkProductQuantityInStock( $basket ) )
    {
      header( 'Location: /koszyk' );
      exit;
    }

    $basket = (new \Domain\Promotion\PromotionRepository($GLOBALS['mdb']))->findPromotion( $basket );

    return \Shared\Tpl\Tpl::view( 'shop-basket/basket', [
          'basket'            => $basket,
          'coupon'            => $coupon,
          'transport_id'      => \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ),
          'transport_methods' => \Shared\Tpl\Tpl::view( 'shop-basket/basket-transport-methods', [
              'transports_methods' => ( new \Domain\Transport\TransportRepository( $GLOBALS['mdb'] ) )->transportMethodsFront( $basket, $coupon ),
              'transport_id'       => $basket_transport_method_id,
              'free_delivery'      => (float)($settings['free_delivery'] ?? 0),
              'basket_summary'     => (float)\Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon )
          ] ),
          'payment_method_id' => $payment_method_id,
          'basket_details'    => \Shared\Tpl\Tpl::view( 'shop-basket/basket-details', [
              'basket'         => $basket,
              'lang_id'        => $lang_id,
              'coupon'         => $coupon,
              'basket_message' => \Shared\Helpers\Helpers::get_session( 'basket_message' ),
              'settings' => $settings
          ] )
      ] );
  }

  public function basketUpdateCustomFields()
  {
    $basket = \Shared\Helpers\Helpers::get_session( 'basket' );
    $product_code = \Shared\Helpers\Helpers::get( 'product_code' );

    if ( !isset( $basket[ $product_code ] ) )
    {
      echo json_encode( [ 'result' => 'error', 'message' => 'Pozycja nie istnieje w koszyku' ] );
      exit;
    }

    $position = $basket[ $product_code ];
    $new_custom_fields = [];
    $custom_fields_raw = \Shared\Helpers\Helpers::get( 'custom_field' );

    if ( is_array( $custom_fields_raw ) )
    {
      foreach ( $custom_fields_raw as $field_id => $value )
      {
        $new_custom_fields[ (int)$field_id ] = $value;
      }
    }

    $productRepo = new \Domain\Product\ProductRepository( $GLOBALS['mdb'] );
    $missing_fields = [];

    foreach ( $new_custom_fields as $field_id => $value )
    {
      $field_meta = $productRepo->findCustomFieldCached( $field_id );
      if ( $field_meta && (int)$field_meta['is_required'] === 1 && trim( $value ) === '' )
      {
        $missing_fields[] = $field_meta['name'];
      }
    }

    if ( count( $missing_fields ) > 0 )
    {
      echo json_encode( [ 'result' => 'error', 'message' => 'Wypełnij wymagane pola: ' . implode( ', ', $missing_fields ) ] );
      exit;
    }

    $attributes_implode = '';
    if ( isset( $position['attributes'] ) && is_array( $position['attributes'] ) && count( $position['attributes'] ) > 0 )
    {
      $attributes_implode = implode( '|', $position['attributes'] );
    }

    $message = isset( $position['message'] ) ? $position['message'] : '';
    $new_product_code = md5( $position['product-id'] . $attributes_implode . $message . json_encode( $new_custom_fields ) );

    if ( $new_product_code === $product_code )
    {
      $basket[ $product_code ]['custom_fields'] = $new_custom_fields;
    }
    elseif ( isset( $basket[ $new_product_code ] ) )
    {
      $basket[ $new_product_code ]['quantity'] += $position['quantity'];
      unset( $basket[ $product_code ] );
    }
    else
    {
      $position['custom_fields'] = $new_custom_fields;
      $basket[ $new_product_code ] = $position;
      unset( $basket[ $product_code ] );
    }

    $basket = ( new \Domain\Promotion\PromotionRepository( $GLOBALS['mdb'] ) )->findPromotion( $basket );
    \Shared\Helpers\Helpers::set_session( 'basket', $basket );

    echo json_encode( [ 'result' => 'ok' ] );
    exit;
  }

  private function jsonBasketResponse( $basket, $coupon, $lang_id, $basket_transport_method_id )
  {
    global $settings;

    echo json_encode( [
        'basket'            => \Shared\Tpl\Tpl::view( 'shop-basket/basket-details', [
            'basket'  => $basket,
            'lang_id' => $lang_id,
            'coupon'  => $coupon
        ] ),
        'basket_mini_count' => \Domain\Basket\BasketCalculator::countProductsText( \Domain\Basket\BasketCalculator::countProducts( $basket ) ),
        'basket_mini_value' => \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon, $lang_id ),
        'products_count'    => count( $basket ),
        'transport_methods' => \Shared\Tpl\Tpl::view( 'shop-basket/basket-transport-methods', [
            'transports_methods' => ( new \Domain\Transport\TransportRepository( $GLOBALS['mdb'] ) )->transportMethodsFront( $basket, $coupon ),
            'transport_id'       => $basket_transport_method_id,
            'free_delivery'      => (float)($settings['free_delivery'] ?? 0),
            'basket_summary'     => (float)\Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon )
        ] )
    ] );
    exit;
  }

  private function createOrderSubmitToken()
  {
    $sessionData = isset( $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] )
      ? $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ]
      : null;

    if ( is_array( $sessionData ) && isset( $sessionData['token'], $sessionData['created_at'] ) )
    {
      if ( ( time() - $sessionData['created_at'] ) < self::ORDER_SUBMIT_TOKEN_TTL )
      {
        return $sessionData['token'];
      }
    }

    $token = $this->generateOrderSubmitToken();
    \Shared\Helpers\Helpers::set_session( self::ORDER_SUBMIT_TOKEN_SESSION_KEY, [
      'token' => $token,
      'created_at' => time()
    ] );
    \Shared\Helpers\Helpers::delete_session( self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY );

    return $token;
  }

  private function generateOrderSubmitToken()
  {
    try
    {
      return bin2hex( random_bytes( 16 ) );
    }
    catch ( \Exception $exception )
    {
      return md5( uniqid( (string)mt_rand(), true ) );
    }
  }

  private function isValidOrderSubmitToken( $token )
  {
    if ( !$token )
      return false;

    $sessionData = isset( $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] )
      ? $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ]
      : null;

    if ( !$sessionData )
      return false;

    // Backward compatibility: stary format (plain string)
    if ( is_string( $sessionData ) )
    {
      $sessionToken = $sessionData;
    }
    elseif ( is_array( $sessionData ) && isset( $sessionData['token'], $sessionData['created_at'] ) )
    {
      if ( ( time() - $sessionData['created_at'] ) >= self::ORDER_SUBMIT_TOKEN_TTL )
        return false;
      $sessionToken = $sessionData['token'];
    }
    else
    {
      return false;
    }

    if ( function_exists( 'hash_equals' ) )
      return hash_equals( $sessionToken, $token );

    return $sessionToken === $token;
  }

  private function consumeOrderSubmitToken()
  {
    \Shared\Helpers\Helpers::delete_session( self::ORDER_SUBMIT_TOKEN_SESSION_KEY );
  }

  private function logOrder( $message )
  {
    $logFile = __DIR__ . '/../../../logs/logs-order-' . date( 'Y-m-d' ) . '.log';
    $line = '[' . date( 'Y-m-d H:i:s' ) . '] ' . $message . "\n";
    @file_put_contents( $logFile, $line, FILE_APPEND );
  }
}
