<?php

namespace Drupal\commerce_license\EventSubscriber;

use Drupal\commerce_cart\Event\CartEntityAddEvent;
use Drupal\commerce_cart\Event\CartEvents;
use Drupal\commerce_cart\Event\CartOrderItemUpdateEvent;
use Drupal\commerce_order\Entity\OrderItemInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Ensures only 1 of each license may be added to the cart.
 *
 * We need to act on the cart event, as need to change the item quantity before
 * \Drupal\commerce_cart\EventSubscriber\CartEventSubscriber can show a message
 * to the user about adding the item to the cart.
 *
 * @see \Drupal\commerce_license\LicenseOrderProcessorMultiples
 */
class LicenseMultiplesCartEventSubscriber implements EventSubscriberInterface {

  use MessengerTrait;
  use StringTranslationTrait;

  /**
   * Constructs a new LicenseMultiplesCartEventSubscriber object.
   *
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger.
   */
  public function __construct(MessengerInterface $messenger) {
    $this->setMessenger($messenger);
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    return [
      // These need to run before
      // \Drupal\commerce_cart\EventSubscriber\CartEventSubscriber.
      // Also expects the patch at
      // https://www.drupal.org/project/commerce/issues/2930979
      // so that CartEventSubscriber doesn't show its message when we prevent
      // an item from being added.
      CartEvents::CART_ENTITY_ADD => ['onCartEntityAdd', 100],
      CartEvents::CART_ORDER_ITEM_UPDATE => ['onCartItemUpdate', 100],
    ];
  }

  /**
   * Enforces the maximum count when a product is added to the cart.
   *
   * Note that this event is triggered when adding a product to the cart from
   * the add to cart form when an item for this product is already in the cart.
   * This is treated as an entity add, even though technically it's an order
   * item that is being updated.
   *
   * @param \Drupal\commerce_cart\Event\CartEntityAddEvent $event
   *   The cart event.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function onCartEntityAdd(CartEntityAddEvent $event): void {
    $order_item = $event->getOrderItem();

    // Only act if the order item has a license reference field.
    if (!$order_item->hasField('license')) {
      return;
    }

    // @todo Allow license type plugins to respond here, as for types that
    // collect user data in the checkout form, the same product variation can
    // result in different licenses.
    // Only act if the order item quantity is greater than 1.
    if ($order_item->getQuantity() == 1) {
      return;
    }

    // Force the quantity back to 1.
    $this->forceOrderItemQuantity($order_item);

    $this->messenger()->addError($this->t('You may only have one of @product-label in <a href="@cart-url">your cart</a>.', [
      '@product-label' => $order_item->getPurchasedEntity()->label(),
      '@cart-url' => Url::fromRoute('commerce_cart.page')->toString(),
    ]));
    // Since we have reverted the quantity, we will stop propagation, so that
    // add to cart messages are not logged nor displayed.
    $event->stopPropagation();
  }

  /**
   * Enforces the maximum count when an item is updated.
   *
   * @param \Drupal\commerce_cart\Event\CartOrderItemUpdateEvent $event
   *   The cart event.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function onCartItemUpdate(CartOrderItemUpdateEvent $event): void {
    $order_item = $event->getOrderItem();

    // Only act if the order item has a license reference field.
    if (!$order_item->hasField('license')) {
      return;
    }

    // @todo Allow license type plugins to respond here, as for types that
    // collect user data in the checkout form, the same product variation can
    // result in different licenses.
    // Only act if the order item quantity is greater than 1.
    if ($order_item->getQuantity() == 1) {
      return;
    }

    // Force the quantity back to 1.
    $this->forceOrderItemQuantity($order_item);

    // Don't show a link to the cart as the user will typically be on the cart
    // page.
    $this->messenger()->addError($this->t('You may only have one of @product-label in your cart.', [
      '@product-label' => $order_item->getPurchasedEntity()->label(),
    ]));
  }

  /**
   * Forces the quantity of an order item to 1, and saves it.
   *
   * @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
   *   The order item.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  protected function forceOrderItemQuantity(OrderItemInterface $order_item): void {
    // Force the quantity back to 1.
    $order_item->setQuantity(1);

    // Save the order item again.
    $order_item->save();
  }

}
