<?php

namespace Drupal\acquia_contenthub\EventSubscriber\UnserializeContentField;

use Drupal\acquia_contenthub\Event\UnserializeCdfEntityFieldEvent;
use Drupal\acquia_contenthub\PrunedEntitiesTracker;
use Drupal\acquia_contenthub_subscriber\Exception\ContentHubImportException;
use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\depcalc\DependencyStack;
use Psr\Log\LoggerInterface;

/**
 * Base class for unserializers that handle referenced entities.
 */
class FieldEntityReferenceBase {

  /**
   * Pruned entities tracker.
   *
   * @var \Drupal\acquia_contenthub\PrunedEntitiesTracker
   */
  protected PrunedEntitiesTracker $prunedEntitiesTracker;

  /**
   * Content hub logger.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected LoggerInterface $logger;

  /**
   * FieldEntityReferenceBase constructor.
   *
   * @param \Psr\Log\LoggerInterface $logger
   *   Content hub logger.
   * @param \Drupal\acquia_contenthub\PrunedEntitiesTracker $tracker
   *   Pruned entities tracker.
   */
  public function __construct(LoggerInterface $logger, PrunedEntitiesTracker $tracker) {
    $this->logger = $logger;
    $this->prunedEntitiesTracker = $tracker;
  }

  /**
   * Get an entity from the dependency stack or from local.
   *
   * The function allows more flexibility. In case of config entities, we allow
   * them to be loaded by id from local as a very last resort if:
   *  * it cannot be loaded from the dependency stack
   *  * it cannot be loaded using its uuid.
   *
   * @param string $id
   *   The identifier of the entity to retrieve.
   * @param \Drupal\acquia_contenthub\Event\UnserializeCdfEntityFieldEvent $event
   *   The subscribed event.
   * @param string|null $entity_type
   *   Specify the entity type directly. Overrides field metadata, target
   *   attribute.
   *
   * @return \Drupal\Core\Entity\EntityInterface|null
   *   The retrieved entity.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Exception
   */
  protected function getEntity(string $id, UnserializeCdfEntityFieldEvent $event, ?string $entity_type = NULL): ?EntityInterface {
    if (is_null($entity_type)) {
      $entity_type = $event->getFieldMetadata()['target'] ?? '';
    }
    $stack = $event->getStack();
    if (Uuid::isValid($id)) {
      return $this->getEntityByUuid($entity_type, $id, $stack, $event->getFieldName());
    }

    return $this->getConfigEntityById($entity_type, $id);
  }

  /**
   * Returns an entity by its uuid from the stack or loads by uuid.
   *
   * @param string $entity_type
   *   The entity's type.
   * @param string $uuid
   *   The uuid of the entity to retrieve.
   * @param \Drupal\depcalc\DependencyStack $stack
   *   The dependency stack used for lookup.
   * @param string $field_name
   *   The field name.
   *
   * @return \Drupal\Core\Entity\EntityInterface|null
   *   The entity or null.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  protected function getEntityByUuid(string $entity_type, string $uuid, DependencyStack $stack, string $field_name): ?EntityInterface {
    // Check stack first because uuids are referenced from origin, not local.
    if ($stack->hasDependency($uuid)) {
      return $stack->getDependency($uuid)->getEntity();
    }
    // Only fall back to local uuids as an absolute last resort.
    if (empty($entity_type)) {
      throw new \Exception(sprintf("The %s field does not specify a metadata target. This is likely due to an unresolved dependency export process. Please check your relationships.", $field_name));
    }
    $storage = \Drupal::entityTypeManager()->getStorage($entity_type);
    $entities = $storage->loadByProperties(['uuid' => $uuid]);
    return !empty($entities) ? reset($entities) : NULL;
  }

  /**
   * Returns a config entity based on the provided id.
   *
   * @param string $entity_type
   *   The config entity type.
   * @param string $id
   *   The machine name of the config entity.
   *
   * @return \Drupal\Core\Config\Entity\ConfigEntityInterface|null
   *   The config entity or null.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\acquia_contenthub_subscriber\Exception\ContentHubImportException
   */
  protected function getConfigEntityById(string $entity_type, string $id): ?ConfigEntityInterface {
    $definition = \Drupal::entityTypeManager()->getDefinition($entity_type);
    if (!$definition->entityClassImplements(ConfigEntityInterface::class)) {
      return NULL;
    }
    $storage = \Drupal::entityTypeManager()->getStorage($entity_type);
    /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
    $entity = $storage->load($id);
    if (!$entity) {
      throw new ContentHubImportException(sprintf('%s config entity was not found locally.', $id));
    }
    return $entity;
  }

  /**
   * Log message if entity is missing and is not pruned.
   *
   * @param string $uuid
   *   Entity uuid.
   */
  protected function log(string $uuid): void {
    // This means that this entity was deliberately removed
    // so no need to log.
    if ($this->prunedEntitiesTracker->isPruned($uuid)) {
      return;
    }
    $this->logger->error(
      'Entity with {uuid} not found in DependencyStack or subscriber\'s database while unserializing field values.',
      [
        'uuid' => $uuid,
      ]
    );
  }

}
