<?php

namespace Drupal\eca\Entity;

use Drupal\Core\Config\Entity\ConfigEntityStorage;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\State\StateInterface;
use Random\RandomException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Storage handler for ECA configurations.
 */
class EcaStorage extends ConfigEntityStorage {

  /**
   * The logger.
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected LoggerChannelInterface $logger;

  /**
   * The event dispatcher.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected EventDispatcherInterface $eventDispatcher;

  /**
   * The dynamic event subscriber.
   *
   * @var \Symfony\Component\EventDispatcher\EventSubscriberInterface
   */
  protected EventSubscriberInterface $eventSubscriber;

  /**
   * The Drupal state.
   *
   * @var \Drupal\Core\State\StateInterface
   */
  protected StateInterface $state;

  /**
   * The lock backend.
   *
   * @var \Drupal\Core\Lock\LockBackendInterface
   */
  protected LockBackendInterface $lock;

  /**
   * {@inheritdoc}
   */
  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type): EcaStorage {
    /** @var static $instance */
    $instance = parent::createInstance($container, $entity_type);
    /** @var \Drupal\Core\Logger\LoggerChannelInterface $loggerChannel */
    $loggerChannel = $container->get('logger.channel.eca');
    $instance->setLogger($loggerChannel);
    $instance->eventDispatcher = $container->get('event_dispatcher');
    $instance->eventSubscriber = $container->get('eca.dynamic_subscriber');
    $instance->lock = $container->get('lock');
    $instance->state = $container->get('state');
    return $instance;
  }

  /**
   * The maximum number of lock acquisition retries.
   */
  protected const MAX_LOCK_RETRIES = 10;

  /**
   * Rebuilds the state of subscribed events.
   */
  public function rebuildSubscribedEvents(): void {
    $lock_name = 'eca_rebuild_subscribed_events';
    $retries = 0;
    while (!$this->lock->acquire($lock_name)) {
      if (++$retries > static::MAX_LOCK_RETRIES) {
        $this->logger->warning('Could not acquire lock for rebuilding ECA subscribed events after @retries attempts.', ['@retries' => static::MAX_LOCK_RETRIES]);
        return;
      }
      try {
        $sleep = random_int(1000, 50000);
      }
      catch (\Exception | RandomException) {
        $sleep = 2500;
      }
      usleep($sleep);
    }

    $subscribedEvents = $this->doRebuildSubscribedEvents();

    if ($this->state->get('eca.subscribed') !== $subscribedEvents) {
      $this->state->set('eca.subscribed', $subscribedEvents);
      $this->eventDispatcher->removeSubscriber($this->eventSubscriber);
      $this->eventDispatcher->addSubscriber($this->eventSubscriber);
    }

    $this->doRebuildTemplates();

    $this->lock->release($lock_name);
  }

  /**
   * {@inheritdoc}
   */
  protected function doPostSave(EntityInterface $entity, $update): void {
    parent::doPostSave($entity, $update);
    $this->rebuildSubscribedEvents();
  }

  /**
   * {@inheritdoc}
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function delete(array $entities): void {
    parent::delete($entities);
    $this->rebuildSubscribedEvents();
  }

  /**
   * {@inheritdoc}
   */
  public function loadMultiple(?array $ids = NULL): array {
    if ($ids) {
      // If ids are provided, load them all.
      return parent::loadMultiple($ids);
    }

    // If no ids are provided, only return the ECA models that are no
    // templates.
    $entities = [];
    /** @var \Drupal\eca\Entity\Eca $entity */
    foreach (parent::loadMultiple() as $entity) {
      if (!$entity->isTemplate()) {
        $entities[$entity->id()] = $entity;
      }
    }
    return $entities;
  }

  /**
   * Rebuilds the list of subscribed events based on the current configuration.
   *
   * @return array
   *   The list of ECA configuration IDs, grouped by their subscribed events.
   */
  protected function doRebuildSubscribedEvents(): array {
    $subscribed = [];
    $entities = $this->loadMultiple();
    // Sort the configurations by weight and label.
    uasort($entities, [$this->entityType->getClass(), 'sort']);
    /**
     * @var \Drupal\eca\Entity\Eca $eca
     */
    foreach ($entities as $eca) {
      if (!$eca->status()) {
        continue;
      }
      foreach ($eca->getUsedEvents() as $eca_event_id => $ecaEvent) {
        $eca_id = $eca->id();
        $plugin = $ecaEvent->getPlugin();
        $name = $plugin->eventName();
        $priority = $plugin->subscriberPriority();
        $wildcard = $plugin->generateWildcard($eca_id, $ecaEvent);
        $subscribed[$name][$priority][$eca_id][$eca_event_id] = $wildcard;
      }
    }

    // Make sure that priorities are always distinct.
    foreach ($subscribed as $prioritized) {
      if (count($prioritized) > 1) {
        throw new \LogicException("Priority for event subscription must be distinct.");
      }
    }

    $this->logger->debug('Rebuilt subscribed events of ECA configuration.');
    return $subscribed;
  }

  /**
   * Rebuilds the list of templates.
   */
  protected function doRebuildTemplates(): void {
    $templates = [];
    /**
     * @var \Drupal\eca\Entity\Eca $eca
     */
    foreach ($this->loadByProperties(['status' => TRUE, 'template' => TRUE]) as $eca) {
      foreach ($eca->getUsedEvents() as $eca_event_id => $ecaEvent) {
        $eca_id = $eca->id();
        $plugin = $ecaEvent->getPlugin();
        $name = $plugin->eventName();
        $wildcard = $plugin->generateWildcard($eca_id, $ecaEvent);
        $templates[$name][$eca_id][$eca_event_id] = $wildcard;
      }
    }
    $this->state->set('eca.templates', $templates);
  }

  /**
   * Set the logger.
   *
   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
   *   The logger.
   */
  public function setLogger(LoggerChannelInterface $logger): void {
    $this->logger = $logger;
  }

}
